diff --git a/cmd/csaf_aggregator/indices.go b/cmd/csaf_aggregator/indices.go index 192ec6d..f59d21b 100644 --- a/cmd/csaf_aggregator/indices.go +++ b/cmd/csaf_aggregator/indices.go @@ -91,6 +91,22 @@ func (w *worker) writeInterims(label string, summaries []summary) error { func (w *worker) writeCSV(label string, summaries []summary) error { + fname := filepath.Join(w.dir, label, changesCSV) + + // If we don't have any entries remove existing file. + if len(summaries) == 0 { + // Does it really exist? + if err := os.RemoveAll(fname); err != nil { + return fmt.Errorf("unable to remove %q: %w", fname, err) + } + return nil + } + + f, err := os.Create(fname) + if err != nil { + return err + } + // Do not sort in-place. ss := make([]summary, len(summaries)) copy(ss, summaries) @@ -100,11 +116,6 @@ func (w *worker) writeCSV(label string, summaries []summary) error { ss[j].summary.CurrentReleaseDate) }) - fname := filepath.Join(w.dir, label, changesCSV) - f, err := os.Create(fname) - if err != nil { - return err - } out := util.NewFullyQuotedCSWWriter(f) record := make([]string, 2) @@ -137,6 +148,16 @@ func (w *worker) writeCSV(label string, summaries []summary) error { func (w *worker) writeIndex(label string, summaries []summary) error { fname := filepath.Join(w.dir, label, indexTXT) + + // If we don't have any entries remove existing file. + if len(summaries) == 0 { + // Does it really exist? + if err := os.RemoveAll(fname); err != nil { + return fmt.Errorf("unable to remove %q: %w", fname, err) + } + return nil + } + f, err := os.Create(fname) if err != nil { return err @@ -157,6 +178,46 @@ func (w *worker) writeIndex(label string, summaries []summary) error { return err2 } +func (w *worker) writeROLIENoSummaries(label string) error { + + labelFolder := strings.ToLower(label) + + fname := "csaf-feed-tlp-" + labelFolder + ".json" + + feedURL := w.processor.cfg.Domain + "/.well-known/csaf-aggregator/" + + w.provider.Name + "/" + labelFolder + "/" + fname + + links := []csaf.Link{{ + Rel: "self", + HRef: feedURL, + }} + + if w.provider.serviceDocument(w.processor.cfg) { + links = append(links, csaf.Link{ + Rel: "service", + HRef: w.processor.cfg.Domain + "/.well-known/csaf-aggregator/" + + w.provider.Name + "/service.json", + }) + } + + rolie := &csaf.ROLIEFeed{ + Feed: csaf.FeedData{ + ID: "csaf-feed-tlp-" + strings.ToLower(label), + Title: "CSAF feed (TLP:" + strings.ToUpper(label) + ")", + Link: links, + Category: []csaf.ROLIECategory{{ + Scheme: "urn:ietf:params:rolie:category:information-type", + Term: "csaf", + }}, + Updated: csaf.TimeStamp(time.Now().UTC()), + Entry: []*csaf.Entry{}, + }, + } + + path := filepath.Join(w.dir, labelFolder, fname) + return util.WriteToFile(path, rolie) +} + func (w *worker) writeROLIE(label string, summaries []summary) error { labelFolder := strings.ToLower(label) @@ -311,6 +372,7 @@ func (w *worker) writeService() error { func (w *worker) writeIndices() error { if len(w.summaries) == 0 || w.dir == "" { + w.writeROLIENoSummaries("undefined") return nil } diff --git a/cmd/csaf_checker/processor.go b/cmd/csaf_checker/processor.go index 64f0db2..485f6ae 100644 --- a/cmd/csaf_checker/processor.go +++ b/cmd/csaf_checker/processor.go @@ -86,8 +86,6 @@ type reporter interface { var ( // errContinue indicates that the current check should continue. errContinue = errors.New("continue") - // errStop indicates that the current check should stop. - errStop = errors.New("stop") ) type whereType byte @@ -262,10 +260,9 @@ func (p *processor) run(domains []string) (*Report, error) { continue } if err := p.checkDomain(d); err != nil { - if err == errContinue || err == errStop { - continue - } - return nil, err + log.Printf("Failed to find valid provider-metadata.json for domain %s: %v. "+ + "Continuing with next domain.", d, err) + continue } domain := &Domain{Name: d} @@ -354,17 +351,17 @@ func (p *processor) domainChecks(domain string) []func(*processor, string) error return checks } +// checkDomain runs a set of domain specific checks on a given +// domain. func (p *processor) checkDomain(domain string) error { - for _, check := range p.domainChecks(domain) { - if err := check(p, domain); err != nil && err != errContinue { - if err == errStop { - return nil + if err := check(p, domain); err != nil { + if err == errContinue { + continue } return err } } - return nil } @@ -503,12 +500,15 @@ func (p *processor) rolieFeedEntries(feed string) ([]csaf.AdvisoryFile, error) { var rolieDoc any err = json.NewDecoder(bytes.NewReader(all)).Decode(&rolieDoc) return rfeed, rolieDoc, err - }() if err != nil { p.badProviderMetadata.error("Loading ROLIE feed failed: %v.", err) return nil, errContinue } + + if rfeed.CountEntries() == 0 { + p.badROLIEFeed.warn("No entries in %s", feed) + } errors, err := csaf.ValidateROLIE(rolieDoc) if err != nil { return nil, err @@ -1208,8 +1208,6 @@ func (p *processor) checkProviderMetadata(domain string) bool { } if !lpmd.Valid() { - p.badProviderMetadata.error("No valid provider-metadata.json found.") - p.badProviderMetadata.error("STOPPING here - cannot perform other checks.") return false } diff --git a/cmd/csaf_provider/config.go b/cmd/csaf_provider/config.go index b653361..172d044 100644 --- a/cmd/csaf_provider/config.go +++ b/cmd/csaf_provider/config.go @@ -31,6 +31,7 @@ const ( defaultWeb = "/var/www/html" // Default web path. defaultNoWebUI = true defaultUploadLimit = 50 * 1024 * 1024 // Default limit size of the uploaded file. + defaultServiceDocument = true ) type providerMetadataConfig struct { @@ -226,7 +227,8 @@ func loadConfig() (*config, error) { // Preset defaults cfg := config{ - NoWebUI: defaultNoWebUI, + NoWebUI: defaultNoWebUI, + ServiceDocument: defaultServiceDocument, } md, err := toml.DecodeFile(path, &cfg) diff --git a/cmd/csaf_provider/create.go b/cmd/csaf_provider/create.go index 83f24a3..83252ba 100644 --- a/cmd/csaf_provider/create.go +++ b/cmd/csaf_provider/create.go @@ -17,6 +17,7 @@ import ( "path" "path/filepath" "strings" + "time" "unicode" "github.com/ProtonMail/gopenpgp/v2/crypto" @@ -153,10 +154,56 @@ func createFeedFolders(c *config, wellknown string) error { return err } } + // Create an empty ROLIE feed document + if err := createROLIEfeed(c, t, tlpLink); err != nil { + return err + } } return nil } +// createROLIEfeed creates an empty ROLIE feed +func createROLIEfeed(c *config, t tlp, folder string) error { + ts := string(t) + feedName := "csaf-feed-tlp-" + ts + ".json" + + feed := filepath.Join(folder, feedName) + + feedURL := csaf.JSONURL( + c.CanonicalURLPrefix + + "/.well-known/csaf/" + ts + "/" + feedName) + + tlpLabel := csaf.TLPLabel(strings.ToUpper(ts)) + + links := []csaf.Link{{ + Rel: "self", + HRef: string(feedURL), + }} + // If we have a service document we need to link it. + if c.ServiceDocument { + links = append(links, csaf.Link{ + Rel: "service", + HRef: c.CanonicalURLPrefix + "/.well-known/csaf/service.json", + }) + } + rolie := &csaf.ROLIEFeed{ + Feed: csaf.FeedData{ + ID: "csaf-feed-tlp-" + ts, + Title: "CSAF feed (TLP:" + string(tlpLabel) + ")", + Link: links, + Category: []csaf.ROLIECategory{{ + Scheme: "urn:ietf:params:rolie:category:information-type", + Term: "csaf", + }}, + Updated: csaf.TimeStamp(time.Now().UTC()), + Entry: []*csaf.Entry{}, + }, + } + + return util.WriteToFile(feed, rolie) + +} + // createOpenPGPFolder creates an openpgp folder besides // the provider-metadata.json in the csaf folder. func createOpenPGPFolder(c *config, wellknown string) error { diff --git a/cmd/csaf_provider/rolie.go b/cmd/csaf_provider/rolie.go index 2165c0c..16d871e 100644 --- a/cmd/csaf_provider/rolie.go +++ b/cmd/csaf_provider/rolie.go @@ -110,6 +110,7 @@ func (c *controller) extendROLIE( Scheme: "urn:ietf:params:rolie:category:information-type", Term: "csaf", }}, + Entry: []*csaf.Entry{}, }, } } diff --git a/csaf/rolie.go b/csaf/rolie.go index 2e7e812..7652445 100644 --- a/csaf/rolie.go +++ b/csaf/rolie.go @@ -185,7 +185,7 @@ type FeedData struct { Link []Link `json:"link,omitempty"` Category []ROLIECategory `json:"category,omitempty"` Updated TimeStamp `json:"updated"` - Entry []*Entry `json:"entry,omitempty"` + Entry []*Entry `json:"entry"` } // ROLIEFeed is a ROLIE feed. @@ -238,3 +238,8 @@ func (rf *ROLIEFeed) SortEntriesByUpdated() { return time.Time(entries[j].Updated).Before(time.Time(entries[i].Updated)) }) } + +// CountEntries returns the number of entries within the feed +func (rf *ROLIEFeed) CountEntries() int { + return len(rf.Feed.Entry) +} diff --git a/csaf/schema/ROLIE_feed_json_schema.json b/csaf/schema/ROLIE_feed_json_schema.json index e07d152..2911436 100644 --- a/csaf/schema/ROLIE_feed_json_schema.json +++ b/csaf/schema/ROLIE_feed_json_schema.json @@ -20,7 +20,10 @@ "title": "Link", "description": "Specifies the JSON link.", "type": "object", - "required": ["rel", "href"], + "required": [ + "rel", + "href" + ], "properties": { "href": { "title": "Hyper reference", @@ -31,7 +34,9 @@ "title": "Relationship", "description": "Contains the relationship value of the link.", "type": "string", - "enum": ["self"] + "enum": [ + "self" + ] } } } @@ -42,7 +47,10 @@ "title": "Link", "description": "Specifies a single link.", "type": "object", - "required": ["rel", "href"], + "required": [ + "rel", + "href" + ], "properties": { "href": { "title": "Hyper reference", @@ -61,13 +69,22 @@ } }, "type": "object", - "required": ["feed"], + "required": [ + "feed" + ], "properties": { "feed": { "title": "CSAF ROLIE feed", "description": "Contains all information of the feed.", "type": "object", - "required": ["id", "title", "link", "category", "updated", "entry"], + "required": [ + "id", + "title", + "link", + "category", + "updated", + "entry" + ], "properties": { "id": { "title": "ID", @@ -96,19 +113,26 @@ "title": "CSAF ROLIE category", "description": "Contains the required ROLIE category value.", "type": "object", - "required": ["scheme", "term"], + "required": [ + "scheme", + "term" + ], "properties": { "scheme": { "title": "Scheme", "description": "Contains the URI of the scheme to use.", "type": "string", - "enum": ["urn:ietf:params:rolie:category:information-type"] + "enum": [ + "urn:ietf:params:rolie:category:information-type" + ] }, "term": { "title": "Term", "description": "Contains the term that is valid in the context of the scheme.", "type": "string", - "enum": ["csaf"] + "enum": [ + "csaf" + ] } } } @@ -119,7 +143,10 @@ "title": "Category", "description": "Specifies a single category.", "type": "object", - "required": ["scheme", "term"], + "required": [ + "scheme", + "term" + ], "properties": { "scheme": { "title": "Scheme", @@ -146,7 +173,6 @@ "title": "List of Entries", "description": "Contains a list of feed entries.", "type": "array", - "minItems": 1, "uniqueItems": true, "items": { "title": "Entry", @@ -193,13 +219,13 @@ "format": "date-time" }, "summary": { - "title": "", - "description": "", + "title": "Summary", + "description": "Contains the summary of the CSAF document.", "type": "object", "properties": { "content": { - "title": "", - "description": "", + "title": "Content", + "description": "Contains the actual text of the summary.", "type": "string", "minLength": 1 } @@ -209,7 +235,10 @@ "title": "Content of the entry", "description": "Contains information about the content.", "type": "object", - "required": ["type", "src"], + "required": [ + "type", + "src" + ], "properties": { "src": { "title": "Source Code", @@ -220,15 +249,20 @@ "title": "MIME type", "description": "Contains the MIME type of the content.", "type": "string", - "enum": ["application/json"] + "enum": [ + "application/json" + ] } } }, "format": { - "title": "", - "description": "", + "title": "Format", + "description": "Contains information about the format of the entry.", "type": "object", - "required": ["schema", "version"], + "required": [ + "schema", + "version" + ], "properties": { "schema": { "title": "Schema of the entry", @@ -240,9 +274,11 @@ }, "version": { "title": "CSAF Version", - "description": "Contains the CSAF version the document was written in.", + "description": "Contains the CSAF version the document was written in.", "type": "string", - "enum": ["2.0"] + "enum": [ + "2.0" + ] } } } diff --git a/docs/csaf_provider.md b/docs/csaf_provider.md index d1e6b22..8b0117a 100644 --- a/docs/csaf_provider.md +++ b/docs/csaf_provider.md @@ -100,7 +100,7 @@ The following example file documents all available configuration options: #tlps = ["csaf", "white", "amber", "green", "red"] # Make the provider create a ROLIE service document. -#create_service_document = false +#create_service_document = true # Make the provider create a ROLIE category document from a list of strings. # If a list item starts with `expr:`