1
0
Fork 0
mirror of https://github.com/gocsaf/csaf.git synced 2025-12-22 11:55:40 +01:00
gocsaf/csaf/util.go
JanHoefelmeyer 3bb8ea0019
Improve checker regarding PMD location problems
* Change checking to test for Security, wellknown and DNS requirement at once and only throws error if all three fail.
* Use security.txt parser from csaf/util to extract provider url.
* Improve code comments and messages for the reports.

Co-authored-by: Jan Höfelmeyer <Jan Höfelmeyer jhoefelmeyer@intevation.de>
Co-authored-by: Sascha L. Teichmann <sascha.teichmann@intevation.de>
Co-authored-by: Bernhard Reiter <bernhard@intevation.de>
2022-08-26 13:31:56 +02:00

316 lines
8.1 KiB
Go

// 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"
"bytes"
"crypto/sha256"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"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 interface{}
// Hash is a SHA256 sum over the document.
Hash []byte
// Messages are the error message happened while loading.
Messages []string
}
// defaultLogging generates a logging function if given is nil.
func defaultLogging(
logging func(format string, args ...interface{}),
prefix, suffix string,
) func(format string, args ...interface{}) {
if logging != nil {
return logging
}
return func(format string, args ...interface{}) {
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 ...interface{}),
) *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)
err = json.NewDecoder(tee).Decode(&result.Document)
// Before checking the err lets check if we had the same
// document before. If so it will have failed parsing before.
result.Hash = hash.Sum(nil)
var key string
if already != nil {
key = string(result.Hash)
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(result.Document); {
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, `%`, `%%`))
}
}
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 ...interface{}),
) []*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 != nil {
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 ...interface{}),
) *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 != nil {
wellknownGood = wellknownResult
}
// Next load the PMDs from security.txt
secURL := "https://" + domain + "/.well-known/security.txt"
secResults := LoadProviderMetadatasFromSecurity(
client, secURL, already, logging)
if secResults == nil {
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.
// 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
}