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 }