mirror of
https://github.com/gocsaf/csaf.git
synced 2025-12-22 11:55:40 +01:00
WIP: Refactored for simpler reporting.
This commit is contained in:
parent
d8ccf9ff41
commit
534b96d211
4 changed files with 292 additions and 388 deletions
|
|
@ -9,118 +9,46 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"crypto/sha256"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"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 {
|
||||
exec int
|
||||
type (
|
||||
baseReporter struct {
|
||||
num int
|
||||
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 {
|
||||
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) {
|
||||
func (bc *baseReporter) requirement(domain *Domain) *Requirement {
|
||||
req := &Requirement{
|
||||
Num: bc.num,
|
||||
Description: bc.description,
|
||||
Messages: bc.messages,
|
||||
}
|
||||
domain.Requirements = append(domain.Requirements, req)
|
||||
return req
|
||||
}
|
||||
|
||||
func (bc *baseCheck) add(messages ...string) {
|
||||
bc.messages = append(bc.messages, messages...)
|
||||
}
|
||||
|
||||
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 {
|
||||
func (r *tlsReport) report(p *processor, domain *Domain) {
|
||||
req := r.requirement(domain)
|
||||
if len(p.noneTLS) == 0 {
|
||||
tc.add("All tested URLs were https.")
|
||||
} else {
|
||||
req.message("All tested URLs were https.")
|
||||
return
|
||||
}
|
||||
|
||||
urls := make([]string, len(p.noneTLS))
|
||||
var i int
|
||||
for k := range p.noneTLS {
|
||||
|
|
@ -128,16 +56,17 @@ func (tc *tlsCheck) run(p *processor, domain string) error {
|
|||
i++
|
||||
}
|
||||
sort.Strings(urls)
|
||||
tc.add("Following none https URLs were used:")
|
||||
tc.add(urls...)
|
||||
}
|
||||
return nil
|
||||
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 {
|
||||
rc.add("No redirections found.")
|
||||
} else {
|
||||
req.message("No redirections found.")
|
||||
return
|
||||
}
|
||||
|
||||
keys := make([]string, len(p.redirects))
|
||||
var i int
|
||||
for k := range p.redirects {
|
||||
|
|
@ -148,223 +77,84 @@ func (rc *redirectsCheck) run(p *processor, domain string) error {
|
|||
for i, k := range keys {
|
||||
keys[i] = fmt.Sprintf("Redirect %s: %s", k, p.redirects[k])
|
||||
}
|
||||
rc.baseCheck.messages = keys
|
||||
}
|
||||
return nil
|
||||
req.Messages = keys
|
||||
}
|
||||
|
||||
func (pmdc *providerMetadataCheck) run(p *processor, domain string) error {
|
||||
|
||||
if err := p.checkProviderMetadata(domain, pmdc.sprintf); err != nil {
|
||||
return err
|
||||
func (r *providerMetadataReport) report(p *processor, domain *Domain) {
|
||||
req := r.requirement(domain)
|
||||
if len(p.badProviderMetadatas) == 0 {
|
||||
req.message("No problems with provider metadata.")
|
||||
return
|
||||
}
|
||||
req.Messages = p.badProviderMetadatas
|
||||
}
|
||||
|
||||
pmdc.ok("No problems with provider metadata.")
|
||||
return nil
|
||||
func (r *securityReport) report(p *processor, domain *Domain) {
|
||||
req := r.requirement(domain)
|
||||
if len(p.badSecurity) == 0 {
|
||||
req.message("No problems with security.txt.")
|
||||
return
|
||||
}
|
||||
req.Messages = p.badSecurity
|
||||
}
|
||||
|
||||
func (sc *securityCheck) run(p *processor, domain string) error {
|
||||
path := "https://" + domain + "/.well-known/security.txt"
|
||||
client := p.httpClient()
|
||||
res, err := client.Get(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if res.StatusCode != http.StatusOK {
|
||||
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!
|
||||
return nil
|
||||
req := r.requirement(domain)
|
||||
_ = req
|
||||
}
|
||||
|
||||
func (dpc *dnsPathCheck) run(*processor, string) error {
|
||||
func (r *dnsPathReport) report(_ *processor, domain *Domain) {
|
||||
// TODO: Implement me!
|
||||
return nil
|
||||
req := r.requirement(domain)
|
||||
_ = req
|
||||
}
|
||||
|
||||
func (ofpyc *oneFolderPerYearCheck) run(p *processor, domain string) error {
|
||||
|
||||
// TODO: This does not belong here!
|
||||
return p.checkCSAFs(domain, ofpyc.sprintf)
|
||||
}
|
||||
|
||||
func (ic *indexCheck) run(*processor, string) error {
|
||||
func (r *oneFolderPerYearReport) report(_ *processor, domain *Domain) {
|
||||
// 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!
|
||||
return nil
|
||||
req := r.requirement(domain)
|
||||
_ = req
|
||||
}
|
||||
|
||||
func (dlc *directoryListingsCheck) run(*processor, string) error {
|
||||
func (r *changesReport) report(_ *processor, domain *Domain) {
|
||||
// TODO: Implement me!
|
||||
return nil
|
||||
req := r.requirement(domain)
|
||||
_ = req
|
||||
}
|
||||
|
||||
func (ic *integrityCheck) run(p *processor, _ string) error {
|
||||
if len(p.badHashes) > 0 {
|
||||
ic.baseCheck.messages = p.badHashes
|
||||
} else {
|
||||
ic.add("All checksums match.")
|
||||
}
|
||||
return nil
|
||||
func (r *directoryListingsReport) report(_ *processor, domain *Domain) {
|
||||
// TODO: Implement me!
|
||||
req := r.requirement(domain)
|
||||
_ = req
|
||||
}
|
||||
|
||||
func (sc *signaturesCheck) run(p *processor, _ string) error {
|
||||
if len(p.badSignatures) > 0 {
|
||||
sc.baseCheck.messages = p.badSignatures
|
||||
} else {
|
||||
sc.add("All signatures verified.")
|
||||
func (r *integrityReport) report(p *processor, domain *Domain) {
|
||||
req := r.requirement(domain)
|
||||
if len(p.badHashes) == 0 {
|
||||
req.message("All checksums match.")
|
||||
return
|
||||
}
|
||||
return nil
|
||||
req.Messages = p.badHashes
|
||||
}
|
||||
|
||||
func (ppkc *publicPGPKeyCheck) run(p *processor, domain string) error {
|
||||
|
||||
src, err := p.jsonPath("$.pgp_keys")
|
||||
if err != nil {
|
||||
ppkc.sprintf("No PGP keys found: %v.", err)
|
||||
return nil
|
||||
func (r *signaturesReport) report(p *processor, domain *Domain) {
|
||||
req := r.requirement(domain)
|
||||
if len(p.badSignatures) == 0 {
|
||||
req.message("All signatures verified.")
|
||||
}
|
||||
req.Messages = p.badSignatures
|
||||
}
|
||||
|
||||
var keys []csaf.PGPKey
|
||||
if err := util.ReMarshalJSON(&keys, src); err != nil {
|
||||
ppkc.sprintf("PGP keys invalid: %v.", err)
|
||||
return nil
|
||||
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)))
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -98,21 +98,21 @@ func writeReport(report *Report, opts *options) error {
|
|||
return writer(report, w)
|
||||
}
|
||||
|
||||
func buildChecks() []check {
|
||||
return []check{
|
||||
&tlsCheck{baseCheck{exec: 13, num: 3, description: "TLS"}},
|
||||
&redirectsCheck{baseCheck{exec: 12, num: 6, description: "Redirects"}},
|
||||
&providerMetadataCheck{baseCheck{exec: 1, num: 7, description: "provider-metadata.json"}},
|
||||
&securityCheck{baseCheck{exec: 2, num: 8, description: "security.txt"}},
|
||||
&wellknownMetadataCheck{baseCheck{exec: 3, num: 9, description: "/.well-known/csaf/provider-metadata.json"}},
|
||||
&dnsPathCheck{baseCheck{exec: 4, num: 10, description: "DNS path"}},
|
||||
&oneFolderPerYearCheck{baseCheck{exec: 6, num: 11, description: "One folder per year"}},
|
||||
&indexCheck{baseCheck{exec: 7, num: 12, description: "index.txt"}},
|
||||
&changesCheck{baseCheck{exec: 8, num: 13, description: "changes.csv"}},
|
||||
&directoryListingsCheck{baseCheck{exec: 9, num: 14, description: "Directory listings"}},
|
||||
&integrityCheck{baseCheck{exec: 11, num: 18, description: "Integrity"}},
|
||||
&signaturesCheck{baseCheck{exec: 12, num: 19, description: "Signatures"}},
|
||||
&publicPGPKeyCheck{baseCheck{exec: 5, num: 20, description: "Public PGP Key"}},
|
||||
func buildReporters() []Reporter {
|
||||
return []Reporter{
|
||||
&tlsReport{baseReporter{num: 3, description: "TLS"}},
|
||||
&redirectsReport{baseReporter{num: 6, description: "Redirects"}},
|
||||
&providerMetadataReport{baseReporter{num: 7, description: "provider-metadata.json"}},
|
||||
&securityReport{baseReporter{num: 8, description: "security.txt"}},
|
||||
&wellknownMetadataReport{baseReporter{num: 9, description: "/.well-known/csaf/provider-metadata.json"}},
|
||||
&dnsPathReport{baseReporter{num: 10, description: "DNS path"}},
|
||||
&oneFolderPerYearReport{baseReporter{num: 11, description: "One folder per year"}},
|
||||
&indexReport{baseReporter{num: 12, description: "index.txt"}},
|
||||
&changesReport{baseReporter{num: 13, description: "changes.csv"}},
|
||||
&directoryListingsReport{baseReporter{num: 14, description: "Directory listings"}},
|
||||
&integrityReport{baseReporter{num: 18, description: "Integrity"}},
|
||||
&signaturesReport{baseReporter{num: 19, description: "Signatures"}},
|
||||
&publicPGPKeyReport{baseReporter{num: 20, description: "Public PGP Key"}},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -129,7 +129,7 @@ func main() {
|
|||
|
||||
p := newProcessor(opts)
|
||||
|
||||
report, err := p.run(buildChecks(), domains)
|
||||
report, err := p.run(buildReporters(), domains)
|
||||
errCheck(err)
|
||||
|
||||
errCheck(writeReport(report, opts))
|
||||
|
|
|
|||
|
|
@ -21,7 +21,6 @@ import (
|
|||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/PaesslerAG/gval"
|
||||
|
|
@ -39,19 +38,24 @@ type processor struct {
|
|||
pmd256 []byte
|
||||
pmd interface{}
|
||||
keys []*crypto.KeyRing
|
||||
|
||||
badHashes []string
|
||||
badPGPs []string
|
||||
badSignatures []string
|
||||
badProviderMetadatas []string
|
||||
badSecurity []string
|
||||
badIntegrity []string
|
||||
|
||||
builder gval.Language
|
||||
exprs map[string]gval.Evaluable
|
||||
}
|
||||
|
||||
type check interface {
|
||||
executionOrder() int
|
||||
run(*processor, string) error
|
||||
type Reporter interface {
|
||||
report(*processor, *Domain)
|
||||
}
|
||||
|
||||
var errContinue = errors.New("continue")
|
||||
|
||||
func newProcessor(opts *options) *processor {
|
||||
return &processor{
|
||||
opts: opts,
|
||||
|
|
@ -76,28 +80,29 @@ func (p *processor) clean() {
|
|||
p.pmd256 = nil
|
||||
p.pmd = nil
|
||||
p.keys = nil
|
||||
p.badSignatures = 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
|
||||
|
||||
execs := make([]check, len(checks))
|
||||
copy(execs, checks)
|
||||
sort.SliceStable(execs, func(i, j int) bool {
|
||||
return execs[i].executionOrder() < execs[j].executionOrder()
|
||||
})
|
||||
|
||||
domainsLoop:
|
||||
for _, d := range domains {
|
||||
for _, ch := range execs {
|
||||
if err := ch.run(p, d); err != nil {
|
||||
if err := p.checkDomain(d); err != nil {
|
||||
if err == errContinue {
|
||||
continue domainsLoop
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
domain := &Domain{Name: d}
|
||||
for _, ch := range checks {
|
||||
for _, ch := range reporter {
|
||||
ch.report(p, domain)
|
||||
}
|
||||
report.Domains = append(report.Domains, domain)
|
||||
|
|
@ -107,6 +112,15 @@ func (p *processor) run(checks []check, domains []string) (*Report, error) {
|
|||
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) {
|
||||
if p.pmd == nil {
|
||||
return nil, errors.New("no provider metadata loaded")
|
||||
|
|
@ -171,14 +185,22 @@ func (p *processor) httpClient() *http.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...))
|
||||
}
|
||||
|
||||
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...))
|
||||
}
|
||||
|
||||
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(
|
||||
files []string,
|
||||
base string,
|
||||
|
|
@ -250,11 +272,11 @@ func (p *processor) integrity(
|
|||
hashFile := u + "." + x.ext
|
||||
p.checkTLS(hashFile)
|
||||
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
|
||||
}
|
||||
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)
|
||||
continue
|
||||
}
|
||||
|
|
@ -263,15 +285,15 @@ func (p *processor) integrity(
|
|||
return hashFromReader(res.Body)
|
||||
}()
|
||||
if err != nil {
|
||||
p.addBadHash("Reading %s failed: %v.", hashFile, err)
|
||||
p.badHash("Reading %s failed: %v.", hashFile, err)
|
||||
continue
|
||||
}
|
||||
if len(h) == 0 {
|
||||
p.addBadHash("No hash found in %s.", hashFile)
|
||||
p.badHash("No hash found in %s.", hashFile)
|
||||
continue
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
@ -281,11 +303,11 @@ func (p *processor) integrity(
|
|||
p.checkTLS(sigFile)
|
||||
|
||||
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
|
||||
}
|
||||
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)
|
||||
continue
|
||||
}
|
||||
|
|
@ -299,7 +321,7 @@ func (p *processor) integrity(
|
|||
return crypto.NewPGPSignatureFromArmored(string(all))
|
||||
}()
|
||||
if err != nil {
|
||||
p.addBadSignature("Loading signature from %s failed: %v.",
|
||||
p.badSignature("Loading signature from %s failed: %v.",
|
||||
sigFile, err)
|
||||
continue
|
||||
}
|
||||
|
|
@ -315,7 +337,7 @@ func (p *processor) integrity(
|
|||
}
|
||||
}
|
||||
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()
|
||||
res, err := client.Get(feed)
|
||||
if err != nil {
|
||||
lg("Cannot fetch feed %s: %v.", feed, err)
|
||||
return nil
|
||||
lg("Cannot fetch feed %s: %v", feed, err)
|
||||
return errContinue
|
||||
}
|
||||
if res.StatusCode != http.StatusOK {
|
||||
lg("Fetching %s failed. Status code %d (%s)",
|
||||
feed, res.StatusCode, res.Status)
|
||||
return nil
|
||||
return errContinue
|
||||
}
|
||||
rfeed, err := func() (*csaf.ROLIEFeed, error) {
|
||||
defer res.Body.Close()
|
||||
|
|
@ -341,11 +363,12 @@ func (p *processor) processFeed(feed string, lg func(string, ...interface{})) er
|
|||
}()
|
||||
if err != nil {
|
||||
lg("Loading ROLIE feed failed: %v.", err)
|
||||
return nil
|
||||
return errContinue
|
||||
}
|
||||
base, err := basePath(feed)
|
||||
if err != nil {
|
||||
return err
|
||||
lg("Bad base path: %v", err)
|
||||
return errContinue
|
||||
}
|
||||
|
||||
// Extract the CSAF files from feed.
|
||||
|
|
@ -380,7 +403,7 @@ func (p *processor) processFeeds(
|
|||
}
|
||||
feedURL := base.ResolveReference(up).String()
|
||||
p.checkTLS(feedURL)
|
||||
if err := p.processFeed(feedURL, lg); err != nil {
|
||||
if err := p.processFeed(feedURL, lg); err != nil && err != errContinue {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
|
@ -402,20 +425,25 @@ func (p *processor) checkCSAFs(domain string, lg func(string, ...interface{})) e
|
|||
var feeds [][]csaf.Feed
|
||||
if err := util.ReMarshalJSON(&feeds, rolie); err != nil {
|
||||
lg("ROLIE feeds are not compatible: %v.", err)
|
||||
return nil
|
||||
goto noRolie
|
||||
}
|
||||
if err := p.processFeeds(domain, feeds, lg); err != nil {
|
||||
if err == errContinue {
|
||||
goto noRolie
|
||||
}
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
}
|
||||
|
||||
noRolie:
|
||||
|
||||
// No rolie feeds
|
||||
// TODO: Implement me!
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *processor) checkProviderMetadata(domain string, lg func(string, ...interface{})) error {
|
||||
func (p *processor) checkProviderMetadata(domain string) error {
|
||||
|
||||
client := p.httpClient()
|
||||
|
||||
|
|
@ -423,14 +451,14 @@ func (p *processor) checkProviderMetadata(domain string, lg func(string, ...inte
|
|||
|
||||
res, err := client.Get(url)
|
||||
if err != nil {
|
||||
lg("Fetching %s: %v.", url, err)
|
||||
return err
|
||||
p.badProviderMetadata("Fetching %s: %v.", url, err)
|
||||
return errContinue
|
||||
}
|
||||
|
||||
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)
|
||||
return errors.New("Cannot load provider-metadata.json")
|
||||
return errContinue
|
||||
}
|
||||
|
||||
// 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)
|
||||
return json.NewDecoder(tee).Decode(&p.pmd)
|
||||
}(); err != nil {
|
||||
lg("Decoding JSON failed: %v.", err)
|
||||
return err
|
||||
p.badProviderMetadata("Decoding JSON failed: %v", err)
|
||||
return errContinue
|
||||
}
|
||||
|
||||
p.pmd256 = hash.Sum(nil)
|
||||
|
|
@ -452,9 +480,9 @@ func (p *processor) checkProviderMetadata(domain string, lg func(string, ...inte
|
|||
return err
|
||||
}
|
||||
if len(errors) > 0 {
|
||||
lg("Validating against JSON schema failed:")
|
||||
p.badProviderMetadata("Validating against JSON schema failed:")
|
||||
for _, msg := range errors {
|
||||
lg(strings.ReplaceAll(msg, `%`, `%%`))
|
||||
p.badProviderMetadata(strings.ReplaceAll(msg, `%`, `%%`))
|
||||
}
|
||||
}
|
||||
return nil
|
||||
|
|
@ -467,13 +495,14 @@ func (p *processor) checkSecurity(domain string, lg func(string, ...interface{})
|
|||
path := "https://" + domain + "/.well-known/security.txt"
|
||||
res, err := client.Get(path)
|
||||
if err != nil {
|
||||
return err
|
||||
lg("Fetchinig %s failed: %v", err)
|
||||
return errContinue
|
||||
}
|
||||
|
||||
if res.StatusCode != http.StatusOK {
|
||||
lg("Fetching %s failed. Status code %d (%s)",
|
||||
path, res.StatusCode, res.Status)
|
||||
return errors.New("fetching security.txt failed")
|
||||
return errContinue
|
||||
}
|
||||
|
||||
u, err := func() (string, error) {
|
||||
|
|
@ -489,42 +518,42 @@ func (p *processor) checkSecurity(domain string, lg func(string, ...interface{})
|
|||
}()
|
||||
if err != nil {
|
||||
lg("Error while reading security.txt: %v", err)
|
||||
return err
|
||||
return errContinue
|
||||
}
|
||||
if u == "" {
|
||||
lg("No CSAF line found in security.txt.")
|
||||
return errors.New("no CSAF line in security.txt")
|
||||
return errContinue
|
||||
}
|
||||
|
||||
// Try to load
|
||||
up, err := url.Parse(u)
|
||||
if err != nil {
|
||||
lg("CSAF URL '%s' invalid: %v.", u, err.Error())
|
||||
return err
|
||||
lg("CSAF URL '%s' invalid: %v", u, err)
|
||||
return errContinue
|
||||
}
|
||||
|
||||
base, err := url.Parse("https://" + domain + "/.well-known/")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ur := base.ResolveReference(up)
|
||||
u = ur.String()
|
||||
|
||||
u = base.ResolveReference(up).String()
|
||||
p.checkTLS(u)
|
||||
if res, err = client.Get(u); err != nil {
|
||||
lg("Cannot fetch %s from security.txt: %v", u, err)
|
||||
return nil
|
||||
return errContinue
|
||||
}
|
||||
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)
|
||||
return nil
|
||||
return errContinue
|
||||
}
|
||||
defer res.Body.Close()
|
||||
// Compare checksums to already read provider-metadata.json.
|
||||
h := sha256.New()
|
||||
if _, err := io.Copy(h, res.Body); err != nil {
|
||||
lg("Reading %s failed: %v.", u, err)
|
||||
return nil
|
||||
lg("Reading %s failed: %v", u, err)
|
||||
return errContinue
|
||||
}
|
||||
|
||||
if !bytes.Equal(h.Sum(nil), p.pmd256) {
|
||||
|
|
@ -533,3 +562,84 @@ func (p *processor) checkSecurity(domain string, lg func(string, ...interface{})
|
|||
|
||||
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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,3 +25,7 @@ type Domain struct {
|
|||
type Report struct {
|
||||
Domains []*Domain `json:"domains,omitempty"`
|
||||
}
|
||||
|
||||
func (r *Requirement) message(msg ...string) {
|
||||
r.Messages = append(r.Messages, msg...)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue