mirror of
https://github.com/gocsaf/csaf.git
synced 2025-12-22 11:55:40 +01:00
487 lines
13 KiB
Go
487 lines
13 KiB
Go
package csaf
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"regexp"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// TLPLabel is the traffic light policy of the CSAF.
|
|
type TLPLabel string
|
|
|
|
const (
|
|
// TLPLabelUnlabeled is the 'UNLABELED' policy.
|
|
TLPLabelUnlabeled = "UNLABELED"
|
|
// TLPLabelWhite is the 'WHITE' policy.
|
|
TLPLabelWhite = "WHITE"
|
|
// TLPLabelGreen is the 'GREEN' policy.
|
|
TLPLabelGreen = "GREEN"
|
|
// TLPLabelAmber is the 'AMBER' policy.
|
|
TLPLabelAmber = "AMBER"
|
|
// TLPLabelRed is the 'RED' policy.
|
|
TLPLabelRed = "RED"
|
|
)
|
|
|
|
var tlpLabelPattern = alternativesUnmarshal(
|
|
string("UNLABELED"),
|
|
string("WHITE"),
|
|
string("GREEN"),
|
|
string("AMBER"),
|
|
string("RED"))
|
|
|
|
// JSONURL is an URL to JSON document.
|
|
type JSONURL string
|
|
|
|
var jsonURLPattern = patternUnmarshal(`\.json$`)
|
|
|
|
// Feed is CSAF feed.
|
|
type Feed struct {
|
|
Summary string `json:"summary"`
|
|
TLPLabel *TLPLabel `json:"tlp_label"` // required
|
|
URL *JSONURL `json:"url"` // required
|
|
}
|
|
|
|
// ROLIE is the ROLIE extension of the CSAF feed.
|
|
type ROLIE struct {
|
|
Categories []JSONURL `json:"categories,omitempty"`
|
|
Feeds []Feed `json:"feeds"` // required
|
|
Services []JSONURL `json:"services,omitempty"`
|
|
}
|
|
|
|
// Distribution is a distribution of a CSAF feed.
|
|
type Distribution struct {
|
|
DirectoryURL string `json:"directory_url,omitempty"`
|
|
Rolie []ROLIE `json:"rolie"`
|
|
}
|
|
|
|
// TimeStamp represents a time stamp in a CSAF feed.
|
|
type TimeStamp time.Time
|
|
|
|
// Fingerprint is the fingerprint of a OpenPGP key used to sign
|
|
// the CASF documents.
|
|
type Fingerprint string
|
|
|
|
var fingerprintPattern = patternUnmarshal(`^[0-9a-fA-F]{40,}$`)
|
|
|
|
// PGPKey is location and the fingerprint of the key
|
|
// used to sign the CASF documents.
|
|
type PGPKey struct {
|
|
Fingerprint Fingerprint `json:"fingerprint,omitempty"`
|
|
URL *string `json:"url"` // required
|
|
}
|
|
|
|
// Category is the category of the CSAF feed.
|
|
type Category string
|
|
|
|
const (
|
|
// CSAFCategoryCoordinator is the "coordinator" category.
|
|
CSAFCategoryCoordinator Category = "coordinator"
|
|
// CSAFCategoryDiscoverer is the "discoverer" category.
|
|
CSAFCategoryDiscoverer Category = "discoverer"
|
|
// CSAFCategoryOther is the "other" category.
|
|
CSAFCategoryOther Category = "other"
|
|
// CSAFCategoryTranslator is the "translator" category.
|
|
CSAFCategoryTranslator Category = "translator"
|
|
// CSAFCategoryUser is the "user" category.
|
|
CSAFCategoryUser Category = "user"
|
|
// CSAFCategoryVendor is the "vendor" category.
|
|
CSAFCategoryVendor Category = "vendor"
|
|
)
|
|
|
|
var csafCategoryPattern = alternativesUnmarshal(
|
|
string(CSAFCategoryCoordinator),
|
|
string(CSAFCategoryDiscoverer),
|
|
string(CSAFCategoryOther),
|
|
string(CSAFCategoryTranslator),
|
|
string(CSAFCategoryUser),
|
|
string(CSAFCategoryVendor))
|
|
|
|
// Publisher is the publisher of the feed.
|
|
type Publisher struct {
|
|
Category *Category `json:"category" toml:"category"` // required
|
|
Name *string `json:"name" toml:"name"` // required
|
|
Namespace *string `json:"namespace" toml:"namespace"` // required
|
|
ContactDetails string `json:"contact_details,omitempty" toml:"contact_details"`
|
|
IssuingAuthority string `json:"issuing_authority,omitempty" toml:"issuing_authority"`
|
|
}
|
|
|
|
// MetadataVersion is the metadata version of the feed.
|
|
type MetadataVersion string
|
|
|
|
// MetadataVersion20 is the current version of the schema.
|
|
const MetadataVersion20 MetadataVersion = "2.0"
|
|
|
|
var metadataVersionPattern = alternativesUnmarshal(string(MetadataVersion20))
|
|
|
|
// MetadataRole is the role of the feed.
|
|
type MetadataRole string
|
|
|
|
const (
|
|
// MetadataRolePublisher is the "csaf_publisher" role.
|
|
MetadataRolePublisher MetadataRole = "csaf_publisher"
|
|
// MetadataRoleProvider is the "csaf_provider" role.
|
|
MetadataRoleProvider MetadataRole = "csaf_provider"
|
|
// MetadataRoleTrustedProvider is the "csaf_trusted_provider" role.
|
|
MetadataRoleTrustedProvider MetadataRole = "csaf_trusted_provider"
|
|
)
|
|
|
|
var metadataRolePattern = alternativesUnmarshal(
|
|
string(MetadataRolePublisher),
|
|
string(MetadataRoleProvider),
|
|
string(MetadataRoleTrustedProvider))
|
|
|
|
// ProviderURL is the URL of the provider document.
|
|
type ProviderURL string
|
|
|
|
var providerURLPattern = patternUnmarshal(`/provider-metadata\.json$`)
|
|
|
|
// ProviderMetadata contains the metadata of the provider.
|
|
type ProviderMetadata struct {
|
|
CanonicalURL *ProviderURL `json:"canonical_url"` // required
|
|
Distributions []Distribution `json:"distributions,omitempty"`
|
|
LastUpdated *TimeStamp `json:"last_updated"` // required
|
|
ListOnCSAFAggregators *bool `json:"list_on_CSAF_aggregators"`
|
|
MetadataVersion *MetadataVersion `json:"metadata_version"` // required
|
|
MirrorOnCSAFAggregators *bool `json:"mirror_on_CSAF_aggregators"` // required
|
|
PGPKeys []PGPKey `json:"pgp_keys,omitempty"`
|
|
Publisher *Publisher `json:"publisher,omitempty"` // required
|
|
Role *MetadataRole `json:"role"` // required
|
|
}
|
|
|
|
func patternUnmarshal(pattern string) func([]byte) (string, error) {
|
|
r := regexp.MustCompile(pattern)
|
|
return func(data []byte) (string, error) {
|
|
s := string(data)
|
|
if !r.MatchString(s) {
|
|
return "", fmt.Errorf("%s does not match %v", s, r)
|
|
}
|
|
return s, nil
|
|
}
|
|
}
|
|
|
|
func alternativesUnmarshal(alternatives ...string) func([]byte) (string, error) {
|
|
return func(data []byte) (string, error) {
|
|
s := string(data)
|
|
for _, alt := range alternatives {
|
|
if alt == s {
|
|
return s, nil
|
|
}
|
|
}
|
|
return "", fmt.Errorf("%s not in [%s]", s, strings.Join(alternatives, "|"))
|
|
}
|
|
}
|
|
|
|
// UnmarshalText implements the encoding.TextUnmarshaller interface.
|
|
func (tl *TLPLabel) UnmarshalText(data []byte) error {
|
|
s, err := tlpLabelPattern(data)
|
|
if err == nil {
|
|
*tl = TLPLabel(s)
|
|
}
|
|
return err
|
|
}
|
|
|
|
// UnmarshalText implements the encoding.TextUnmarshaller interface.
|
|
func (ju *JSONURL) UnmarshalText(data []byte) error {
|
|
s, err := jsonURLPattern(data)
|
|
if err == nil {
|
|
*ju = JSONURL(s)
|
|
}
|
|
return err
|
|
}
|
|
|
|
// UnmarshalText implements the encoding.TextUnmarshaller interface.
|
|
func (pu *ProviderURL) UnmarshalText(data []byte) error {
|
|
s, err := providerURLPattern(data)
|
|
if err == nil {
|
|
*pu = ProviderURL(s)
|
|
}
|
|
return err
|
|
}
|
|
|
|
// UnmarshalText implements the encoding.TextUnmarshaller interface.
|
|
func (cc *Category) UnmarshalText(data []byte) error {
|
|
s, err := csafCategoryPattern(data)
|
|
if err == nil {
|
|
*cc = Category(s)
|
|
}
|
|
return err
|
|
}
|
|
|
|
// UnmarshalText implements the encoding.TextUnmarshaller interface.
|
|
func (fp *Fingerprint) UnmarshalText(data []byte) error {
|
|
s, err := fingerprintPattern(data)
|
|
if err == nil {
|
|
*fp = Fingerprint(s)
|
|
}
|
|
return err
|
|
}
|
|
|
|
// UnmarshalText implements the encoding.TextUnmarshaller interface.
|
|
func (ts *TimeStamp) UnmarshalText(data []byte) error {
|
|
t, err := time.Parse(time.RFC3339, string(data))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
*ts = TimeStamp(t)
|
|
return nil
|
|
}
|
|
|
|
// MarshalText implements the encoding.TextMarshaller interface.
|
|
func (ts TimeStamp) MarshalText() ([]byte, error) {
|
|
return []byte(time.Time(ts).Format(time.RFC3339)), nil
|
|
}
|
|
|
|
// Defaults fills the correct default values into the provider metadata.
|
|
func (pmd *ProviderMetadata) Defaults() {
|
|
if pmd.Role == nil {
|
|
role := MetadataRoleProvider
|
|
pmd.Role = &role
|
|
}
|
|
if pmd.ListOnCSAFAggregators == nil {
|
|
t := true
|
|
pmd.ListOnCSAFAggregators = &t
|
|
}
|
|
if pmd.MirrorOnCSAFAggregators == nil {
|
|
t := true
|
|
pmd.MirrorOnCSAFAggregators = &t
|
|
}
|
|
if pmd.MetadataVersion == nil {
|
|
mdv := MetadataVersion20
|
|
pmd.MetadataVersion = &mdv
|
|
}
|
|
}
|
|
|
|
// Validate checks if the feed is valid.
|
|
// Returns an error if the validation fails otherwise nil.
|
|
func (f *Feed) Validate() error {
|
|
switch {
|
|
case f.TLPLabel == nil:
|
|
return errors.New("feed[].tlp_label is mandatory")
|
|
case f.URL == nil:
|
|
return errors.New("feed[].url is mandatory")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Validate checks if the ROLIE extension is valid.
|
|
// Returns an error if the validation fails otherwise nil.
|
|
func (r *ROLIE) Validate() error {
|
|
if len(r.Feeds) < 1 {
|
|
return errors.New("ROLIE needs at least one feed")
|
|
}
|
|
for i := range r.Feeds {
|
|
if err := r.Feeds[i].Validate(); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Validate checks if the publisher is valid.
|
|
// Returns an error if the validation fails otherwise nil.
|
|
func (p *Publisher) Validate() error {
|
|
switch {
|
|
case p == nil:
|
|
return errors.New("publisher is mandatory")
|
|
case p.Category == nil:
|
|
return errors.New("publisher.category is mandatory")
|
|
case p.Name == nil:
|
|
return errors.New("publisher.name is mandatory")
|
|
case p.Namespace == nil:
|
|
return errors.New("publisher.namespace is mandatory")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func strPtrEquals(a, b *string) bool {
|
|
switch {
|
|
case a == nil:
|
|
return b == nil
|
|
case b == nil:
|
|
return false
|
|
default:
|
|
return *a == *b
|
|
}
|
|
}
|
|
|
|
// Equals checks if the publisher is equal to other componentwise.
|
|
func (p *Publisher) Equals(o *Publisher) bool {
|
|
switch {
|
|
case p == nil:
|
|
return o == nil
|
|
case o == nil:
|
|
return false
|
|
default:
|
|
return strPtrEquals((*string)(p.Category), (*string)(o.Category)) &&
|
|
strPtrEquals(p.Name, o.Name) &&
|
|
strPtrEquals(p.Namespace, o.Namespace) &&
|
|
p.ContactDetails == o.ContactDetails &&
|
|
p.IssuingAuthority == o.IssuingAuthority
|
|
}
|
|
}
|
|
|
|
// Validate checks if the PGPKey is valid.
|
|
// Returns an error if the validation fails otherwise nil.
|
|
func (pk *PGPKey) Validate() error {
|
|
if pk.URL == nil {
|
|
return errors.New("pgp_key[].url is mandatory")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Validate checks if the distribution is valid.
|
|
// Returns an error if the validation fails otherwise nil.
|
|
func (d *Distribution) Validate() error {
|
|
for i := range d.Rolie {
|
|
if err := d.Rolie[i].Validate(); err != nil {
|
|
return nil
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Validate checks if the provider metadata is valid.
|
|
// Returns an error if the validation fails otherwise nil.
|
|
func (pmd *ProviderMetadata) Validate() error {
|
|
|
|
switch {
|
|
case pmd.CanonicalURL == nil:
|
|
return errors.New("canonical_url is mandatory")
|
|
case pmd.LastUpdated == nil:
|
|
return errors.New("last_updated is mandatory")
|
|
case pmd.MetadataVersion == nil:
|
|
return errors.New("metadata_version is mandatory")
|
|
}
|
|
|
|
if err := pmd.Publisher.Validate(); err != nil {
|
|
return err
|
|
}
|
|
|
|
for i := range pmd.PGPKeys {
|
|
if err := pmd.PGPKeys[i].Validate(); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
for i := range pmd.Distributions {
|
|
if err := pmd.Distributions[i].Validate(); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// SetLastUpdated updates the last updated timestamp of the feed.
|
|
func (pmd *ProviderMetadata) SetLastUpdated(t time.Time) {
|
|
ts := TimeStamp(t.UTC())
|
|
pmd.LastUpdated = &ts
|
|
}
|
|
|
|
// SetPGP sets the fingerprint and URL of the OpenPGP key
|
|
// of the feed. If the feed already has a key with
|
|
// given fingerprint the URL updated.
|
|
// If there is no such key it is append to the list of keys.
|
|
func (pmd *ProviderMetadata) SetPGP(fingerprint, url string) {
|
|
for i := range pmd.PGPKeys {
|
|
if pmd.PGPKeys[i].Fingerprint == Fingerprint(fingerprint) {
|
|
pmd.PGPKeys[i].URL = &url
|
|
return
|
|
}
|
|
}
|
|
pmd.PGPKeys = append(pmd.PGPKeys, PGPKey{
|
|
Fingerprint: Fingerprint(fingerprint),
|
|
URL: &url,
|
|
})
|
|
}
|
|
|
|
// NewProviderMetadata creates a new provider with the given URL.
|
|
// Valid default values are set and the feed is considered to
|
|
// be updated recently.
|
|
func NewProviderMetadata(canonicalURL string) *ProviderMetadata {
|
|
pm := new(ProviderMetadata)
|
|
pm.Defaults()
|
|
pm.SetLastUpdated(time.Now())
|
|
cu := ProviderURL(canonicalURL)
|
|
pm.CanonicalURL = &cu
|
|
return pm
|
|
}
|
|
|
|
// NewProviderMetadataDomain creates a new provider with the given URL
|
|
// and tlps feeds.
|
|
func NewProviderMetadataDomain(domain string, tlps []TLPLabel) *ProviderMetadata {
|
|
|
|
pm := NewProviderMetadata(
|
|
domain + "/.well-known/csaf/provider-metadata.json")
|
|
|
|
if len(tlps) == 0 {
|
|
return pm
|
|
}
|
|
|
|
// Register feeds.
|
|
|
|
feeds := make([]Feed, len(tlps))
|
|
|
|
for i, t := range tlps {
|
|
lt := strings.ToLower(string(t))
|
|
feed := "csaf-feed-tlp-" + lt + ".json"
|
|
url := JSONURL(domain + "/.well-known/csaf/" + lt + "/" + feed)
|
|
|
|
feeds[i] = Feed{
|
|
Summary: "TLP:" + string(t) + " advisories",
|
|
TLPLabel: &t,
|
|
URL: &url,
|
|
}
|
|
}
|
|
|
|
pm.Distributions = []Distribution{{
|
|
Rolie: []ROLIE{{
|
|
Feeds: feeds,
|
|
}},
|
|
}}
|
|
|
|
return pm
|
|
}
|
|
|
|
type nWriter struct {
|
|
io.Writer
|
|
n int64
|
|
}
|
|
|
|
func (nw *nWriter) Write(p []byte) (int, error) {
|
|
n, err := nw.Writer.Write(p)
|
|
nw.n += int64(n)
|
|
return n, err
|
|
}
|
|
|
|
// WriteTo saves a metadata provider to a writer.
|
|
func (pmd *ProviderMetadata) WriteTo(w io.Writer) (int64, error) {
|
|
nw := nWriter{w, 0}
|
|
enc := json.NewEncoder(&nw)
|
|
enc.SetIndent("", " ")
|
|
err := enc.Encode(pmd)
|
|
return nw.n, err
|
|
}
|
|
|
|
// LoadProviderMetadata loads a metadata provider from a reader.
|
|
func LoadProviderMetadata(r io.Reader) (*ProviderMetadata, error) {
|
|
|
|
var pmd ProviderMetadata
|
|
dec := json.NewDecoder(r)
|
|
if err := dec.Decode(&pmd); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if err := pmd.Validate(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Set defaults.
|
|
pmd.Defaults()
|
|
|
|
return &pmd, nil
|
|
}
|