1
0
Fork 0
mirror of https://github.com/gocsaf/csaf.git synced 2025-12-22 18:15:42 +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
}
// 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.
// It returns nil, otherwise an error.
func writeJSON(report *Report, w io.WriteCloser) error {

View file

@ -67,7 +67,11 @@ type processor struct {
badWellknownMetadata topicMessages
badDNSPath topicMessages
badDirListings topicMessages
badROLIEfeed topicMessages
badROLIEFeed topicMessages
badROLIEService topicMessages
badROLIECategory topicMessages
badWhitePermissions topicMessages
badAmberRedPermissions topicMessages
expr *util.PathEval
}
@ -149,6 +153,19 @@ func (m *topicMessages) reset() { *m = nil }
// used returns true if we have used this topic.
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
// and initializing the "alreadyChecked" and "expr" fields.
func newProcessor(opts *options) (*processor, error) {
@ -220,7 +237,11 @@ func (p *processor) clean() {
p.badWellknownMetadata.reset()
p.badDNSPath.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
}
@ -254,10 +275,27 @@ func (p *processor) run(domains []string) (*Report, error) {
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)
}
domain.Passed = rules.eval(p)
report.Domains = append(report.Domains, domain)
p.clean()
}
@ -369,12 +407,8 @@ func (p *processor) checkRedirect(r *http.Request, via []*http.Request) error {
return nil
}
func (p *processor) httpClient() util.Client {
if p.client != nil {
return p.client
}
// fullClient returns a fully configure HTTP client.
func (p *processor) fullClient() util.Client {
hClient := http.Client{}
hClient.CheckRedirect = p.checkRedirect
@ -414,8 +448,29 @@ func (p *processor) httpClient() util.Client {
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
}
@ -655,11 +710,21 @@ func (p *processor) integrity(
// Extract the tlp level of the entry
if tlpa, err := p.expr.Eval(
`$.document.distribution`, doc); err != nil {
p.badROLIEfeed.error(
`$.document`, doc); err != nil {
p.badROLIEFeed.error(
"Extracting 'tlp level' from %s failed: %v", u, err)
} else {
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.
if p.labelChecker != nil {
p.labelChecker.check(p, tlpe, u)
@ -781,7 +846,9 @@ func (p *processor) integrity(
// extractTLP tries to extract a valid TLP label from an advisory
// Returns "UNLABELED" if it does not exist, the label otherwise
func extractTLP(tlpa any) csaf.TLPLabel {
if distribution, ok := tlpa.(map[string]any); ok {
if document, ok := tlpa.(map[string]any); ok {
if distri, ok := document["distribution"]; ok {
if distribution, ok := distri.(map[string]any); ok {
if tlp, ok := distribution["tlp"]; ok {
if label, ok := tlp.(map[string]any); ok {
if labelstring, ok := label["label"].(string); ok {
@ -790,6 +857,8 @@ func extractTLP(tlpa any) csaf.TLPLabel {
}
}
}
}
}
return csaf.TLPLabelUnlabeled
}
@ -979,6 +1048,8 @@ func (p *processor) checkCSAFs(_ string) error {
return err
}
}
// check for service category document
p.serviceCheck(feeds)
}
// No rolie feeds -> try directory_urls.

View file

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

View file

@ -12,8 +12,6 @@ import (
"fmt"
"sort"
"strings"
"github.com/csaf-poc/csaf_distribution/v2/csaf"
)
type (
@ -46,70 +44,30 @@ type (
mirrorReporter struct{ baseReporter }
)
var reporters = [23]reporter{
&validReporter{baseReporter{num: 1, description: "Valid CSAF documents"}},
&filenameReporter{baseReporter{num: 2, description: "Filename"}},
&tlsReporter{baseReporter{num: 3, description: "TLS"}},
&tlpWhiteReporter{baseReporter{num: 4, description: "TLP:WHITE"}},
&tlpAmberRedReporter{baseReporter{num: 5, description: "TLP:AMBER and TLP:RED"}},
&redirectsReporter{baseReporter{num: 6, description: "Redirects"}},
&providerMetadataReport{baseReporter{num: 7, description: "provider-metadata.json"}},
&securityReporter{baseReporter{num: 8, description: "security.txt"}},
&wellknownMetadataReporter{baseReporter{num: 9, description: "/.well-known/csaf/provider-metadata.json"}},
&dnsPathReporter{baseReporter{num: 10, description: "DNS path"}},
&oneFolderPerYearReport{baseReporter{num: 11, description: "One folder per year"}},
&indexReporter{baseReporter{num: 12, description: "index.txt"}},
&changesReporter{baseReporter{num: 13, description: "changes.csv"}},
&directoryListingsReporter{baseReporter{num: 14, description: "Directory listings"}},
&rolieFeedReporter{baseReporter{num: 15, description: "ROLIE feed"}},
&rolieServiceReporter{baseReporter{num: 16, description: "ROLIE service document"}},
&rolieCategoryReporter{baseReporter{num: 17, description: "ROLIE category document"}},
&integrityReporter{baseReporter{num: 18, description: "Integrity"}},
&signaturesReporter{baseReporter{num: 19, description: "Signatures"}},
&publicPGPKeyReporter{baseReporter{num: 20, description: "Public OpenPGP Key"}},
&listReporter{baseReporter{num: 21, description: "List of CSAF providers"}},
&hasTwoReporter{baseReporter{num: 22, description: "Two disjoint issuing parties"}},
&mirrorReporter{baseReporter{num: 23, description: "Mirror"}},
}
var roleImplies = map[csaf.MetadataRole][]csaf.MetadataRole{
csaf.MetadataRoleProvider: {csaf.MetadataRolePublisher},
csaf.MetadataRoleTrustedProvider: {csaf.MetadataRoleProvider},
}
func requirements(role csaf.MetadataRole) [][2]int {
var own [][2]int
switch role {
case csaf.MetadataRoleTrustedProvider:
own = [][2]int{{18, 20}}
case csaf.MetadataRoleProvider:
// TODO: use commented numbers when TLPs should be checked.
own = [][2]int{{6 /* 5 */, 7}, {8, 10}, {11, 14}, {15, 17}}
case csaf.MetadataRolePublisher:
own = [][2]int{{1, 3 /* 4 */}}
}
for _, base := range roleImplies[role] {
own = append(own, requirements(base)...)
}
return own
}
// buildReporters initializes each report by assigning a number and description to it.
// It returns an array of the reporter interface type.
func buildReporters(role csaf.MetadataRole) []reporter {
var reps []reporter
reqs := requirements(role)
// sort to have them ordered by there number.
sort.Slice(reqs, func(i, j int) bool { return reqs[i][0] < reqs[j][0] })
for _, req := range reqs {
from, to := req[0]-1, req[1]-1
for i := from; i <= to; i++ {
if rep := reporters[i]; rep != nil {
reps = append(reps, rep)
}
}
}
return reps
var reporters = [...]reporter{
1: &validReporter{baseReporter{num: 1, description: "Valid CSAF documents"}},
2: &filenameReporter{baseReporter{num: 2, description: "Filename"}},
3: &tlsReporter{baseReporter{num: 3, description: "TLS"}},
4: &tlpWhiteReporter{baseReporter{num: 4, description: "TLP:WHITE"}},
5: &tlpAmberRedReporter{baseReporter{num: 5, description: "TLP:AMBER and TLP:RED"}},
6: &redirectsReporter{baseReporter{num: 6, description: "Redirects"}},
7: &providerMetadataReport{baseReporter{num: 7, description: "provider-metadata.json"}},
8: &securityReporter{baseReporter{num: 8, description: "security.txt"}},
9: &wellknownMetadataReporter{baseReporter{num: 9, description: "/.well-known/csaf/provider-metadata.json"}},
10: &dnsPathReporter{baseReporter{num: 10, description: "DNS path"}},
11: &oneFolderPerYearReport{baseReporter{num: 11, description: "One folder per year"}},
12: &indexReporter{baseReporter{num: 12, description: "index.txt"}},
13: &changesReporter{baseReporter{num: 13, description: "changes.csv"}},
14: &directoryListingsReporter{baseReporter{num: 14, description: "Directory listings"}},
15: &rolieFeedReporter{baseReporter{num: 15, description: "ROLIE feed"}},
16: &rolieServiceReporter{baseReporter{num: 16, description: "ROLIE service document"}},
17: &rolieCategoryReporter{baseReporter{num: 17, description: "ROLIE category document"}},
18: &integrityReporter{baseReporter{num: 18, description: "Integrity"}},
19: &signaturesReporter{baseReporter{num: 19, description: "Signatures"}},
20: &publicPGPKeyReporter{baseReporter{num: 20, description: "Public OpenPGP Key"}},
21: &listReporter{baseReporter{num: 21, description: "List of CSAF providers"}},
22: &hasTwoReporter{baseReporter{num: 22, description: "Two disjoint issuing parties"}},
23: &mirrorReporter{baseReporter{num: 23, description: "Mirror"}},
}
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
// is freely accessible and sets the "message" field value
// of the "Requirement" struct as a result of that.
func (r *tlpWhiteReporter) report(_ *processor, _ *Domain) {
// TODO
func (r *tlpWhiteReporter) report(p *processor, domain *Domain) {
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
// or TLP:RED is access protected
// and sets the "message" field value
// of the "Requirement" struct as a result of that.
func (r *tlpAmberRedReporter) report(_ *processor, _ *Domain) {
// TODO
func (r *tlpAmberRedReporter) report(p *processor, domain *Domain) {
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
@ -366,23 +342,33 @@ func (r *directoryListingsReporter) report(p *processor, domain *Domain) {
// of the "Requirement" struct as a result of that.
func (r *rolieFeedReporter) report(p *processor, domain *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.")
return
}
if len(p.badROLIEfeed) == 0 {
if len(p.badROLIEFeed) == 0 {
req.message(InfoType, "All checked ROLIE feeds validated fine.")
return
}
req.Messages = p.badROLIEfeed
req.Messages = p.badROLIEFeed
}
// report tests whether a ROLIE service document is used and if so,
// whether it is a [RFC8322] conform JSON file that lists the
// ROLIE feed documents and sets the "message" field value
// of the "Requirement" struct as a result of that.
func (r *rolieServiceReporter) report(_ *processor, _ *Domain) {
// TODO
func (r *rolieServiceReporter) report(p *processor, domain *Domain) {
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,
@ -390,8 +376,18 @@ func (r *rolieServiceReporter) report(_ *processor, _ *Domain) {
// documents by certain criteria
// and sets the "message" field value
// of the "Requirement" struct as a result of that.
func (r *rolieCategoryReporter) report(_ *processor, _ *Domain) {
// TODO
func (r *rolieCategoryReporter) report(p *processor, domain *Domain) {
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) {

View file

@ -9,7 +9,10 @@
package main
import (
"net/http"
"net/url"
"sort"
"strings"
"github.com/csaf-poc/csaf_distribution/v2/csaf"
"github.com/csaf-poc/csaf_distribution/v2/util"
@ -21,7 +24,8 @@ type rolieLabelChecker struct {
feedURL string
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.
@ -65,21 +69,21 @@ func (ca *rolieLabelChecker) check(
// Associate advisory label to urls.
advs := ca.advisories[advisoryLabel]
if advs == nil {
advs = make(map[string]struct{})
advs = util.Set[string]{}
ca.advisories[advisoryLabel] = advs
}
advs[advisory] = struct{}{}
advs.Add(advisory)
// If entry shows up in feed of higher tlp level,
// give out info or warning
switch {
case advisoryRank < feedRank:
if advisoryRank == 0 { // All kinds of 'UNLABELED'
p.badROLIEfeed.info(
p.badROLIEFeed.info(
"Found unlabeled advisory %q in feed %q.",
advisory, ca.feedURL)
} else {
p.badROLIEfeed.warn(
p.badROLIEFeed.warn(
"Found advisory %q labled TLP:%s in feed %q (TLP:%s).",
advisory, advisoryLabel,
ca.feedURL, ca.feedLabel)
@ -87,21 +91,57 @@ func (ca *rolieLabelChecker) check(
case advisoryRank > feedRank:
// 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",
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
// integriry and completeness.
// processROLIEFeeds goes through all ROLIE feeds and checks their
// integrity and completeness.
func (p *processor) processROLIEFeeds(feeds [][]csaf.Feed) error {
base, err := url.Parse(p.pmdURL)
if err != nil {
return err
}
p.badROLIEfeed.use()
p.badROLIEFeed.use()
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.
for _, fs := range feeds {
for i := range fs {
@ -158,12 +202,29 @@ func (p *processor) processROLIEFeeds(feeds [][]csaf.Feed) error {
}
label := tlpLabel(feed.TLPLabel)
p.labelChecker = &rolieLabelChecker{
feedURL: feedURL.String(),
feedLabel: label,
advisories: map[csaf.TLPLabel]map[string]struct{}{},
if err := p.categoryCheck(feedBase, label); err != nil {
if err != errContinue {
return err
}
}
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 != errContinue {
@ -175,7 +236,7 @@ func (p *processor) processROLIEFeeds(feeds [][]csaf.Feed) error {
// Phase 3: Check for completeness.
hasSummary := map[csaf.TLPLabel]struct{}{}
hasSummary := util.Set[csaf.TLPLabel]{}
var (
hasUnlabeled = false
@ -214,24 +275,25 @@ func (p *processor) processROLIEFeeds(feeds [][]csaf.Feed) error {
}
reference := p.labelChecker.advisories[label]
advisories := make(map[string]struct{}, len(reference))
advisories := make(util.Set[string], len(reference))
for _, adv := range files {
u, err := url.Parse(adv.URL())
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
}
advisories[makeAbs(u).String()] = struct{}{}
}
if containsAllKeys(reference, advisories) {
hasSummary[label] = struct{}{}
if advisories.ContainsAll(reference) {
hasSummary.Add(label)
}
}
}
if !hasWhite && !hasGreen && !hasUnlabeled {
p.badROLIEfeed.error(
p.badROLIEFeed.error(
"One ROLIE feed with a TLP:WHITE, TLP:GREEN or unlabeled tlp must exist, " +
"but none were found.")
}
@ -244,8 +306,8 @@ func (p *processor) processROLIEFeeds(feeds [][]csaf.Feed) error {
csaf.TLPLabelAmber,
csaf.TLPLabelRed,
} {
if _, ok := hasSummary[label]; !ok && len(p.labelChecker.advisories[label]) > 0 {
p.badROLIEfeed.warn(
if !hasSummary.Contains(label) && len(p.labelChecker.advisories[label]) > 0 {
p.badROLIEFeed.warn(
"ROLIE feed for TLP:%s has no accessible listed feed covering all advisories.",
label)
}
@ -254,12 +316,111 @@ func (p *processor) processROLIEFeeds(feeds [][]csaf.Feed) error {
return nil
}
// containsAllKeys returns if m2 contains all keys of m1.
func containsAllKeys[K comparable, V any](m1, m2 map[K]V) bool {
for k := range m1 {
if _, ok := m2[k]; !ok {
return false
// categoryCheck checks for the existence of a feeds ROLIE category document and if it does,
// whether the category document contains distinguishing categories
func (p *processor) categoryCheck(folderURL string, label csaf.TLPLabel) error {
labelname := strings.ToLower(string(label))
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
}
return true
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)
}
}
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
yet considered to change the overall result,
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
}