mirror of
https://github.com/anchore/syft.git
synced 2026-10-30 22:12:28 +01:00
Replace core SBOM-creation API with builder pattern (#1383)
* remove existing cataloging API Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * add file cataloging config Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * add package cataloging config Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * add configs for cross-cutting concerns Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * rename CLI option configs to not require import aliases later Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * update all nested structs for the Catalog struct Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * update Catalog cli options - add new cataloger selection options (selection and default) - remove the excludeBinaryOverlapByOwnership - deprecate "catalogers" flag - add new javascript configuration Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * migrate relationship capabilities to separate internal package Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * refactor golang cataloger to use configuration options when creating packages Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * create internal object to facilitate reading from and writing to an SBOM Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * create a command-like object (task) to facilitate partial SBOM creation Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * add cataloger selection capability - be able to parse string expressions into a set of resolved actions against sets - be able to use expressions to select/add/remove tasks to/from the final set of tasks to run Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * add package, file, and environment related tasks Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * update existing file catalogers to use nested UI elements Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * add CreateSBOMConfig that drives the SBOM creation process Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * capture SBOM creation info as a struct Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * add CreateSBOM() function Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * fix tests Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * update docs with SBOM selection help + breaking changes Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * fix multiple override default inputs Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * fix deprecation flag printing to stdout Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * refactor cataloger selection description to separate object Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * address review comments Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * keep expression errors and show specific suggestions only Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * address additional review feedback Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * address more review comments Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * addressed additional PR review feedback Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * fix file selection references Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * remove guess language data generation option Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * add tests for coordinatesForSelection Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * rename relationship attributes Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * add descriptions to relationships config fields Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * improve documentation around configuration options Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * add explicit errors around legacy config entries Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> --------- Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,238 @@
|
||||
package task
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"sort"
|
||||
|
||||
"github.com/hashicorp/go-multierror"
|
||||
"github.com/scylladb/go-set/strset"
|
||||
|
||||
"github.com/anchore/syft/syft/cataloging/pkgcataloging"
|
||||
)
|
||||
|
||||
var expressionNodePattern = regexp.MustCompile(`^([a-zA-Z0-9][a-zA-Z0-9-+]*)+$`)
|
||||
|
||||
const (
|
||||
SetOperation Operation = "set"
|
||||
AddOperation Operation = "add"
|
||||
SubSelectOperation Operation = "sub-select"
|
||||
RemoveOperation Operation = "remove"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrEmptyToken = fmt.Errorf("no value given")
|
||||
ErrInvalidToken = fmt.Errorf("invalid token given: only alphanumeric characters and hyphens are allowed")
|
||||
ErrInvalidOperator = fmt.Errorf("invalid operator given")
|
||||
ErrUnknownNameOrTag = fmt.Errorf("unknown name or tag given")
|
||||
ErrTagsNotAllowed = fmt.Errorf("tags are not allowed with this operation (must use exact names)")
|
||||
ErrNamesNotAllowed = fmt.Errorf("names are not allowed with this operation (must use tags)")
|
||||
ErrAllNotAllowed = fmt.Errorf("cannot use the 'all' operand in this context")
|
||||
)
|
||||
|
||||
// ErrInvalidExpression represents an expression that cannot be parsed or can be parsed but is logically invalid.
|
||||
type ErrInvalidExpression struct {
|
||||
Expression string
|
||||
Operation Operation
|
||||
Err error
|
||||
}
|
||||
|
||||
func (e ErrInvalidExpression) Error() string {
|
||||
return fmt.Sprintf("invalid expression: %q: %s", e.Expression, e.Err.Error())
|
||||
}
|
||||
|
||||
func newErrInvalidExpression(exp string, op Operation, err error) ErrInvalidExpression {
|
||||
return ErrInvalidExpression{
|
||||
Expression: exp,
|
||||
Operation: op,
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
|
||||
// Expression represents a single operation-operand pair with (all validation errors).
|
||||
// E.g. "+foo", "-bar", or "something" are all expressions. Some validations are relevant to not only the
|
||||
// syntax (operation and operator) but other are sensitive to the context of the operand (e.g. if a given operand
|
||||
// is a tag or a name, validated against the operation).
|
||||
type Expression struct {
|
||||
Operation Operation
|
||||
Operand string
|
||||
Errors []error
|
||||
}
|
||||
|
||||
// Operation represents the type of operation to perform on the operand (set, add, remove, sub-select).
|
||||
type Operation string
|
||||
|
||||
// Expressions represents a list of expressions.
|
||||
type Expressions []Expression
|
||||
|
||||
// expressionContext represents all information needed to validate an expression (e.g. the set of all tasks and their tags).
|
||||
type expressionContext struct {
|
||||
Names *strset.Set
|
||||
Tags *strset.Set
|
||||
}
|
||||
|
||||
func newExpressionContext(ts []Task) *expressionContext {
|
||||
ec := &expressionContext{
|
||||
Names: strset.New(tasks(ts).Names()...),
|
||||
Tags: strset.New(tasks(ts).Tags()...),
|
||||
}
|
||||
|
||||
ec.Tags.Add("all")
|
||||
|
||||
return ec
|
||||
}
|
||||
|
||||
// newExpression creates a new validated Expression object relative to the task names and tags.
|
||||
func (ec expressionContext) newExpression(exp string, operation Operation, token string) Expression {
|
||||
if token == "" {
|
||||
return Expression{
|
||||
Operation: operation,
|
||||
Operand: token,
|
||||
Errors: []error{newErrInvalidExpression(exp, operation, ErrEmptyToken)},
|
||||
}
|
||||
}
|
||||
|
||||
if !isValidNode(token) {
|
||||
return Expression{
|
||||
Operation: operation,
|
||||
Operand: token,
|
||||
Errors: []error{newErrInvalidExpression(exp, operation, ErrInvalidToken)},
|
||||
}
|
||||
}
|
||||
|
||||
var err error
|
||||
switch operation {
|
||||
case SetOperation, RemoveOperation:
|
||||
// names and tags allowed
|
||||
if !ec.Tags.Has(token) && !ec.Names.Has(token) {
|
||||
err = newErrInvalidExpression(exp, operation, ErrUnknownNameOrTag)
|
||||
}
|
||||
case AddOperation:
|
||||
// only names are allowed
|
||||
if !ec.Names.Has(token) {
|
||||
if ec.Tags.Has(token) {
|
||||
err = newErrInvalidExpression(exp, operation, ErrTagsNotAllowed)
|
||||
} else {
|
||||
err = newErrInvalidExpression(exp, operation, ErrUnknownNameOrTag)
|
||||
}
|
||||
}
|
||||
case SubSelectOperation:
|
||||
if token == "all" {
|
||||
// special case: we cannot sub-select all (this is most likely a misconfiguration and the user intended to use the set operation)
|
||||
err = newErrInvalidExpression(exp, operation, ErrAllNotAllowed)
|
||||
} else if !ec.Tags.Has(token) {
|
||||
// only tags are allowed...
|
||||
if ec.Names.Has(token) {
|
||||
err = newErrInvalidExpression(exp, operation, ErrNamesNotAllowed)
|
||||
} else {
|
||||
err = newErrInvalidExpression(exp, operation, ErrUnknownNameOrTag)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var errs []error
|
||||
if err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
|
||||
return Expression{
|
||||
Operation: operation,
|
||||
Operand: token,
|
||||
Errors: errs,
|
||||
}
|
||||
}
|
||||
|
||||
func newExpressionsFromSelectionRequest(nc *expressionContext, selectionRequest pkgcataloging.SelectionRequest) Expressions {
|
||||
var all Expressions
|
||||
|
||||
for _, exp := range selectionRequest.DefaultNamesOrTags {
|
||||
all = append(all, nc.newExpression(exp, SetOperation, exp))
|
||||
}
|
||||
|
||||
for _, exp := range selectionRequest.SubSelectTags {
|
||||
all = append(all, nc.newExpression(exp, SubSelectOperation, exp))
|
||||
}
|
||||
|
||||
for _, exp := range selectionRequest.AddNames {
|
||||
all = append(all, nc.newExpression(exp, AddOperation, exp))
|
||||
}
|
||||
|
||||
for _, exp := range selectionRequest.RemoveNamesOrTags {
|
||||
all = append(all, nc.newExpression(exp, RemoveOperation, exp))
|
||||
}
|
||||
|
||||
sort.Sort(all)
|
||||
return all
|
||||
}
|
||||
|
||||
func isValidNode(s string) bool {
|
||||
return expressionNodePattern.Match([]byte(s))
|
||||
}
|
||||
|
||||
func (e Expressions) Clone() Expressions {
|
||||
clone := make(Expressions, len(e))
|
||||
copy(clone, e)
|
||||
return clone
|
||||
}
|
||||
|
||||
func (e Expression) String() string {
|
||||
var op string
|
||||
switch e.Operation {
|
||||
case AddOperation:
|
||||
op = "+"
|
||||
case RemoveOperation:
|
||||
op = "-"
|
||||
case SubSelectOperation:
|
||||
op = ""
|
||||
case SetOperation:
|
||||
op = ""
|
||||
default:
|
||||
op = "?"
|
||||
}
|
||||
return op + e.Operand
|
||||
}
|
||||
|
||||
func (e Expressions) Len() int {
|
||||
return len(e)
|
||||
}
|
||||
|
||||
func (e Expressions) Swap(i, j int) {
|
||||
e[i], e[j] = e[j], e[i]
|
||||
}
|
||||
|
||||
// order of operations
|
||||
var orderOfOps = map[Operation]int{
|
||||
SetOperation: 1,
|
||||
SubSelectOperation: 2,
|
||||
RemoveOperation: 3,
|
||||
AddOperation: 4,
|
||||
}
|
||||
|
||||
func (e Expressions) Less(i, j int) bool {
|
||||
ooi := orderOfOps[e[i].Operation]
|
||||
ooj := orderOfOps[e[j].Operation]
|
||||
|
||||
if ooi != ooj {
|
||||
return ooi < ooj
|
||||
}
|
||||
|
||||
return i < j
|
||||
}
|
||||
|
||||
func (e Expressions) Errors() (errs []error) {
|
||||
for _, n := range e {
|
||||
if len(n.Errors) > 0 {
|
||||
errs = append(errs, n.Errors...)
|
||||
}
|
||||
}
|
||||
return errs
|
||||
}
|
||||
|
||||
func (e Expressions) Validate() error {
|
||||
errs := e.Errors()
|
||||
if len(errs) == 0 {
|
||||
return nil
|
||||
}
|
||||
var err error
|
||||
return multierror.Append(err, e.Errors()...)
|
||||
}
|
||||
Reference in New Issue
Block a user