mirror of
https://github.com/gocsaf/csaf.git
synced 2025-12-22 05:40:11 +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
|
||||
}
|
||||
|
||||
// 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 {
|
||||
|
|
|
|||
|
|
@ -54,20 +54,24 @@ type processor struct {
|
|||
keys *crypto.KeyRing
|
||||
labelChecker *rolieLabelChecker
|
||||
|
||||
invalidAdvisories topicMessages
|
||||
badFilenames topicMessages
|
||||
badIntegrities topicMessages
|
||||
badPGPs topicMessages
|
||||
badSignatures topicMessages
|
||||
badProviderMetadata topicMessages
|
||||
badSecurity topicMessages
|
||||
badIndices topicMessages
|
||||
badChanges topicMessages
|
||||
badFolders topicMessages
|
||||
badWellknownMetadata topicMessages
|
||||
badDNSPath topicMessages
|
||||
badDirListings topicMessages
|
||||
badROLIEfeed topicMessages
|
||||
invalidAdvisories topicMessages
|
||||
badFilenames topicMessages
|
||||
badIntegrities topicMessages
|
||||
badPGPs topicMessages
|
||||
badSignatures topicMessages
|
||||
badProviderMetadata topicMessages
|
||||
badSecurity topicMessages
|
||||
badIndices topicMessages
|
||||
badChanges topicMessages
|
||||
badFolders topicMessages
|
||||
badWellknownMetadata topicMessages
|
||||
badDNSPath topicMessages
|
||||
badDirListings 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,11 +846,15 @@ 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 tlp, ok := distribution["tlp"]; ok {
|
||||
if label, ok := tlp.(map[string]any); ok {
|
||||
if labelstring, ok := label["label"].(string); ok {
|
||||
return csaf.TLPLabel(labelstring)
|
||||
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 {
|
||||
return csaf.TLPLabel(labelstring)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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,13 +202,30 @@ 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 {
|
||||
return err
|
||||
|
|
@ -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
|
||||
}
|
||||
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
|
||||
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
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