mirror of
https://github.com/gocsaf/csaf.git
synced 2025-12-22 05:40:11 +01:00
Add support for remote validation services. (#185)
* Simple tool to test the remote validation * Added remote validator support to provider. * Added remote validation to aggregator. * Calm golint * Removed csaf_remote_validator tool as it was only for dev. * Re-added csaf_remote_validator tool. Testing is not done. * Embed the document entirely * Include testing the remote validator in the Itests * Change permission of the script * Remove code for Itests * As these will be done in another branch Co-authored-by: Fadi Abbud <fadi.abbud@intevation.de>
This commit is contained in:
parent
7cbbb4bf81
commit
78d8b89aca
16 changed files with 466 additions and 43 deletions
|
|
@ -21,3 +21,4 @@
|
||||||
| github.com/gofrs/flock | BSD-3-Clause |
|
| github.com/gofrs/flock | BSD-3-Clause |
|
||||||
| github.com/PuerkitoBio/goquery | BSD-3-Clause |
|
| github.com/PuerkitoBio/goquery | BSD-3-Clause |
|
||||||
| github.com/andybalholm/cascadia | BSD-2-Clause |
|
| github.com/andybalholm/cascadia | BSD-2-Clause |
|
||||||
|
| go.etcd.io/bbolt | MIT |
|
||||||
|
|
|
||||||
|
|
@ -67,6 +67,9 @@ type config struct {
|
||||||
// for interim advisories. Less/equal zero means forever.
|
// for interim advisories. Less/equal zero means forever.
|
||||||
InterimYears int `toml:"interim_years"`
|
InterimYears int `toml:"interim_years"`
|
||||||
|
|
||||||
|
// RemoteValidator configures an optional remote validation.
|
||||||
|
RemoteValidatorOptions *csaf.RemoteValidatorOptions `toml:"remote_validator"`
|
||||||
|
|
||||||
keyMu sync.Mutex
|
keyMu sync.Mutex
|
||||||
key *crypto.Key
|
key *crypto.Key
|
||||||
keyErr error
|
keyErr error
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,7 @@ func (w *worker) setupProviderFull(provider *provider) error {
|
||||||
w.provider = provider
|
w.provider = provider
|
||||||
|
|
||||||
// Each job needs a separate client.
|
// Each job needs a separate client.
|
||||||
w.client = w.cfg.httpClient(provider)
|
w.client = w.processor.cfg.httpClient(provider)
|
||||||
|
|
||||||
// We need the provider metadata in all cases.
|
// We need the provider metadata in all cases.
|
||||||
if err := w.locateProviderMetadata(provider.Domain); err != nil {
|
if err := w.locateProviderMetadata(provider.Domain); err != nil {
|
||||||
|
|
@ -83,6 +83,22 @@ func (p *processor) full() error {
|
||||||
var doWork fullWorkFunc
|
var doWork fullWorkFunc
|
||||||
|
|
||||||
if p.cfg.runAsMirror() {
|
if p.cfg.runAsMirror() {
|
||||||
|
|
||||||
|
// check if we need to setup a remote validator
|
||||||
|
if p.cfg.RemoteValidatorOptions != nil {
|
||||||
|
validator, err := p.cfg.RemoteValidatorOptions.Open()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Not sure if we really need it to be serialized.
|
||||||
|
p.remoteValidator = csaf.SynchronizedRemoteValidator(validator)
|
||||||
|
defer func() {
|
||||||
|
p.remoteValidator.Close()
|
||||||
|
p.remoteValidator = nil
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
doWork = (*worker).mirror
|
doWork = (*worker).mirror
|
||||||
log.Println("Running in aggregator mode")
|
log.Println("Running in aggregator mode")
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -96,7 +112,7 @@ func (p *processor) full() error {
|
||||||
log.Printf("Starting %d workers.\n", p.cfg.Workers)
|
log.Printf("Starting %d workers.\n", p.cfg.Workers)
|
||||||
for i := 1; i <= p.cfg.Workers; i++ {
|
for i := 1; i <= p.cfg.Workers; i++ {
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
w := newWorker(i, p.cfg)
|
w := newWorker(i, p)
|
||||||
go w.fullWork(&wg, doWork, queue)
|
go w.fullWork(&wg, doWork, queue)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -143,7 +143,7 @@ func (w *worker) writeROLIE(label string, summaries []summary) error {
|
||||||
|
|
||||||
fname := "csaf-feed-tlp-" + labelFolder + ".json"
|
fname := "csaf-feed-tlp-" + labelFolder + ".json"
|
||||||
|
|
||||||
feedURL := w.cfg.Domain + "/.well-known/csaf-aggregator/" +
|
feedURL := w.processor.cfg.Domain + "/.well-known/csaf-aggregator/" +
|
||||||
w.provider.Name + "/" + labelFolder + "/" + fname
|
w.provider.Name + "/" + labelFolder + "/" + fname
|
||||||
|
|
||||||
entries := make([]*csaf.Entry, len(summaries))
|
entries := make([]*csaf.Entry, len(summaries))
|
||||||
|
|
@ -156,7 +156,7 @@ func (w *worker) writeROLIE(label string, summaries []summary) error {
|
||||||
for i := range summaries {
|
for i := range summaries {
|
||||||
s := &summaries[i]
|
s := &summaries[i]
|
||||||
|
|
||||||
csafURL := w.cfg.Domain + "/.well-known/csaf-aggregator/" +
|
csafURL := w.processor.cfg.Domain + "/.well-known/csaf-aggregator/" +
|
||||||
w.provider.Name + "/" + label + "/" +
|
w.provider.Name + "/" + label + "/" +
|
||||||
strconv.Itoa(s.summary.InitialReleaseDate.Year()) + "/" +
|
strconv.Itoa(s.summary.InitialReleaseDate.Year()) + "/" +
|
||||||
s.filename
|
s.filename
|
||||||
|
|
|
||||||
|
|
@ -149,12 +149,12 @@ func (w *worker) setupProviderInterim(provider *provider) {
|
||||||
w.provider = provider
|
w.provider = provider
|
||||||
|
|
||||||
// Each job needs a separate client.
|
// Each job needs a separate client.
|
||||||
w.client = w.cfg.httpClient(provider)
|
w.client = w.processor.cfg.httpClient(provider)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *worker) interimWork(wg *sync.WaitGroup, jobs <-chan *interimJob) {
|
func (w *worker) interimWork(wg *sync.WaitGroup, jobs <-chan *interimJob) {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
path := filepath.Join(w.cfg.Web, ".well-known", "csaf-aggregator")
|
path := filepath.Join(w.processor.cfg.Web, ".well-known", "csaf-aggregator")
|
||||||
|
|
||||||
for j := range jobs {
|
for j := range jobs {
|
||||||
w.setupProviderInterim(j.provider)
|
w.setupProviderInterim(j.provider)
|
||||||
|
|
@ -162,7 +162,7 @@ func (w *worker) interimWork(wg *sync.WaitGroup, jobs <-chan *interimJob) {
|
||||||
providerPath := filepath.Join(path, j.provider.Name)
|
providerPath := filepath.Join(path, j.provider.Name)
|
||||||
|
|
||||||
j.err = func() error {
|
j.err = func() error {
|
||||||
tx := newLazyTransaction(providerPath, w.cfg.Folder)
|
tx := newLazyTransaction(providerPath, w.processor.cfg.Folder)
|
||||||
defer tx.rollback()
|
defer tx.rollback()
|
||||||
|
|
||||||
// Try all the labels
|
// Try all the labels
|
||||||
|
|
@ -178,7 +178,7 @@ func (w *worker) interimWork(wg *sync.WaitGroup, jobs <-chan *interimJob) {
|
||||||
|
|
||||||
interimsCSV := filepath.Join(labelPath, "interims.csv")
|
interimsCSV := filepath.Join(labelPath, "interims.csv")
|
||||||
interims, err := readInterims(
|
interims, err := readInterims(
|
||||||
interimsCSV, w.cfg.InterimYears)
|
interimsCSV, w.processor.cfg.InterimYears)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -240,7 +240,7 @@ func (p *processor) interim() error {
|
||||||
log.Printf("Starting %d workers.\n", p.cfg.Workers)
|
log.Printf("Starting %d workers.\n", p.cfg.Workers)
|
||||||
for i := 1; i <= p.cfg.Workers; i++ {
|
for i := 1; i <= p.cfg.Workers; i++ {
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
w := newWorker(i, p.cfg)
|
w := newWorker(i, p)
|
||||||
go w.interimWork(&wg, queue)
|
go w.interimWork(&wg, queue)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -183,7 +183,7 @@ func (w *worker) mirrorInternal() (*csaf.AggregatorCSAFProvider, error) {
|
||||||
// Add us as a mirror.
|
// Add us as a mirror.
|
||||||
mirrorURL := csaf.ProviderURL(
|
mirrorURL := csaf.ProviderURL(
|
||||||
fmt.Sprintf("%s/.well-known/csaf-aggregator/%s/provider-metadata.json",
|
fmt.Sprintf("%s/.well-known/csaf-aggregator/%s/provider-metadata.json",
|
||||||
w.cfg.Domain, w.provider.Name))
|
w.processor.cfg.Domain, w.provider.Name))
|
||||||
|
|
||||||
acp.Mirrors = []csaf.ProviderURL{
|
acp.Mirrors = []csaf.ProviderURL{
|
||||||
mirrorURL,
|
mirrorURL,
|
||||||
|
|
@ -207,7 +207,7 @@ func (w *worker) writeProviderMetadata() error {
|
||||||
fname := filepath.Join(w.dir, "provider-metadata.json")
|
fname := filepath.Join(w.dir, "provider-metadata.json")
|
||||||
|
|
||||||
pm := csaf.NewProviderMetadataPrefix(
|
pm := csaf.NewProviderMetadataPrefix(
|
||||||
w.cfg.Domain+"/.well-known/csaf-aggregator/"+w.provider.Name,
|
w.processor.cfg.Domain+"/.well-known/csaf-aggregator/"+w.provider.Name,
|
||||||
w.labelsFromSummaries())
|
w.labelsFromSummaries())
|
||||||
|
|
||||||
// Figure out the role
|
// Figure out the role
|
||||||
|
|
@ -255,7 +255,7 @@ func (w *worker) mirrorPGPKeys(pm *csaf.ProviderMetadata) error {
|
||||||
|
|
||||||
localKeyURL := func(fingerprint string) string {
|
localKeyURL := func(fingerprint string) string {
|
||||||
return fmt.Sprintf("%s/.well-known/csaf-aggregator/%s/openpgp/%s.asc",
|
return fmt.Sprintf("%s/.well-known/csaf-aggregator/%s/openpgp/%s.asc",
|
||||||
w.cfg.Domain, w.provider.Name, fingerprint)
|
w.processor.cfg.Domain, w.provider.Name, fingerprint)
|
||||||
}
|
}
|
||||||
|
|
||||||
for i := range pm.PGPKeys {
|
for i := range pm.PGPKeys {
|
||||||
|
|
@ -311,12 +311,12 @@ func (w *worker) mirrorPGPKeys(pm *csaf.ProviderMetadata) error {
|
||||||
|
|
||||||
// If we have public key configured copy it into the new folder
|
// If we have public key configured copy it into the new folder
|
||||||
|
|
||||||
if w.cfg.OpenPGPPublicKey == "" {
|
if w.processor.cfg.OpenPGPPublicKey == "" {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load the key for the fingerprint.
|
// Load the key for the fingerprint.
|
||||||
data, err := os.ReadFile(w.cfg.OpenPGPPublicKey)
|
data, err := os.ReadFile(w.processor.cfg.OpenPGPPublicKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
os.RemoveAll(openPGPFolder)
|
os.RemoveAll(openPGPFolder)
|
||||||
return err
|
return err
|
||||||
|
|
@ -390,7 +390,7 @@ func (w *worker) createAggregatorProvider() (*csaf.AggregatorCSAFProvider, error
|
||||||
func (w *worker) doMirrorTransaction() error {
|
func (w *worker) doMirrorTransaction() error {
|
||||||
|
|
||||||
webTarget := filepath.Join(
|
webTarget := filepath.Join(
|
||||||
w.cfg.Web, ".well-known", "csaf-aggregator", w.provider.Name)
|
w.processor.cfg.Web, ".well-known", "csaf-aggregator", w.provider.Name)
|
||||||
|
|
||||||
var oldWeb string
|
var oldWeb string
|
||||||
|
|
||||||
|
|
@ -408,7 +408,7 @@ func (w *worker) doMirrorTransaction() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if there is a sysmlink already.
|
// Check if there is a sysmlink already.
|
||||||
target := filepath.Join(w.cfg.Folder, w.provider.Name)
|
target := filepath.Join(w.processor.cfg.Folder, w.provider.Name)
|
||||||
log.Printf("target: '%s'\n", target)
|
log.Printf("target: '%s'\n", target)
|
||||||
|
|
||||||
exists, err := util.PathExists(target)
|
exists, err := util.PathExists(target)
|
||||||
|
|
@ -472,14 +472,14 @@ func (w *worker) downloadSignature(path string) (string, error) {
|
||||||
// sign signs the given data with the configured key.
|
// sign signs the given data with the configured key.
|
||||||
func (w *worker) sign(data []byte) (string, error) {
|
func (w *worker) sign(data []byte) (string, error) {
|
||||||
if w.signRing == nil {
|
if w.signRing == nil {
|
||||||
key, err := w.cfg.privateOpenPGPKey()
|
key, err := w.processor.cfg.privateOpenPGPKey()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
if key == nil {
|
if key == nil {
|
||||||
return "", nil
|
return "", nil
|
||||||
}
|
}
|
||||||
if pp := w.cfg.Passphrase; pp != nil {
|
if pp := w.processor.cfg.Passphrase; pp != nil {
|
||||||
if key, err = key.Unlock([]byte(*pp)); err != nil {
|
if key, err = key.Unlock([]byte(*pp)); err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
@ -544,17 +544,32 @@ func (w *worker) mirrorFiles(tlpLabel *csaf.TLPLabel, files []string) error {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check against CSAF schema.
|
||||||
errors, err := csaf.ValidateCSAF(advisory)
|
errors, err := csaf.ValidateCSAF(advisory)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("error: %s: %v", file, err)
|
log.Printf("error: %s: %v", file, err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if len(errors) > 0 {
|
if len(errors) > 0 {
|
||||||
log.Printf("CSAF file %s has %d validation errors.",
|
log.Printf("CSAF file %s has %d validation errors.\n",
|
||||||
file, len(errors))
|
file, len(errors))
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check against remote validator.
|
||||||
|
if rmv := w.processor.remoteValidator; rmv != nil {
|
||||||
|
valid, err := rmv.Validate(advisory)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Calling remote validator failed: %s\n", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !valid {
|
||||||
|
log.Printf(
|
||||||
|
"CSAF file %s does not validate remotely.\n", file)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
sum, err := csaf.NewAdvisorySummary(w.expr, advisory)
|
sum, err := csaf.NewAdvisorySummary(w.expr, advisory)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("error: %s: %v\n", file, err)
|
log.Printf("error: %s: %v\n", file, err)
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,11 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type processor struct {
|
type processor struct {
|
||||||
|
// cfg is the global configuration.
|
||||||
cfg *config
|
cfg *config
|
||||||
|
|
||||||
|
// remoteValidator is a globally configured remote validator.
|
||||||
|
remoteValidator csaf.RemoteValidator
|
||||||
}
|
}
|
||||||
|
|
||||||
type summary struct {
|
type summary struct {
|
||||||
|
|
@ -30,9 +34,10 @@ type summary struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type worker struct {
|
type worker struct {
|
||||||
num int
|
num int
|
||||||
|
processor *processor
|
||||||
|
|
||||||
expr *util.PathEval
|
expr *util.PathEval
|
||||||
cfg *config
|
|
||||||
signRing *crypto.KeyRing
|
signRing *crypto.KeyRing
|
||||||
|
|
||||||
client util.Client // client per provider
|
client util.Client // client per provider
|
||||||
|
|
@ -43,11 +48,11 @@ type worker struct {
|
||||||
summaries map[string][]summary // the summaries of the advisories.
|
summaries map[string][]summary // the summaries of the advisories.
|
||||||
}
|
}
|
||||||
|
|
||||||
func newWorker(num int, config *config) *worker {
|
func newWorker(num int, processor *processor) *worker {
|
||||||
return &worker{
|
return &worker{
|
||||||
num: num,
|
num: num,
|
||||||
cfg: config,
|
processor: processor,
|
||||||
expr: util.NewPathEval(),
|
expr: util.NewPathEval(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -64,7 +69,7 @@ func (w *worker) createDir() (string, error) {
|
||||||
return w.dir, nil
|
return w.dir, nil
|
||||||
}
|
}
|
||||||
dir, err := util.MakeUniqDir(
|
dir, err := util.MakeUniqDir(
|
||||||
filepath.Join(w.cfg.Folder, w.provider.Name))
|
filepath.Join(w.processor.cfg.Folder, w.provider.Name))
|
||||||
if err == nil {
|
if err == nil {
|
||||||
w.dir = dir
|
w.dir = dir
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -167,6 +167,21 @@ func (c *controller) upload(r *http.Request) (interface{}, error) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Validate against remote validator
|
||||||
|
if c.cfg.RemoteValidator != nil {
|
||||||
|
validator, err := c.cfg.RemoteValidator.Open()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
valid, err := validator.Validate(content)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if !valid {
|
||||||
|
return nil, errors.New("does not validate against remote validator")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ex, err := csaf.NewAdvisorySummary(util.NewPathEval(), content)
|
ex, err := csaf.NewAdvisorySummary(util.NewPathEval(), content)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
|
||||||
|
|
@ -40,21 +40,22 @@ type providerMetadataConfig struct {
|
||||||
|
|
||||||
// configs contains the config values for the provider.
|
// configs contains the config values for the provider.
|
||||||
type config struct {
|
type config struct {
|
||||||
Password *string `toml:"password"`
|
Password *string `toml:"password"`
|
||||||
OpenPGPPublicKey string `toml:"openpgp_public_key"`
|
OpenPGPPublicKey string `toml:"openpgp_public_key"`
|
||||||
OpenPGPPrivateKey string `toml:"openpgp_private_key"`
|
OpenPGPPrivateKey string `toml:"openpgp_private_key"`
|
||||||
Folder string `toml:"folder"`
|
Folder string `toml:"folder"`
|
||||||
Web string `toml:"web"`
|
Web string `toml:"web"`
|
||||||
TLPs []tlp `toml:"tlps"`
|
TLPs []tlp `toml:"tlps"`
|
||||||
UploadSignature bool `toml:"upload_signature"`
|
UploadSignature bool `toml:"upload_signature"`
|
||||||
CanonicalURLPrefix string `toml:"canonical_url_prefix"`
|
CanonicalURLPrefix string `toml:"canonical_url_prefix"`
|
||||||
NoPassphrase bool `toml:"no_passphrase"`
|
NoPassphrase bool `toml:"no_passphrase"`
|
||||||
NoValidation bool `toml:"no_validation"`
|
NoValidation bool `toml:"no_validation"`
|
||||||
NoWebUI bool `toml:"no_web_ui"`
|
NoWebUI bool `toml:"no_web_ui"`
|
||||||
DynamicProviderMetaData bool `toml:"dynamic_provider_metadata"`
|
DynamicProviderMetaData bool `toml:"dynamic_provider_metadata"`
|
||||||
ProviderMetaData *providerMetadataConfig `toml:"provider_metadata"`
|
ProviderMetaData *providerMetadataConfig `toml:"provider_metadata"`
|
||||||
UploadLimit *int64 `toml:"upload_limit"`
|
UploadLimit *int64 `toml:"upload_limit"`
|
||||||
Issuer *string `toml:"issuer"`
|
Issuer *string `toml:"issuer"`
|
||||||
|
RemoteValidator *csaf.RemoteValidatorOptions `toml:"remote_validator"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (pmdc *providerMetadataConfig) apply(pmd *csaf.ProviderMetadata) {
|
func (pmdc *providerMetadataConfig) apply(pmd *csaf.ProviderMetadata) {
|
||||||
|
|
|
||||||
79
cmd/csaf_remote_validator/main.go
Normal file
79
cmd/csaf_remote_validator/main.go
Normal file
|
|
@ -0,0 +1,79 @@
|
||||||
|
// 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: 2022 German Federal Office for Information Security (BSI) <https://www.bsi.bund.de>
|
||||||
|
// Software-Engineering: 2022 Intevation GmbH <https://intevation.de>
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/csaf-poc/csaf_distribution/csaf"
|
||||||
|
)
|
||||||
|
|
||||||
|
func loadJSONFromFile(fname string) (interface{}, error) {
|
||||||
|
f, err := os.Open(fname)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
var doc interface{}
|
||||||
|
err = json.NewDecoder(f).Decode(&doc)
|
||||||
|
return doc, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func process(options *csaf.RemoteValidatorOptions, fnames []string) error {
|
||||||
|
validator, err := options.Open()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer validator.Close()
|
||||||
|
|
||||||
|
for _, fname := range fnames {
|
||||||
|
doc, err := loadJSONFromFile(fname)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
valid, err := validator.Validate(doc)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fmt.Printf("%s: %t\n", fname, valid)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
|
||||||
|
var (
|
||||||
|
url = flag.String("url", "", "URL to the validation service")
|
||||||
|
presets = flag.String("presets", "", "validation presets")
|
||||||
|
cache = flag.String("cache", "", "cache")
|
||||||
|
)
|
||||||
|
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
var pres []string
|
||||||
|
|
||||||
|
if *presets != "" {
|
||||||
|
pres = strings.Split(*presets, ",")
|
||||||
|
}
|
||||||
|
|
||||||
|
options := csaf.RemoteValidatorOptions{
|
||||||
|
URL: *url,
|
||||||
|
Presets: pres,
|
||||||
|
Cache: *cache,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := process(&options, flag.Args()); err != nil {
|
||||||
|
log.Fatalf("error: %v\n", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -313,7 +313,7 @@ func (a *Aggregator) Validate() error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if a.LastUpdated == nil {
|
if a.LastUpdated == nil {
|
||||||
return errors.New("Aggregator.LastUpdate == nil")
|
return errors.New("aggregator.LastUpdate == nil")
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
281
csaf/remotevalidation.go
Normal file
281
csaf/remotevalidation.go
Normal file
|
|
@ -0,0 +1,281 @@
|
||||||
|
// 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: 2022 German Federal Office for Information Security (BSI) <https://www.bsi.bund.de>
|
||||||
|
// Software-Engineering: 2022 Intevation GmbH <https://intevation.de>
|
||||||
|
|
||||||
|
package csaf
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
bolt "go.etcd.io/bbolt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// defaultURL is default URL where to look for
|
||||||
|
// the validation service.
|
||||||
|
const (
|
||||||
|
defaultURL = "http://localhost:3000"
|
||||||
|
validationPath = "/api/v1/validate"
|
||||||
|
)
|
||||||
|
|
||||||
|
// defaultPresets are the presets to check.
|
||||||
|
var defaultPresets = []string{"mandatory"}
|
||||||
|
|
||||||
|
var (
|
||||||
|
validationsBucket = []byte("validations")
|
||||||
|
validFalse = []byte{0}
|
||||||
|
validTrue = []byte{1}
|
||||||
|
)
|
||||||
|
|
||||||
|
// RemoteValidatorOptions are the configuation options
|
||||||
|
// the remote validation service.
|
||||||
|
type RemoteValidatorOptions struct {
|
||||||
|
URL string `json:"url" toml:"url"`
|
||||||
|
Presets []string `json:"presets" toml:"presets"`
|
||||||
|
Cache string `json:"cache" toml:"cache"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type test struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// outDocument is the document send to the remote validation service.
|
||||||
|
type outDocument struct {
|
||||||
|
Tests []test `json:"tests"`
|
||||||
|
Document interface{} `json:"document"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// inDocument is the document recieved from the remote validation service.
|
||||||
|
type inDocument struct {
|
||||||
|
Valid bool `json:"isValid"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var errNotFound = errors.New("not found")
|
||||||
|
|
||||||
|
type cache interface {
|
||||||
|
get(key []byte) (bool, error)
|
||||||
|
set(key []byte, valid bool) error
|
||||||
|
Close() error
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoteValidator validates an advisory document remotely.
|
||||||
|
type RemoteValidator interface {
|
||||||
|
Validate(doc interface{}) (bool, error)
|
||||||
|
Close() error
|
||||||
|
}
|
||||||
|
|
||||||
|
// SynchronizedRemoteValidator returns a serialized variant
|
||||||
|
// of the given remote validator.
|
||||||
|
func SynchronizedRemoteValidator(validator RemoteValidator) RemoteValidator {
|
||||||
|
return &syncedRemoteValidator{RemoteValidator: validator}
|
||||||
|
}
|
||||||
|
|
||||||
|
// remoteValidator is an implementation of an RemoteValidator.
|
||||||
|
type remoteValidator struct {
|
||||||
|
url string
|
||||||
|
tests []test
|
||||||
|
cache cache
|
||||||
|
}
|
||||||
|
|
||||||
|
// syncedRemoteValidator is a serialized variant of a remote validator.
|
||||||
|
type syncedRemoteValidator struct {
|
||||||
|
sync.Mutex
|
||||||
|
RemoteValidator
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate implements the validation part of the RemoteValidator interface.
|
||||||
|
func (srv *syncedRemoteValidator) Validate(doc interface{}) (bool, error) {
|
||||||
|
srv.Lock()
|
||||||
|
defer srv.Unlock()
|
||||||
|
return srv.RemoteValidator.Validate(doc)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate implements the closing part of the RemoteValidator interface.
|
||||||
|
func (srv *syncedRemoteValidator) Close() error {
|
||||||
|
srv.Lock()
|
||||||
|
defer srv.Unlock()
|
||||||
|
return srv.RemoteValidator.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// prepareTests precompiles the presets for the remote check.
|
||||||
|
func prepareTests(presets []string) []test {
|
||||||
|
if len(presets) == 0 {
|
||||||
|
presets = defaultPresets
|
||||||
|
}
|
||||||
|
tests := make([]test, len(presets))
|
||||||
|
for i := range tests {
|
||||||
|
tests[i] = test{Type: "preset", Name: presets[i]}
|
||||||
|
}
|
||||||
|
return tests
|
||||||
|
}
|
||||||
|
|
||||||
|
// prepareURL prepares the URL to be called for validation.
|
||||||
|
func prepareURL(url string) string {
|
||||||
|
if url == "" {
|
||||||
|
return defaultURL + validationPath
|
||||||
|
}
|
||||||
|
return url + validationPath
|
||||||
|
}
|
||||||
|
|
||||||
|
// prepareCache sets up the cache if it is configured.
|
||||||
|
func prepareCache(config string) (cache, error) {
|
||||||
|
if config == "" {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
db, err := bolt.Open(config, 0600, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the bucket.
|
||||||
|
if err := db.Update(func(tx *bolt.Tx) error {
|
||||||
|
_, err := tx.CreateBucketIfNotExists(validationsBucket)
|
||||||
|
return err
|
||||||
|
}); err != nil {
|
||||||
|
db.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return boltCache{db}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// boltCache is cache implementation based on the bolt datastore.
|
||||||
|
type boltCache struct{ *bolt.DB }
|
||||||
|
|
||||||
|
// get implements the fetch part of the cache interface.
|
||||||
|
func (bc boltCache) get(key []byte) (valid bool, err error) {
|
||||||
|
err2 := bc.View(func(tx *bolt.Tx) error {
|
||||||
|
b := tx.Bucket(validationsBucket)
|
||||||
|
v := b.Get(key)
|
||||||
|
if v == nil {
|
||||||
|
err = errNotFound
|
||||||
|
} else {
|
||||||
|
valid = v[0] != 0
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err2 != nil {
|
||||||
|
err = err2
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// get implements the store part of the cache interface.
|
||||||
|
func (bc boltCache) set(key []byte, valid bool) error {
|
||||||
|
return bc.Update(func(tx *bolt.Tx) error {
|
||||||
|
b := tx.Bucket(validationsBucket)
|
||||||
|
if valid {
|
||||||
|
return b.Put(key, validTrue)
|
||||||
|
}
|
||||||
|
return b.Put(key, validFalse)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open opens a new remoteValidator.
|
||||||
|
func (rvo *RemoteValidatorOptions) Open() (RemoteValidator, error) {
|
||||||
|
cache, err := prepareCache(rvo.Cache)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &remoteValidator{
|
||||||
|
url: prepareURL(rvo.URL),
|
||||||
|
tests: prepareTests(rvo.Presets),
|
||||||
|
cache: cache,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close closes the remote validator.
|
||||||
|
func (v *remoteValidator) Close() error {
|
||||||
|
if v.cache != nil {
|
||||||
|
return v.cache.Close()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// key calculates the key for an advisory document and presets.
|
||||||
|
func (v *remoteValidator) key(doc interface{}) ([]byte, error) {
|
||||||
|
h := sha256.New()
|
||||||
|
if err := json.NewEncoder(h).Encode(doc); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for i := range v.tests {
|
||||||
|
if _, err := h.Write([]byte(v.tests[i].Name)); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return h.Sum(nil), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate executes a remote validation of an advisory.
|
||||||
|
func (v *remoteValidator) Validate(doc interface{}) (bool, error) {
|
||||||
|
|
||||||
|
var key []byte
|
||||||
|
|
||||||
|
if v.cache != nil {
|
||||||
|
var err error
|
||||||
|
if key, err = v.key(doc); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
valid, err := v.cache.get(key)
|
||||||
|
if err != errNotFound {
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
return valid, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
o := outDocument{
|
||||||
|
Document: doc,
|
||||||
|
Tests: v.tests,
|
||||||
|
}
|
||||||
|
|
||||||
|
var buf bytes.Buffer
|
||||||
|
if err := json.NewEncoder(&buf).Encode(&o); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := http.Post(
|
||||||
|
v.url,
|
||||||
|
"application/json",
|
||||||
|
bytes.NewReader(buf.Bytes()))
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return false, fmt.Errorf(
|
||||||
|
"POST failed: %s (%d)", resp.Status, resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
valid, err := func() (bool, error) {
|
||||||
|
defer resp.Body.Close()
|
||||||
|
var in inDocument
|
||||||
|
return in.Valid, json.NewDecoder(resp.Body).Decode(&in)
|
||||||
|
}()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if key != nil {
|
||||||
|
// store in cache
|
||||||
|
if err := v.cache.set(key, valid); err != nil {
|
||||||
|
return valid, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return valid, nil
|
||||||
|
}
|
||||||
|
|
@ -87,6 +87,7 @@ lock_file // path to lockfile, to stop other instances if one is not
|
||||||
interim_years // limiting the years for which interim documents are searched
|
interim_years // limiting the years for which interim documents are searched
|
||||||
verbose // print more diagnostic output, e.g. https request
|
verbose // print more diagnostic output, e.g. https request
|
||||||
allow_single_provider // debugging option
|
allow_single_provider // debugging option
|
||||||
|
remote_validator // use remote validation checker
|
||||||
```
|
```
|
||||||
|
|
||||||
Rates are specified as floats in HTTPS operations per second.
|
Rates are specified as floats in HTTPS operations per second.
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,8 @@ Following options are supported in the config file:
|
||||||
- provider_metadata: Configure the provider metadata.
|
- provider_metadata: Configure the provider metadata.
|
||||||
- provider_metadata.list_on_CSAF_aggregators: List on aggregators
|
- provider_metadata.list_on_CSAF_aggregators: List on aggregators
|
||||||
- provider_metadata.mirror_on_CSAF_aggregators: Mirror on aggregators
|
- provider_metadata.mirror_on_CSAF_aggregators: Mirror on aggregators
|
||||||
|
- remote_validator: Use a remote validator service. Not used by default.
|
||||||
|
`{ "url" = "http://localhost:3000", "presets" = ["mandatory"], "cache" = "/var/lib/csaf/validations.db" }`
|
||||||
- provider_metadata.publisher: Set the publisher. Default:
|
- provider_metadata.publisher: Set the publisher. Default:
|
||||||
```toml
|
```toml
|
||||||
[provider_metadata.publisher]
|
[provider_metadata.publisher]
|
||||||
|
|
@ -36,4 +38,4 @@ name = "Example Company"
|
||||||
namespace = "https://example.com"
|
namespace = "https://example.com"
|
||||||
issuing_authority = "We at Example Company are responsible for publishing and maintaining Product Y."
|
issuing_authority = "We at Example Company are responsible for publishing and maintaining Product Y."
|
||||||
contact_details = "Example Company can be reached at contact_us@example.com, or via our website at https://www.example.com/contact."
|
contact_details = "Example Company can be reached at contact_us@example.com, or via our website at https://www.example.com/contact."
|
||||||
|
```
|
||||||
|
|
|
||||||
1
go.mod
1
go.mod
|
|
@ -12,6 +12,7 @@ require (
|
||||||
github.com/jessevdk/go-flags v1.5.0
|
github.com/jessevdk/go-flags v1.5.0
|
||||||
github.com/mitchellh/go-homedir v1.1.0
|
github.com/mitchellh/go-homedir v1.1.0
|
||||||
github.com/santhosh-tekuri/jsonschema/v5 v5.0.0
|
github.com/santhosh-tekuri/jsonschema/v5 v5.0.0
|
||||||
|
go.etcd.io/bbolt v1.3.6
|
||||||
golang.org/x/crypto v0.0.0-20220513210258-46612604a0f9
|
golang.org/x/crypto v0.0.0-20220513210258-46612604a0f9
|
||||||
golang.org/x/term v0.0.0-20220526004731-065cf7ba2467
|
golang.org/x/term v0.0.0-20220526004731-065cf7ba2467
|
||||||
golang.org/x/time v0.0.0-20220411224347-583f2d630306
|
golang.org/x/time v0.0.0-20220411224347-583f2d630306
|
||||||
|
|
|
||||||
3
go.sum
3
go.sum
|
|
@ -41,6 +41,8 @@ github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
|
||||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
|
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
|
||||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||||
|
go.etcd.io/bbolt v1.3.6 h1:/ecaJf0sk1l4l6V4awd65v2C3ILy7MSj+s/x1ADCIMU=
|
||||||
|
go.etcd.io/bbolt v1.3.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4=
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
|
|
@ -66,6 +68,7 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200923182605-d9f96fdee20d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue