Skip to content

Commit

Permalink
⭐️ newcnspec bundle lint cmd (#253)
Browse files Browse the repository at this point in the history
**Problem:**

As a developer of policies I want to verify that my developed bundle
meets the requirements and conforms to best-practices.

**Solution:**

The new `cnspec bundle lint` command (previously called `cnspec bundle
validate` ships with a set of rules:

- "MQL compile error"
- "UID is not valid"
- "Missing policy UID"
- "Missing policy name"
- "No unique policy UID"
- "Policy is missing checks"
- "Assigned query missing"
- "Policy version is missing"
- "Policy version is wrong"
- "Missing query UID"
- "Missing query title"
- "No unique query UID"
- "Unassigned Query"

A major improvement compared to the previous implementation is the
detection of file name and line number. The allows the output to
highlights the rule id and message with the file and line number. To see
the new linting output, just run:

```
cnspec bundle lint policy.mql.yaml
```

The new `cnspec bundle lint` also allows users to export the output as
sarif with the `-o sarif` option. Best is to pipe the output into a
file:

```
cnspec bundle lint -o sarif --output-file report.sarif policy.mql.yaml 
``` 

The report can then be viewed in Visual Studio Code and the [Sarif
Extension](https://marketplace.visualstudio.com/items?itemName=MS-SarifVSCode.sarif-viewer)

We also improved renamed `cnspec bundle upload` to `cnspec bundle
publish`.
  • Loading branch information
chris-rock authored Jan 3, 2023
1 parent f32ec5c commit 3aeec4d
Show file tree
Hide file tree
Showing 20 changed files with 1,211 additions and 331 deletions.
197 changes: 62 additions & 135 deletions apps/cnspec/cmd/bundle.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,16 @@ import (
"context"
_ "embed"
"fmt"
"io/fs"
"os"
"path/filepath"
"strconv"
"strings"

"github.com/rs/zerolog/log"
"github.com/spf13/cobra"
"github.com/spf13/viper"
cnquery_config "go.mondoo.com/cnquery/apps/cnquery/cmd/config"
"go.mondoo.com/cnquery/cli/config"
"go.mondoo.com/cnquery/stringx"
"go.mondoo.com/cnquery/upstream"
"go.mondoo.com/cnspec/apps/cnspec/cmd/fmtbundle"
"go.mondoo.com/cnspec/internal/bundle"
"go.mondoo.com/cnspec/policy"
"go.mondoo.com/ranger-rpc"
)
Expand All @@ -27,14 +23,16 @@ func init() {
policyBundlesCmd.AddCommand(policyInitCmd)

// validate
policyBundlesCmd.AddCommand(policyValidateCmd)
policyLintCmd.Flags().StringP("output", "o", "cli", "Set output format: compact, sarif")
policyLintCmd.Flags().String("output-file", "", "Set output file")
policyBundlesCmd.AddCommand(policyLintCmd)

// fmt
policyBundlesCmd.AddCommand(policyFmtCmd)

// bundle add
policyUploadCmd.Flags().String("policy-version", "", "Override the version of each policy in the bundle.")
policyBundlesCmd.AddCommand(policyUploadCmd)
// publish
policyPublishCmd.Flags().String("policy-version", "", "Override the version of each policy in the bundle.")
policyBundlesCmd.AddCommand(policyPublishCmd)

rootCmd.AddCommand(policyBundlesCmd)
}
Expand Down Expand Up @@ -70,131 +68,56 @@ var policyInitCmd = &cobra.Command{
},
}

func validate(policyBundle *policy.Bundle) []string {
errors := []string{}

// check that we have uids for policies and queries
for i := range policyBundle.Policies {
policy := policyBundle.Policies[i]
policyId := strconv.Itoa(i)

if policy.Uid == "" {
errors = append(errors, fmt.Sprintf("policy %s does not define a UID", policyId))
} else {
policyId = policy.Uid
}

if policy.Name == "" {
errors = append(errors, fmt.Sprintf("policy %s does not define a name", policyId))
}
}

for j := range policyBundle.Queries {
query := policyBundle.Queries[j]
queryId := strconv.Itoa(j)
if query.Uid == "" {
errors = append(errors, fmt.Sprintf("query %s does not define a UID", queryId))
} else {
queryId = query.Uid
}
var policyLintCmd = &cobra.Command{
Use: "lint [path]",
Aliases: []string{"validate"},
Short: "Lint a policy bundle",
Args: cobra.ExactArgs(1),
PreRun: func(cmd *cobra.Command, args []string) {
viper.BindPFlag("output", cmd.Flags().Lookup("output"))
viper.BindPFlag("output-file", cmd.Flags().Lookup("output-file"))
},
Run: func(cmd *cobra.Command, args []string) {
log.Info().Str("file", args[0]).Msg("lint policy bundle")

if query.Title == "" {
errors = append(errors, fmt.Sprintf("query %s does not define a name", queryId))
files, err := policy.WalkPolicyBundleFiles(args[0])
if err != nil {
log.Fatal().Err(err).Msg("could not find bundle files")
}
}

// we compile after the checks because it removes the uids and replaces it with mrns
_, err := policyBundle.Compile(context.Background(), nil)
if err != nil {
log.Fatal().Err(err).Msg("could not validate policy bundle")
}

return errors
}

var policyValidateCmd = &cobra.Command{
Use: "validate [path]",
Short: "Validates a policy bundle",
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
log.Info().Str("file", args[0]).Msg("validate policy bundle")
policyBundle, err := policy.BundleFromPaths(args[0])
result, err := bundle.Lint(files...)
if err != nil {
log.Fatal().Err(err).Msg("could not load policy bundle")
log.Fatal().Err(err).Msg("could not lint bundle files")
}

errors := validate(policyBundle)
if len(errors) > 0 {
log.Error().Msg("could not validate policy bundle")
for i := range errors {
fmt.Fprintf(os.Stderr, stringx.Indent(2, errors[i]))
out := os.Stdout
if viper.GetString("output-file") != "" {
out, err = os.Create(viper.GetString("output-file"))
if err != nil {
log.Fatal().Err(err).Msg("could not create output file")
}
os.Exit(1)
defer out.Close()
}
log.Info().Msg("valid policy bundle")
},
}

func formatPath(mqlBundlePath string) error {
log.Info().Str("file", mqlBundlePath).Msg("format policy bundle(s)")
fi, err := os.Stat(mqlBundlePath)
if err != nil {
fmt.Println(err)
os.Exit(1)
}

if fi.IsDir() {
filepath.WalkDir(mqlBundlePath, func(path string, d fs.DirEntry, err error) error {
switch viper.GetString("output") {
case "cli":
out.Write(result.ToCli())
case "sarif":
data, err := result.ToSarif(filepath.Dir(args[0]))
if err != nil {
return err
}
// we ignore nested directories
if d.IsDir() {
return nil
log.Fatal().Err(err).Msg("could not generate sarif report")
}
out.Write(data)
}

// only consider .yaml|.yml files
if strings.HasSuffix(d.Name(), ".mql.yaml") {
err := formatFile(path)
if err != nil {
fmt.Println(err)
os.Exit(1)
}
if viper.GetString("output-file") == "" {
if result.HasError() {
log.Fatal().Msg("invalid policy bundle")
} else {
log.Info().Msg("valid policy bundle")
}
return nil
})
} else {
err := formatFile(mqlBundlePath)
if err != nil {
return err
}
}
return nil
}

func formatFile(filename string) error {
log.Info().Str("file", filename).Msg("format file")
data, err := os.ReadFile(filename)
if err != nil {
return err
}

bundle, err := fmtbundle.ParseYaml(data)
if err != nil {
return err
}

data, err = fmtbundle.Format(bundle)
if err != nil {
return err
}

err = os.WriteFile(filename, data, 0o644)
if err != nil {
return err
}

return nil
},
}

var policyFmtCmd = &cobra.Command{
Expand All @@ -204,7 +127,7 @@ var policyFmtCmd = &cobra.Command{
Args: cobra.MinimumNArgs(1),
Run: func(cmd *cobra.Command, args []string) {
for _, path := range args {
err := formatPath(path)
err := bundle.FormatRecursive(path)
if err != nil {
fmt.Println(err)
os.Exit(1)
Expand All @@ -215,10 +138,11 @@ var policyFmtCmd = &cobra.Command{
},
}

var policyUploadCmd = &cobra.Command{
Use: "upload [path]",
Short: "Add a user-owned policy to Mondoo Query Hub.",
Args: cobra.ExactArgs(1),
var policyPublishCmd = &cobra.Command{
Use: "publish [path]",
Aliases: []string{"upload"},
Short: "Add a user-owned policy to Mondoo Query Hub.",
Args: cobra.ExactArgs(1),
PreRun: func(cmd *cobra.Command, args []string) {
viper.BindPFlag("policy-version", cmd.Flags().Lookup("policy-version"))
},
Expand All @@ -231,23 +155,26 @@ var policyUploadCmd = &cobra.Command{

filename := args[0]
log.Info().Str("file", filename).Msg("load policy bundle")
policyBundle, err := policy.BundleFromPaths(filename)
files, err := policy.WalkPolicyBundleFiles(args[0])
if err != nil {
log.Fatal().Err(err).Msg("could not load policy bundle")
log.Fatal().Err(err).Msg("could not find bundle files")
}

errors := validate(policyBundle)
if len(errors) > 0 {
log.Error().Msg("could not validate policy bundle")
for i := range errors {
fmt.Fprintf(os.Stderr, stringx.Indent(2, errors[i]))
}
os.Exit(1)
result, err := bundle.Lint(files...)
if err != nil {
log.Fatal().Err(err).Msg("could not lint bundle files")
}

// render cli output
os.Stdout.Write(result.ToCli())

if result.HasError() {
log.Fatal().Msg("invalid policy bundle")
}
log.Info().Msg("valid policy bundle")

// compile manipulates the bundle, therefore we read it again
policyBundle, err = policy.BundleFromPaths(filename)
policyBundle, err := policy.BundleFromPaths(filename)
if err != nil {
log.Fatal().Err(err).Msg("could not load policy bundle")
}
Expand Down
Loading

0 comments on commit 3aeec4d

Please sign in to comment.