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

WIP: Refactored for simpler reporting.

This commit is contained in:
Sascha L. Teichmann 2021-12-15 19:41:29 +01:00
parent d8ccf9ff41
commit 534b96d211
4 changed files with 292 additions and 388 deletions

View file

@ -9,362 +9,152 @@
package main package main
import ( import (
"bufio"
"bytes"
"crypto/sha256"
"fmt" "fmt"
"io"
"net/http"
"net/url"
"sort" "sort"
"strings"
"github.com/ProtonMail/gopenpgp/v2/crypto"
"github.com/csaf-poc/csaf_distribution/csaf"
"github.com/csaf-poc/csaf_distribution/util"
) )
type baseCheck struct { type (
exec int baseReporter struct {
num int num int
description string description string
messages []string }
} tlsReport struct{ baseReporter }
redirectsReport struct{ baseReporter }
providerMetadataReport struct{ baseReporter }
securityReport struct{ baseReporter }
wellknownMetadataReport struct{ baseReporter }
dnsPathReport struct{ baseReporter }
oneFolderPerYearReport struct{ baseReporter }
indexReport struct{ baseReporter }
changesReport struct{ baseReporter }
directoryListingsReport struct{ baseReporter }
integrityReport struct{ baseReporter }
signaturesReport struct{ baseReporter }
publicPGPKeyReport struct{ baseReporter }
)
type tlsCheck struct { func (bc *baseReporter) requirement(domain *Domain) *Requirement {
baseCheck
}
type redirectsCheck struct {
baseCheck
}
type providerMetadataCheck struct {
baseCheck
}
type securityCheck struct {
baseCheck
}
type wellknownMetadataCheck struct {
baseCheck
}
type dnsPathCheck struct {
baseCheck
}
type oneFolderPerYearCheck struct {
baseCheck
}
type indexCheck struct {
baseCheck
}
type changesCheck struct {
baseCheck
}
type directoryListingsCheck struct {
baseCheck
}
type integrityCheck struct {
baseCheck
}
type signaturesCheck struct {
baseCheck
}
type publicPGPKeyCheck struct {
baseCheck
}
func (bc *baseCheck) executionOrder() int {
return bc.exec
}
func (bc *baseCheck) run(*processor, string) error {
return nil
}
func (bc *baseCheck) report(_ *processor, domain *Domain) {
req := &Requirement{ req := &Requirement{
Num: bc.num, Num: bc.num,
Description: bc.description, Description: bc.description,
Messages: bc.messages,
} }
domain.Requirements = append(domain.Requirements, req) domain.Requirements = append(domain.Requirements, req)
return req
} }
func (bc *baseCheck) add(messages ...string) { func (r *tlsReport) report(p *processor, domain *Domain) {
bc.messages = append(bc.messages, messages...) req := r.requirement(domain)
}
func (bc *baseCheck) sprintf(format string, args ...interface{}) {
msg := fmt.Sprintf(format, args...)
bc.messages = append(bc.messages, msg)
}
func (bc *baseCheck) ok(message string) bool {
k := len(bc.messages) == 0
if k {
bc.messages = []string{message}
}
return k
}
func (tc *tlsCheck) run(p *processor, domain string) error {
if len(p.noneTLS) == 0 { if len(p.noneTLS) == 0 {
tc.add("All tested URLs were https.") req.message("All tested URLs were https.")
} else { return
urls := make([]string, len(p.noneTLS))
var i int
for k := range p.noneTLS {
urls[i] = k
i++
}
sort.Strings(urls)
tc.add("Following none https URLs were used:")
tc.add(urls...)
} }
return nil
urls := make([]string, len(p.noneTLS))
var i int
for k := range p.noneTLS {
urls[i] = k
i++
}
sort.Strings(urls)
req.message("Following none https URLs were used:")
req.message(urls...)
} }
func (rc *redirectsCheck) run(p *processor, domain string) error { func (r *redirectsReport) report(p *processor, domain *Domain) {
req := r.requirement(domain)
if len(p.redirects) == 0 { if len(p.redirects) == 0 {
rc.add("No redirections found.") req.message("No redirections found.")
} else { return
keys := make([]string, len(p.redirects))
var i int
for k := range p.redirects {
keys[i] = k
i++
}
sort.Strings(keys)
for i, k := range keys {
keys[i] = fmt.Sprintf("Redirect %s: %s", k, p.redirects[k])
}
rc.baseCheck.messages = keys
} }
return nil
keys := make([]string, len(p.redirects))
var i int
for k := range p.redirects {
keys[i] = k
i++
}
sort.Strings(keys)
for i, k := range keys {
keys[i] = fmt.Sprintf("Redirect %s: %s", k, p.redirects[k])
}
req.Messages = keys
} }
func (pmdc *providerMetadataCheck) run(p *processor, domain string) error { func (r *providerMetadataReport) report(p *processor, domain *Domain) {
req := r.requirement(domain)
if err := p.checkProviderMetadata(domain, pmdc.sprintf); err != nil { if len(p.badProviderMetadatas) == 0 {
return err req.message("No problems with provider metadata.")
return
} }
req.Messages = p.badProviderMetadatas
pmdc.ok("No problems with provider metadata.")
return nil
} }
func (sc *securityCheck) run(p *processor, domain string) error { func (r *securityReport) report(p *processor, domain *Domain) {
path := "https://" + domain + "/.well-known/security.txt" req := r.requirement(domain)
client := p.httpClient() if len(p.badSecurity) == 0 {
res, err := client.Get(path) req.message("No problems with security.txt.")
if err != nil { return
return err
} }
if res.StatusCode != http.StatusOK { req.Messages = p.badSecurity
sc.sprintf("Fetching security failed. Status code %d (%s)", res.StatusCode, res.Status)
return nil
}
u, err := func() (string, error) {
defer res.Body.Close()
lines := bufio.NewScanner(res.Body)
for lines.Scan() {
line := lines.Text()
if strings.HasPrefix(line, "CSAF:") {
return strings.TrimSpace(line[6:]), nil
}
}
return "", lines.Err()
}()
if err != nil {
sc.sprintf("Error while reading security.txt: %s", err.Error())
}
if u == "" {
sc.add("No CSAF line found in security.txt.")
return nil
}
// Try to load
up, err := url.Parse(u)
if err != nil {
sc.sprintf("CSAF URL '%s' invalid: %s.", u, err.Error())
return nil
}
base, err := url.Parse("https://" + domain + "/.well-known/")
if err != nil {
return err
}
ur := base.ResolveReference(up)
u = ur.String()
p.checkTLS(u)
if res, err = client.Get(u); err != nil {
sc.sprintf("Cannot fetch %s from security.txt: %v", u, err)
return nil
}
if res.StatusCode != http.StatusOK {
sc.sprintf("Fetching %s failed. Status code %d (%s).",
u, res.StatusCode, res.Status)
return nil
}
defer res.Body.Close()
// Compare checksums to already read provider-metadata.json.
h := sha256.New()
if _, err := io.Copy(h, res.Body); err != nil {
sc.sprintf("Reading %s failed: %v.", u, err)
return nil
}
if !bytes.Equal(h.Sum(nil), p.pmd256) {
sc.sprintf(
"Content of %s from security.txt is not identical to .well-known/csaf/provider-metadata.json", u)
}
sc.ok("Valid CSAF line in security.txt found.")
return nil
} }
func (wmdc *wellknownMetadataCheck) run(*processor, string) error { func (r *wellknownMetadataReport) report(_ *processor, domain *Domain) {
// TODO: Implement me! // TODO: Implement me!
return nil req := r.requirement(domain)
_ = req
} }
func (dpc *dnsPathCheck) run(*processor, string) error { func (r *dnsPathReport) report(_ *processor, domain *Domain) {
// TODO: Implement me! // TODO: Implement me!
return nil req := r.requirement(domain)
_ = req
} }
func (ofpyc *oneFolderPerYearCheck) run(p *processor, domain string) error { func (r *oneFolderPerYearReport) report(_ *processor, domain *Domain) {
// TODO: This does not belong here!
return p.checkCSAFs(domain, ofpyc.sprintf)
}
func (ic *indexCheck) run(*processor, string) error {
// TODO: Implement me! // TODO: Implement me!
return nil req := r.requirement(domain)
_ = req
} }
func (cc *changesCheck) run(*processor, string) error { func (r *indexReport) report(_ *processor, domain *Domain) {
// TODO: Implement me! // TODO: Implement me!
return nil req := r.requirement(domain)
_ = req
} }
func (dlc *directoryListingsCheck) run(*processor, string) error { func (r *changesReport) report(_ *processor, domain *Domain) {
// TODO: Implement me! // TODO: Implement me!
return nil req := r.requirement(domain)
_ = req
} }
func (ic *integrityCheck) run(p *processor, _ string) error { func (r *directoryListingsReport) report(_ *processor, domain *Domain) {
if len(p.badHashes) > 0 { // TODO: Implement me!
ic.baseCheck.messages = p.badHashes req := r.requirement(domain)
} else { _ = req
ic.add("All checksums match.")
}
return nil
} }
func (sc *signaturesCheck) run(p *processor, _ string) error { func (r *integrityReport) report(p *processor, domain *Domain) {
if len(p.badSignatures) > 0 { req := r.requirement(domain)
sc.baseCheck.messages = p.badSignatures if len(p.badHashes) == 0 {
} else { req.message("All checksums match.")
sc.add("All signatures verified.") return
} }
return nil req.Messages = p.badHashes
} }
func (ppkc *publicPGPKeyCheck) run(p *processor, domain string) error { func (r *signaturesReport) report(p *processor, domain *Domain) {
req := r.requirement(domain)
src, err := p.jsonPath("$.pgp_keys") if len(p.badSignatures) == 0 {
if err != nil { req.message("All signatures verified.")
ppkc.sprintf("No PGP keys found: %v.", err) }
return nil req.Messages = p.badSignatures
}
func (r *publicPGPKeyReport) report(p *processor, domain *Domain) {
req := r.requirement(domain)
req.Messages = p.badPGPs
if len(p.keys) > 0 {
req.message(fmt.Sprintf("%d PGP key(s) loaded successfully.", len(p.keys)))
} }
var keys []csaf.PGPKey
if err := util.ReMarshalJSON(&keys, src); err != nil {
ppkc.sprintf("PGP keys invalid: %v.", err)
return nil
}
if len(keys) == 0 {
ppkc.add("No PGP keys found.")
return nil
}
// Try to load
client := p.httpClient()
base, err := url.Parse("https://" + domain + "/.well-known/csaf/provider-metadata.json")
if err != nil {
return err
}
for i := range keys {
key := &keys[i]
if key.URL == nil {
ppkc.sprintf("Missing URL for fingerprint %x.", key.Fingerprint)
continue
}
up, err := url.Parse(*key.URL)
if err != nil {
ppkc.sprintf("Invalid URL '%s': %v", *key.URL, err)
continue
}
up = base.ResolveReference(up)
u := up.String()
p.checkTLS(u)
res, err := client.Get(u)
if err != nil {
ppkc.sprintf("Fetching PGP key %s failed: %v.", u, err)
continue
}
if res.StatusCode != http.StatusOK {
ppkc.sprintf("Fetching PGP key %s status code: %d (%s)", u, res.StatusCode, res.Status)
continue
}
ckey, err := func() (*crypto.Key, error) {
defer res.Body.Close()
return crypto.NewKeyFromArmoredReader(res.Body)
}()
if err != nil {
ppkc.sprintf("Reading PGP key %s failed: %v", u, err)
continue
}
if ckey.GetFingerprint() != string(key.Fingerprint) {
ppkc.sprintf("Fingerprint of PGP key %s do not match remotely loaded.", u)
continue
}
keyring, err := crypto.NewKeyRing(ckey)
if err != nil {
ppkc.sprintf("Creatin key ring for %s failed: %v.", u, err)
continue
}
p.keys = append(p.keys, keyring)
}
if len(p.keys) == 0 {
ppkc.add("No PGP keys loaded.")
return nil
}
ppkc.ok(fmt.Sprintf("%d PGP key(s) loaded successfully.", len(p.keys)))
return nil
} }

View file

@ -98,21 +98,21 @@ func writeReport(report *Report, opts *options) error {
return writer(report, w) return writer(report, w)
} }
func buildChecks() []check { func buildReporters() []Reporter {
return []check{ return []Reporter{
&tlsCheck{baseCheck{exec: 13, num: 3, description: "TLS"}}, &tlsReport{baseReporter{num: 3, description: "TLS"}},
&redirectsCheck{baseCheck{exec: 12, num: 6, description: "Redirects"}}, &redirectsReport{baseReporter{num: 6, description: "Redirects"}},
&providerMetadataCheck{baseCheck{exec: 1, num: 7, description: "provider-metadata.json"}}, &providerMetadataReport{baseReporter{num: 7, description: "provider-metadata.json"}},
&securityCheck{baseCheck{exec: 2, num: 8, description: "security.txt"}}, &securityReport{baseReporter{num: 8, description: "security.txt"}},
&wellknownMetadataCheck{baseCheck{exec: 3, num: 9, description: "/.well-known/csaf/provider-metadata.json"}}, &wellknownMetadataReport{baseReporter{num: 9, description: "/.well-known/csaf/provider-metadata.json"}},
&dnsPathCheck{baseCheck{exec: 4, num: 10, description: "DNS path"}}, &dnsPathReport{baseReporter{num: 10, description: "DNS path"}},
&oneFolderPerYearCheck{baseCheck{exec: 6, num: 11, description: "One folder per year"}}, &oneFolderPerYearReport{baseReporter{num: 11, description: "One folder per year"}},
&indexCheck{baseCheck{exec: 7, num: 12, description: "index.txt"}}, &indexReport{baseReporter{num: 12, description: "index.txt"}},
&changesCheck{baseCheck{exec: 8, num: 13, description: "changes.csv"}}, &changesReport{baseReporter{num: 13, description: "changes.csv"}},
&directoryListingsCheck{baseCheck{exec: 9, num: 14, description: "Directory listings"}}, &directoryListingsReport{baseReporter{num: 14, description: "Directory listings"}},
&integrityCheck{baseCheck{exec: 11, num: 18, description: "Integrity"}}, &integrityReport{baseReporter{num: 18, description: "Integrity"}},
&signaturesCheck{baseCheck{exec: 12, num: 19, description: "Signatures"}}, &signaturesReport{baseReporter{num: 19, description: "Signatures"}},
&publicPGPKeyCheck{baseCheck{exec: 5, num: 20, description: "Public PGP Key"}}, &publicPGPKeyReport{baseReporter{num: 20, description: "Public PGP Key"}},
} }
} }
@ -129,7 +129,7 @@ func main() {
p := newProcessor(opts) p := newProcessor(opts)
report, err := p.run(buildChecks(), domains) report, err := p.run(buildReporters(), domains)
errCheck(err) errCheck(err)
errCheck(writeReport(report, opts)) errCheck(writeReport(report, opts))

View file

@ -21,7 +21,6 @@ import (
"io" "io"
"net/http" "net/http"
"net/url" "net/url"
"sort"
"strings" "strings"
"github.com/PaesslerAG/gval" "github.com/PaesslerAG/gval"
@ -39,19 +38,24 @@ type processor struct {
pmd256 []byte pmd256 []byte
pmd interface{} pmd interface{}
keys []*crypto.KeyRing keys []*crypto.KeyRing
badHashes []string
badSignatures []string badHashes []string
badPGPs []string
badSignatures []string
badProviderMetadatas []string
badSecurity []string
badIntegrity []string
builder gval.Language builder gval.Language
exprs map[string]gval.Evaluable exprs map[string]gval.Evaluable
} }
type check interface { type Reporter interface {
executionOrder() int
run(*processor, string) error
report(*processor, *Domain) report(*processor, *Domain)
} }
var errContinue = errors.New("continue")
func newProcessor(opts *options) *processor { func newProcessor(opts *options) *processor {
return &processor{ return &processor{
opts: opts, opts: opts,
@ -76,28 +80,29 @@ func (p *processor) clean() {
p.pmd256 = nil p.pmd256 = nil
p.pmd = nil p.pmd = nil
p.keys = nil p.keys = nil
p.badSignatures = nil
p.badHashes = nil p.badHashes = nil
p.badPGPs = nil
p.badSignatures = nil
p.badProviderMetadatas = nil
p.badSecurity = nil
p.badIntegrity = nil
} }
func (p *processor) run(checks []check, domains []string) (*Report, error) { func (p *processor) run(reporter []Reporter, domains []string) (*Report, error) {
var report Report var report Report
execs := make([]check, len(checks)) domainsLoop:
copy(execs, checks)
sort.SliceStable(execs, func(i, j int) bool {
return execs[i].executionOrder() < execs[j].executionOrder()
})
for _, d := range domains { for _, d := range domains {
for _, ch := range execs { if err := p.checkDomain(d); err != nil {
if err := ch.run(p, d); err != nil { if err == errContinue {
return nil, err continue domainsLoop
} }
return nil, err
} }
domain := &Domain{Name: d} domain := &Domain{Name: d}
for _, ch := range checks { for _, ch := range reporter {
ch.report(p, domain) ch.report(p, domain)
} }
report.Domains = append(report.Domains, domain) report.Domains = append(report.Domains, domain)
@ -107,6 +112,15 @@ func (p *processor) run(checks []check, domains []string) (*Report, error) {
return &report, nil return &report, nil
} }
func (p *processor) checkDomain(domain string) error {
// TODO: Implement me!
if err := p.checkProviderMetadata(domain); err != nil && err != errContinue {
return err
}
return nil
}
func (p *processor) jsonPath(expr string) (interface{}, error) { func (p *processor) jsonPath(expr string) (interface{}, error) {
if p.pmd == nil { if p.pmd == nil {
return nil, errors.New("no provider metadata loaded") return nil, errors.New("no provider metadata loaded")
@ -171,14 +185,22 @@ func (p *processor) httpClient() *http.Client {
return &client return &client
} }
func (p *processor) addBadHash(format string, args ...interface{}) { func (p *processor) badHash(format string, args ...interface{}) {
p.badHashes = append(p.badHashes, fmt.Sprintf(format, args...)) p.badHashes = append(p.badHashes, fmt.Sprintf(format, args...))
} }
func (p *processor) addBadSignature(format string, args ...interface{}) { func (p *processor) badSignature(format string, args ...interface{}) {
p.badSignatures = append(p.badSignatures, fmt.Sprintf(format, args...)) p.badSignatures = append(p.badSignatures, fmt.Sprintf(format, args...))
} }
func (p *processor) badProviderMetadata(format string, args ...interface{}) {
p.badProviderMetadatas = append(p.badProviderMetadatas, fmt.Sprintf(format, args...))
}
func (p *processor) badPGP(format string, args ...interface{}) {
p.badPGPs = append(p.badPGPs, fmt.Sprintf(format, args...))
}
func (p *processor) integrity( func (p *processor) integrity(
files []string, files []string,
base string, base string,
@ -250,11 +272,11 @@ func (p *processor) integrity(
hashFile := u + "." + x.ext hashFile := u + "." + x.ext
p.checkTLS(hashFile) p.checkTLS(hashFile)
if res, err = client.Get(hashFile); err != nil { if res, err = client.Get(hashFile); err != nil {
p.addBadHash("Fetching %s failed: %v.", hashFile, err) p.badHash("Fetching %s failed: %v.", hashFile, err)
continue continue
} }
if res.StatusCode != http.StatusOK { if res.StatusCode != http.StatusOK {
p.addBadHash("Fetching %s failed: Status code %d (%s)", p.badHash("Fetching %s failed: Status code %d (%s)",
hashFile, res.StatusCode, res.Status) hashFile, res.StatusCode, res.Status)
continue continue
} }
@ -263,15 +285,15 @@ func (p *processor) integrity(
return hashFromReader(res.Body) return hashFromReader(res.Body)
}() }()
if err != nil { if err != nil {
p.addBadHash("Reading %s failed: %v.", hashFile, err) p.badHash("Reading %s failed: %v.", hashFile, err)
continue continue
} }
if len(h) == 0 { if len(h) == 0 {
p.addBadHash("No hash found in %s.", hashFile) p.badHash("No hash found in %s.", hashFile)
continue continue
} }
if !bytes.Equal(h, x.hash) { if !bytes.Equal(h, x.hash) {
p.addBadHash("%s hash of %s does not match %s.", p.badHash("%s hash of %s does not match %s.",
strings.ToUpper(x.ext), u, hashFile) strings.ToUpper(x.ext), u, hashFile)
} }
} }
@ -281,11 +303,11 @@ func (p *processor) integrity(
p.checkTLS(sigFile) p.checkTLS(sigFile)
if res, err = client.Get(sigFile); err != nil { if res, err = client.Get(sigFile); err != nil {
p.addBadSignature("Fetching %s failed: %v.", sigFile, err) p.badSignature("Fetching %s failed: %v.", sigFile, err)
continue continue
} }
if res.StatusCode != http.StatusOK { if res.StatusCode != http.StatusOK {
p.addBadSignature("Fetching %s failed: status code %d (%s)", p.badSignature("Fetching %s failed: status code %d (%s)",
sigFile, res.StatusCode, res.Status) sigFile, res.StatusCode, res.Status)
continue continue
} }
@ -299,7 +321,7 @@ func (p *processor) integrity(
return crypto.NewPGPSignatureFromArmored(string(all)) return crypto.NewPGPSignatureFromArmored(string(all))
}() }()
if err != nil { if err != nil {
p.addBadSignature("Loading signature from %s failed: %v.", p.badSignature("Loading signature from %s failed: %v.",
sigFile, err) sigFile, err)
continue continue
} }
@ -315,7 +337,7 @@ func (p *processor) integrity(
} }
} }
if !verified { if !verified {
p.addBadSignature("Signature of %s could not be verified.", u) p.badSignature("Signature of %s could not be verified.", u)
} }
} }
} }
@ -327,13 +349,13 @@ func (p *processor) processFeed(feed string, lg func(string, ...interface{})) er
client := p.httpClient() client := p.httpClient()
res, err := client.Get(feed) res, err := client.Get(feed)
if err != nil { if err != nil {
lg("Cannot fetch feed %s: %v.", feed, err) lg("Cannot fetch feed %s: %v", feed, err)
return nil return errContinue
} }
if res.StatusCode != http.StatusOK { if res.StatusCode != http.StatusOK {
lg("Fetching %s failed. Status code %d (%s)", lg("Fetching %s failed. Status code %d (%s)",
feed, res.StatusCode, res.Status) feed, res.StatusCode, res.Status)
return nil return errContinue
} }
rfeed, err := func() (*csaf.ROLIEFeed, error) { rfeed, err := func() (*csaf.ROLIEFeed, error) {
defer res.Body.Close() defer res.Body.Close()
@ -341,11 +363,12 @@ func (p *processor) processFeed(feed string, lg func(string, ...interface{})) er
}() }()
if err != nil { if err != nil {
lg("Loading ROLIE feed failed: %v.", err) lg("Loading ROLIE feed failed: %v.", err)
return nil return errContinue
} }
base, err := basePath(feed) base, err := basePath(feed)
if err != nil { if err != nil {
return err lg("Bad base path: %v", err)
return errContinue
} }
// Extract the CSAF files from feed. // Extract the CSAF files from feed.
@ -380,7 +403,7 @@ func (p *processor) processFeeds(
} }
feedURL := base.ResolveReference(up).String() feedURL := base.ResolveReference(up).String()
p.checkTLS(feedURL) p.checkTLS(feedURL)
if err := p.processFeed(feedURL, lg); err != nil { if err := p.processFeed(feedURL, lg); err != nil && err != errContinue {
return err return err
} }
} }
@ -402,20 +425,25 @@ func (p *processor) checkCSAFs(domain string, lg func(string, ...interface{})) e
var feeds [][]csaf.Feed var feeds [][]csaf.Feed
if err := util.ReMarshalJSON(&feeds, rolie); err != nil { if err := util.ReMarshalJSON(&feeds, rolie); err != nil {
lg("ROLIE feeds are not compatible: %v.", err) lg("ROLIE feeds are not compatible: %v.", err)
return nil goto noRolie
} }
if err := p.processFeeds(domain, feeds, lg); err != nil { if err := p.processFeeds(domain, feeds, lg); err != nil {
if err == errContinue {
goto noRolie
}
return err return err
} }
} else {
// No rolie feeds
// TODO: Implement me!
} }
noRolie:
// No rolie feeds
// TODO: Implement me!
return nil return nil
} }
func (p *processor) checkProviderMetadata(domain string, lg func(string, ...interface{})) error { func (p *processor) checkProviderMetadata(domain string) error {
client := p.httpClient() client := p.httpClient()
@ -423,14 +451,14 @@ func (p *processor) checkProviderMetadata(domain string, lg func(string, ...inte
res, err := client.Get(url) res, err := client.Get(url)
if err != nil { if err != nil {
lg("Fetching %s: %v.", url, err) p.badProviderMetadata("Fetching %s: %v.", url, err)
return err return errContinue
} }
if res.StatusCode != http.StatusOK { if res.StatusCode != http.StatusOK {
lg("Fetching %s failed. Status code: %d (%s).", p.badProviderMetadata("Fetching %s failed. Status code: %d (%s)",
url, res.StatusCode, res.Status) url, res.StatusCode, res.Status)
return errors.New("Cannot load provider-metadata.json") return errContinue
} }
// Calculate checksum for later comparison. // Calculate checksum for later comparison.
@ -441,8 +469,8 @@ func (p *processor) checkProviderMetadata(domain string, lg func(string, ...inte
tee := io.TeeReader(res.Body, hash) tee := io.TeeReader(res.Body, hash)
return json.NewDecoder(tee).Decode(&p.pmd) return json.NewDecoder(tee).Decode(&p.pmd)
}(); err != nil { }(); err != nil {
lg("Decoding JSON failed: %v.", err) p.badProviderMetadata("Decoding JSON failed: %v", err)
return err return errContinue
} }
p.pmd256 = hash.Sum(nil) p.pmd256 = hash.Sum(nil)
@ -452,9 +480,9 @@ func (p *processor) checkProviderMetadata(domain string, lg func(string, ...inte
return err return err
} }
if len(errors) > 0 { if len(errors) > 0 {
lg("Validating against JSON schema failed:") p.badProviderMetadata("Validating against JSON schema failed:")
for _, msg := range errors { for _, msg := range errors {
lg(strings.ReplaceAll(msg, `%`, `%%`)) p.badProviderMetadata(strings.ReplaceAll(msg, `%`, `%%`))
} }
} }
return nil return nil
@ -467,13 +495,14 @@ func (p *processor) checkSecurity(domain string, lg func(string, ...interface{})
path := "https://" + domain + "/.well-known/security.txt" path := "https://" + domain + "/.well-known/security.txt"
res, err := client.Get(path) res, err := client.Get(path)
if err != nil { if err != nil {
return err lg("Fetchinig %s failed: %v", err)
return errContinue
} }
if res.StatusCode != http.StatusOK { if res.StatusCode != http.StatusOK {
lg("Fetching %s failed. Status code %d (%s)", lg("Fetching %s failed. Status code %d (%s)",
path, res.StatusCode, res.Status) path, res.StatusCode, res.Status)
return errors.New("fetching security.txt failed") return errContinue
} }
u, err := func() (string, error) { u, err := func() (string, error) {
@ -489,42 +518,42 @@ func (p *processor) checkSecurity(domain string, lg func(string, ...interface{})
}() }()
if err != nil { if err != nil {
lg("Error while reading security.txt: %v", err) lg("Error while reading security.txt: %v", err)
return err return errContinue
} }
if u == "" { if u == "" {
lg("No CSAF line found in security.txt.") lg("No CSAF line found in security.txt.")
return errors.New("no CSAF line in security.txt") return errContinue
} }
// Try to load // Try to load
up, err := url.Parse(u) up, err := url.Parse(u)
if err != nil { if err != nil {
lg("CSAF URL '%s' invalid: %v.", u, err.Error()) lg("CSAF URL '%s' invalid: %v", u, err)
return err return errContinue
} }
base, err := url.Parse("https://" + domain + "/.well-known/") base, err := url.Parse("https://" + domain + "/.well-known/")
if err != nil { if err != nil {
return err return err
} }
ur := base.ResolveReference(up)
u = ur.String() u = base.ResolveReference(up).String()
p.checkTLS(u) p.checkTLS(u)
if res, err = client.Get(u); err != nil { if res, err = client.Get(u); err != nil {
lg("Cannot fetch %s from security.txt: %v", u, err) lg("Cannot fetch %s from security.txt: %v", u, err)
return nil return errContinue
} }
if res.StatusCode != http.StatusOK { if res.StatusCode != http.StatusOK {
lg("Fetching %s failed. Status code %d (%s).", lg("Fetching %s failed. Status code %d (%s)",
u, res.StatusCode, res.Status) u, res.StatusCode, res.Status)
return nil return errContinue
} }
defer res.Body.Close() defer res.Body.Close()
// Compare checksums to already read provider-metadata.json. // Compare checksums to already read provider-metadata.json.
h := sha256.New() h := sha256.New()
if _, err := io.Copy(h, res.Body); err != nil { if _, err := io.Copy(h, res.Body); err != nil {
lg("Reading %s failed: %v.", u, err) lg("Reading %s failed: %v", u, err)
return nil return errContinue
} }
if !bytes.Equal(h.Sum(nil), p.pmd256) { if !bytes.Equal(h.Sum(nil), p.pmd256) {
@ -533,3 +562,84 @@ func (p *processor) checkSecurity(domain string, lg func(string, ...interface{})
return nil return nil
} }
func (p *processor) checkPGPKeys(domain string, lg func(string, ...interface{})) error {
src, err := p.jsonPath("$.pgp_keys")
if err != nil {
lg("No PGP keys found: %v.", err)
return errContinue
}
var keys []csaf.PGPKey
if err := util.ReMarshalJSON(&keys, src); err != nil {
lg("PGP keys invalid: %v.", err)
return errContinue
}
if len(keys) == 0 {
lg("No PGP keys found.")
return errContinue
}
// Try to load
client := p.httpClient()
base, err := url.Parse("https://" + domain + "/.well-known/csaf/provider-metadata.json")
if err != nil {
return err
}
for i := range keys {
key := &keys[i]
if key.URL == nil {
lg("Missing URL for fingerprint %x.", key.Fingerprint)
continue
}
up, err := url.Parse(*key.URL)
if err != nil {
lg("Invalid URL '%s': %v", *key.URL, err)
continue
}
u := base.ResolveReference(up).String()
p.checkTLS(u)
res, err := client.Get(u)
if err != nil {
lg("Fetching PGP key %s failed: %v.", u, err)
continue
}
if res.StatusCode != http.StatusOK {
lg("Fetching PGP key %s status code: %d (%s)", u, res.StatusCode, res.Status)
continue
}
ckey, err := func() (*crypto.Key, error) {
defer res.Body.Close()
return crypto.NewKeyFromArmoredReader(res.Body)
}()
if err != nil {
lg("Reading PGP key %s failed: %v", u, err)
continue
}
if ckey.GetFingerprint() != string(key.Fingerprint) {
lg("Fingerprint of PGP key %s do not match remotely loaded.", u)
continue
}
keyring, err := crypto.NewKeyRing(ckey)
if err != nil {
lg("Creating key ring for %s failed: %v.", u, err)
continue
}
p.keys = append(p.keys, keyring)
}
if len(p.keys) == 0 {
lg("No PGP keys loaded.")
}
return nil
}

View file

@ -25,3 +25,7 @@ type Domain struct {
type Report struct { type Report struct {
Domains []*Domain `json:"domains,omitempty"` Domains []*Domain `json:"domains,omitempty"`
} }
func (r *Requirement) message(msg ...string) {
r.Messages = append(r.Messages, msg...)
}