mirror of
https://github.com/gocsaf/csaf.git
synced 2025-12-22 18:15:42 +01:00
Merge pull request #64 from csaf-poc/factor-out-summary-extraction
Factor out summary extraction from advisories.
This commit is contained in:
commit
b4341cf8ec
4 changed files with 84 additions and 73 deletions
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
36
util/json.go
36
util/json.go
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue