github.com/seilagamo/poc-lava-release@v0.3.3-rc3/internal/config/config.go (about)

     1  // Copyright 2023 Adevinta
     2  
     3  // Package config implements parsing of Lava configurations.
     4  package config
     5  
     6  import (
     7  	"errors"
     8  	"fmt"
     9  	"io"
    10  	"log/slog"
    11  	"os"
    12  	"strings"
    13  
    14  	agentconfig "github.com/adevinta/vulcan-agent/config"
    15  	types "github.com/adevinta/vulcan-types"
    16  	"golang.org/x/mod/semver"
    17  	"gopkg.in/yaml.v3"
    18  
    19  	"github.com/seilagamo/poc-lava-release/internal/assettypes"
    20  )
    21  
    22  var (
    23  	// ErrInvalidLavaVersion means that the Lava version does not
    24  	// have a valid format according to the Semantic Versioning
    25  	// Specification.
    26  	ErrInvalidLavaVersion = errors.New("invalid Lava version")
    27  
    28  	// ErrNoChecktypeURLs means that no checktypes URLs were
    29  	// specified.
    30  	ErrNoChecktypeURLs = errors.New("no checktype catalogs")
    31  
    32  	// ErrNoTargets means that no targets were specified.
    33  	ErrNoTargets = errors.New("no targets")
    34  
    35  	// ErrNoTargetIdentifier means that the target does not have
    36  	// an identifier.
    37  	ErrNoTargetIdentifier = errors.New("no target identifier")
    38  
    39  	// ErrNoTargetAssetType means that the target does not have an
    40  	// asset type.
    41  	ErrNoTargetAssetType = errors.New("no target asset type")
    42  
    43  	// ErrInvalidAssetType means that the asset type is invalid.
    44  	ErrInvalidAssetType = errors.New("invalid asset type")
    45  
    46  	// ErrInvalidSeverity means that the severity is invalid.
    47  	ErrInvalidSeverity = errors.New("invalid severity")
    48  
    49  	// ErrInvalidOutputFormat means that the output format is
    50  	// invalid.
    51  	ErrInvalidOutputFormat = errors.New("invalid output format")
    52  )
    53  
    54  // Config represents a Lava configuration.
    55  type Config struct {
    56  	// LavaVersion is the minimum required version of Lava.
    57  	LavaVersion string `yaml:"lava"`
    58  
    59  	// AgentConfig is the configuration of the vulcan-agent.
    60  	AgentConfig AgentConfig `yaml:"agent"`
    61  
    62  	// ReportConfig is the configuration of the report.
    63  	ReportConfig ReportConfig `yaml:"report"`
    64  
    65  	// ChecktypeURLs is a list of URLs pointing to checktype
    66  	// catalogs.
    67  	ChecktypeURLs []string `yaml:"checktypes"`
    68  
    69  	// Targets is the list of targets.
    70  	Targets []Target `yaml:"targets"`
    71  
    72  	// LogLevel is the logging level.
    73  	LogLevel slog.Level `yaml:"log"`
    74  }
    75  
    76  // Parse returns a parsed Lava configuration given an [io.Reader].
    77  func Parse(r io.Reader) (Config, error) {
    78  	var cfg Config
    79  	if err := yaml.NewDecoder(r).Decode(&cfg); err != nil {
    80  		return Config{}, fmt.Errorf("decode config: %w", err)
    81  	}
    82  	if err := cfg.validate(); err != nil {
    83  		return Config{}, fmt.Errorf("validate config: %w", err)
    84  	}
    85  	return cfg, nil
    86  }
    87  
    88  // ParseFile returns a parsed Lava configuration given a path to a
    89  // file.
    90  func ParseFile(path string) (Config, error) {
    91  	f, err := os.Open(path)
    92  	if err != nil {
    93  		return Config{}, fmt.Errorf("open config file: %w", err)
    94  	}
    95  	defer f.Close()
    96  	return Parse(f)
    97  }
    98  
    99  // validate validates the Lava configuration.
   100  func (c Config) validate() error {
   101  	// Lava version validation.
   102  	if !semver.IsValid(c.LavaVersion) {
   103  		return ErrInvalidLavaVersion
   104  	}
   105  
   106  	// Checktype URLs validation.
   107  	if len(c.ChecktypeURLs) == 0 {
   108  		return ErrNoChecktypeURLs
   109  	}
   110  
   111  	// Targets validation.
   112  	if len(c.Targets) == 0 {
   113  		return ErrNoTargets
   114  	}
   115  	for _, t := range c.Targets {
   116  		if err := t.validate(); err != nil {
   117  			return err
   118  		}
   119  	}
   120  	return nil
   121  }
   122  
   123  // AgentConfig is the configuration passed to the vulcan-agent.
   124  type AgentConfig struct {
   125  	// PullPolicy is the pull policy passed to vulcan-agent.
   126  	PullPolicy agentconfig.PullPolicy `yaml:"pullPolicy"`
   127  
   128  	// Parallel is the maximum number of checks that can run in
   129  	// parallel.
   130  	Parallel int `yaml:"parallel"`
   131  
   132  	// Vars is the environment variables required by the Vulcan
   133  	// checktypes.
   134  	Vars map[string]string `yaml:"vars"`
   135  
   136  	// RegistryAuths contains the credentials for a set of
   137  	// container registries.
   138  	RegistryAuths []RegistryAuth `yaml:"registries"`
   139  }
   140  
   141  // ReportConfig is the configuration of the report.
   142  type ReportConfig struct {
   143  	// Severity is the minimum severity required to report a
   144  	// finding.
   145  	Severity Severity `yaml:"severity"`
   146  
   147  	// Format is the output format.
   148  	Format OutputFormat `yaml:"format"`
   149  
   150  	// OutputFile is the path of the output file.
   151  	OutputFile string `yaml:"output"`
   152  
   153  	// Exclusions is a list of findings that will be ignored. For
   154  	// instance, accepted risks, false positives, etc.
   155  	Exclusions []Exclusion `yaml:"exclusions"`
   156  
   157  	// Metrics is the file where the metrics will be written.
   158  	// If Metrics is an empty string or not specified in the yaml file, then
   159  	// the metrics report is not saved.
   160  	Metrics string `yaml:"metrics"`
   161  }
   162  
   163  // Target represents the target of a scan.
   164  type Target struct {
   165  	// Identifier is a string that identifies the target. For
   166  	// instance, a path, a URL, a container image, etc.
   167  	Identifier string `yaml:"identifier"`
   168  
   169  	// AssetType is the asset type of the target.
   170  	AssetType types.AssetType `yaml:"type"`
   171  
   172  	// Options is a list of specific options for the target.
   173  	Options map[string]any `yaml:"options"`
   174  }
   175  
   176  // validate reports whether the target is a valid configuration value.
   177  func (t Target) validate() error {
   178  	if t.Identifier == "" {
   179  		return ErrNoTargetIdentifier
   180  	}
   181  	if t.AssetType == "" {
   182  		return ErrNoTargetAssetType
   183  	}
   184  	if !t.AssetType.IsValid() && !assettypes.IsValid(t.AssetType) {
   185  		return fmt.Errorf("%w: %v", ErrInvalidAssetType, t.AssetType)
   186  	}
   187  	return nil
   188  }
   189  
   190  // RegistryAuth contains the credentials for a container registry.
   191  type RegistryAuth struct {
   192  	// Server is the URL of the registry.
   193  	Server string `yaml:"server"`
   194  
   195  	// Username is the username used to log into the registry.
   196  	Username string `yaml:"username"`
   197  
   198  	// Password is the password used to log into the registry.
   199  	Password string `yaml:"password"`
   200  }
   201  
   202  // Severity is the severity of a given finding.
   203  type Severity int
   204  
   205  // Severity levels.
   206  const (
   207  	SeverityCritical Severity = 1
   208  	SeverityHigh     Severity = 0
   209  	SeverityMedium   Severity = -1
   210  	SeverityLow      Severity = -2
   211  	SeverityInfo     Severity = -3
   212  )
   213  
   214  // severityNames maps each severity name with its level.
   215  var severityNames = map[string]Severity{
   216  	"critical": SeverityCritical,
   217  	"high":     SeverityHigh,
   218  	"medium":   SeverityMedium,
   219  	"low":      SeverityLow,
   220  	"info":     SeverityInfo,
   221  }
   222  
   223  // parseSeverity converts a string into a [Severity] value.
   224  func parseSeverity(severity string) (Severity, error) {
   225  	if val, ok := severityNames[severity]; ok {
   226  		return val, nil
   227  	}
   228  	return Severity(0), fmt.Errorf("%w: %v", ErrInvalidSeverity, severity)
   229  }
   230  
   231  // IsValid checks if a severity is valid.
   232  func (s Severity) IsValid() bool {
   233  	return s >= SeverityInfo && s <= SeverityCritical
   234  }
   235  
   236  // String returns value of a severity.
   237  func (s Severity) String() string {
   238  	for k, v := range severityNames {
   239  		if v == s {
   240  			return k
   241  		}
   242  	}
   243  	return ""
   244  }
   245  
   246  // MarshalText encode a [Severity] as a text.
   247  func (s Severity) MarshalText() (text []byte, err error) {
   248  	if !s.IsValid() {
   249  		return nil, ErrInvalidSeverity
   250  	}
   251  	return []byte(s.String()), nil
   252  }
   253  
   254  // UnmarshalText decodes a Severity text into a
   255  // [Severity] value. It returns error if the provided
   256  // string does not match any known severity.
   257  func (s *Severity) UnmarshalText(text []byte) error {
   258  	severity, err := parseSeverity(string(text))
   259  	if err != nil {
   260  		return err
   261  	}
   262  	*s = severity
   263  	return nil
   264  }
   265  
   266  // OutputFormat is the format of the generated report.
   267  type OutputFormat int
   268  
   269  // Output formats available for the report.
   270  const (
   271  	OutputFormatHuman OutputFormat = iota
   272  	OutputFormatJSON
   273  )
   274  
   275  var outputFormatNames = map[string]OutputFormat{
   276  	"human": OutputFormatHuman,
   277  	"json":  OutputFormatJSON,
   278  }
   279  
   280  // parseOutputFormat converts a string into an [OutputFormat] value.
   281  func parseOutputFormat(format string) (OutputFormat, error) {
   282  	if val, ok := outputFormatNames[strings.ToLower(format)]; ok {
   283  		return val, nil
   284  	}
   285  	return OutputFormat(0), fmt.Errorf("%w: %v", ErrInvalidOutputFormat, format)
   286  }
   287  
   288  // UnmarshalYAML decodes an OutputFormat yaml node containing a string
   289  // into an [OutputFormat] value. It returns error if the provided
   290  // string does not match any known output format.
   291  func (f *OutputFormat) UnmarshalYAML(value *yaml.Node) error {
   292  	format, err := parseOutputFormat(value.Value)
   293  	if err != nil {
   294  		return err
   295  	}
   296  	*f = format
   297  	return nil
   298  }
   299  
   300  // Exclusion represents the criteria to exclude a given finding.
   301  type Exclusion struct {
   302  	// Target is a regular expression that matches the name of the
   303  	// affected target.
   304  	Target string `yaml:"target"`
   305  
   306  	// Resource is a regular expression that matches the name of
   307  	// the affected resource.
   308  	Resource string `yaml:"resource"`
   309  
   310  	// Fingerprint defines the context in where the vulnerability
   311  	// has been found. It includes the checktype image, the
   312  	// affected target, the asset type and the checktype options.
   313  	Fingerprint string `yaml:"fingerprint"`
   314  
   315  	// Summary is a regular expression that matches the summary of
   316  	// the vulnerability.
   317  	Summary string `yaml:"summary"`
   318  
   319  	// Description describes the exclusion.
   320  	Description string `yaml:"description"`
   321  }