1
0
Fork 0
mirror of https://github.com/gocsaf/csaf.git synced 2025-12-22 11:55:40 +01:00

Port over logic to new PMD loader

This commit is contained in:
Sascha L. Teichmann 2023-05-15 00:35:35 +02:00
parent dd15eea48e
commit e0928f58ad

View file

@ -9,8 +9,12 @@
package csaf package csaf
import ( import (
"errors" "bytes"
"log" "crypto/sha256"
"encoding/json"
"fmt"
"io"
"net/http"
"strings" "strings"
"github.com/csaf-poc/csaf_distribution/util" "github.com/csaf-poc/csaf_distribution/util"
@ -19,8 +23,9 @@ import (
// ProviderMetadataLoader helps load provider-metadata.json from // ProviderMetadataLoader helps load provider-metadata.json from
// the various locations. // the various locations.
type ProviderMetadataLoader struct { type ProviderMetadataLoader struct {
client *util.Client client util.Client
logging func(string, ...any) already map[string]*LoadedProviderMetadata
messages ProviderMetadataLoadMessages
} }
// ProviderMetadataLoadMessageType is the type of the message. // ProviderMetadataLoadMessageType is the type of the message.
@ -33,6 +38,15 @@ const (
SchemaValidationFailed SchemaValidationFailed
// SchemaValidationFailedDetail is a failure detail in schema validation. // SchemaValidationFailedDetail is a failure detail in schema validation.
SchemaValidationFailedDetail SchemaValidationFailedDetail
// HTTPFailed indicates that loading on HTTP level failed.
HTTPFailed
// ExtraProviderMetadataFound indicates an extra PMD found in security.txt.
ExtraProviderMetadataFound
// WellknownSecurityMismatch indicates that the PMDs found under wellknown and
// in the security do not match.
WellknownSecurityMismatch
// IgnoreProviderMetadata indicates that a extra PMD was ignored.
IgnoreProviderMetadata
) )
// ProviderMetadataLoadMessage is a message generated while loading // ProviderMetadataLoadMessage is a message generated while loading
@ -42,6 +56,9 @@ type ProviderMetadataLoadMessage struct {
Message string Message string
} }
// ProviderMetadataLoadMessages is a list of loading messages.
type ProviderMetadataLoadMessages []ProviderMetadataLoadMessage
// LoadedProviderMetadata represents a loaded provider metadata. // LoadedProviderMetadata represents a loaded provider metadata.
type LoadedProviderMetadata struct { type LoadedProviderMetadata struct {
// URL is location where the document was found. // URL is location where the document was found.
@ -51,7 +68,31 @@ type LoadedProviderMetadata struct {
// Hash is a SHA256 sum over the document. // Hash is a SHA256 sum over the document.
Hash []byte Hash []byte
// Messages are the error message happened while loading. // Messages are the error message happened while loading.
Messages []ProviderMetadataLoadMessage Messages ProviderMetadataLoadMessages
}
// Add appends a message to the list of loading messages.
func (pmlm *ProviderMetadataLoadMessages) Add(
typ ProviderMetadataLoadMessageType,
msg string,
) {
*pmlm = append(*pmlm, ProviderMetadataLoadMessage{
Type: typ,
Message: msg,
})
}
// AppendUnique appends unique messages from a second list.
func (pmlm *ProviderMetadataLoadMessages) AppendUnique(other ProviderMetadataLoadMessages) {
next:
for _, o := range other {
for _, m := range *pmlm {
if m == o {
continue next
}
}
*pmlm = append(*pmlm, o)
}
} }
// Valid returns true if the loaded document is valid. // Valid returns true if the loaded document is valid.
@ -60,42 +101,240 @@ func (lpm *LoadedProviderMetadata) Valid() bool {
} }
// NewProviderMetadataLoader create a new loader. // NewProviderMetadataLoader create a new loader.
func NewProviderMetadataLoader( func NewProviderMetadataLoader(client util.Client) *ProviderMetadataLoader {
client *util.Client,
logging func(string, ...any),
) *ProviderMetadataLoader {
// If no logging was given log to stdout.
if logging == nil {
logging = func(format string, args ...any) {
log.Printf("ProviderMetadataLoader: "+format+"\n", args...)
}
}
return &ProviderMetadataLoader{ return &ProviderMetadataLoader{
client: client, client: client,
logging: logging, already: map[string]*LoadedProviderMetadata{},
} }
} }
// Load loads a provider metadata for a given path. // Load loads a provider metadata for a given path.
// If the domain starts with `https://` it only attemps to load // If the domain starts with `https://` it only attemps to load
// the data from that URL. // the data from that URL.
func (pmdl *ProviderMetadataLoader) Load(path string) (*LoadedProviderMetadata, error) { func (pmdl *ProviderMetadataLoader) Load(domain string) *LoadedProviderMetadata {
// check direct path // Check direct path
if strings.HasPrefix(path, "https://") { if strings.HasPrefix(domain, "https://") {
return pmdl.loadFromURL(path) lpmd, err := pmdl.loadFromURL(domain)
if err != nil {
lpmd = new(LoadedProviderMetadata)
lpmd.Messages.Add(HTTPFailed, err.Error())
}
return lpmd
} }
// TODO: Implement me! // First try the well-known path.
return nil, errors.New("not implemented, yet") wellknownURL := "https://" + domain + "/.well-known/csaf/provider-metadata.json"
wellknownResult, err := pmdl.loadFromURL(wellknownURL)
if err != nil {
pmdl.messages.Add(HTTPFailed, err.Error())
}
// Valid provider metadata under well-known.
var wellknownGood *LoadedProviderMetadata
// We have a candidate.
if wellknownResult.Valid() {
wellknownGood = wellknownResult
}
// Next load the PMDs from security.txt
secURL := "https://" + domain + "/.well-known/security.txt"
secResults := pmdl.loadFromSecurity(secURL)
// Filter out the results which are valid.
var secGoods []*LoadedProviderMetadata
for _, result := range secResults {
if len(result.Messages) > 0 {
// If there where validation issues append them
// to the overall report
pmdl.messages.AppendUnique(pmdl.messages)
} else {
secGoods = append(secGoods, result)
}
}
// Mention extra CSAF entries in security.txt.
ignoreExtras := func() {
for _, extra := range secGoods[1:] {
pmdl.messages.Add(
ExtraProviderMetadataFound,
fmt.Sprintf("Ignoring extra CSAF entry in security.txt: %s", extra.URL))
}
}
// security.txt contains good entries.
if len(secGoods) > 0 {
// we already have a good wellknown, take it.
if wellknownGood != nil {
// check if first of security urls is identical to wellknown.
if bytes.Equal(wellknownGood.Hash, secGoods[0].Hash) {
ignoreExtras()
} else {
// Complaint about not matching.
pmdl.messages.Add(
WellknownSecurityMismatch,
"First entry of security.txt and well-known don't match.")
// List all the security urls.
for _, sec := range secGoods {
pmdl.messages.Add(
IgnoreProviderMetadata,
fmt.Sprintf("Ignoring CSAF entry in security.txt: %s", sec.URL))
}
}
// Take the good well-known.
wellknownGood.Messages.AppendUnique(pmdl.messages)
return wellknownGood
}
// Don't have well-known. Take first good from security.txt.
ignoreExtras()
secGoods[0].Messages.AppendUnique(pmdl.messages)
return secGoods[0]
}
// If we have a good well-known take it.
if wellknownGood != nil {
wellknownGood.Messages.AppendUnique(pmdl.messages)
return wellknownGood
}
// Last resort: fall back to DNS.
dnsURL := "https://csaf.data.security." + domain
dnsResult, err := pmdl.loadFromURL(dnsURL)
if err != nil {
dnsResult = new(LoadedProviderMetadata)
pmdl.messages.Add(
HTTPFailed,
err.Error())
}
dnsResult.Messages.AppendUnique(pmdl.messages)
return dnsResult
}
// loadFromSecurity loads the PMDs mentioned in the security.txt.
func (pmdl *ProviderMetadataLoader) loadFromSecurity(path string) []*LoadedProviderMetadata {
res, err := pmdl.client.Get(path)
if err != nil {
pmdl.messages.Add(
HTTPFailed,
fmt.Sprintf("Fetching %q failed: %v", path, err))
return nil
}
if res.StatusCode != http.StatusOK {
pmdl.messages.Add(
HTTPFailed,
fmt.Sprintf("Fetching %q failed: %s (%d)", path, res.Status, res.StatusCode))
return nil
}
// Extract all potential URLs from CSAF.
urls, err := func() ([]string, error) {
defer res.Body.Close()
return ExtractProviderURL(res.Body, true)
}()
if err != nil {
pmdl.messages.Add(
HTTPFailed,
fmt.Sprintf("Loading %q failed: %v", path, err))
return nil
}
var loaded []*LoadedProviderMetadata
// Load the URLs
nextURL:
for _, url := range urls {
lpmd, err := pmdl.loadFromURL(url)
// If loading failed note it down.
if err != nil {
pmdl.messages.Add(
HTTPFailed,
fmt.Sprintf("Loading %q failed: %v", url, err))
continue
}
// Check for duplicates
for _, l := range loaded {
if l == lpmd {
continue nextURL
}
}
loaded = append(loaded, lpmd)
}
return loaded
} }
// loadFromURL loads a provider metadata from a given URL. // loadFromURL loads a provider metadata from a given URL.
func (pmdl *ProviderMetadataLoader) loadFromURL(path string) (*LoadedProviderMetadata, error) { func (pmdl *ProviderMetadataLoader) loadFromURL(path string) (*LoadedProviderMetadata, error) {
_ = path res, err := pmdl.client.Get(path)
if err != nil {
return nil, fmt.Errorf("fetching %q failed: %v", path, err)
}
if res.StatusCode != http.StatusOK {
return nil, fmt.Errorf("fetching %q failed: %s (%d)", path, res.Status, res.StatusCode)
}
// TODO: Implement me! // TODO: Check for application/json and log it.
return nil, errors.New("not implemented, yet")
defer res.Body.Close()
// Calculate checksum for later comparison.
hash := sha256.New()
result := LoadedProviderMetadata{URL: path}
tee := io.TeeReader(res.Body, hash)
var doc any
if err := json.NewDecoder(tee).Decode(&doc); err != nil {
return nil, fmt.Errorf("JSON decoding failed: %w", err)
}
// Before checking the err lets check if we had the same
// document before. If so it will have failed parsing before.
sum := hash.Sum(nil)
key := string(sum)
// If we already have loaded it return the cached result.
if r := pmdl.already[key]; r != nil {
return r, nil
}
// write it back as loaded
switch errors, err := ValidateProviderMetadata(doc); {
case err != nil:
result.Messages = []ProviderMetadataLoadMessage{{
Type: SchemaValidationFailed,
Message: fmt.Sprintf("%s: Validating against JSON schema failed: %v", path, err),
}}
case len(errors) > 0:
result.Messages = []ProviderMetadataLoadMessage{{
Type: SchemaValidationFailed,
Message: fmt.Sprintf("%s: Validating against JSON schema failed: %v", path, err),
}}
for _, msg := range errors {
result.Messages = append(result.Messages, ProviderMetadataLoadMessage{
Type: SchemaValidationFailedDetail,
Message: strings.ReplaceAll(msg, `%`, `%%`),
})
}
default:
// Only store in result if validation passed.
result.Document = doc
result.Hash = sum
}
pmdl.already[key] = &result
return &result, nil
} }