1
0
Fork 0
mirror of https://github.com/gocsaf/csaf.git synced 2025-12-22 11:55:40 +01:00

Factor out summary extraction from advisories.

This commit is contained in:
Sascha L. Teichmann 2022-02-24 12:22:10 +01:00
parent d4270e2d39
commit b12ad718c5
4 changed files with 84 additions and 73 deletions

View file

@ -11,7 +11,6 @@ package main
import ( import (
"bufio" "bufio"
"bytes" "bytes"
"context"
"crypto/sha256" "crypto/sha256"
"crypto/sha512" "crypto/sha512"
"crypto/tls" "crypto/tls"
@ -29,8 +28,6 @@ import (
"strings" "strings"
"time" "time"
"github.com/PaesslerAG/gval"
"github.com/PaesslerAG/jsonpath"
"github.com/ProtonMail/gopenpgp/v2/crypto" "github.com/ProtonMail/gopenpgp/v2/crypto"
"github.com/csaf-poc/csaf_distribution/csaf" "github.com/csaf-poc/csaf_distribution/csaf"
@ -58,8 +55,7 @@ type processor struct {
badChanges []string badChanges []string
badFolders []string badFolders []string
builder gval.Language expr *util.PathEval
exprs map[string]gval.Evaluable
} }
type reporter interface { type reporter interface {
@ -110,8 +106,7 @@ func newProcessor(opts *options) *processor {
return &processor{ return &processor{
opts: opts, opts: opts,
alreadyChecked: map[string]whereType{}, alreadyChecked: map[string]whereType{},
builder: gval.Full(jsonpath.Language()), expr: util.NewPathEval(),
exprs: map[string]gval.Evaluable{},
} }
} }
@ -178,21 +173,6 @@ func (p *processor) checkDomain(domain string) error {
return nil return nil
} }
func (p *processor) jsonPath(expr string, doc interface{}) (interface{}, error) {
if doc == nil {
return nil, errors.New("no document to extract data from")
}
eval := p.exprs[expr]
if eval == nil {
var err error
if eval, err = p.builder.NewEvaluable(expr); err != nil {
return nil, err
}
p.exprs[expr] = eval
}
return eval(context.Background(), doc)
}
func (p *processor) checkTLS(u string) { func (p *processor) checkTLS(u string) {
if p.noneTLS == nil { if p.noneTLS == nil {
p.noneTLS = map[string]struct{}{} p.noneTLS = map[string]struct{}{}
@ -359,7 +339,7 @@ func (p *processor) integrity(
// Check if file is in the right folder. // Check if file is in the right folder.
use(&p.badFolders) use(&p.badFolders)
if date, err := p.jsonPath( if date, err := p.expr.Eval(
`$.document.tracking.initial_release_date`, doc); err != nil { `$.document.tracking.initial_release_date`, doc); err != nil {
p.badFolder( p.badFolder(
"Extracting 'initial_release_date' from %s failed: %v", u, err) "Extracting 'initial_release_date' from %s failed: %v", u, err)
@ -638,7 +618,7 @@ func (p *processor) processROLIEFeeds(domain string, feeds [][]csaf.Feed) error
func (p *processor) checkCSAFs(domain string) error { func (p *processor) checkCSAFs(domain string) error {
// Check for ROLIE // Check for ROLIE
rolie, err := p.jsonPath("$.distributions[*].rolie.feeds", p.pmd) rolie, err := p.expr.Eval("$.distributions[*].rolie.feeds", p.pmd)
if err != nil { if err != nil {
return err return err
} }
@ -931,7 +911,7 @@ func (p *processor) checkPGPKeys(domain string) error {
use(&p.badPGPs) use(&p.badPGPs)
src, err := p.jsonPath("$.pgp_keys", p.pmd) src, err := p.expr.Eval("$.pgp_keys", p.pmd)
if err != nil { if err != nil {
p.badPGP("No PGP keys found: %v.", err) p.badPGP("No PGP keys found: %v.", err)
return errContinue return errContinue

View file

@ -167,7 +167,7 @@ func (c *controller) upload(r *http.Request) (interface{}, error) {
} }
} }
ex, err := newExtraction(content) ex, err := csaf.NewAdvisorySummary(util.NewPathEval(), content)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -179,7 +179,7 @@ func (c *controller) upload(r *http.Request) (interface{}, error) {
// Extract real TLP from document. // Extract real TLP from document.
if t == tlpCSAF { if t == tlpCSAF {
if t = tlp(strings.ToLower(ex.tlpLabel)); !t.valid() || t == tlpCSAF { if t = tlp(strings.ToLower(ex.TLPLabel)); !t.valid() || t == tlpCSAF {
return nil, fmt.Errorf( return nil, fmt.Errorf(
"valid TLP label missing in document (found '%s')", t) "valid TLP label missing in document (found '%s')", t)
} }
@ -239,20 +239,20 @@ func (c *controller) upload(r *http.Request) (interface{}, error) {
rolie.Feed.Updated = csaf.TimeStamp(time.Now()) rolie.Feed.Updated = csaf.TimeStamp(time.Now())
year := strconv.Itoa(ex.initialReleaseDate.Year()) year := strconv.Itoa(ex.InitialReleaseDate.Year())
csafURL := c.cfg.Domain + csafURL := c.cfg.Domain +
"/.well-known/csaf/" + ts + "/" + year + "/" + newCSAF "/.well-known/csaf/" + ts + "/" + year + "/" + newCSAF
e := rolie.EntryByID(ex.id) e := rolie.EntryByID(ex.ID)
if e == nil { if e == nil {
e = &csaf.Entry{ID: ex.id} e = &csaf.Entry{ID: ex.ID}
rolie.Feed.Entry = append(rolie.Feed.Entry, e) rolie.Feed.Entry = append(rolie.Feed.Entry, e)
} }
e.Titel = ex.title e.Titel = ex.Title
e.Published = csaf.TimeStamp(ex.initialReleaseDate) e.Published = csaf.TimeStamp(ex.InitialReleaseDate)
e.Updated = csaf.TimeStamp(ex.currentReleaseDate) e.Updated = csaf.TimeStamp(ex.CurrentReleaseDate)
e.Link = []csaf.Link{{ e.Link = []csaf.Link{{
Rel: "self", Rel: "self",
HRef: csafURL, HRef: csafURL,
@ -265,8 +265,8 @@ func (c *controller) upload(r *http.Request) (interface{}, error) {
Type: "application/json", Type: "application/json",
Src: csafURL, Src: csafURL,
} }
if ex.summary != "" { if ex.Summary != "" {
e.Summary = &csaf.Summary{Content: ex.summary} e.Summary = &csaf.Summary{Content: ex.Summary}
} else { } else {
e.Summary = nil e.Summary = nil
} }
@ -302,7 +302,7 @@ func (c *controller) upload(r *http.Request) (interface{}, error) {
if err := updateIndices( if err := updateIndices(
folder, filepath.Join(year, newCSAF), folder, filepath.Join(year, newCSAF),
ex.currentReleaseDate, ex.CurrentReleaseDate,
); err != nil { ); err != nil {
return err return err
} }
@ -313,9 +313,9 @@ func (c *controller) upload(r *http.Request) (interface{}, error) {
warn("Publisher in provider metadata is not initialized. Forgot to configure?") warn("Publisher in provider metadata is not initialized. Forgot to configure?")
if c.cfg.DynamicProviderMetaData { if c.cfg.DynamicProviderMetaData {
warn("Taking publisher from CSAF") warn("Taking publisher from CSAF")
pmd.Publisher = ex.publisher pmd.Publisher = ex.Publisher
} }
case !pmd.Publisher.Equals(ex.publisher): case !pmd.Publisher.Equals(ex.Publisher):
warn("Publishers in provider metadata and CSAF do not match.") warn("Publishers in provider metadata and CSAF do not match.")
} }
@ -334,7 +334,7 @@ func (c *controller) upload(r *http.Request) (interface{}, error) {
Error error `json:"-"` Error error `json:"-"`
}{ }{
Name: newCSAF, Name: newCSAF,
ReleaseDate: ex.currentReleaseDate.Format(dateFormat), ReleaseDate: ex.CurrentReleaseDate.Format(dateFormat),
Warnings: warnings, Warnings: warnings,
} }

View file

@ -6,17 +6,12 @@
// SPDX-FileCopyrightText: 2021 German Federal Office for Information Security (BSI) <https://www.bsi.bund.de> // SPDX-FileCopyrightText: 2021 German Federal Office for Information Security (BSI) <https://www.bsi.bund.de>
// Software-Engineering: 2021 Intevation GmbH <https://intevation.de> // Software-Engineering: 2021 Intevation GmbH <https://intevation.de>
package main package csaf
import ( import (
"context"
"errors" "errors"
"time" "time"
"github.com/PaesslerAG/gval"
"github.com/PaesslerAG/jsonpath"
"github.com/csaf-poc/csaf_distribution/csaf"
"github.com/csaf-poc/csaf_distribution/util" "github.com/csaf-poc/csaf_distribution/util"
) )
@ -30,39 +25,39 @@ const (
summaryExpr = `$.document.notes[? @.category=="summary" || @.type=="summary"].text` summaryExpr = `$.document.notes[? @.category=="summary" || @.type=="summary"].text`
) )
type extraction struct { // AdvisorySummary is a summary of some essentials of an CSAF advisory.
id string type AdvisorySummary struct {
title string ID string
publisher *csaf.Publisher Title string
initialReleaseDate time.Time Publisher *Publisher
currentReleaseDate time.Time InitialReleaseDate time.Time
summary string CurrentReleaseDate time.Time
tlpLabel string Summary string
TLPLabel string
} }
type extractFunc func(string) (interface{}, error) type extractFunc func(string) (interface{}, error)
func newExtraction(content interface{}) (*extraction, error) { // NewAdvisorySummary creates a summary from an advisory doc
// with the help of an expression evaluator expr.
func NewAdvisorySummary(
expr *util.PathEval,
doc interface{},
) (*AdvisorySummary, error) {
builder := gval.Full(jsonpath.Language()) e := new(AdvisorySummary)
path := func(expr string) (interface{}, error) { path := func(s string) (interface{}, error) {
eval, err := builder.NewEvaluable(expr) return expr.Eval(s, doc)
if err != nil {
return nil, err
}
return eval(context.Background(), content)
} }
e := new(extraction)
for _, fn := range []func(extractFunc) error{ for _, fn := range []func(extractFunc) error{
extractText(idExpr, &e.id), extractText(idExpr, &e.ID),
extractText(titleExpr, &e.title), extractText(titleExpr, &e.Title),
extractTime(currentReleaseDateExpr, &e.currentReleaseDate), extractTime(currentReleaseDateExpr, &e.CurrentReleaseDate),
extractTime(initialReleaseDateExpr, &e.initialReleaseDate), extractTime(initialReleaseDateExpr, &e.InitialReleaseDate),
extractText(summaryExpr, &e.summary), extractText(summaryExpr, &e.Summary),
extractText(tlpLabelExpr, &e.tlpLabel), extractText(tlpLabelExpr, &e.TLPLabel),
e.extractPublisher, e.extractPublisher,
} { } {
if err := fn(path); err != nil { if err := fn(path); err != nil {
@ -95,7 +90,7 @@ func extractTime(expr string, store *time.Time) func(extractFunc) error {
if !ok { if !ok {
return errors.New("not a string") return errors.New("not a string")
} }
date, err := time.Parse(dateFormat, text) date, err := time.Parse(time.RFC3339, text)
if err == nil { if err == nil {
*store = date.UTC() *store = date.UTC()
} }
@ -103,7 +98,7 @@ func extractTime(expr string, store *time.Time) func(extractFunc) error {
} }
} }
func (e *extraction) extractPublisher(path extractFunc) error { func (e *AdvisorySummary) extractPublisher(path extractFunc) error {
p, err := path(publisherExpr) p, err := path(publisherExpr)
if err != nil { if err != nil {
return err return err
@ -111,13 +106,13 @@ func (e *extraction) extractPublisher(path extractFunc) error {
// XXX: It's a bit cumbersome to serialize and deserialize // XXX: It's a bit cumbersome to serialize and deserialize
// it into our own structure. // it into our own structure.
publisher := new(csaf.Publisher) publisher := new(Publisher)
if err := util.ReMarshalJSON(publisher, p); err != nil { if err := util.ReMarshalJSON(publisher, p); err != nil {
return err return err
} }
if err := publisher.Validate(); err != nil { if err := publisher.Validate(); err != nil {
return err return err
} }
e.publisher = publisher e.Publisher = publisher
return nil return nil
} }

View file

@ -9,7 +9,12 @@
package util package util
import ( import (
"context"
"encoding/json" "encoding/json"
"errors"
"github.com/PaesslerAG/gval"
"github.com/PaesslerAG/jsonpath"
) )
// ReMarshalJSON transforms data from src to dst via JSON marshalling. // ReMarshalJSON transforms data from src to dst via JSON marshalling.
@ -20,3 +25,34 @@ func ReMarshalJSON(dst, src interface{}) error {
} }
return json.Unmarshal(intermediate, dst) return json.Unmarshal(intermediate, dst)
} }
// PathEval is a helper to evaluate JSON paths on documents.
type PathEval struct {
builder gval.Language
exprs map[string]gval.Evaluable
}
// NewPathEval creates a new PathEval.
func NewPathEval() *PathEval {
return &PathEval{
builder: gval.Full(jsonpath.Language()),
exprs: map[string]gval.Evaluable{},
}
}
// Eval evalutes expression expr on document doc.
// Returns the result of the expression.
func (pe *PathEval) Eval(expr string, doc interface{}) (interface{}, error) {
if doc == nil {
return nil, errors.New("no document to extract data from")
}
eval := pe.exprs[expr]
if eval == nil {
var err error
if eval, err = pe.builder.NewEvaluable(expr); err != nil {
return nil, err
}
pe.exprs[expr] = eval
}
return eval(context.Background(), doc)
}