1
0
Fork 0
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:
Sascha L. Teichmann 2022-05-10 18:12:38 +02:00 committed by GitHub
parent 9da0589236
commit 8a1ebe0b7a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
30 changed files with 2789 additions and 88 deletions

View file

@ -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
}

View 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"
}
}
}

View file

@ -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
View 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
}

View file

@ -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)
}