Skip to content

Commit

Permalink
🐛 load flags via viper/cnquery providers (#1508)
Browse files Browse the repository at this point in the history
Holy 🐮  cow! Are you ready for this fix!??

<img src="https://media4.giphy.com/media/Vp3ftHKvKpASA/giphy.gif"/>

Based of mondoohq/cnquery#4863 where we now properly load provider flags via `viper` and from environment variables, we depend of the viper binding of such flags and, since `cnspec` did not have a `PreRun()` function but a `PreRunE()`, we did not load them.

This simple change fixes that issue, we now load the flags since we have the `PreRun()` function that is required to bind our flags at https://github.com/mondoohq/cnquery/blob/main/cli/providers/providers.go#L378-L399 

This code is very similar to the code in `cnquery` at:

https://github.com/mondoohq/cnquery/blob/main/apps/cnquery/cmd/scan.go#L88

* 🐛 fix help messages inside tests
* 🎉 add integration tests

---------

Signed-off-by: Salim Afiune Maya <[email protected]>
  • Loading branch information
afiune authored Dec 4, 2024
1 parent 7faffe7 commit c50accf
Show file tree
Hide file tree
Showing 12 changed files with 389 additions and 11 deletions.
10 changes: 4 additions & 6 deletions apps/cnspec/cmd/scan.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ To manually configure a policy, use this:
$ cnspec scan local -f bundle.mql.yaml --incognito
`,
PreRunE: func(cmd *cobra.Command, args []string) error {
PreRun: func(cmd *cobra.Command, _ []string) {
// Special handling for users that want to see what output options are
// available. We have to do this before printing the help because we
// don't have a target connection or provider.
Expand Down Expand Up @@ -131,18 +131,16 @@ To manually configure a policy, use this:

_ = viper.BindPFlag("json", cmd.Flags().Lookup("json"))
_ = viper.BindPFlag("output", cmd.Flags().Lookup("output"))
if err := viper.BindPFlag("output-target", cmd.Flags().Lookup("output-target")); err != nil {
return err
}

return nil
_ = viper.BindPFlag("output-target", cmd.Flags().Lookup("output-target"))
},
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
if len(args) == 0 {
return []string{"yml", "yaml", "json"}, cobra.ShellCompDirectiveFilterFileExt
}
return []string{}, cobra.ShellCompDirectiveNoFileComp
},
// we have to initialize an empty run so it shows up as a runnable command in --help
Run: func(cmd *cobra.Command, args []string) {},
}

var scanCmdRun = func(cmd *cobra.Command, runtime *providers.Runtime, cliRes *plugin.ParseCLIRes) {
Expand Down
4 changes: 2 additions & 2 deletions cli/reporter/cnspec_report.pb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion policy/cnspec_policy.pb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions policy/scan/scan.pb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions test/cli/testdata/cnspec_scan.ct
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ To manually configure a policy, use this:
$ cnspec scan local -f bundle.mql.yaml --incognito

Usage:
cnspec scan [flags]
cnspec scan [command]

Available Commands:
Expand Down
226 changes: 226 additions & 0 deletions test/providers/os_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
// Copyright (c) Mondoo, Inc.
// SPDX-License-Identifier: BUSL-1.1

package providers

import (
"os"
"sync"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.mondoo.com/cnquery/v11/test"
)

var once sync.Once

const mqlPackagesQuery = "packages"

type mqlPackages []struct {
Packages []struct {
Name string `json:"name,omitempty"`
Version string `json:"version,omitempty"`
} `json:"packages.list,omitempty"`
}

const mqlPlatformQuery = "asset.platform"

type mqlPlatform []struct {
Platform string `json:"asset.platform,omitempty"`
}

type connections []struct {
name string
binary string
args []string
tests []mqlTest
}

type mqlTest struct {
query string
expected func(*testing.T, test.Runner)
}

func TestOsProviderSharedTests(t *testing.T) {
once.Do(setup)

connections := connections{
{
name: "local",
binary: "./cnspec",
args: []string{"run", "local"},
tests: []mqlTest{
{
mqlPackagesQuery,
func(t *testing.T, r test.Runner) {
var c mqlPackages
err := r.Json(&c)
assert.NoError(t, err)

x := c[0]
assert.NotNil(t, x.Packages)
assert.True(t, len(x.Packages) > 0)
},
},
{
mqlPlatformQuery,
func(t *testing.T, r test.Runner) {
var c mqlPlatform
err := r.Json(&c)
assert.NoError(t, err)

x := c[0]
assert.True(t, len(x.Platform) > 0)
},
},
},
},
{
name: "fs",
binary: "./cnspec",
args: []string{"run", "fs", "--path", "./testdata/fs"},
tests: []mqlTest{
{
mqlPackagesQuery,
func(t *testing.T, r test.Runner) {
var c mqlPackages
err := r.Json(&c)
assert.NoError(t, err)

x := c[0]
assert.NotNil(t, x.Packages)
assert.True(t, len(x.Packages) > 0)
},
},
{
mqlPlatformQuery,
func(t *testing.T, r test.Runner) {
var c mqlPlatform
err := r.Json(&c)
assert.NoError(t, err)

x := c[0]
assert.Equal(t, "debian", x.Platform)
},
},
},
},
{
name: "docker",
binary: "./cnspec",
args: []string{"run", "docker", "alpine:latest"},
tests: []mqlTest{
{
mqlPackagesQuery,
func(t *testing.T, r test.Runner) {
var c mqlPackages
err := r.Json(&c)
assert.NoError(t, err)

x := c[0]
assert.NotNil(t, x.Packages)
assert.True(t, len(x.Packages) > 0)
},
},
{
mqlPlatformQuery,
func(t *testing.T, r test.Runner) {
var c mqlPlatform
err := r.Json(&c)
assert.NoError(t, err)

x := c[0]
assert.Equal(t, "alpine", x.Platform)
},
},
},
},
}

// iterate over all tests for all connections
for _, cc := range connections {
for _, tt := range cc.tests {

t.Run(cc.name+"/"+tt.query, func(t *testing.T) {
r := test.NewCliTestRunner(cc.binary, append(cc.args, "-c", tt.query, "-j")...)
err := r.Run()
require.NoError(t, err)
assert.Equal(t, 0, r.ExitCode())
assert.NotNil(t, r.Stdout())
assert.NotNil(t, r.Stderr())

tt.expected(t, r)
})
}
}
}

func TestProvidersEnvVarsLoading(t *testing.T) {
once.Do(setup)

t.Run("command WITHOUT path should not find any package", func(t *testing.T) {
r := test.NewCliTestRunner("./cnspec", "run", "fs", "-c", mqlPackagesQuery, "-j")
err := r.Run()
require.NoError(t, err)
assert.Equal(t, 0, r.ExitCode())
assert.NotNil(t, r.Stdout())
assert.NotNil(t, r.Stderr())

var c mqlPackages
err = r.Json(&c)
assert.NoError(t, err)

// No packages
assert.Empty(t, c)
})
t.Run("command WITH path should find packages", func(t *testing.T) {
os.Setenv("MONDOO_PATH", "./testdata/fs")
defer os.Unsetenv("MONDOO_PATH")
// Note we are not passing the flag "--path ./testdata/fs"
r := test.NewCliTestRunner("./cnspec", "run", "fs", "-c", mqlPackagesQuery, "-j")
err := r.Run()
require.NoError(t, err)
assert.Equal(t, 0, r.ExitCode())
assert.NotNil(t, r.Stdout())
assert.NotNil(t, r.Stderr())

var c mqlPackages
err = r.Json(&c)
assert.NoError(t, err)

// Should have packages
if assert.NotEmpty(t, c) {
x := c[0]
assert.NotNil(t, x.Packages)
assert.True(t, len(x.Packages) > 0)
}
})

t.Run("command with flags set to not bind to config (ConfigEntry=\"-\")", func(t *testing.T) {
t.Run("should work via direct flag", func(t *testing.T) {
r := test.NewCliTestRunner("./cnspec", "run", "ssh", "localhost", "-c", "ls", "-p", "test", "-v")
err := r.Run()
require.NoError(t, err)
assert.Equal(t, 0, r.ExitCode())
assert.NotNil(t, r.Stdout())
if assert.NotNil(t, r.Stderr()) {
assert.Contains(t, string(r.Stderr()), "skipping config binding for password")
assert.Contains(t, string(r.Stderr()), "enabled ssh password authentication")
}
})
t.Run("should NOT work via config/env-vars", func(t *testing.T) {
os.Setenv("MONDOO_PASSWORD", "test")
defer os.Unsetenv("MONDOO_PASSWORD")
r := test.NewCliTestRunner("./cnspec", "run", "ssh", "localhost", "-c", "ls", "-v")
err := r.Run()
require.NoError(t, err)
assert.Equal(t, 0, r.ExitCode())
assert.NotNil(t, r.Stdout())
if assert.NotNil(t, r.Stderr()) {
assert.Contains(t, string(r.Stderr()), "skipping config binding for password")
assert.NotContains(t, string(r.Stderr()), "enabled ssh password authentication")
}
})
})
}
76 changes: 76 additions & 0 deletions test/providers/scan_flags_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
// Copyright (c) Mondoo, Inc.
// SPDX-License-Identifier: BUSL-1.1

package providers

import (
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.mondoo.com/cnquery/v11/test"
"go.mondoo.com/cnspec/v11/policy"
)

func TestScanFlags(t *testing.T) {
once.Do(setup)

t.Run("successful scan without flags", func(t *testing.T) {
r := test.NewCliTestRunner("./cnspec", "scan", "docker", "alpine:latest", "--json")
err := r.Run()
require.NoError(t, err)
assert.Equal(t, 0, r.ExitCode())
assert.NotNil(t, r.Stdout())
assert.NotNil(t, r.Stderr())

var c policy.ReportCollection
err = r.Json(&c)
assert.NoError(t, err)

// Assest must be found
assert.NotEmpty(t, c.Assets)
})
t.Run("github scan WITHOUT flags", func(t *testing.T) {
// NOTE this will fail but, it will load the flags and fail with the right message
r := test.NewCliTestRunner("./cnspec", "scan", "github", "repo", "foo")
err := r.Run()
require.NoError(t, err)
assert.Equal(t, 0, r.ExitCode())
assert.NotNil(t, r.Stdout())
assert.NotNil(t, r.Stderr())

assert.Contains(t, string(r.Stderr()),
"a valid GitHub authentication is required",
)
})
t.Run("github scan WITH flags but missing app auth key", func(t *testing.T) {
// NOTE this will fail but, it will load the flags and fail with the right message
r := test.NewCliTestRunner("./cnspec", "scan", "github", "repo", "foo",
"--app-id", "123", "--app-installation-id", "456",
)
err := r.Run()
require.NoError(t, err)
assert.Equal(t, 1, r.ExitCode())
assert.NotNil(t, r.Stdout())
assert.NotNil(t, r.Stderr())

assert.Contains(t, string(r.Stderr()),
"app-private-key is required for GitHub App authentication", // expected! it means we loaded the flags
)
})
t.Run("github scan WITH all required flags for app auth", func(t *testing.T) {
// NOTE this will fail but, it will load the flags and fail with the right message
r := test.NewCliTestRunner("./cnspec", "scan", "github", "repo", "foo",
"--app-id", "123", "--app-installation-id", "456", "--app-private-key", "private-key.pem",
)
err := r.Run()
require.NoError(t, err)
assert.Equal(t, 1, r.ExitCode())
assert.NotNil(t, r.Stdout())
assert.NotNil(t, r.Stderr())

assert.Contains(t, string(r.Stderr()),
"could not read private key", // expected! it means we loaded the flags
)
})
}
17 changes: 17 additions & 0 deletions test/providers/setup_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// Copyright (c) Mondoo, Inc.
// SPDX-License-Identifier: BUSL-1.1

package providers

import (
"log"
"os/exec"
)

// setup builds cnspec locally
func setup() {
// build cnspec
if err := exec.Command("go", "build", "../../apps/cnspec/cnspec.go").Run(); err != nil {
log.Fatalf("building cnspec: %v", err)
}
}
1 change: 1 addition & 0 deletions test/providers/testdata/fs/etc/debian_version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
12.5
1 change: 1 addition & 0 deletions test/providers/testdata/fs/etc/hostname
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
debianfs
Loading

0 comments on commit c50accf

Please sign in to comment.