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:
commit
540d02d367
8 changed files with 636 additions and 141 deletions
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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
205
cmd/csaf_checker/rules.go
Normal 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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
53
util/set.go
Normal 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
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue