diff --git a/cmd/csaf_aggregator/mirror.go b/cmd/csaf_aggregator/mirror.go index 7fe71d7..fd37b3e 100644 --- a/cmd/csaf_aggregator/mirror.go +++ b/cmd/csaf_aggregator/mirror.go @@ -451,23 +451,32 @@ func (w *worker) extractCategories(label string, advisory any) error { w.categories[label] = cats } - var result string - matcher := util.StringMatcher(&result) - const exprPrefix = "expr:" + var dynamic []string + matcher := util.StringTreeMatcher(&dynamic) + for _, cat := range categories { if strings.HasPrefix(cat, exprPrefix) { expr := cat[len(exprPrefix):] - if err := w.expr.Extract(expr, matcher, true, advisory); err != nil { - return err + // Compile first to check that the expression is okay. + if _, err := w.expr.Compile(expr); err != nil { + fmt.Printf("Compiling category expression %q failed: %v\n", + expr, err) + continue } - cats[result] = true + // Ignore errors here as they result from not matching. + w.expr.Extract(expr, matcher, true, advisory) } else { // Normal cats[cat] = true } } + // Add dynamic categories. + for _, cat := range dynamic { + cats[cat] = true + } + return nil } diff --git a/cmd/csaf_provider/actions.go b/cmd/csaf_provider/actions.go index ea099e3..0fa4e5b 100644 --- a/cmd/csaf_provider/actions.go +++ b/cmd/csaf_provider/actions.go @@ -194,10 +194,17 @@ func (c *controller) upload(r *http.Request) (any, error) { // Check if we have to search for dynamic categories. var dynamicCategories []string if catExprs := c.cfg.DynamicCategories(); len(catExprs) > 0 { - var err error - if dynamicCategories, err = pe.Strings(catExprs, true, content); err != nil { - // XXX: Should we die here? - log.Printf("eval of dynamic catecory expressions failed: %v\n", err) + matcher := util.StringTreeMatcher(&dynamicCategories) + + for _, expr := range catExprs { + // Compile first to check that the expression is okay. + if _, err := pe.Compile(expr); err != nil { + log.Printf("Compiling category expression %q failed: %v\n", + expr, err) + continue + } + // Ignore errors here as they result from not matching. + pe.Extract(expr, matcher, true, content) } } diff --git a/docs/csaf_provider.md b/docs/csaf_provider.md index 81a45fa..d1e6b22 100644 --- a/docs/csaf_provider.md +++ b/docs/csaf_provider.md @@ -106,6 +106,18 @@ The following example file documents all available configuration options: # If a list item starts with `expr:` # the rest of the string is used as a JsonPath expression # to extract a string from the incoming advisories. +# If the result of the expression is a string this string +# is used. If the result is an array each element of +# this array is tested if it is a string or an array. +# If this test fails the expression fails. If the +# test succeeds the rules are applied recursively to +# collect all strings in the result. +# Suggested expressions are: +# - vendor, product family and product names: "expr:$.product_tree..branches[?(@.category==\"vendor\" || @.category==\"product_family\" || @.category==\"product_name\")].name" +# - CVEs: "expr:$.vulnerabilities[*].cve" +# - CWEs: "expr:$.vulnerabilities[*].cwe.id" +# The used implementation to evaluate JSONPath expressions does +# not support the use of single-quotes. Double quotes have to be quoted. # Strings not starting with `expr:` are taken verbatim. # By default no category documents are created. # This example provides an overview over the syntax, diff --git a/util/json.go b/util/json.go index ed0fd70..be9f330 100644 --- a/util/json.go +++ b/util/json.go @@ -42,6 +42,20 @@ func NewPathEval() *PathEval { } } +// Compile compiles an expression and stores it in the +// internal cache on success. +func (pe *PathEval) Compile(expr string) (gval.Evaluable, error) { + if eval := pe.exprs[expr]; eval != nil { + return eval, nil + } + eval, err := pe.builder.NewEvaluable(expr) + if err != nil { + return nil, err + } + pe.exprs[expr] = eval + return eval, nil +} + // Eval evalutes expression expr on document doc. // Returns the result of the expression. func (pe *PathEval) Eval(expr string, doc any) (any, error) { @@ -101,6 +115,37 @@ func StringMatcher(dst *string) func(any) error { } } +// StringTreeMatcher returns a matcher which adds strings +// to a slice and recursively strings from arrays of strings. +func StringTreeMatcher(strings *[]string) func(any) error { + // Only add unique strings. + unique := func(s string) { + for _, t := range *strings { + if s == t { + return + } + } + *strings = append(*strings, s) + } + var recurse func(any) error + recurse = func(x any) error { + switch y := x.(type) { + case string: + unique(y) + case []any: + for _, z := range y { + if err := recurse(z); err != nil { + return err + } + } + default: + return fmt.Errorf("unsupported type: %T", x) + } + return nil + } + return recurse +} + // TimeMatcher stores a time with a given format. func TimeMatcher(dst *time.Time, format string) func(any) error { return func(x any) error {