Skip to content

Commit

Permalink
Add data validator, validate subcommand (#188)
Browse files Browse the repository at this point in the history
* Add validate subcommand

Signed-off-by: Adolfo García Veytia (Puerco) <adolfo.garcia@uservers.net>

* Prevent rendering with invalid data data

Signed-off-by: Adolfo García Veytia (Puerco) <adolfo.garcia@uservers.net>

---------

Signed-off-by: Adolfo García Veytia (Puerco) <adolfo.garcia@uservers.net>
  • Loading branch information
puerco authored Feb 19, 2025
1 parent ec2d100 commit e21f2e3
Show file tree
Hide file tree
Showing 5 changed files with 122 additions and 19 deletions.
22 changes: 21 additions & 1 deletion cmd/internal/cmd/compile.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ type compileOptions struct {
outPath string
baselinePath string
templatePath string
validate bool
}

// Validate the options in context with arguments
Expand Down Expand Up @@ -44,6 +45,10 @@ func (o *compileOptions) AddFlags(cmd *cobra.Command) {
cmd.PersistentFlags().StringVarP(
&o.templatePath, "template", "t", "template.md", "path to the markdown template file",
)

cmd.PersistentFlags().BoolVarP(
&o.validate, "validate", "v", true, "validate data inegrity before rendering",
)
}

// addCompile adds the compile subcommand to the parent command
Expand Down Expand Up @@ -71,6 +76,7 @@ func addCompile(parentCmd *cobra.Command) {

cmd.SilenceUsage = true

// Load the baseline data
loader := baseline.NewLoader()
loader.DataPath = opts.baselinePath

Expand All @@ -79,6 +85,20 @@ func addCompile(parentCmd *cobra.Command) {
return err
}

// Validate the data
validator := baseline.NewValidator()
err = validator.Check(bline)
if err != nil {
if opts.validate {
fmt.Fprint(os.Stderr, "\n❌ Error validating the baseline data:\n")
return err
}

// if the validation flag is off, we still validate but only warn
// the user about it
fmt.Fprint(os.Stderr, "\n⚠️ Error validating the baseline data (still rendering)")
}

// Generate the rendered version
gen := baseline.NewGenerator()
gen.TemplatePath = opts.templatePath
Expand All @@ -87,7 +107,7 @@ func addCompile(parentCmd *cobra.Command) {
return fmt.Errorf("writing mardown render: %w", err)
}

fmt.Fprintf(os.Stderr, "\nBaseline rendered to %s:\n\nCategories:\n", opts.outPath)
fmt.Fprintf(os.Stderr, "\n✅ Baseline rendered to %s:\n\nCategories:\n", opts.outPath)
for c := range bline.Categories {
fmt.Fprintf(os.Stderr, " OSPS-%s: %d criteria\n", c, len(bline.Categories[c].Criteria))
}
Expand Down
3 changes: 3 additions & 0 deletions cmd/internal/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,10 @@ func Execute() error {
cmd.Help() //nolint
},
}

// Add the subcommands
addCompile(rootCmd)
addValidate(rootCmd)

return rootCmd.Execute()
}
83 changes: 83 additions & 0 deletions cmd/internal/cmd/validate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
// SPDX-FileCopyrightText: Copyright 2025 The OSPS Authors
// SPDX-License-Identifier: Apache-2.0

package cmd

import (
"errors"
"fmt"
"os"

"github.com/ossf/security-baseline/pkg/baseline"
"github.com/spf13/cobra"
)

type validateOptions struct {
baselinePath string
}

// Validate the options in context with arguments
func (o *validateOptions) Validate() error {
errs := []error{}

if o.baselinePath == "" {
errs = append(errs, errors.New("baseline data path not specified"))
}
return errors.Join(errs...)
}

func (o *validateOptions) AddFlags(cmd *cobra.Command) {
cmd.PersistentFlags().StringVarP(
&o.baselinePath, "baseline", "b", "", "path to directory containing the baseline YAML definitions",
)

}

// addValidate adds the compile subcommand to the parent command
func addValidate(parentCmd *cobra.Command) {
opts := validateOptions{}
validateCmd := &cobra.Command{
Use: "validate",
Short: "Validate the baseline data files",
SilenceUsage: false,
SilenceErrors: true,
PreRunE: func(_ *cobra.Command, args []string) error {
if opts.baselinePath != "" && len(args) > 0 && opts.baselinePath != args[0] {
return fmt.Errorf("baseline data path specified twice")
}

if len(args) > 0 {
opts.baselinePath = args[0]
}
return nil
},
RunE: func(cmd *cobra.Command, args []string) error {
if err := opts.Validate(); err != nil {
return err
}

cmd.SilenceUsage = true

// Parse the data files
loader := baseline.NewLoader()
loader.DataPath = opts.baselinePath

bline, err := loader.Load()
if err != nil {
return err
}

// Generate the rendered version
validator := baseline.NewValidator()

if err = validator.Check(bline); err != nil {
fmt.Fprint(os.Stderr, "\n❌ Error validating the baseline data:\n")
return err
}
fmt.Fprint(os.Stderr, "\n✅ Baseline YAML data OK\n\n")
return nil
},
}
opts.AddFlags(validateCmd)
parentCmd.AddCommand(validateCmd)
}
2 changes: 1 addition & 1 deletion cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import (

func main() {
if err := cmd.Execute(); err != nil {
fmt.Fprintf(os.Stderr, "ERROR: %v\n", err)
fmt.Fprintf(os.Stderr, "ERROR: %s\n", err.Error())
os.Exit(1)
}
}
31 changes: 14 additions & 17 deletions cmd/pkg/baseline/validator.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,48 +4,45 @@
package baseline

import (
"errors"
"fmt"
"log"
"slices"

"github.com/ossf/security-baseline/pkg/types"
)

func NewValidator() *Validator {
return &Validator{}
}

type Validator struct {
}

// Check verifies the data parsed for consistency and completeness
func Check(b *types.Baseline) error {
func (v *Validator) Check(b *types.Baseline) error {
var entryIDs []string
var failed bool
var errs = []error{}
for _, category := range b.Categories {
for _, entry := range category.Criteria {
if slices.Contains(entryIDs, entry.ID) {
failed = true
log.Printf("duplicate ID for 'criterion' for %s", entry.ID)
errs = append(errs, fmt.Errorf("duplicate ID for 'criterion' for %s", entry.ID))
}
if entry.ID == "" {
failed = true
log.Printf("missing ID for 'criterion' %s", entry.ID)
errs = append(errs, fmt.Errorf("missing ID for 'criterion' %s", entry.ID))
}
if entry.CriterionText == "" {
failed = true
log.Printf("missing 'criterion' text for %s", entry.ID)
errs = append(errs, fmt.Errorf("missing 'criterion' text for %s", entry.ID))
}
// For after all fields are populated:
// if entry.Rationale == "" {
// failed = true
// log.Printf("missing 'rationale' for %s", entry.ID)
// errs = append(errs, fmt.Errorf("missing 'rationale' for %s", entry.ID))
// }
// if entry.Details == "" {
// failed = true
// log.Printf("missing 'details' for %s", entry.ID)
// errs = append(errs, fmt.Errorf("missing 'details' for %s", entry.ID))
// }
entryIDs = append(entryIDs, entry.ID)
}
}
if failed {
return fmt.Errorf("error validating baseline")
}
return nil

return errors.Join(errs...)
}

0 comments on commit e21f2e3

Please sign in to comment.