mirror of
https://github.com/gocsaf/csaf.git
synced 2025-12-22 18:15:42 +01:00
349 lines
8 KiB
Go
349 lines
8 KiB
Go
package csaf
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"regexp"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
type TLPLabel string
|
|
|
|
const (
|
|
TLPLabelUnlabeled = "UNLABELED"
|
|
TLPLabelWhite = "WHITE"
|
|
TLPLabelGreen = "GREEN"
|
|
TLPLabelAmber = "AMBER"
|
|
TLPLabelRed = "RED"
|
|
)
|
|
|
|
var tlpLabelPattern = alternativesUnmarshal(
|
|
string("UNLABELED"),
|
|
string("WHITE"),
|
|
string("GREEN"),
|
|
string("AMBER"),
|
|
string("RED"))
|
|
|
|
type JsonURL string
|
|
|
|
var jsonURLPattern = patternUnmarshal(`\.json$`)
|
|
|
|
type Feed struct {
|
|
Summary string `json:"summary"`
|
|
TLPLabel *TLPLabel `json:"tlp_label"` // required
|
|
URL *JsonURL `json:"url"` // required
|
|
}
|
|
|
|
type ROLIE struct {
|
|
Categories []JsonURL `json:"categories"`
|
|
Feeds []Feed `json:"feeds"` // required
|
|
}
|
|
|
|
type Distribution struct {
|
|
DirectoryURL string `json:"directory_url"`
|
|
Rolie []ROLIE `json:"rolie"`
|
|
}
|
|
|
|
type TimeStamp time.Time
|
|
|
|
type Fingerprint string
|
|
|
|
var fingerprintPattern = patternUnmarshal(`^[0-9a-fA-F]{40,}$`)
|
|
|
|
type PGPKey struct {
|
|
Fingerprint Fingerprint `json:"fingerprint,omitempty"`
|
|
URL *string `json:"url"` // required
|
|
}
|
|
|
|
type CSAFCategory string
|
|
|
|
const (
|
|
CSAFCategoryCoordinator CSAFCategory = "coordinator"
|
|
CSAFCategoryDiscoverer CSAFCategory = "discoverer"
|
|
CSAFCategoryOther CSAFCategory = "other"
|
|
CSAFCategoryTranslator CSAFCategory = "translator"
|
|
CSAFCategoryUser CSAFCategory = "user"
|
|
CSAFCategoryVendor CSAFCategory = "vendor"
|
|
)
|
|
|
|
var csafCategoryPattern = alternativesUnmarshal(
|
|
string(CSAFCategoryCoordinator),
|
|
string(CSAFCategoryDiscoverer),
|
|
string(CSAFCategoryOther),
|
|
string(CSAFCategoryTranslator),
|
|
string(CSAFCategoryUser),
|
|
string(CSAFCategoryVendor))
|
|
|
|
type CSAFPublisher struct {
|
|
Category *CSAFCategory `json:"category"` // required
|
|
Name *string `json:"name"` // required
|
|
Namespace *string `json:"namespace"` // required
|
|
ContactDetails string `json:"contact_details,omitempty"`
|
|
IssuingAuthority string `json:"issuing_authority,omitempty"`
|
|
}
|
|
|
|
type MetadataVersion string
|
|
|
|
const MetadataVersion20 MetadataVersion = "2.0"
|
|
|
|
var metadataVersionPattern = alternativesUnmarshal(string(MetadataVersion20))
|
|
|
|
type MetadataRole string
|
|
|
|
const (
|
|
MetadataRolePublisher MetadataRole = "csaf_publisher"
|
|
MetadataRoleProvider MetadataRole = "csaf_provider"
|
|
MetadataRoleTrustedProvider MetadataRole = "csaf_trusted_provider"
|
|
)
|
|
|
|
var metadataRolePattern = alternativesUnmarshal(
|
|
string(MetadataRolePublisher),
|
|
string(MetadataRoleProvider),
|
|
string(MetadataRoleTrustedProvider))
|
|
|
|
type ProviderURL string
|
|
|
|
var providerURLPattern = patternUnmarshal(`/provider-metadata\.json$`)
|
|
|
|
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 *CSAFPublisher `json:"publisher"` // 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, "|"))
|
|
}
|
|
}
|
|
|
|
func (tl *TLPLabel) UnmarshalText(data []byte) error {
|
|
s, err := tlpLabelPattern(data)
|
|
if err == nil {
|
|
*tl = TLPLabel(s)
|
|
}
|
|
return err
|
|
}
|
|
|
|
func (ju *JsonURL) UnmarshalText(data []byte) error {
|
|
s, err := jsonURLPattern(data)
|
|
if err == nil {
|
|
*ju = JsonURL(s)
|
|
}
|
|
return err
|
|
}
|
|
|
|
func (pu *ProviderURL) UnmarshalText(data []byte) error {
|
|
s, err := providerURLPattern(data)
|
|
if err == nil {
|
|
*pu = ProviderURL(s)
|
|
}
|
|
return err
|
|
}
|
|
|
|
func (cc *CSAFCategory) UnmarshalText(data []byte) error {
|
|
s, err := csafCategoryPattern(data)
|
|
if err == nil {
|
|
*cc = CSAFCategory(s)
|
|
}
|
|
return err
|
|
}
|
|
|
|
func (fp *Fingerprint) UnmarshalText(data []byte) error {
|
|
s, err := fingerprintPattern(data)
|
|
if err == nil {
|
|
*fp = Fingerprint(s)
|
|
}
|
|
return err
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
func (ts TimeStamp) MarshalText() ([]byte, error) {
|
|
return []byte(time.Time(ts).Format(time.RFC3339)), nil
|
|
}
|
|
|
|
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
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
func (cp *CSAFPublisher) Validate() error {
|
|
switch {
|
|
case cp == nil:
|
|
return errors.New("publisher is mandatory")
|
|
case cp.Category == nil:
|
|
return errors.New("publisher.category is mandatory")
|
|
case cp.Name == nil:
|
|
return errors.New("publisher.name is mandatory")
|
|
case cp.Namespace == nil:
|
|
return errors.New("publisher.namespace is mandatory")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (pk *PGPKey) Validate() error {
|
|
if pk.URL == nil {
|
|
return errors.New("pgp_key[].url is mandatory")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (d *Distribution) Validate() error {
|
|
for i := range d.Rolie {
|
|
if err := d.Rolie[i].Validate(); err != nil {
|
|
return nil
|
|
}
|
|
}
|
|
return 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
|
|
}
|
|
|
|
func (pmd *ProviderMetadata) SetLastUpdated(t time.Time) {
|
|
ts := TimeStamp(t.UTC())
|
|
pmd.LastUpdated = &ts
|
|
}
|
|
|
|
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,
|
|
})
|
|
}
|
|
|
|
func NewProviderMetadata(canonicalURL string) *ProviderMetadata {
|
|
pmd := new(ProviderMetadata)
|
|
pmd.Defaults()
|
|
pmd.SetLastUpdated(time.Now())
|
|
cu := ProviderURL(canonicalURL)
|
|
pmd.CanonicalURL = &cu
|
|
return pmd
|
|
}
|
|
|
|
func (pm *ProviderMetadata) Save(w io.Writer) error {
|
|
enc := json.NewEncoder(w)
|
|
enc.SetIndent("", " ")
|
|
return enc.Encode(pm)
|
|
}
|
|
|
|
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
|
|
}
|