1
0
Fork 0
mirror of https://github.com/gocsaf/csaf.git synced 2025-12-22 11:55:40 +01:00
gocsaf/cmd/csaf_provider/create.go
JanHoefelmeyer b61912410a
Empty rolie (#357)
* Create ROLIE feed if summaries are empty

* Formatting, Remove sorting of 0 elements

* Handle minimum entry length error as warning in checker

* Use empty array instead of creating an empty array to reference

* Change schema to allow for empty entry arrays

* Use https://raw.githubusercontent.com/oasis-tcs/csaf/81b2663697958bc5f85d14372712a40028fb8338/csaf_2.0/json_schema/ROLIE_feed_json_schema.json as schema for ROLIE feeds

* Change label name from empty to undefined

* Change default of create_service_document for csaf_provider to true

* Config

* Count entries in csaf-checker, warn if there are none.

* Add Comments to csaf/rolie.go's CountEntries function

* Delete index.txt and changes.csv in aggregator if there are no entries.

* Create an empty ROLIE feed document when setting up folders during create

* nit: set update time stamp in structure init.

* Instantiate label checker only once.

* Ignore domain not having roles.

* provider: Create empty entry section in ROLIE feed.

* Stop check for domain if PMD check fails

* Add missing continue statement

* Report missing ROLIE feed entries in ROLIE feed, not Provider Metadata

* Do not ommit empty entries in ROLIE feeds.

* Fixed error handling problem introduced by faulty merge. Removed unused errStop handling while there.

---------

Co-authored-by: JanHoefelmeyer <hoefelmeyer.jan@gmail.com>
Co-authored-by: Sascha L. Teichmann <sascha.teichmann@intevation.de>
Co-authored-by: JanHoefelmeyer <Jan Höfelmeyer jhoefelmeyer@intevation.de>
2023-06-30 23:34:43 +02:00

368 lines
9.1 KiB
Go

// 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: 2021 German Federal Office for Information Security (BSI) <https://www.bsi.bund.de>
// Software-Engineering: 2021 Intevation GmbH <https://intevation.de>
package main
import (
"bufio"
"bytes"
"errors"
"fmt"
"os"
"path"
"path/filepath"
"strings"
"time"
"unicode"
"github.com/ProtonMail/gopenpgp/v2/crypto"
"github.com/csaf-poc/csaf_distribution/v2/csaf"
"github.com/csaf-poc/csaf_distribution/v2/util"
)
// ensureFolders initializes the paths and call functions to create
// the directories and files.
func ensureFolders(c *config) error {
wellknown := filepath.Join(c.Web, ".well-known")
wellknownCSAF := filepath.Join(wellknown, "csaf")
for _, create := range []func(*config, string) error{
createWellknown,
createFeedFolders,
createService,
createOpenPGPFolder,
createProviderMetadata,
} {
if err := create(c, wellknownCSAF); err != nil {
return err
}
}
// Only write/modify security.txt if configured.
if c.WriteSecurity {
if err := setupSecurity(c, wellknown); err != nil {
return err
}
}
return nil
}
// createWellknown creates ".well-known" directory if not exist and returns nil.
// An error is returned if the it is not a directory.
func createWellknown(_ *config, wellknown string) error {
st, err := os.Stat(wellknown)
if err != nil {
if os.IsNotExist(err) {
return os.MkdirAll(wellknown, 0755)
}
return err
}
if !st.IsDir() {
return errors.New(".well-known/csaf is not a directory")
}
return nil
}
// createService creates the ROLIE service document (if configured).
func createService(c *config, wellknownCSAF string) error {
// no service document needed.
if !c.ServiceDocument {
return nil
}
categories := csaf.ROLIEServiceWorkspaceCollectionCategories{
Category: []csaf.ROLIEServiceWorkspaceCollectionCategoriesCategory{{
Scheme: "urn:ietf:params:rolie:category:information-type",
Term: "csaf",
}},
}
var collections []csaf.ROLIEServiceWorkspaceCollection
for _, t := range c.TLPs {
if t == tlpCSAF {
continue
}
ts := string(t)
feedName := "csaf-feed-tlp-" + ts + ".json"
href := c.CanonicalURLPrefix +
"/.well-known/csaf/" + ts + "/" + feedName
collection := csaf.ROLIEServiceWorkspaceCollection{
Title: "CSAF feed (TLP:" + strings.ToUpper(ts) + ")",
HRef: href,
Categories: categories,
}
collections = append(collections, collection)
}
rsd := &csaf.ROLIEServiceDocument{
Service: csaf.ROLIEService{
Workspace: []csaf.ROLIEServiceWorkspace{{
Title: "CSAF feeds",
Collection: collections,
}},
},
}
path := filepath.Join(wellknownCSAF, "service.json")
return util.WriteToFile(path, rsd)
}
// createFeedFolders creates the feed folders according to the tlp values
// in the "tlps" config option if they do not already exist.
// No creation for the "csaf" option will be done.
// It creates also symbolic links to feed folders.
func createFeedFolders(c *config, wellknown string) error {
// If we have static configured categories we need to create
// the category documents.
var catDoc *csaf.ROLIECategoryDocument
if categories := c.StaticCategories(); len(categories) > 0 {
catDoc = csaf.NewROLIECategoryDocument(categories...)
}
for _, t := range c.TLPs {
if t == tlpCSAF {
continue
}
tlpLink := filepath.Join(wellknown, string(t))
if _, err := filepath.EvalSymlinks(tlpLink); err != nil {
if os.IsNotExist(err) {
tlpFolder := filepath.Join(c.Folder, string(t))
if tlpFolder, err = util.MakeUniqDir(tlpFolder); err != nil {
return err
}
if err = os.Symlink(tlpFolder, tlpLink); err != nil {
return err
}
} else {
return err
}
}
// Store the category document.
if catDoc != nil {
catPath := path.Join(tlpLink, "category-"+string(t)+".json")
if err := util.WriteToFile(catPath, catDoc); err != nil {
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 {
openPGPFolder := filepath.Join(wellknown, "openpgp")
if _, err := os.Stat(openPGPFolder); err != nil {
if os.IsNotExist(err) {
if err := os.MkdirAll(openPGPFolder, 0755); err != nil {
return err
}
} else {
return err
}
}
keyData, err := os.ReadFile(c.OpenPGPPublicKey)
if err != nil {
return fmt.Errorf("cannot load public OpenPGP key: %v", err)
}
key, err := crypto.NewKeyFromArmoredReader(bytes.NewReader(keyData))
if err != nil {
return err
}
fp := strings.ToUpper(key.GetFingerprint())
dst := filepath.Join(openPGPFolder, fp+".asc")
// If we don't have it write it.
if _, err = os.Stat(dst); err != nil {
if os.IsNotExist(err) {
err = os.WriteFile(dst, keyData, 0644)
}
}
return err
}
// setupSecurity creates the "security.txt" file if does not exist
// and writes the CSAF field inside the file. If the file exists
// it checks ig the CSAF entry with the provider-metadata.json
// path is already in. If its not it is added in front of all lines.
// Otherwise the file is left untouched.
func setupSecurity(c *config, wellknown string) error {
security := filepath.Join(wellknown, "security.txt")
path := fmt.Sprintf(
"%s/.well-known/csaf/provider-metadata.json",
c.CanonicalURLPrefix)
st, err := os.Stat(security)
if err != nil {
if os.IsNotExist(err) {
f, err := os.Create(security)
if err != nil {
return err
}
fmt.Fprintf(f, "CSAF: %s\n", path)
return f.Close()
}
return err
}
// Load it line wise
found, lines, err := func() (bool, []string, error) {
f, err := os.Open(security)
if err != nil {
return false, nil, err
}
defer f.Close()
var lines []string
sc := bufio.NewScanner(f)
for sc.Scan() {
line := sc.Text()
if s := strings.TrimLeftFunc(line, unicode.IsSpace); strings.HasPrefix(s, "CSAF:") {
// Check if we are already in.
if strings.TrimSpace(s[len("CSAF:"):]) == path {
return true, nil, nil
}
}
lines = append(lines, line)
}
return false, lines, sc.Err()
}()
if err != nil {
return err
}
// we are already in the file.
if found {
return nil
}
// Insert our CSAF line at the beginning
// to get higher priority over possible existing CSAF lines.
csafLine := fmt.Sprintf("CSAF: %s", path)
lines = append([]string{csafLine, ""}, lines...)
// Write back to second file and switch over afterwards.
newSecurity, nf, err := util.MakeUniqFile(security + ".tmp")
if err != nil {
return err
}
for _, line := range lines {
if _, err := fmt.Fprintln(nf, line); err != nil {
nf.Close()
os.RemoveAll(newSecurity)
return err
}
}
if err := nf.Close(); err != nil {
os.RemoveAll(newSecurity)
return err
}
// Swap atomically.
if err := os.Rename(newSecurity, security); err != nil {
os.RemoveAll(newSecurity)
return err
}
// Re-establish old permissions.
return os.Chmod(security, st.Mode().Perm())
}
// createProviderMetadata creates the provider-metadata.json file if does not exist.
func createProviderMetadata(c *config, wellknownCSAF string) error {
path := filepath.Join(wellknownCSAF, "provider-metadata.json")
_, err := os.Stat(path)
if err == nil {
return nil
}
if !os.IsNotExist(err) {
return err
}
pm := csaf.NewProviderMetadataDomain(c.CanonicalURLPrefix, c.modelTLPs())
c.ProviderMetaData.apply(pm)
// We have directory based distributions.
if c.WriteIndices {
// Every TLP as a distribution?
for _, t := range c.TLPs {
if t != tlpCSAF {
pm.AddDirectoryDistribution(
c.CanonicalURLPrefix + "/.well-known/csaf/" + string(t))
}
}
}
key, err := loadCryptoKeyFromFile(c.OpenPGPPublicKey)
if err != nil {
return fmt.Errorf("cannot load public key: %v", err)
}
fingerprint := strings.ToUpper(key.GetFingerprint())
pm.SetPGP(fingerprint, c.openPGPPublicURL(fingerprint))
return util.WriteToFile(path, pm)
}