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

Merge pull request #373 from csaf-poc/role-requirements

Role requirements 11-14 or 15-17
This commit is contained in:
JanHoefelmeyer 2023-06-28 09:24:36 +02:00 committed by GitHub
commit 540d02d367
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 636 additions and 141 deletions

View file

@ -74,6 +74,14 @@ func (o *options) prepare() error {
return nil return nil
} }
// protectedAccess returns true if we have client certificates or
// extra http headers configured.
// This may be a wrong assumption, because the certs are not checked
// for their domain and custom headers may have other purposes.
func (o *options) protectedAccess() bool {
return len(o.clientCerts) > 0 || len(o.ExtraHeader) > 0
}
// writeJSON writes the JSON encoding of the given report to the given stream. // writeJSON writes the JSON encoding of the given report to the given stream.
// It returns nil, otherwise an error. // It returns nil, otherwise an error.
func writeJSON(report *Report, w io.WriteCloser) error { func writeJSON(report *Report, w io.WriteCloser) error {

View file

@ -54,20 +54,24 @@ type processor struct {
keys *crypto.KeyRing keys *crypto.KeyRing
labelChecker *rolieLabelChecker labelChecker *rolieLabelChecker
invalidAdvisories topicMessages invalidAdvisories topicMessages
badFilenames topicMessages badFilenames topicMessages
badIntegrities topicMessages badIntegrities topicMessages
badPGPs topicMessages badPGPs topicMessages
badSignatures topicMessages badSignatures topicMessages
badProviderMetadata topicMessages badProviderMetadata topicMessages
badSecurity topicMessages badSecurity topicMessages
badIndices topicMessages badIndices topicMessages
badChanges topicMessages badChanges topicMessages
badFolders topicMessages badFolders topicMessages
badWellknownMetadata topicMessages badWellknownMetadata topicMessages
badDNSPath topicMessages badDNSPath topicMessages
badDirListings topicMessages badDirListings topicMessages
badROLIEfeed topicMessages badROLIEFeed topicMessages
badROLIEService topicMessages
badROLIECategory topicMessages
badWhitePermissions topicMessages
badAmberRedPermissions topicMessages
expr *util.PathEval expr *util.PathEval
} }
@ -149,6 +153,19 @@ func (m *topicMessages) reset() { *m = nil }
// used returns true if we have used this topic. // used returns true if we have used this topic.
func (m *topicMessages) used() bool { return *m != nil } func (m *topicMessages) used() bool { return *m != nil }
// hasErrors checks if there are any error messages.
func (m *topicMessages) hasErrors() bool {
if !m.used() {
return false
}
for _, msg := range *m {
if msg.Type == ErrorType {
return true
}
}
return false
}
// newProcessor returns a processor structure after assigning the given options to the opts attribute // newProcessor returns a processor structure after assigning the given options to the opts attribute
// and initializing the "alreadyChecked" and "expr" fields. // and initializing the "alreadyChecked" and "expr" fields.
func newProcessor(opts *options) (*processor, error) { func newProcessor(opts *options) (*processor, error) {
@ -220,7 +237,11 @@ func (p *processor) clean() {
p.badWellknownMetadata.reset() p.badWellknownMetadata.reset()
p.badDNSPath.reset() p.badDNSPath.reset()
p.badDirListings.reset() p.badDirListings.reset()
p.badROLIEfeed.reset() p.badROLIEFeed.reset()
p.badROLIEService.reset()
p.badROLIECategory.reset()
p.badWhitePermissions.reset()
p.badAmberRedPermissions.reset()
p.labelChecker = nil p.labelChecker = nil
} }
@ -254,10 +275,27 @@ func (p *processor) run(domains []string) (*Report, error) {
continue continue
} }
for _, r := range buildReporters(*domain.Role) { if domain.Role == nil {
log.Printf("No role found in meta data. Ignoring domain %q\n", d)
continue
}
rules := roleRequirements(*domain.Role)
// TODO: store error base on rules eval in report.
if rules == nil {
log.Printf(
"WARN: Cannot find requirement rules for role %q. Assuming trusted provider.\n",
*domain.Role)
rules = trustedProviderRules
}
// 18, 19, 20 should always be checked.
for _, r := range rules.reporters([]int{18, 19, 20}) {
r.report(p, domain) r.report(p, domain)
} }
domain.Passed = rules.eval(p)
report.Domains = append(report.Domains, domain) report.Domains = append(report.Domains, domain)
p.clean() p.clean()
} }
@ -369,12 +407,8 @@ func (p *processor) checkRedirect(r *http.Request, via []*http.Request) error {
return nil return nil
} }
func (p *processor) httpClient() util.Client { // fullClient returns a fully configure HTTP client.
func (p *processor) fullClient() util.Client {
if p.client != nil {
return p.client
}
hClient := http.Client{} hClient := http.Client{}
hClient.CheckRedirect = p.checkRedirect hClient.CheckRedirect = p.checkRedirect
@ -414,8 +448,29 @@ func (p *processor) httpClient() util.Client {
Limiter: rate.NewLimiter(rate.Limit(*p.opts.Rate), 1), Limiter: rate.NewLimiter(rate.Limit(*p.opts.Rate), 1),
} }
} }
return client
}
p.client = client // basicClient returns a http Client w/o certs and headers.
func (p *processor) basicClient() *http.Client {
if p.opts.Insecure {
tr := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
return &http.Client{Transport: tr}
}
return &http.Client{}
}
// httpClient returns a cached HTTP client to be used to
// download remote ressources.
func (p *processor) httpClient() util.Client {
if p.client != nil {
return p.client
}
p.client = p.fullClient()
return p.client return p.client
} }
@ -655,11 +710,21 @@ func (p *processor) integrity(
// Extract the tlp level of the entry // Extract the tlp level of the entry
if tlpa, err := p.expr.Eval( if tlpa, err := p.expr.Eval(
`$.document.distribution`, doc); err != nil { `$.document`, doc); err != nil {
p.badROLIEfeed.error( p.badROLIEFeed.error(
"Extracting 'tlp level' from %s failed: %v", u, err) "Extracting 'tlp level' from %s failed: %v", u, err)
} else { } else {
tlpe := extractTLP(tlpa) tlpe := extractTLP(tlpa)
// If the client has no authorization it shouldn't be able
// to access TLP:AMBER or TLP:RED advisories
if !p.opts.protectedAccess() &&
(tlpe == csaf.TLPLabelAmber || tlpe == csaf.TLPLabelRed) {
p.badAmberRedPermissions.use()
p.badAmberRedPermissions.error(
"Advisory %s of TLP level %v is not access protected.",
u, tlpe)
}
// check if current feed has correct or all of their tlp levels entries. // check if current feed has correct or all of their tlp levels entries.
if p.labelChecker != nil { if p.labelChecker != nil {
p.labelChecker.check(p, tlpe, u) p.labelChecker.check(p, tlpe, u)
@ -781,11 +846,15 @@ func (p *processor) integrity(
// extractTLP tries to extract a valid TLP label from an advisory // extractTLP tries to extract a valid TLP label from an advisory
// Returns "UNLABELED" if it does not exist, the label otherwise // Returns "UNLABELED" if it does not exist, the label otherwise
func extractTLP(tlpa any) csaf.TLPLabel { func extractTLP(tlpa any) csaf.TLPLabel {
if distribution, ok := tlpa.(map[string]any); ok { if document, ok := tlpa.(map[string]any); ok {
if tlp, ok := distribution["tlp"]; ok { if distri, ok := document["distribution"]; ok {
if label, ok := tlp.(map[string]any); ok { if distribution, ok := distri.(map[string]any); ok {
if labelstring, ok := label["label"].(string); ok { if tlp, ok := distribution["tlp"]; ok {
return csaf.TLPLabel(labelstring) if label, ok := tlp.(map[string]any); ok {
if labelstring, ok := label["label"].(string); ok {
return csaf.TLPLabel(labelstring)
}
}
} }
} }
} }
@ -979,6 +1048,8 @@ func (p *processor) checkCSAFs(_ string) error {
return err return err
} }
} }
// check for service category document
p.serviceCheck(feeds)
} }
// No rolie feeds -> try directory_urls. // No rolie feeds -> try directory_urls.

View file

@ -46,6 +46,7 @@ type Domain struct {
Publisher *csaf.Publisher `json:"publisher,omitempty"` Publisher *csaf.Publisher `json:"publisher,omitempty"`
Role *csaf.MetadataRole `json:"role,omitempty"` Role *csaf.MetadataRole `json:"role,omitempty"`
Requirements []*Requirement `json:"requirements,omitempty"` Requirements []*Requirement `json:"requirements,omitempty"`
Passed bool `json:"passed"`
} }
// ReportTime stores the time of the report. // ReportTime stores the time of the report.
@ -80,12 +81,7 @@ func (r *Requirement) Append(msgs []Message) {
// HasErrors tells if this domain has errors. // HasErrors tells if this domain has errors.
func (d *Domain) HasErrors() bool { func (d *Domain) HasErrors() bool {
for _, r := range d.Requirements { return !d.Passed
if r.HasErrors() {
return true
}
}
return false
} }
// String implements fmt.Stringer interface. // String implements fmt.Stringer interface.

View file

@ -12,8 +12,6 @@ import (
"fmt" "fmt"
"sort" "sort"
"strings" "strings"
"github.com/csaf-poc/csaf_distribution/v2/csaf"
) )
type ( type (
@ -46,70 +44,30 @@ type (
mirrorReporter struct{ baseReporter } mirrorReporter struct{ baseReporter }
) )
var reporters = [23]reporter{ var reporters = [...]reporter{
&validReporter{baseReporter{num: 1, description: "Valid CSAF documents"}}, 1: &validReporter{baseReporter{num: 1, description: "Valid CSAF documents"}},
&filenameReporter{baseReporter{num: 2, description: "Filename"}}, 2: &filenameReporter{baseReporter{num: 2, description: "Filename"}},
&tlsReporter{baseReporter{num: 3, description: "TLS"}}, 3: &tlsReporter{baseReporter{num: 3, description: "TLS"}},
&tlpWhiteReporter{baseReporter{num: 4, description: "TLP:WHITE"}}, 4: &tlpWhiteReporter{baseReporter{num: 4, description: "TLP:WHITE"}},
&tlpAmberRedReporter{baseReporter{num: 5, description: "TLP:AMBER and TLP:RED"}}, 5: &tlpAmberRedReporter{baseReporter{num: 5, description: "TLP:AMBER and TLP:RED"}},
&redirectsReporter{baseReporter{num: 6, description: "Redirects"}}, 6: &redirectsReporter{baseReporter{num: 6, description: "Redirects"}},
&providerMetadataReport{baseReporter{num: 7, description: "provider-metadata.json"}}, 7: &providerMetadataReport{baseReporter{num: 7, description: "provider-metadata.json"}},
&securityReporter{baseReporter{num: 8, description: "security.txt"}}, 8: &securityReporter{baseReporter{num: 8, description: "security.txt"}},
&wellknownMetadataReporter{baseReporter{num: 9, description: "/.well-known/csaf/provider-metadata.json"}}, 9: &wellknownMetadataReporter{baseReporter{num: 9, description: "/.well-known/csaf/provider-metadata.json"}},
&dnsPathReporter{baseReporter{num: 10, description: "DNS path"}}, 10: &dnsPathReporter{baseReporter{num: 10, description: "DNS path"}},
&oneFolderPerYearReport{baseReporter{num: 11, description: "One folder per year"}}, 11: &oneFolderPerYearReport{baseReporter{num: 11, description: "One folder per year"}},
&indexReporter{baseReporter{num: 12, description: "index.txt"}}, 12: &indexReporter{baseReporter{num: 12, description: "index.txt"}},
&changesReporter{baseReporter{num: 13, description: "changes.csv"}}, 13: &changesReporter{baseReporter{num: 13, description: "changes.csv"}},
&directoryListingsReporter{baseReporter{num: 14, description: "Directory listings"}}, 14: &directoryListingsReporter{baseReporter{num: 14, description: "Directory listings"}},
&rolieFeedReporter{baseReporter{num: 15, description: "ROLIE feed"}}, 15: &rolieFeedReporter{baseReporter{num: 15, description: "ROLIE feed"}},
&rolieServiceReporter{baseReporter{num: 16, description: "ROLIE service document"}}, 16: &rolieServiceReporter{baseReporter{num: 16, description: "ROLIE service document"}},
&rolieCategoryReporter{baseReporter{num: 17, description: "ROLIE category document"}}, 17: &rolieCategoryReporter{baseReporter{num: 17, description: "ROLIE category document"}},
&integrityReporter{baseReporter{num: 18, description: "Integrity"}}, 18: &integrityReporter{baseReporter{num: 18, description: "Integrity"}},
&signaturesReporter{baseReporter{num: 19, description: "Signatures"}}, 19: &signaturesReporter{baseReporter{num: 19, description: "Signatures"}},
&publicPGPKeyReporter{baseReporter{num: 20, description: "Public OpenPGP Key"}}, 20: &publicPGPKeyReporter{baseReporter{num: 20, description: "Public OpenPGP Key"}},
&listReporter{baseReporter{num: 21, description: "List of CSAF providers"}}, 21: &listReporter{baseReporter{num: 21, description: "List of CSAF providers"}},
&hasTwoReporter{baseReporter{num: 22, description: "Two disjoint issuing parties"}}, 22: &hasTwoReporter{baseReporter{num: 22, description: "Two disjoint issuing parties"}},
&mirrorReporter{baseReporter{num: 23, description: "Mirror"}}, 23: &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 {
@ -194,16 +152,34 @@ func (r *tlsReporter) report(p *processor, domain *Domain) {
// report tests if a document labeled TLP:WHITE // report tests if a document labeled TLP:WHITE
// is freely accessible and sets the "message" field value // is freely accessible 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 *tlpWhiteReporter) report(_ *processor, _ *Domain) { func (r *tlpWhiteReporter) report(p *processor, domain *Domain) {
// TODO req := r.requirement(domain)
if !p.badWhitePermissions.used() {
req.message(InfoType, "No advisories labeled TLP:WHITE tested for accessibility.")
return
}
if len(p.badWhitePermissions) == 0 {
req.message(InfoType, "All advisories labeled TLP:WHITE were freely accessible.")
return
}
req.Messages = p.badWhitePermissions
} }
// report tests if a document labeled TLP:AMBER // report tests if a document labeled TLP:AMBER
// or TLP:RED is access protected // or TLP:RED is access protected
// and sets the "message" field value // 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 *tlpAmberRedReporter) report(_ *processor, _ *Domain) { func (r *tlpAmberRedReporter) report(p *processor, domain *Domain) {
// TODO req := r.requirement(domain)
if !p.badAmberRedPermissions.used() {
req.message(InfoType, "No advisories labeled TLP:AMBER or TLP:RED tested for accessibility.")
return
}
if len(p.badAmberRedPermissions) == 0 {
req.message(InfoType, "All tested advisories labeled TLP:WHITE or TLP:RED were access-protected.")
return
}
req.Messages = p.badAmberRedPermissions
} }
// report tests if redirects are used and sets the "message" field value // report tests if redirects are used and sets the "message" field value
@ -366,23 +342,33 @@ func (r *directoryListingsReporter) report(p *processor, domain *Domain) {
// of the "Requirement" struct as a result of that. // of the "Requirement" struct as a result of that.
func (r *rolieFeedReporter) report(p *processor, domain *Domain) { func (r *rolieFeedReporter) report(p *processor, domain *Domain) {
req := r.requirement(domain) req := r.requirement(domain)
if !p.badROLIEfeed.used() { if !p.badROLIEFeed.used() {
req.message(InfoType, "No checks on the validity of ROLIE feeds performed.") req.message(InfoType, "No checks on the validity of ROLIE feeds performed.")
return return
} }
if len(p.badROLIEfeed) == 0 { if len(p.badROLIEFeed) == 0 {
req.message(InfoType, "All checked ROLIE feeds validated fine.") req.message(InfoType, "All checked ROLIE feeds validated fine.")
return return
} }
req.Messages = p.badROLIEfeed req.Messages = p.badROLIEFeed
} }
// report tests whether a ROLIE service document is used and if so, // report tests whether a ROLIE service document is used and if so,
// whether it is a [RFC8322] conform JSON file that lists the // whether it is a [RFC8322] conform JSON file that lists the
// ROLIE feed documents and sets the "message" field value // ROLIE feed documents 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 *rolieServiceReporter) report(_ *processor, _ *Domain) { func (r *rolieServiceReporter) report(p *processor, domain *Domain) {
// TODO req := r.requirement(domain)
if !p.badROLIEService.used() {
req.message(InfoType, "ROLIE service document was not checked.")
return
}
if len(p.badROLIEService) == 0 {
req.message(InfoType, "ROLIE service document validated fine.")
return
}
req.Messages = p.badROLIEService
} }
// report tests whether a ROLIE category document is used and if so, // report tests whether a ROLIE category document is used and if so,
@ -390,8 +376,18 @@ func (r *rolieServiceReporter) report(_ *processor, _ *Domain) {
// documents by certain criteria // documents by certain criteria
// and sets the "message" field value // 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 *rolieCategoryReporter) report(_ *processor, _ *Domain) { func (r *rolieCategoryReporter) report(p *processor, domain *Domain) {
// TODO req := r.requirement(domain)
if !p.badROLIECategory.used() {
req.message(InfoType, "No checks on the existence of ROLIE category documents performed.")
return
}
if len(p.badROLIECategory) == 0 {
req.message(InfoType, "All checked ROLIE category documents exist.")
return
}
req.Messages = p.badROLIECategory
} }
func (r *integrityReporter) report(p *processor, domain *Domain) { func (r *integrityReporter) report(p *processor, domain *Domain) {

View file

@ -9,7 +9,10 @@
package main package main
import ( import (
"net/http"
"net/url" "net/url"
"sort"
"strings"
"github.com/csaf-poc/csaf_distribution/v2/csaf" "github.com/csaf-poc/csaf_distribution/v2/csaf"
"github.com/csaf-poc/csaf_distribution/v2/util" "github.com/csaf-poc/csaf_distribution/v2/util"
@ -21,7 +24,8 @@ type rolieLabelChecker struct {
feedURL string feedURL string
feedLabel csaf.TLPLabel feedLabel csaf.TLPLabel
advisories map[csaf.TLPLabel]map[string]struct{} advisories map[csaf.TLPLabel]util.Set[string]
openClient util.Client
} }
// tlpLevel returns an inclusion order of TLP colors. // tlpLevel returns an inclusion order of TLP colors.
@ -65,21 +69,21 @@ func (ca *rolieLabelChecker) check(
// Associate advisory label to urls. // Associate advisory label to urls.
advs := ca.advisories[advisoryLabel] advs := ca.advisories[advisoryLabel]
if advs == nil { if advs == nil {
advs = make(map[string]struct{}) advs = util.Set[string]{}
ca.advisories[advisoryLabel] = advs ca.advisories[advisoryLabel] = advs
} }
advs[advisory] = struct{}{} advs.Add(advisory)
// If entry shows up in feed of higher tlp level, // If entry shows up in feed of higher tlp level,
// give out info or warning // give out info or warning
switch { switch {
case advisoryRank < feedRank: case advisoryRank < feedRank:
if advisoryRank == 0 { // All kinds of 'UNLABELED' if advisoryRank == 0 { // All kinds of 'UNLABELED'
p.badROLIEfeed.info( p.badROLIEFeed.info(
"Found unlabeled advisory %q in feed %q.", "Found unlabeled advisory %q in feed %q.",
advisory, ca.feedURL) advisory, ca.feedURL)
} else { } else {
p.badROLIEfeed.warn( p.badROLIEFeed.warn(
"Found advisory %q labled TLP:%s in feed %q (TLP:%s).", "Found advisory %q labled TLP:%s in feed %q (TLP:%s).",
advisory, advisoryLabel, advisory, advisoryLabel,
ca.feedURL, ca.feedLabel) ca.feedURL, ca.feedLabel)
@ -87,21 +91,57 @@ func (ca *rolieLabelChecker) check(
case advisoryRank > feedRank: case advisoryRank > feedRank:
// Must not happen, give error // Must not happen, give error
p.badROLIEfeed.error( p.badROLIEFeed.error(
"%s of TLP level %s must not be listed in feed %s of TLP level %s", "%s of TLP level %s must not be listed in feed %s of TLP level %s",
advisory, advisoryLabel, ca.feedURL, ca.feedLabel) advisory, advisoryLabel, ca.feedURL, ca.feedLabel)
} }
// If we have an open client then the actual data was downloaded
// through an authorizing client.
if ca.openClient != nil {
switch {
// If we are checking WHITE and we have a test client
// and we get a status forbidden then the access is not open.
case ca.feedLabel == csaf.TLPLabelWhite:
p.badWhitePermissions.use()
res, err := ca.openClient.Get(advisory)
if err != nil {
p.badWhitePermissions.error(
"Unexpected Error %v when trying to fetch: %s", err, advisory)
} else if res.StatusCode == http.StatusForbidden {
p.badWhitePermissions.error(
"Advisory %s of TLP level WHITE is access protected.", advisory)
}
// If we are checking AMBER or above we need to download
// the data again with the open client.
// If this does not result in status forbidden the
// server may be wrongly configured.
case ca.feedLabel >= csaf.TLPLabelAmber:
p.badAmberRedPermissions.use()
res, err := ca.openClient.Get(advisory)
if err != nil {
p.badAmberRedPermissions.error(
"Unexpected Error %v when trying to fetch: %s", err, advisory)
} else if res.StatusCode == http.StatusOK {
p.badAmberRedPermissions.error(
"Advisory %s of TLP level %v is not properly access protected.",
advisory, advisoryLabel)
}
}
}
} }
// processROLIEFeeds goes through all ROLIE feeds and checks there // processROLIEFeeds goes through all ROLIE feeds and checks their
// integriry and completeness. // integrity and completeness.
func (p *processor) processROLIEFeeds(feeds [][]csaf.Feed) error { func (p *processor) processROLIEFeeds(feeds [][]csaf.Feed) error {
base, err := url.Parse(p.pmdURL) base, err := url.Parse(p.pmdURL)
if err != nil { if err != nil {
return err return err
} }
p.badROLIEfeed.use() p.badROLIEFeed.use()
advisories := map[*csaf.Feed][]csaf.AdvisoryFile{} advisories := map[*csaf.Feed][]csaf.AdvisoryFile{}
@ -132,6 +172,10 @@ func (p *processor) processROLIEFeeds(feeds [][]csaf.Feed) error {
} }
} }
p.labelChecker = &rolieLabelChecker{
advisories: map[csaf.TLPLabel]util.Set[string]{},
}
// Phase 2: check for integrity. // Phase 2: check for integrity.
for _, fs := range feeds { for _, fs := range feeds {
for i := range fs { for i := range fs {
@ -158,13 +202,30 @@ func (p *processor) processROLIEFeeds(feeds [][]csaf.Feed) error {
} }
label := tlpLabel(feed.TLPLabel) label := tlpLabel(feed.TLPLabel)
if err := p.categoryCheck(feedBase, label); err != nil {
p.labelChecker = &rolieLabelChecker{ if err != errContinue {
feedURL: feedURL.String(), return err
feedLabel: label, }
advisories: map[csaf.TLPLabel]map[string]struct{}{},
} }
p.labelChecker.feedURL = feedURL.String()
p.labelChecker.feedLabel = label
// If we are using an authorizing client
// we need an open client to check
// WHITE, AMBER and RED feeds.
var openClient util.Client
if (label == csaf.TLPLabelWhite || label >= csaf.TLPLabelAmber) &&
p.opts.protectedAccess() {
openClient = p.basicClient()
}
p.labelChecker.openClient = openClient
// TODO: Issue a warning if we want check AMBER+ without an
// authorizing client.
// TODO: Complete criteria for requirement 4.
if err := p.integrity(files, feedBase, rolieMask, p.badProviderMetadata.add); err != nil { if err := p.integrity(files, feedBase, rolieMask, p.badProviderMetadata.add); err != nil {
if err != errContinue { if err != errContinue {
return err return err
@ -175,7 +236,7 @@ func (p *processor) processROLIEFeeds(feeds [][]csaf.Feed) error {
// Phase 3: Check for completeness. // Phase 3: Check for completeness.
hasSummary := map[csaf.TLPLabel]struct{}{} hasSummary := util.Set[csaf.TLPLabel]{}
var ( var (
hasUnlabeled = false hasUnlabeled = false
@ -214,24 +275,25 @@ func (p *processor) processROLIEFeeds(feeds [][]csaf.Feed) error {
} }
reference := p.labelChecker.advisories[label] reference := p.labelChecker.advisories[label]
advisories := make(map[string]struct{}, len(reference)) advisories := make(util.Set[string], len(reference))
for _, adv := range files { for _, adv := range files {
u, err := url.Parse(adv.URL()) u, err := url.Parse(adv.URL())
if err != nil { if err != nil {
p.badProviderMetadata.error("Invalid URL %s in feed: %v.", *feed.URL, err) p.badProviderMetadata.error(
"Invalid URL %s in feed: %v.", *feed.URL, err)
continue continue
} }
advisories[makeAbs(u).String()] = struct{}{} advisories[makeAbs(u).String()] = struct{}{}
} }
if containsAllKeys(reference, advisories) { if advisories.ContainsAll(reference) {
hasSummary[label] = struct{}{} hasSummary.Add(label)
} }
} }
} }
if !hasWhite && !hasGreen && !hasUnlabeled { if !hasWhite && !hasGreen && !hasUnlabeled {
p.badROLIEfeed.error( p.badROLIEFeed.error(
"One ROLIE feed with a TLP:WHITE, TLP:GREEN or unlabeled tlp must exist, " + "One ROLIE feed with a TLP:WHITE, TLP:GREEN or unlabeled tlp must exist, " +
"but none were found.") "but none were found.")
} }
@ -244,8 +306,8 @@ func (p *processor) processROLIEFeeds(feeds [][]csaf.Feed) error {
csaf.TLPLabelAmber, csaf.TLPLabelAmber,
csaf.TLPLabelRed, csaf.TLPLabelRed,
} { } {
if _, ok := hasSummary[label]; !ok && len(p.labelChecker.advisories[label]) > 0 { if !hasSummary.Contains(label) && len(p.labelChecker.advisories[label]) > 0 {
p.badROLIEfeed.warn( p.badROLIEFeed.warn(
"ROLIE feed for TLP:%s has no accessible listed feed covering all advisories.", "ROLIE feed for TLP:%s has no accessible listed feed covering all advisories.",
label) label)
} }
@ -254,12 +316,111 @@ func (p *processor) processROLIEFeeds(feeds [][]csaf.Feed) error {
return nil return nil
} }
// containsAllKeys returns if m2 contains all keys of m1. // categoryCheck checks for the existence of a feeds ROLIE category document and if it does,
func containsAllKeys[K comparable, V any](m1, m2 map[K]V) bool { // whether the category document contains distinguishing categories
for k := range m1 { func (p *processor) categoryCheck(folderURL string, label csaf.TLPLabel) error {
if _, ok := m2[k]; !ok { labelname := strings.ToLower(string(label))
return false urlrc := folderURL + "category-" + labelname + ".json"
p.badROLIECategory.use()
client := p.httpClient()
res, err := client.Get(urlrc)
if err != nil {
p.badROLIECategory.error(
"Cannot fetch rolie category document %s: %v", urlrc, err)
return errContinue
}
if res.StatusCode != http.StatusOK {
p.badROLIECategory.warn("Fetching %s failed. Status code %d (%s)",
urlrc, res.StatusCode, res.Status)
return errContinue
}
rolieCategory, err := func() (*csaf.ROLIECategoryDocument, error) {
defer res.Body.Close()
return csaf.LoadROLIECategoryDocument(res.Body)
}()
if err != nil {
p.badROLIECategory.error(
"Loading ROLIE category document %s failed: %v.", urlrc, err)
return errContinue
}
if len(rolieCategory.Categories.Category) == 0 {
p.badROLIECategory.warn(
"No distinguishing categories in ROLIE category document: %s", urlrc)
}
return nil
}
// serviceCheck checks if a ROLIE service document exists and if it does,
// whether it contains all ROLIE feeds.
func (p *processor) serviceCheck(feeds [][]csaf.Feed) error {
// service category document should be next to the pmd
pmdURL, err := url.Parse(p.pmdURL)
if err != nil {
return err
}
baseURL, err := util.BaseURL(pmdURL)
if err != nil {
return err
}
urls := baseURL + "service.json"
// load service document
p.badROLIEService.use()
client := p.httpClient()
res, err := client.Get(urls)
if err != nil {
p.badROLIEService.error(
"Cannot fetch rolie service document %s: %v", urls, err)
return errContinue
}
if res.StatusCode != http.StatusOK {
p.badROLIEService.warn("Fetching %s failed. Status code %d (%s)",
urls, res.StatusCode, res.Status)
return errContinue
}
rolieService, err := func() (*csaf.ROLIEServiceDocument, error) {
defer res.Body.Close()
return csaf.LoadROLIEServiceDocument(res.Body)
}()
if err != nil {
p.badROLIEService.error(
"Loading ROLIE service document %s failed: %v.", urls, err)
return errContinue
}
// Build lists of all feeds in feeds and in the Service Document
var (
sfeeds = util.Set[string]{}
ffeeds = util.Set[string]{}
)
for _, col := range rolieService.Service.Workspace {
for _, fd := range col.Collection {
sfeeds.Add(fd.HRef)
} }
} }
return true for _, r := range feeds {
for _, s := range r {
ffeeds.Add(string(*s.URL))
}
}
// Check if ROLIE Service Document contains exactly all ROLIE feeds
if m1 := sfeeds.Difference(ffeeds).Keys(); len(m1) != 0 {
sort.Strings(m1)
p.badROLIEService.error(
"The ROLIE service document %s contains nonexistent feed entries: %v", urls, m1)
}
if m2 := ffeeds.Difference(sfeeds).Keys(); len(m2) != 0 {
sort.Strings(m2)
p.badROLIEService.error(
"The ROLIE service document %s is missing feed entries: %v", urls, m2)
}
// TODO: Check conformity with RFC8322
return nil
} }

205
cmd/csaf_checker/rules.go Normal file
View file

@ -0,0 +1,205 @@
// 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 main
import (
"fmt"
"sort"
"github.com/csaf-poc/csaf_distribution/v2/csaf"
)
type ruleCondition int
const (
condAll ruleCondition = iota
condOneOf
)
type requirementRules struct {
cond ruleCondition
satisfies int
subs []*requirementRules
}
var (
publisherRules = &requirementRules{
cond: condAll,
subs: ruleAtoms(1, 2, 3 /* 4 */),
}
providerRules = &requirementRules{
cond: condAll,
subs: []*requirementRules{
publisherRules,
{cond: condAll, subs: ruleAtoms(5, 6, 7)},
{cond: condOneOf, subs: ruleAtoms(8, 9, 10)},
{cond: condOneOf, subs: []*requirementRules{
{cond: condAll, subs: ruleAtoms(11, 12, 13, 14)},
{cond: condAll, subs: ruleAtoms(15, 16, 17)},
}},
},
}
trustedProviderRules = &requirementRules{
cond: condAll,
subs: []*requirementRules{
providerRules,
{cond: condAll, subs: ruleAtoms(18, 19, 20)},
},
}
)
// roleRequirements returns the rules for the given role.
func roleRequirements(role csaf.MetadataRole) *requirementRules {
switch role {
case csaf.MetadataRoleTrustedProvider:
return trustedProviderRules
case csaf.MetadataRoleProvider:
return providerRules
case csaf.MetadataRolePublisher:
return publisherRules
default:
return nil
}
}
// ruleAtoms is a helper function to build the leaves of
// a rules tree.
func ruleAtoms(nums ...int) []*requirementRules {
rules := make([]*requirementRules, len(nums))
for i, num := range nums {
rules[i] = &requirementRules{
cond: condAll,
satisfies: num,
}
}
return rules
}
// reporters assembles a list of reporters needed for a given set
// of rules. The given nums are mandatory.
func (rules *requirementRules) reporters(nums []int) []reporter {
if rules == nil {
return nil
}
var recurse func(*requirementRules)
recurse = func(rules *requirementRules) {
if rules.satisfies != 0 {
// There should not be any dupes.
for _, n := range nums {
if n == rules.satisfies {
goto doRecurse
}
}
nums = append(nums, rules.satisfies)
}
doRecurse:
for _, sub := range rules.subs {
recurse(sub)
}
}
recurse(rules)
sort.Ints(nums)
reps := make([]reporter, len(nums))
for i, n := range nums {
reps[i] = reporters[n]
}
return reps
}
// eval evalutes a set of rules given a given processor state.
func (rules *requirementRules) eval(p *processor) bool {
if rules == nil {
return false
}
var recurse func(*requirementRules) bool
recurse = func(rules *requirementRules) bool {
if rules.satisfies != 0 {
return p.eval(rules.satisfies)
}
switch rules.cond {
case condAll:
for _, sub := range rules.subs {
if !recurse(sub) {
return false
}
}
return true
case condOneOf:
for _, sub := range rules.subs {
if recurse(sub) {
return true
}
}
return false
default:
panic(fmt.Sprintf("unexpected cond %v in eval", rules.cond))
}
}
return recurse(rules)
}
// eval evalutes the processing state for a given requirement.
func (p *processor) eval(requirement int) bool {
switch requirement {
case 1:
return !p.invalidAdvisories.hasErrors()
case 2:
return !p.badFilenames.hasErrors()
case 3:
return len(p.noneTLS) == 0
case 5:
return !p.badAmberRedPermissions.hasErrors()
case 6:
return len(p.redirects) == 0
case 7:
return !p.badProviderMetadata.hasErrors()
case 8:
return !p.badSecurity.hasErrors()
case 9:
return !p.badWellknownMetadata.hasErrors()
case 10:
return !p.badDNSPath.hasErrors()
case 11:
return !p.badFolders.hasErrors()
case 12:
return !p.badIndices.hasErrors()
case 13:
return !p.badChanges.hasErrors()
case 14:
return !p.badDirListings.hasErrors()
case 15:
return !p.badROLIEFeed.hasErrors()
case 16:
return !p.badROLIEService.hasErrors()
case 17:
return !p.badROLIECategory.hasErrors()
case 18:
return !p.badIntegrities.hasErrors()
case 19:
return !p.badSignatures.hasErrors()
case 20:
return !p.badPGPs.hasErrors()
default:
panic(fmt.Sprintf("evaluating unexpected requirement %d", requirement))
}
}

View file

@ -49,3 +49,8 @@ The checker result is a success if no checks resulted in type 2, and a failure o
The `role` given in the `provider-metadata.json` is not The `role` given in the `provider-metadata.json` is not
yet considered to change the overall result, yet considered to change the overall result,
see https://github.com/csaf-poc/csaf_distribution/issues/221 . see https://github.com/csaf-poc/csaf_distribution/issues/221 .
If a provider hosts one or more advisories with a TLP level of AMBER or RED, then these advisories must be access protected.
To check these advisories, authorization can be given via custom headers or certificates.
The authorization method chosen needs to grant access to all advisories, as otherwise the
checker will be unable to check the advisories it doesn't have permission for, falsifying the result.

53
util/set.go Normal file
View file

@ -0,0 +1,53 @@
// 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 util
// Set is a simple set type.
type Set[K comparable] map[K]struct{}
// Contains returns if the set contains a given key or not.
func (s Set[K]) Contains(k K) bool {
_, found := s[k]
return found
}
// Add adds a key to the set.
func (s Set[K]) Add(k K) {
s[k] = struct{}{}
}
// Keys returns the keys of the set.
func (s Set[K]) Keys() []K {
keys := make([]K, 0, len(s))
for k := range s {
keys = append(keys, k)
}
return keys
}
// Difference returns the differnce of two sets.
func (s Set[K]) Difference(t Set[K]) Set[K] {
d := Set[K]{}
for k := range s {
if !t.Contains(k) {
d.Add(k)
}
}
return d
}
// ContainsAll returns true if all keys of a given set are in this set.
func (s Set[K]) ContainsAll(t Set[K]) bool {
for k := range t {
if !s.Contains(k) {
return false
}
}
return true
}