1
0
Fork 0
mirror of https://github.com/gocsaf/csaf.git synced 2025-12-22 18:15:42 +01:00

Merge pull request #366 from csaf-poc/cleanup_provider_metadata_loading

Prepare infrastructure for role based reporting
This commit is contained in:
Bernhard Herzog 2023-05-16 17:53:18 +02:00 committed by GitHub
commit 02d476360b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 511 additions and 335 deletions

View file

@ -79,11 +79,17 @@ func (w *worker) createDir() (string, error) {
func (w *worker) locateProviderMetadata(domain string) error { func (w *worker) locateProviderMetadata(domain string) error {
lpmd := csaf.LoadProviderMetadataForDomain( loader := csaf.NewProviderMetadataLoader(w.client)
w.client, domain, func(format string, args ...any) {
lpmd := loader.Load(domain)
if w.processor.cfg.Verbose {
for i := range lpmd.Messages {
log.Printf( log.Printf(
"Looking for provider-metadata.json of '"+domain+"': "+format+"\n", args...) "Loading provider-metadata.json of %q: %s\n",
}) domain, lpmd.Messages[i].Message)
}
}
if !lpmd.Valid() { if !lpmd.Valid() {
return fmt.Errorf("no valid provider-metadata.json found for '%s'", domain) return fmt.Errorf("no valid provider-metadata.json found for '%s'", domain)

View file

@ -140,28 +140,6 @@ func writeReport(report *Report, opts *options) error {
return writer(report, w) return writer(report, w)
} }
// buildReporters initializes each report by assigning a number and description to it.
// It returns an array of the reporter interface type.
func buildReporters() []reporter {
return []reporter{
&validReporter{baseReporter{num: 1, description: "Valid CSAF documents"}},
&filenameReporter{baseReporter{num: 2, description: "Filename"}},
&tlsReporter{baseReporter{num: 3, description: "TLS"}},
&redirectsReporter{baseReporter{num: 6, description: "Redirects"}},
&providerMetadataReport{baseReporter{num: 7, description: "provider-metadata.json"}},
&securityReporter{baseReporter{num: 8, description: "security.txt"}},
&wellknownMetadataReporter{baseReporter{num: 9, description: "/.well-known/csaf/provider-metadata.json"}},
&dnsPathReporter{baseReporter{num: 10, description: "DNS path"}},
&oneFolderPerYearReport{baseReporter{num: 11, description: "One folder per year"}},
&indexReporter{baseReporter{num: 12, description: "index.txt"}},
&changesReporter{baseReporter{num: 13, description: "changes.csv"}},
&directoryListingsReporter{baseReporter{num: 14, description: "Directory listings"}},
&integrityReporter{baseReporter{num: 18, description: "Integrity"}},
&signaturesReporter{baseReporter{num: 19, description: "Signatures"}},
&publicPGPKeyReporter{baseReporter{num: 20, description: "Public OpenPGP Key"}},
}
}
// run uses a processor to check all the given domains or direct urls // run uses a processor to check all the given domains or direct urls
// and generates a report. // and generates a report.
func run(opts *options, domains []string) (*Report, error) { func run(opts *options, domains []string) (*Report, error) {
@ -170,7 +148,7 @@ func run(opts *options, domains []string) (*Report, error) {
return nil, err return nil, err
} }
defer p.close() defer p.close()
return p.run(buildReporters(), domains) return p.run(domains)
} }
func main() { func main() {

View file

@ -223,7 +223,7 @@ func (p *processor) clean() {
// run calls checkDomain function for each domain in the given "domains" parameter. // run calls checkDomain function for each domain in the given "domains" parameter.
// Then it calls the report method on each report from the given "reporters" parameter for each domain. // Then it calls the report method on each report from the given "reporters" parameter for each domain.
// It returns a pointer to the report and nil, otherwise an error. // It returns a pointer to the report and nil, otherwise an error.
func (p *processor) run(reporters []reporter, domains []string) (*Report, error) { func (p *processor) run(domains []string) (*Report, error) {
report := Report{ report := Report{
Date: ReportTime{Time: time.Now().UTC()}, Date: ReportTime{Time: time.Now().UTC()},
@ -231,19 +231,24 @@ func (p *processor) run(reporters []reporter, domains []string) (*Report, error)
} }
for _, d := range domains { for _, d := range domains {
if p.checkProviderMetadata(d) {
if err := p.checkDomain(d); err != nil { if err := p.checkDomain(d); err != nil {
if err == errContinue || err == errStop { if err == errContinue || err == errStop {
continue continue
} }
return nil, err return nil, err
} }
domain := &Domain{Name: d}
for _, r := range reporters {
r.report(p, domain)
} }
domain := &Domain{Name: d}
if err := p.fillMeta(domain); err != nil { if err := p.fillMeta(domain); err != nil {
log.Printf("Filling meta data failed: %v\n", err) log.Printf("Filling meta data failed: %v\n", err)
// reporters depend on role.
continue
}
for _, r := range buildReporters(*domain.Role) {
r.report(p, domain)
} }
report.Domains = append(report.Domains, domain) report.Domains = append(report.Domains, domain)
@ -287,7 +292,6 @@ func (p *processor) domainChecks(domain string) []func(*processor, string) error
direct := strings.HasPrefix(domain, "https://") direct := strings.HasPrefix(domain, "https://")
checks := []func(*processor, string) error{ checks := []func(*processor, string) error{
(*processor).checkProviderMetadata,
(*processor).checkPGPKeys, (*processor).checkPGPKeys,
} }
@ -1113,26 +1117,32 @@ func (p *processor) checkListing(string) error {
// decodes, and validates against the JSON schema. // decodes, and validates against the JSON schema.
// According to the result, the respective error messages added to // According to the result, the respective error messages added to
// badProviderMetadata. // badProviderMetadata.
// It returns nil if all checks are passed. func (p *processor) checkProviderMetadata(domain string) bool {
func (p *processor) checkProviderMetadata(domain string) error {
p.badProviderMetadata.use() p.badProviderMetadata.use()
client := p.httpClient() client := p.httpClient()
lpmd := csaf.LoadProviderMetadataForDomain(client, domain, p.badProviderMetadata.warn) loader := csaf.NewProviderMetadataLoader(client)
lpmd := loader.Load(domain)
for i := range lpmd.Messages {
// TODO: Filter depending on the role.
p.badProviderMetadata.error(lpmd.Messages[i].Message)
}
if !lpmd.Valid() { if !lpmd.Valid() {
p.badProviderMetadata.error("No valid provider-metadata.json found.") p.badProviderMetadata.error("No valid provider-metadata.json found.")
p.badProviderMetadata.error("STOPPING here - cannot perform other checks.") p.badProviderMetadata.error("STOPPING here - cannot perform other checks.")
return errStop return false
} }
p.pmdURL = lpmd.URL p.pmdURL = lpmd.URL
p.pmd256 = lpmd.Hash p.pmd256 = lpmd.Hash
p.pmd = lpmd.Document p.pmd = lpmd.Document
return nil return true
} }
// checkSecurity checks the security.txt file by making HTTP request to fetch it. // checkSecurity checks the security.txt file by making HTTP request to fetch it.

View file

@ -12,6 +12,8 @@ import (
"fmt" "fmt"
"sort" "sort"
"strings" "strings"
"github.com/csaf-poc/csaf_distribution/csaf"
) )
type ( type (
@ -22,6 +24,8 @@ type (
validReporter struct{ baseReporter } validReporter struct{ baseReporter }
filenameReporter struct{ baseReporter } filenameReporter struct{ baseReporter }
tlsReporter struct{ baseReporter } tlsReporter struct{ baseReporter }
tlpWhiteReporter struct{ baseReporter }
tlpAmberRedReporter struct{ baseReporter }
redirectsReporter struct{ baseReporter } redirectsReporter struct{ baseReporter }
providerMetadataReport struct{ baseReporter } providerMetadataReport struct{ baseReporter }
securityReporter struct{ baseReporter } securityReporter struct{ baseReporter }
@ -31,11 +35,83 @@ type (
indexReporter struct{ baseReporter } indexReporter struct{ baseReporter }
changesReporter struct{ baseReporter } changesReporter struct{ baseReporter }
directoryListingsReporter struct{ baseReporter } directoryListingsReporter struct{ baseReporter }
rolieFeedReporter struct{ baseReporter }
rolieServiceReporter struct{ baseReporter }
rolieCategoryReporter struct{ baseReporter }
integrityReporter struct{ baseReporter } integrityReporter struct{ baseReporter }
signaturesReporter struct{ baseReporter } signaturesReporter struct{ baseReporter }
publicPGPKeyReporter struct{ baseReporter } publicPGPKeyReporter struct{ baseReporter }
listReporter struct{ baseReporter }
hasTwoReporter struct{ baseReporter }
mirrorReporter struct{ baseReporter }
) )
var reporters = [23]reporter{
&validReporter{baseReporter{num: 1, description: "Valid CSAF documents"}},
&filenameReporter{baseReporter{num: 2, description: "Filename"}},
&tlsReporter{baseReporter{num: 3, description: "TLS"}},
&tlpWhiteReporter{baseReporter{num: 4, description: "TLP:WHITE"}},
&tlpAmberRedReporter{baseReporter{num: 5, description: "TLP:AMBER and TLP:RED"}},
&redirectsReporter{baseReporter{num: 6, description: "Redirects"}},
&providerMetadataReport{baseReporter{num: 7, description: "provider-metadata.json"}},
&securityReporter{baseReporter{num: 8, description: "security.txt"}},
&wellknownMetadataReporter{baseReporter{num: 9, description: "/.well-known/csaf/provider-metadata.json"}},
&dnsPathReporter{baseReporter{num: 10, description: "DNS path"}},
&oneFolderPerYearReport{baseReporter{num: 11, description: "One folder per year"}},
&indexReporter{baseReporter{num: 12, description: "index.txt"}},
&changesReporter{baseReporter{num: 13, description: "changes.csv"}},
&directoryListingsReporter{baseReporter{num: 14, description: "Directory listings"}},
&rolieFeedReporter{baseReporter{num: 15, description: "ROLIE feed"}},
&rolieServiceReporter{baseReporter{num: 16, description: "ROLIE service document"}},
&rolieCategoryReporter{baseReporter{num: 17, description: "ROLIE category document"}},
&integrityReporter{baseReporter{num: 18, description: "Integrity"}},
&signaturesReporter{baseReporter{num: 19, description: "Signatures"}},
&publicPGPKeyReporter{baseReporter{num: 20, description: "Public OpenPGP Key"}},
&listReporter{baseReporter{num: 21, description: "List of CSAF providers"}},
&hasTwoReporter{baseReporter{num: 22, description: "Two disjoint issuing parties"}},
&mirrorReporter{baseReporter{num: 23, description: "Mirror"}},
}
var roleImplies = map[csaf.MetadataRole][]csaf.MetadataRole{
csaf.MetadataRoleProvider: {csaf.MetadataRolePublisher},
csaf.MetadataRoleTrustedProvider: {csaf.MetadataRoleProvider},
}
func requirements(role csaf.MetadataRole) [][2]int {
var own [][2]int
switch role {
case csaf.MetadataRoleTrustedProvider:
own = [][2]int{{18, 20}}
case csaf.MetadataRoleProvider:
// TODO: use commented numbers when TLPs should be checked.
own = [][2]int{{6 /* 5 */, 7}, {8, 10}, {11, 14}, {15, 17}}
case csaf.MetadataRolePublisher:
own = [][2]int{{1, 3 /* 4 */}}
}
for _, base := range roleImplies[role] {
own = append(own, requirements(base)...)
}
return own
}
// buildReporters initializes each report by assigning a number and description to it.
// It returns an array of the reporter interface type.
func buildReporters(role csaf.MetadataRole) []reporter {
var reps []reporter
reqs := requirements(role)
// sort to have them ordered by there number.
sort.Slice(reqs, func(i, j int) bool { return reqs[i][0] < reqs[j][0] })
for _, req := range reqs {
from, to := req[0]-1, req[1]-1
for i := from; i <= to; i++ {
if rep := reporters[i]; rep != nil {
reps = append(reps, rep)
}
}
}
return reps
}
func (bc *baseReporter) requirement(domain *Domain) *Requirement { func (bc *baseReporter) requirement(domain *Domain) *Requirement {
req := &Requirement{ req := &Requirement{
Num: bc.num, Num: bc.num,
@ -115,6 +191,21 @@ func (r *tlsReporter) report(p *processor, domain *Domain) {
req.message(ErrorType, urls...) req.message(ErrorType, urls...)
} }
// report tests if a document labeled TLP:WHITE
// is freely accessible and sets the "message" field value
// of the "Requirement" struct as a result of that.
func (r *tlpWhiteReporter) report(_ *processor, _ *Domain) {
// TODO
}
// report tests if a document labeled TLP:AMBER
// or TLP:RED is access protected
// and sets the "message" field value
// of the "Requirement" struct as a result of that.
func (r *tlpAmberRedReporter) report(_ *processor, _ *Domain) {
// TODO
}
// report tests if redirects are used and sets the "message" field value // report tests if redirects are used and sets the "message" field value
// of the "Requirement" struct as a result of that. // of the "Requirement" struct as a result of that.
func (r *redirectsReporter) report(p *processor, domain *Domain) { func (r *redirectsReporter) report(p *processor, domain *Domain) {
@ -269,6 +360,31 @@ func (r *directoryListingsReporter) report(p *processor, domain *Domain) {
req.Messages = p.badDirListings req.Messages = p.badDirListings
} }
// report checks whether there is only a single ROLIE feed for a
// given TLP level and whether any of the TLP levels
// TLP:WHITE, TLP:GREEN or unlabeled exists and sets the "message" field value
// of the "Requirement" struct as a result of that.
func (r *rolieFeedReporter) report(_ *processor, _ *Domain) {
// TODO
}
// report tests whether a ROLIE service document is used and if so,
// whether it is a [RFC8322] conform JSON file that lists the
// ROLIE feed documents and sets the "message" field value
// of the "Requirement" struct as a result of that.
func (r *rolieServiceReporter) report(_ *processor, _ *Domain) {
// TODO
}
// report tests whether a ROLIE category document is used and if so,
// whether it is a [RFC8322] conform JSON file and is used to dissect
// documents by certain criteria
// and sets the "message" field value
// of the "Requirement" struct as a result of that.
func (r *rolieCategoryReporter) report(_ *processor, _ *Domain) {
// TODO
}
func (r *integrityReporter) report(p *processor, domain *Domain) { func (r *integrityReporter) report(p *processor, domain *Domain) {
req := r.requirement(domain) req := r.requirement(domain)
if !p.badIntegrities.used() { if !p.badIntegrities.used() {
@ -306,3 +422,25 @@ func (r *publicPGPKeyReporter) report(p *processor, domain *Domain) {
p.keys.CountEntities())) p.keys.CountEntities()))
} }
} }
// report tests whether a CSAF aggregator JSON schema conform
// aggregator.json exists without being adjacent to a
// provider-metadata.json
func (r *listReporter) report(_ *processor, _ *Domain) {
// TODO
}
// report tests whether the aggregator.json lists at least
// two disjoint issuing parties. TODO: reevaluate phrasing (Req 7.1.22)
func (r *hasTwoReporter) report(_ *processor, _ *Domain) {
// TODO
}
// report tests whether the CSAF documents of each issuing mirrored party
// is in a different folder, which are adjacent to the aggregator.json and
// if the folder name is retrieved from the name of the issuing authority.
// It also tests whether each folder has a provider-metadata.json for their
// party and provides ROLIE feed documents.
func (r *mirrorReporter) report(_ *processor, _ *Domain) {
// TODO
}

View file

@ -118,11 +118,16 @@ func (d *downloader) httpClient() util.Client {
func (d *downloader) download(ctx context.Context, domain string) error { func (d *downloader) download(ctx context.Context, domain string) error {
client := d.httpClient() client := d.httpClient()
lpmd := csaf.LoadProviderMetadataForDomain( loader := csaf.NewProviderMetadataLoader(client)
client, domain, func(format string, args ...any) {
log.Printf( lpmd := loader.Load(domain)
"Looking for provider-metadata.json of '"+domain+"': "+format+"\n", args...)
}) if d.opts.Verbose {
for i := range lpmd.Messages {
log.Printf("Loading provider-metadata.json for %q: %s\n",
domain, lpmd.Messages[i].Message)
}
}
if !lpmd.Valid() { if !lpmd.Valid() {
return fmt.Errorf("no valid provider-metadata.json found for '%s'", domain) return fmt.Errorf("no valid provider-metadata.json found for '%s'", domain)

328
csaf/providermetaloader.go Normal file
View file

@ -0,0 +1,328 @@
// 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: 2023 German Federal Office for Information Security (BSI) <https://www.bsi.bund.de>
// Software-Engineering: 2023 Intevation GmbH <https://intevation.de>
package csaf
import (
"bytes"
"crypto/sha256"
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
"github.com/csaf-poc/csaf_distribution/util"
)
// ProviderMetadataLoader helps load provider-metadata.json from
// the various locations.
type ProviderMetadataLoader struct {
client util.Client
already map[string]*LoadedProviderMetadata
messages ProviderMetadataLoadMessages
}
// ProviderMetadataLoadMessageType is the type of the message.
type ProviderMetadataLoadMessageType int
const (
//JSONDecodingFailed indicates problems with JSON decoding
JSONDecodingFailed ProviderMetadataLoadMessageType = iota
// SchemaValidationFailed indicates a general problem with schema validation.
SchemaValidationFailed
// SchemaValidationFailedDetail is a failure detail in schema validation.
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
// a provider meta data file.
type ProviderMetadataLoadMessage struct {
Type ProviderMetadataLoadMessageType
Message string
}
// ProviderMetadataLoadMessages is a list of loading messages.
type ProviderMetadataLoadMessages []ProviderMetadataLoadMessage
// LoadedProviderMetadata represents a loaded provider metadata.
type LoadedProviderMetadata struct {
// URL is location where the document was found.
URL string
// Document is the de-serialized JSON document.
Document any
// Hash is a SHA256 sum over the document.
Hash []byte
// Messages are the error message happened while loading.
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.
func (lpm *LoadedProviderMetadata) Valid() bool {
return lpm != nil && lpm.Document != nil && lpm.Hash != nil
}
// NewProviderMetadataLoader create a new loader.
func NewProviderMetadataLoader(client util.Client) *ProviderMetadataLoader {
return &ProviderMetadataLoader{
client: client,
already: map[string]*LoadedProviderMetadata{},
}
}
// Load loads a provider metadata for a given path.
// If the domain starts with `https://` it only attemps to load
// the data from that URL.
func (pmdl *ProviderMetadataLoader) Load(domain string) *LoadedProviderMetadata {
// Check direct path
if strings.HasPrefix(domain, "https://") {
return pmdl.loadFromURL(domain)
}
// First try the well-known path.
wellknownURL := "https://" + domain + "/.well-known/csaf/provider-metadata.json"
wellknownResult := pmdl.loadFromURL(wellknownURL)
// 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
return pmdl.loadFromURL(dnsURL)
}
// 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 := pmdl.loadFromURL(url)
// If loading failed note it down.
if !lpmd.Valid() {
pmdl.messages.AppendUnique(lpmd.Messages)
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.
func (pmdl *ProviderMetadataLoader) loadFromURL(path string) *LoadedProviderMetadata {
result := LoadedProviderMetadata{URL: path}
res, err := pmdl.client.Get(path)
if err != nil {
result.Messages.Add(
HTTPFailed,
fmt.Sprintf("fetching %q failed: %v", path, err))
return &result
}
if res.StatusCode != http.StatusOK {
result.Messages.Add(
HTTPFailed,
fmt.Sprintf("fetching %q failed: %s (%d)", path, res.Status, res.StatusCode))
return &result
}
// TODO: Check for application/json and log it.
defer res.Body.Close()
// Calculate checksum for later comparison.
hash := sha256.New()
tee := io.TeeReader(res.Body, hash)
var doc any
if err := json.NewDecoder(tee).Decode(&doc); err != nil {
result.Messages.Add(
JSONDecodingFailed,
fmt.Sprintf("JSON decoding failed: %v", err))
return &result
}
// 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
}
// write it back as loaded
switch errors, err := ValidateProviderMetadata(doc); {
case err != nil:
result.Messages.Add(
SchemaValidationFailed,
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.Add(
SchemaValidationFailedDetail,
strings.ReplaceAll(msg, `%`, `%%`))
}
default:
// Only store in result if validation passed.
result.Document = doc
result.Hash = sum
}
pmdl.already[key] = &result
return &result
}

View file

@ -10,299 +10,10 @@ package csaf
import ( import (
"bufio" "bufio"
"bytes"
"crypto/sha256"
"encoding/json"
"fmt"
"io" "io"
"log"
"net/http"
"strings" "strings"
"github.com/csaf-poc/csaf_distribution/util"
) )
// LoadedProviderMetadata represents a loaded provider metadata.
type LoadedProviderMetadata struct {
// URL is location where the document was found.
URL string
// Document is the de-serialized JSON document.
Document any
// Hash is a SHA256 sum over the document.
Hash []byte
// Messages are the error message happened while loading.
Messages []string
}
// Valid returns true if the loaded document is valid.
func (lpm *LoadedProviderMetadata) Valid() bool {
return lpm != nil && lpm.Document != nil && lpm.Hash != nil
}
// defaultLogging generates a logging function if given is nil.
func defaultLogging(
logging func(format string, args ...any),
prefix, suffix string,
) func(format string, args ...any) {
if logging != nil {
return logging
}
return func(format string, args ...any) {
log.Printf(prefix+format+suffix, args...)
}
}
// LoadProviderMetadataFromURL loads a provider metadata from a given URL.
// Returns nil if the document was not found.
func LoadProviderMetadataFromURL(
client util.Client,
url string,
already map[string]*LoadedProviderMetadata,
logging func(format string, args ...any),
) *LoadedProviderMetadata {
logging = defaultLogging(logging, "LoadProviderMetadataFromURL: ", "\n")
res, err := client.Get(url)
if err != nil {
logging("Fetching %q failed: %v", url, err)
return nil
}
if res.StatusCode != http.StatusOK {
logging("Fetching %q failed: %s (%d)", url, res.Status, res.StatusCode)
return nil
}
// TODO: Check for application/json and log it.
defer res.Body.Close()
// Calculate checksum for later comparison.
hash := sha256.New()
result := LoadedProviderMetadata{URL: url}
tee := io.TeeReader(res.Body, hash)
var doc any
err = json.NewDecoder(tee).Decode(&doc)
// 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)
var key string
if already != nil {
key = string(sum)
if r, ok := already[key]; ok {
return r
}
}
// write it back as loaded
storeLoaded := func() {
if already != nil {
already[key] = &result
}
}
// We have loaded it the first time.
if err != nil {
result.Messages = []string{fmt.Sprintf("%s: Decoding JSON failed: %v", url, err)}
storeLoaded()
return &result
}
switch errors, err := ValidateProviderMetadata(doc); {
case err != nil:
result.Messages = []string{
fmt.Sprintf("%s: Validating against JSON schema failed: %v", url, err)}
case len(errors) > 0:
result.Messages = []string{
fmt.Sprintf("%s: Validating against JSON schema failed: %v", url, err)}
for _, msg := range errors {
result.Messages = append(result.Messages, strings.ReplaceAll(msg, `%`, `%%`))
}
default:
// Only store in result if validation passed.
result.Document = doc
result.Hash = sum
}
storeLoaded()
return &result
}
// LoadProviderMetadatasFromSecurity loads a secturity.txt,
// extracts and the CSAF urls from the document.
// Returns nil if no url was successfully found.
func LoadProviderMetadatasFromSecurity(
client util.Client,
path string,
already map[string]*LoadedProviderMetadata,
logging func(format string, args ...any),
) []*LoadedProviderMetadata {
logging = defaultLogging(logging, "LoadProviderMetadataFromSecurity: ", "\n")
res, err := client.Get(path)
if err != nil {
logging("Fetching %q failed: %v", path, err)
return nil
}
if res.StatusCode != http.StatusOK {
logging("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 {
// Treat as not found
return nil
}
var results []*LoadedProviderMetadata
// Load the URLs
for _, url := range urls {
if result := LoadProviderMetadataFromURL(
client, url, already, logging,
); result.Valid() {
results = append(results, result)
}
}
return results
}
// LoadProviderMetadataForDomain loads a provider metadata for a given domain.
// Returns nil if no provider metadata (PMD) was found.
// If the domain starts with `https://` it only attemps to load
// the data from that URL.
// The logging can be used to track the errors happening while loading.
func LoadProviderMetadataForDomain(
client util.Client,
domain string,
logging func(format string, args ...any),
) *LoadedProviderMetadata {
logging = defaultLogging(logging, "LoadProviderMetadataForDomain: ", "\n")
// As many URLs may lead to the same content only log once per content.
alreadyLogged := map[*LoadedProviderMetadata]string{}
lg := func(result *LoadedProviderMetadata, url string) {
if result == nil {
logging("%q not found.", url)
return
}
if other := alreadyLogged[result]; other != "" {
logging("%q is same %q.", url, other)
return
}
alreadyLogged[result] = url
for _, msg := range result.Messages {
logging(msg)
}
}
// keey track of already loaded pmds.
already := map[string]*LoadedProviderMetadata{}
// check direct path
if strings.HasPrefix(domain, "https://") {
result := LoadProviderMetadataFromURL(
client, domain, already, logging)
lg(result, domain)
return result
}
// Valid provider metadata under well-known.
var wellknownGood *LoadedProviderMetadata
// First try the well-known path.
wellknownURL := "https://" + domain + "/.well-known/csaf/provider-metadata.json"
wellknownResult := LoadProviderMetadataFromURL(
client, wellknownURL, already, logging)
lg(wellknownResult, wellknownURL)
// We have a candidate.
if wellknownResult.Valid() {
wellknownGood = wellknownResult
}
// Next load the PMDs from security.txt
secURL := "https://" + domain + "/.well-known/security.txt"
secResults := LoadProviderMetadatasFromSecurity(
client, secURL, already, logging)
if len(secResults) == 0 {
logging("%s failed to load.", secURL)
} else {
// Filter out the results which are valid.
var secGoods []*LoadedProviderMetadata
for _, result := range secResults {
if len(result.Messages) > 0 {
lg(result, result.URL)
} else {
secGoods = append(secGoods, result)
}
}
// 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) {
// Mention extra CSAF entries
for _, extra := range secGoods[1:] {
logging("Ignoring extra CSAF entry in security.txt: %s", extra.URL)
}
} else {
// Complaint about not matching.
logging("First entry of security.txt and well-known don't match.")
// List all the security urls.
for _, sec := range secGoods {
logging("Ignoring CSAF entry in security.txt: %s", sec.URL)
}
}
// Take the good well-known.
return wellknownGood
}
// Don't have well-known. Take first good from security.txt.
// Mention extra CSAF entries
for _, extra := range secGoods[1:] {
logging("Ignoring extra CSAF entry in security.txt: %s", extra.URL)
}
return secGoods[0]
}
}
// If we have a good well-known take it.
if wellknownGood != nil {
return wellknownGood
}
// Last resort: fall back to DNS.
dnsURL := "https://csaf.data.security." + domain
dnsResult := LoadProviderMetadataFromURL(
client, dnsURL, already, logging)
lg(dnsResult, dnsURL)
return dnsResult
}
// ExtractProviderURL extracts URLs of provider metadata. // ExtractProviderURL extracts URLs of provider metadata.
// If all is true all URLs are returned. Otherwise only the first is returned. // If all is true all URLs are returned. Otherwise only the first is returned.
func ExtractProviderURL(r io.Reader, all bool) ([]string, error) { func ExtractProviderURL(r io.Reader, all bool) ([]string, error) {