mirror of
https://github.com/gocsaf/csaf.git
synced 2025-12-22 11:55:40 +01:00
Add aggregator; improve itest workflow
* Factor JSON evaluation and construction base URLs out of of checker. * Move json path matching to util. * Add csaf_aggregator (as additional command) * Improve itest workflow to checkout the branch where it is running on. resolve #105 resolve #72 Co-authored-by: tschmidtb51 <65305130+tschmidtb51@users.noreply.github.com> Co-authored-by: Bernhard Reiter <bernhard@intevation.de> Co-authored-by: Fadi Abbud <fadi.abbud@intevation.de>
This commit is contained in:
parent
9da0589236
commit
8a1ebe0b7a
30 changed files with 2789 additions and 88 deletions
202
csaf/models.go
202
csaf/models.go
|
|
@ -37,11 +37,12 @@ const (
|
|||
)
|
||||
|
||||
var tlpLabelPattern = alternativesUnmarshal(
|
||||
string("UNLABELED"),
|
||||
string("WHITE"),
|
||||
string("GREEN"),
|
||||
string("AMBER"),
|
||||
string("RED"))
|
||||
string(TLPLabelUnlabeled),
|
||||
string(TLPLabelWhite),
|
||||
string(TLPLabelGreen),
|
||||
string(TLPLabelAmber),
|
||||
string(TLPLabelRed),
|
||||
)
|
||||
|
||||
// JSONURL is an URL to JSON document.
|
||||
type JSONURL string
|
||||
|
|
@ -162,6 +163,188 @@ type ProviderMetadata struct {
|
|||
Role *MetadataRole `json:"role"` // required
|
||||
}
|
||||
|
||||
// AggregatorCategory is the category of the aggregator.
|
||||
type AggregatorCategory string
|
||||
|
||||
const (
|
||||
// AggregatorAggregator represents the "aggregator" type of aggregators.
|
||||
AggregatorAggregator AggregatorCategory = "aggregator"
|
||||
// AggregatorLister represents the "listers" type of aggregators.
|
||||
AggregatorLister AggregatorCategory = "lister"
|
||||
)
|
||||
|
||||
var aggregatorCategoryPattern = alternativesUnmarshal(
|
||||
string(AggregatorAggregator),
|
||||
string(AggregatorLister),
|
||||
)
|
||||
|
||||
// AggregatorVersion is the version of the aggregator.
|
||||
type AggregatorVersion string
|
||||
|
||||
const (
|
||||
// AggregatorVersion20 is version 2.0 of the aggregator.
|
||||
AggregatorVersion20 AggregatorVersion = "2.0"
|
||||
)
|
||||
|
||||
var aggregatorVersionPattern = alternativesUnmarshal(
|
||||
string(AggregatorVersion20),
|
||||
)
|
||||
|
||||
// AggregatorInfo reflects the 'aggregator' object in the aggregator.
|
||||
type AggregatorInfo struct {
|
||||
Category *AggregatorCategory `json:"category,omitempty" toml:"category"` // required
|
||||
Name string `json:"name" toml:"name"` // required
|
||||
ContactDetails string `json:"contact_details,omitempty" toml:"contact_details"`
|
||||
IssuingAuthority string `json:"issuing_authority,omitempty" toml:"issuing_authority"`
|
||||
Namespace string `json:"namespace" toml:"namespace"` // required
|
||||
}
|
||||
|
||||
// AggregatorURL is the URL of the aggregator document.
|
||||
type AggregatorURL string
|
||||
|
||||
var aggregatorURLPattern = patternUnmarshal(`/aggregator\.json$`)
|
||||
|
||||
// AggregatorCSAFProviderMetadata reflects 'csaf_providers.metadata' in an aggregator.
|
||||
type AggregatorCSAFProviderMetadata struct {
|
||||
LastUpdated *TimeStamp `json:"last_updated,omitempty"` // required
|
||||
Publisher *Publisher `json:"publisher,omitempty"` // required
|
||||
Role *MetadataRole `json:"role,omitempty"`
|
||||
URL *ProviderURL `json:"url,omitempty"` // required
|
||||
}
|
||||
|
||||
// AggregatorCSAFProvider reflects one 'csaf_trusted_provider' in an aggregator.
|
||||
type AggregatorCSAFProvider struct {
|
||||
Metadata *AggregatorCSAFProviderMetadata `json:"metadata,omitempty"` // required
|
||||
Mirrors []ProviderURL `json:"mirrors,omitempty"` // required
|
||||
}
|
||||
|
||||
// Aggregator is the CSAF Aggregator.
|
||||
type Aggregator struct {
|
||||
Aggregator *AggregatorInfo `json:"aggregator,omitempty"` // required
|
||||
Version *AggregatorVersion `json:"aggregator_version,omitempty"` // required
|
||||
CanonicalURL *AggregatorURL `json:"canonical_url,omitempty"` // required
|
||||
CSAFProviders []*AggregatorCSAFProvider `json:"csaf_providers,omitempty"` // required
|
||||
LastUpdated *TimeStamp `json:"last_updated,omitempty"` // required
|
||||
}
|
||||
|
||||
// Validate validates the current state of the AggregatorCategory.
|
||||
func (ac *AggregatorCategory) Validate() error {
|
||||
if ac == nil {
|
||||
return errors.New("aggregator.aggregator.category is mandatory")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Validate validates the current state of the AggregatorVersion.
|
||||
func (av *AggregatorVersion) Validate() error {
|
||||
if av == nil {
|
||||
return errors.New("aggregator.aggregator_version is mandatory")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Validate validates the current state of the AggregatorURL.
|
||||
func (au *AggregatorURL) Validate() error {
|
||||
if au == nil {
|
||||
return errors.New("aggregator.aggregator_url is mandatory")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Validate validates the current state of the AggregatorInfo.
|
||||
func (ai *AggregatorInfo) Validate() error {
|
||||
if err := ai.Category.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
if ai.Name == "" {
|
||||
return errors.New("aggregator.aggregator.name is mandatory")
|
||||
}
|
||||
if ai.Namespace == "" {
|
||||
return errors.New("aggregator.aggregator.namespace is mandatory")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Validate validates the current state of the AggregatorCSAFProviderMetadata.
|
||||
func (acpm *AggregatorCSAFProviderMetadata) Validate() error {
|
||||
if acpm == nil {
|
||||
return errors.New("aggregator.csaf_providers[].metadata is mandatory")
|
||||
}
|
||||
if acpm.LastUpdated == nil {
|
||||
return errors.New("aggregator.csaf_providers[].metadata.last_updated is mandatory")
|
||||
}
|
||||
if acpm.Publisher == nil {
|
||||
return errors.New("aggregator.csaf_providers[].metadata.publisher is mandatory")
|
||||
}
|
||||
if err := acpm.Publisher.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
if acpm.URL == nil {
|
||||
return errors.New("aggregator.csaf_providers[].metadata.url is mandatory")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Validate validates the current state of the AggregatorCSAFProvider.
|
||||
func (acp *AggregatorCSAFProvider) Validate() error {
|
||||
if acp == nil {
|
||||
return errors.New("aggregator.csaf_providers[] not allowed to be nil")
|
||||
}
|
||||
if err := acp.Metadata.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Validate validates the current state of the Aggregator.
|
||||
func (a *Aggregator) Validate() error {
|
||||
if err := a.Aggregator.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := a.Version.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := a.CanonicalURL.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, provider := range a.CSAFProviders {
|
||||
if err := provider.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if a.LastUpdated == nil {
|
||||
return errors.New("Aggregator.LastUpdate == nil")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// UnmarshalText implements the encoding.TextUnmarshaller interface.
|
||||
func (ac *AggregatorCategory) UnmarshalText(data []byte) error {
|
||||
s, err := aggregatorCategoryPattern(data)
|
||||
if err == nil {
|
||||
*ac = AggregatorCategory(s)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// UnmarshalText implements the encoding.TextUnmarshaller interface.
|
||||
func (av *AggregatorVersion) UnmarshalText(data []byte) error {
|
||||
s, err := aggregatorVersionPattern(data)
|
||||
if err == nil {
|
||||
*av = AggregatorVersion(s)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// UnmarshalText implements the encoding.TextUnmarshaller interface.
|
||||
func (au *AggregatorURL) UnmarshalText(data []byte) error {
|
||||
s, err := aggregatorURLPattern(data)
|
||||
if err == nil {
|
||||
*au = AggregatorURL(s)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func patternUnmarshal(pattern string) func([]byte) (string, error) {
|
||||
r := regexp.MustCompile(pattern)
|
||||
return func(data []byte) (string, error) {
|
||||
|
|
@ -485,3 +668,12 @@ func LoadProviderMetadata(r io.Reader) (*ProviderMetadata, error) {
|
|||
|
||||
return &pmd, nil
|
||||
}
|
||||
|
||||
// WriteTo saves an AggregatorURL to a writer.
|
||||
func (a *Aggregator) WriteTo(w io.Writer) (int64, error) {
|
||||
nw := util.NWriter{Writer: w, N: 0}
|
||||
enc := json.NewEncoder(&nw)
|
||||
enc.SetIndent("", " ")
|
||||
err := enc.Encode(a)
|
||||
return nw.N, err
|
||||
}
|
||||
|
|
|
|||
215
csaf/schema/aggregator_json_schema.json
Normal file
215
csaf/schema/aggregator_json_schema.json
Normal file
|
|
@ -0,0 +1,215 @@
|
|||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://docs.oasis-open.org/csaf/csaf/v2.0/aggregator_json_schema.json",
|
||||
"title": "CSAF aggregator",
|
||||
"description": "Representation of information where to find CSAF providers as a JSON document.",
|
||||
"type": "object",
|
||||
"$defs": {
|
||||
"aggregator_url_t": {
|
||||
"title": "Aggregator URL type",
|
||||
"description": "Contains a URL.",
|
||||
"type": "string",
|
||||
"format": "uri",
|
||||
"pattern": "/aggregator\\.json$"
|
||||
},
|
||||
"metadata_t": {
|
||||
"title": "CSAF issuing party metadata.",
|
||||
"description": "Contains the metadata of a single CSAF issuing party.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"last_updated",
|
||||
"publisher",
|
||||
"url"
|
||||
],
|
||||
"properties": {
|
||||
"last_updated": {
|
||||
"title": "Last updated",
|
||||
"description": "Holds the date and time when this entry was last updated.",
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"publisher": {
|
||||
"title": "Publisher",
|
||||
"description": "Provides information about the issuing party for this entry.",
|
||||
"$ref": "https://docs.oasis-open.org/csaf/csaf/v2.0/provider_json_schema.json#/properties/publisher"
|
||||
},
|
||||
"role": {
|
||||
"title": "Role of the issuing party",
|
||||
"description": "Contains the role of the issuing party according to section 7 in the CSAF standard.",
|
||||
"$ref": "https://docs.oasis-open.org/csaf/csaf/v2.0/provider_json_schema.json#/properties/role"
|
||||
},
|
||||
"url": {
|
||||
"title": "URL of the metadata",
|
||||
"description": "Contains the URL of the provider-metadata.json for that entry.",
|
||||
"$ref": "https://docs.oasis-open.org/csaf/csaf/v2.0/provider_json_schema.json#/properties/canonical_url"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mirrors_t": {
|
||||
"title": "List of mirrors",
|
||||
"description": "Contains a list of URLs or mirrors for this issuing party.",
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"uniqueItems": true,
|
||||
"items": {
|
||||
"title": "Mirror",
|
||||
"description": "Contains the base URL of the mirror for this issuing party.",
|
||||
"$ref": "https://docs.oasis-open.org/csaf/csaf/v2.0/provider_json_schema.json#/$defs/provider_url_t"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"aggregator",
|
||||
"aggregator_version",
|
||||
"canonical_url",
|
||||
"csaf_providers",
|
||||
"last_updated"
|
||||
],
|
||||
"properties": {
|
||||
"aggregator": {
|
||||
"title": "Aggregator",
|
||||
"description": "Provides information about the aggregator.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"category",
|
||||
"name",
|
||||
"namespace"
|
||||
],
|
||||
"properties": {
|
||||
"category": {
|
||||
"title": "Category of aggregator",
|
||||
"description": "Provides information about the category of aggregator.",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"aggregator",
|
||||
"lister"
|
||||
]
|
||||
},
|
||||
"contact_details": {
|
||||
"title": "Contact details",
|
||||
"description": "Information on how to contact the aggregator, possibly including details such as web sites, email addresses, phone numbers, and postal mail addresses.",
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"examples": [
|
||||
"Aggregator can be reached at contact_us@aggregator.example.com, or via our website at https://www.example.com/security/csaf/aggregator/contact."
|
||||
]
|
||||
},
|
||||
"issuing_authority": {
|
||||
"title": "Issuing authority",
|
||||
"description": "Provides information about the authority of the aggregator to release the list, in particular, the party's constituency and responsibilities or other obligations.",
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"name": {
|
||||
"title": "Name of aggregator",
|
||||
"description": "Contains the name of the aggregator.",
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"examples": [
|
||||
"BSI",
|
||||
"CISA",
|
||||
"CSAF TC"
|
||||
]
|
||||
},
|
||||
"namespace": {
|
||||
"title": "Namespace of aggregator",
|
||||
"description": "Contains a URL which is under control of the aggregator and can be used as a globally unique identifier for that aggregator.",
|
||||
"type": "string",
|
||||
"format": "uri",
|
||||
"examples": [
|
||||
"https://www.example.com",
|
||||
"https://csaf.io"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"aggregator_version": {
|
||||
"title": "CSAF aggregator version",
|
||||
"description": "Gives the version of the CSAF aggregator specification which the document was generated for.",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"2.0"
|
||||
]
|
||||
},
|
||||
"canonical_url": {
|
||||
"title": "Canonical URL",
|
||||
"description": "Contains the URL for this document.",
|
||||
"$ref": "#/$defs/aggregator_url_t"
|
||||
},
|
||||
"csaf_providers": {
|
||||
"title": "List of CSAF providers",
|
||||
"description": "Contains a list with information from CSAF providers.",
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"uniqueItems": true,
|
||||
"items": {
|
||||
"title": "CSAF provider entry",
|
||||
"description": "Contains information from a CSAF provider.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"metadata"
|
||||
],
|
||||
"properties": {
|
||||
"metadata": {
|
||||
"title": "CSAF provider metadata.",
|
||||
"description": "Contains the metadata of a single CSAF provider.",
|
||||
"$ref": "#/$defs/metadata_t"
|
||||
},
|
||||
"mirrors": {
|
||||
"title": "List of mirrors",
|
||||
"description": "Contains a list of URLs or mirrors for this CSAF provider.",
|
||||
"$ref": "#/$defs/mirrors_t"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"csaf_publishers": {
|
||||
"title": "List of CSAF publishers",
|
||||
"description": "Contains a list with information from CSAF publishers.",
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"uniqueItems": true,
|
||||
"items": {
|
||||
"title": "CSAF publisher entry",
|
||||
"description": "Contains information from a CSAF publisher.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"metadata",
|
||||
"mirror",
|
||||
"update_interval"
|
||||
],
|
||||
"properties": {
|
||||
"metadata": {
|
||||
"title": "CSAF publisher metadata.",
|
||||
"description": "Contains the metadata of a single CSAF publisher extracted from one of its CSAF documents.",
|
||||
"$ref": "#/$defs/metadata_t"
|
||||
},
|
||||
"mirrors": {
|
||||
"title": "List of mirrors",
|
||||
"description": "Contains a list of URLs or mirrors for this CSAF publisher.",
|
||||
"$ref": "#/$defs/mirrors_t"
|
||||
},
|
||||
"update_interval": {
|
||||
"title": "Update interval",
|
||||
"description": "Contains information about how often the CSAF publisher is checked for new CSAF documents.",
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"examples": [
|
||||
"daily",
|
||||
"weekly",
|
||||
"monthly",
|
||||
"on best effort",
|
||||
"on notification by CSAF publisher"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"last_updated": {
|
||||
"title": "Last updated",
|
||||
"description": "Holds the date and time when the document was last updated.",
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -22,6 +22,7 @@ const (
|
|||
currentReleaseDateExpr = `$.document.tracking.current_release_date`
|
||||
tlpLabelExpr = `$.document.distribution.tlp.label`
|
||||
summaryExpr = `$.document.notes[? @.category=="summary" || @.type=="summary"].text`
|
||||
statusExpr = `$.document.tracking.status`
|
||||
)
|
||||
|
||||
// AdvisorySummary is a summary of some essentials of an CSAF advisory.
|
||||
|
|
@ -33,6 +34,7 @@ type AdvisorySummary struct {
|
|||
CurrentReleaseDate time.Time
|
||||
Summary string
|
||||
TLPLabel string
|
||||
Status string
|
||||
}
|
||||
|
||||
// NewAdvisorySummary creates a summary from an advisory doc
|
||||
|
|
@ -54,6 +56,7 @@ func NewAdvisorySummary(
|
|||
{Expr: summaryExpr, Action: util.StringMatcher(&e.Summary), Optional: true},
|
||||
{Expr: tlpLabelExpr, Action: util.StringMatcher(&e.TLPLabel), Optional: true},
|
||||
{Expr: publisherExpr, Action: util.ReMarshalMatcher(e.Publisher)},
|
||||
{Expr: statusExpr, Action: util.StringMatcher(&e.Status)},
|
||||
}, doc); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
|
|||
38
csaf/util.go
Normal file
38
csaf/util.go
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
// This file is Free Software under the MIT License
|
||||
// without warranty, see README.md and LICENSES/MIT.txt for details.
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
//
|
||||
// SPDX-FileCopyrightText: 2022 German Federal Office for Information Security (BSI) <https://www.bsi.bund.de>
|
||||
// Software-Engineering: 2022 Intevation GmbH <https://intevation.de>
|
||||
|
||||
package csaf
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"io"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ExtractProviderURL extracts URLs of provider metadata.
|
||||
// If all is true all URLs are returned. Otherwise only the first is returned.
|
||||
func ExtractProviderURL(r io.Reader, all bool) ([]string, error) {
|
||||
const csaf = "CSAF:"
|
||||
|
||||
var urls []string
|
||||
|
||||
sc := bufio.NewScanner(r)
|
||||
for sc.Scan() {
|
||||
line := sc.Text()
|
||||
if strings.HasPrefix(line, csaf) {
|
||||
urls = append(urls, strings.TrimSpace(line[len(csaf):]))
|
||||
if !all {
|
||||
return urls, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
if err := sc.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return urls, nil
|
||||
}
|
||||
|
|
@ -33,9 +33,13 @@ var cvss31 []byte
|
|||
//go:embed schema/provider_json_schema.json
|
||||
var providerSchema []byte
|
||||
|
||||
//go:embed schema/aggregator_json_schema.json
|
||||
var aggregatorSchema []byte
|
||||
|
||||
var (
|
||||
compiledCSAFSchema compiledSchema
|
||||
compiledProviderSchema compiledSchema
|
||||
compiledCSAFSchema compiledSchema
|
||||
compiledProviderSchema compiledSchema
|
||||
compiledAggregatorSchema compiledSchema
|
||||
)
|
||||
|
||||
func init() {
|
||||
|
|
@ -49,6 +53,11 @@ func init() {
|
|||
{"https://docs.oasis-open.org/csaf/csaf/v2.0/provider_json_schema.json", providerSchema},
|
||||
{"https://docs.oasis-open.org/csaf/csaf/v2.0/csaf_json_schema.json", csafSchema},
|
||||
})
|
||||
compiledAggregatorSchema.compiler([]schemaData{
|
||||
{"https://docs.oasis-open.org/csaf/csaf/v2.0/aggregator_json_schema.json", aggregatorSchema},
|
||||
{"https://docs.oasis-open.org/csaf/csaf/v2.0/provider_json_schema.json", providerSchema},
|
||||
{"https://docs.oasis-open.org/csaf/csaf/v2.0/csaf_json_schema.json", csafSchema},
|
||||
})
|
||||
}
|
||||
|
||||
type schemaData struct {
|
||||
|
|
@ -146,3 +155,9 @@ func ValidateCSAF(doc interface{}) ([]string, error) {
|
|||
func ValidateProviderMetadata(doc interface{}) ([]string, error) {
|
||||
return compiledProviderSchema.validate(doc)
|
||||
}
|
||||
|
||||
// ValidateAggregator validates the document doc against the JSON schema
|
||||
// of aggregator.
|
||||
func ValidateAggregator(doc interface{}) ([]string, error) {
|
||||
return compiledAggregatorSchema.validate(doc)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue