mirror of
https://github.com/gocsaf/csaf.git
synced 2025-12-22 11:55:40 +01:00
Added files from the first prototype.
This commit is contained in:
parent
c2a483fc95
commit
fed66c4e27
13 changed files with 1529 additions and 0 deletions
92
cmd/csaf_provider/config.go
Normal file
92
cmd/csaf_provider/config.go
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/BurntSushi/toml"
|
||||
)
|
||||
|
||||
const (
|
||||
configEnv = "CSAF_CONFIG"
|
||||
defaultConfigPath = "/usr/lib/casf/config.toml"
|
||||
defaultFolder = "/var/www/"
|
||||
defaultWeb = "/var/www/html"
|
||||
defaultPGPURL = "http://pgp.mit.edu/pks/lookup?search=${KEY}&op=index"
|
||||
)
|
||||
|
||||
type config struct {
|
||||
Key string `toml:"key"`
|
||||
Folder string `toml:"folder"`
|
||||
Web string `toml:"web"`
|
||||
TLPs []tlp `toml:"tlps"`
|
||||
UploadSignature bool `toml:"upload_signature"`
|
||||
PGPURL string `toml:"pgp_url"`
|
||||
Domain string `toml:"domain"`
|
||||
}
|
||||
|
||||
type tlp string
|
||||
|
||||
const (
|
||||
tlpCSAF tlp = "csaf"
|
||||
tlpWhite tlp = "white"
|
||||
tlpGreen tlp = "green"
|
||||
tlpAmber tlp = "amber"
|
||||
tlpRed tlp = "red"
|
||||
)
|
||||
|
||||
func (t tlp) valid() bool {
|
||||
switch t {
|
||||
case tlpCSAF, tlpWhite, tlpGreen, tlpAmber, tlpRed:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func (t *tlp) UnmarshalText(text []byte) error {
|
||||
if s := tlp(text); s.valid() {
|
||||
*t = s
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cfg *config) GetPGPURL(key string) string {
|
||||
return strings.ReplaceAll(cfg.PGPURL, "${KEY}", key)
|
||||
}
|
||||
|
||||
func loadConfig() (*config, error) {
|
||||
path := os.Getenv(configEnv)
|
||||
if path == "" {
|
||||
path = defaultConfigPath
|
||||
}
|
||||
var cfg config
|
||||
if _, err := toml.DecodeFile(path, &cfg); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Preset defaults
|
||||
|
||||
if cfg.Folder == "" {
|
||||
cfg.Folder = defaultFolder
|
||||
}
|
||||
|
||||
if cfg.Web == "" {
|
||||
cfg.Web = defaultWeb
|
||||
}
|
||||
|
||||
if cfg.Domain == "" {
|
||||
cfg.Domain = "http://" + os.Getenv("SERVER_NAME")
|
||||
}
|
||||
|
||||
if cfg.TLPs == nil {
|
||||
cfg.TLPs = []tlp{tlpCSAF, tlpWhite, tlpGreen, tlpAmber, tlpRed}
|
||||
}
|
||||
|
||||
if cfg.PGPURL == "" {
|
||||
cfg.PGPURL = defaultPGPURL
|
||||
}
|
||||
|
||||
return &cfg, nil
|
||||
}
|
||||
399
cmd/csaf_provider/controller.go
Normal file
399
cmd/csaf_provider/controller.go
Normal file
|
|
@ -0,0 +1,399 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha256"
|
||||
"crypto/sha512"
|
||||
"embed"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"hash"
|
||||
"html/template"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/ProtonMail/gopenpgp/v2/crypto"
|
||||
|
||||
"github.com/intevation/csaf_trusted/csaf"
|
||||
)
|
||||
|
||||
const dateFormat = time.RFC3339
|
||||
|
||||
//go:embed tmpl
|
||||
var tmplFS embed.FS
|
||||
|
||||
type controller struct {
|
||||
cfg *config
|
||||
tmpl *template.Template
|
||||
}
|
||||
|
||||
func newController(cfg *config) (*controller, error) {
|
||||
|
||||
c := controller{cfg: cfg}
|
||||
var err error
|
||||
|
||||
if c.tmpl, err = template.ParseFS(tmplFS, "tmpl/*.html"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &c, nil
|
||||
}
|
||||
|
||||
func (c *controller) bind(pim *pathInfoMux) {
|
||||
pim.handleFunc("/", c.index)
|
||||
pim.handleFunc("/upload", c.upload)
|
||||
pim.handleFunc("/create", c.create)
|
||||
}
|
||||
|
||||
func (c *controller) render(rw http.ResponseWriter, tmpl string, arg interface{}) {
|
||||
rw.Header().Set("Content-type", "text/html; charset=utf-8")
|
||||
if err := c.tmpl.ExecuteTemplate(rw, tmpl, arg); err != nil {
|
||||
log.Printf("warn: %v\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *controller) failed(rw http.ResponseWriter, tmpl string, err error) {
|
||||
rw.Header().Set("Content-type", "text/html; charset=utf-8")
|
||||
result := map[string]interface{}{"Error": err}
|
||||
if err := c.tmpl.ExecuteTemplate(rw, tmpl, result); err != nil {
|
||||
log.Printf("warn: %v\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *controller) index(rw http.ResponseWriter, r *http.Request) {
|
||||
c.render(rw, "index.html", map[string]interface{}{
|
||||
"Config": c.cfg,
|
||||
})
|
||||
}
|
||||
|
||||
func (c *controller) create(rw http.ResponseWriter, r *http.Request) {
|
||||
if err := ensureFolders(c.cfg); err != nil {
|
||||
c.failed(rw, "create.html", err)
|
||||
return
|
||||
}
|
||||
c.render(rw, "create.html", nil)
|
||||
}
|
||||
|
||||
func (c *controller) tlpParam(r *http.Request) (tlp, error) {
|
||||
t := tlp(strings.ToLower(r.FormValue("tlp")))
|
||||
for _, x := range c.cfg.TLPs {
|
||||
if x == t {
|
||||
return t, nil
|
||||
}
|
||||
}
|
||||
return "", fmt.Errorf("unsupported TLP type '%s'", t)
|
||||
}
|
||||
|
||||
func cleanFileName(s string) string {
|
||||
s = strings.ReplaceAll(s, `/`, ``)
|
||||
s = strings.ReplaceAll(s, `\`, ``)
|
||||
r := regexp.MustCompile(`\.{2,}`)
|
||||
s = r.ReplaceAllString(s, `.`)
|
||||
return s
|
||||
}
|
||||
|
||||
func loadCSAF(r *http.Request) (string, []byte, error) {
|
||||
file, handler, err := r.FormFile("csaf")
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
var buf bytes.Buffer
|
||||
lr := io.LimitReader(file, 10*1024*1024)
|
||||
if _, err := io.Copy(&buf, lr); err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
return cleanFileName(handler.Filename), buf.Bytes(), nil
|
||||
}
|
||||
|
||||
func writeHash(fname, name string, h hash.Hash, data []byte) error {
|
||||
|
||||
if _, err := io.Copy(h, bytes.NewReader(data)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
f, err := os.Create(fname)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Fprintf(f, "%x %s\n", h.Sum(nil), name)
|
||||
return f.Close()
|
||||
}
|
||||
|
||||
func (c *controller) loadCryptoKey() (*crypto.Key, error) {
|
||||
f, err := os.Open(c.cfg.Key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer f.Close()
|
||||
return crypto.NewKeyFromArmoredReader(f)
|
||||
}
|
||||
|
||||
func (c *controller) handleSignature(r *http.Request, data []byte) (string, string, error) {
|
||||
|
||||
// Either way ... we need the key.
|
||||
key, err := c.loadCryptoKey()
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
fingerprint := key.GetFingerprint()
|
||||
|
||||
// Was the signature given via request?
|
||||
if c.cfg.UploadSignature {
|
||||
sigText := r.FormValue("signature")
|
||||
if sigText == "" {
|
||||
return "", "", errors.New("missing signature in request")
|
||||
}
|
||||
|
||||
pgpSig, err := crypto.NewPGPSignatureFromArmored(sigText)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
// Use as public key
|
||||
signRing, err := crypto.NewKeyRing(key)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
if err := signRing.VerifyDetached(
|
||||
crypto.NewPlainMessage(data),
|
||||
pgpSig, crypto.GetUnixTime(),
|
||||
); err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
return sigText, fingerprint, nil
|
||||
}
|
||||
|
||||
// Sign ourself
|
||||
|
||||
if passwd := r.FormValue("passphrase"); passwd != "" {
|
||||
if key, err = key.Unlock([]byte(passwd)); err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
}
|
||||
|
||||
// Use as private key
|
||||
signRing, err := crypto.NewKeyRing(key)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
sig, err := signRing.SignDetached(crypto.NewPlainMessage(data))
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
armored, err := sig.GetArmored()
|
||||
return armored, fingerprint, err
|
||||
}
|
||||
|
||||
func (c *controller) upload(rw http.ResponseWriter, r *http.Request) {
|
||||
|
||||
newCSAF, data, err := loadCSAF(r)
|
||||
if err != nil {
|
||||
c.failed(rw, "upload.html", err)
|
||||
return
|
||||
}
|
||||
|
||||
var content interface{}
|
||||
if err := json.Unmarshal(data, &content); err != nil {
|
||||
c.failed(rw, "upload.html", err)
|
||||
return
|
||||
}
|
||||
|
||||
ex, err := newExtraction(content)
|
||||
if err != nil {
|
||||
c.failed(rw, "upload.html", err)
|
||||
return
|
||||
}
|
||||
|
||||
t, err := c.tlpParam(r)
|
||||
if err != nil {
|
||||
c.failed(rw, "upload.html", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Extract real TLP from document.
|
||||
if t == tlpCSAF {
|
||||
if t = tlp(strings.ToLower(ex.tlpLabel)); !t.valid() || t == tlpCSAF {
|
||||
c.failed(
|
||||
rw, "upload.html", fmt.Errorf("not a valid TL: %s", ex.tlpLabel))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
armored, fingerprint, err := c.handleSignature(r, data)
|
||||
if err != nil {
|
||||
c.failed(rw, "upload.html", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := c.doTransaction(t, func(folder string, pmd *csaf.ProviderMetadata) error {
|
||||
|
||||
year := strconv.Itoa(ex.currentReleaseDate.Year())
|
||||
|
||||
subDir := filepath.Join(folder, year)
|
||||
|
||||
// Create folder if it does not exists.
|
||||
if _, err := os.Stat(subDir); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
if err := os.Mkdir(subDir, 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
fname := filepath.Join(subDir, newCSAF)
|
||||
|
||||
// Write the file itself.
|
||||
if err := ioutil.WriteFile(fname, data, 0644); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Write SHA256 sum.
|
||||
if err := writeHash(fname+".sha256", newCSAF, sha256.New(), data); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Write SHA512 sum.
|
||||
if err := writeHash(fname+".sha512", newCSAF, sha512.New(), data); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Write signature.
|
||||
if err := ioutil.WriteFile(fname+".asc", []byte(armored), 0644); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := updateIndices(
|
||||
folder, filepath.Join(year, newCSAF),
|
||||
ex.currentReleaseDate,
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Take over publisher
|
||||
// TODO: Check for conflicts.
|
||||
pmd.Publisher = ex.publisher
|
||||
|
||||
pmd.SetPGP(fingerprint, c.cfg.GetPGPURL(fingerprint))
|
||||
|
||||
return nil
|
||||
}); err != nil {
|
||||
c.failed(rw, "upload.html", err)
|
||||
return
|
||||
}
|
||||
|
||||
result := map[string]interface{}{
|
||||
"Name": newCSAF,
|
||||
"ReleaseDate": ex.currentReleaseDate.Format(dateFormat),
|
||||
}
|
||||
|
||||
c.render(rw, "upload.html", result)
|
||||
}
|
||||
|
||||
func (c *controller) doTransaction(
|
||||
t tlp,
|
||||
fn func(string, *csaf.ProviderMetadata) error,
|
||||
) error {
|
||||
|
||||
wellknown := filepath.Join(c.cfg.Web, ".well-known", "csaf")
|
||||
|
||||
metadata := filepath.Join(wellknown, "provider-metadata.json")
|
||||
|
||||
pmd, err := func() (*csaf.ProviderMetadata, error) {
|
||||
f, err := os.Open(metadata)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return csaf.NewProviderMetadata(
|
||||
c.cfg.Domain + "/.wellknown/csaf/provider-metadata.json"), nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
defer f.Close()
|
||||
return csaf.LoadProviderMetadata(f)
|
||||
}()
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
webTLP := filepath.Join(wellknown, string(t))
|
||||
|
||||
oldDir, err := filepath.EvalSymlinks(webTLP)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
folderTLP := filepath.Join(c.cfg.Folder, string(t))
|
||||
|
||||
newDir, err := mkUniqDir(folderTLP)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Copy old content into new.
|
||||
if err := deepCopy(newDir, oldDir); err != nil {
|
||||
os.RemoveAll(newDir)
|
||||
return err
|
||||
}
|
||||
|
||||
// Work with new folder.
|
||||
if err := fn(newDir, pmd); err != nil {
|
||||
os.RemoveAll(newDir)
|
||||
return err
|
||||
}
|
||||
|
||||
// Write back provider metadata.
|
||||
newMetaName, newMetaFile, err := mkUniqFile(metadata)
|
||||
if err != nil {
|
||||
os.RemoveAll(newDir)
|
||||
return err
|
||||
}
|
||||
|
||||
if err := pmd.Save(newMetaFile); err != nil {
|
||||
newMetaFile.Close()
|
||||
os.Remove(newMetaName)
|
||||
os.RemoveAll(newDir)
|
||||
return err
|
||||
}
|
||||
|
||||
if err := newMetaFile.Close(); err != nil {
|
||||
os.Remove(newMetaName)
|
||||
os.RemoveAll(newDir)
|
||||
return err
|
||||
}
|
||||
|
||||
if err := os.Rename(newMetaName, metadata); err != nil {
|
||||
os.RemoveAll(newDir)
|
||||
return err
|
||||
}
|
||||
|
||||
// Switch directories.
|
||||
symlink := filepath.Join(newDir, string(t))
|
||||
if err := os.Symlink(newDir, symlink); err != nil {
|
||||
os.RemoveAll(newDir)
|
||||
return err
|
||||
}
|
||||
if err := os.Rename(symlink, webTLP); err != nil {
|
||||
os.RemoveAll(newDir)
|
||||
return err
|
||||
}
|
||||
|
||||
return os.RemoveAll(oldDir)
|
||||
}
|
||||
176
cmd/csaf_provider/dir.go
Normal file
176
cmd/csaf_provider/dir.go
Normal file
|
|
@ -0,0 +1,176 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
func ensureFolders(c *config) error {
|
||||
|
||||
wellknown, err := createWellknown(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := createFeedFolders(c, wellknown); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return createSecurity(c)
|
||||
}
|
||||
|
||||
func createSecurity(c *config) error {
|
||||
security := filepath.Join(c.Web, "security.txt")
|
||||
if _, err := os.Stat(security); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
f, err := os.Create(security)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Fprintf(
|
||||
f, "CSAF: https://%s/.wellknown/csaf/provider-metadata.json\n",
|
||||
c.Domain)
|
||||
return f.Close()
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func createFeedFolders(c *config, wellknown string) error {
|
||||
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 = mkUniqDir(tlpFolder); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = os.Symlink(tlpFolder, tlpLink); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func createWellknown(c *config) (string, error) {
|
||||
wellknown := filepath.Join(c.Web, ".well-known", "csaf")
|
||||
|
||||
st, err := os.Stat(wellknown)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
if err := os.MkdirAll(wellknown, 0755); err != nil {
|
||||
return "", err
|
||||
}
|
||||
} else {
|
||||
return "", err
|
||||
}
|
||||
} else {
|
||||
if !st.IsDir() {
|
||||
return "", errors.New(".well-known/csaf is not a directory")
|
||||
}
|
||||
}
|
||||
return wellknown, nil
|
||||
}
|
||||
|
||||
func deepCopy(dst, src string) error {
|
||||
|
||||
stack := []string{dst, src}
|
||||
|
||||
for len(stack) > 0 {
|
||||
src = stack[len(stack)-1]
|
||||
dst = stack[len(stack)-2]
|
||||
stack = stack[:len(stack)-2]
|
||||
|
||||
if err := func() error {
|
||||
dir, err := os.Open(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer dir.Close()
|
||||
|
||||
// Use Readdir as we need no sorting.
|
||||
files, err := dir.Readdir(-1)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, f := range files {
|
||||
nsrc := filepath.Join(src, f.Name())
|
||||
ndst := filepath.Join(dst, f.Name())
|
||||
if f.IsDir() {
|
||||
// Create new sub dir
|
||||
if err := os.Mkdir(ndst, 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
stack = append(stack, ndst, nsrc)
|
||||
} else if f.Mode().IsRegular() {
|
||||
// Create hard link.
|
||||
if err := os.Link(nsrc, ndst); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func mkUniqFile(prefix string) (string, *os.File, error) {
|
||||
var file *os.File
|
||||
name, err := mkUniq(prefix, func(name string) error {
|
||||
var err error
|
||||
file, err = os.OpenFile(name, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0644)
|
||||
return err
|
||||
})
|
||||
return name, file, err
|
||||
}
|
||||
|
||||
func mkUniqDir(prefix string) (string, error) {
|
||||
return mkUniq(prefix, func(name string) error { return os.Mkdir(name, 0755) })
|
||||
}
|
||||
|
||||
func mkUniq(prefix string, create func(string) error) (string, error) {
|
||||
now := time.Now()
|
||||
stamp := now.Format("-2006-01-02-150405")
|
||||
name := prefix + stamp
|
||||
err := create(name)
|
||||
if err == nil {
|
||||
return name, nil
|
||||
}
|
||||
if os.IsExist(err) {
|
||||
rnd := rand.New(rand.NewSource(now.Unix()))
|
||||
|
||||
for i := 0; i < 10000; i++ {
|
||||
nname := name + "-" + strconv.FormatUint(uint64(rnd.Uint32()&0xff_ffff), 16)
|
||||
err := create(nname)
|
||||
if err == nil {
|
||||
return nname, nil
|
||||
}
|
||||
if os.IsExist(err) {
|
||||
continue
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
return "", &os.PathError{Op: "mkuniq", Path: name, Err: os.ErrExist}
|
||||
}
|
||||
|
||||
return "", err
|
||||
}
|
||||
126
cmd/csaf_provider/extract.go
Normal file
126
cmd/csaf_provider/extract.go
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/PaesslerAG/gval"
|
||||
"github.com/PaesslerAG/jsonpath"
|
||||
|
||||
"github.com/intevation/csaf_trusted/csaf"
|
||||
)
|
||||
|
||||
const (
|
||||
idExpr = `$.document.tracking.id`
|
||||
titleExpr = `$.document.title`
|
||||
publisherExpr = `$.document.publisher`
|
||||
initialReleaseDateExpr = `$.document.tracking.initial_release_date`
|
||||
currentReleaseDateExpr = `$.document.tracking.current_release_date`
|
||||
tlpLabelExpr = `$.document.distribution.tlp.label`
|
||||
summaryExpr = `$.document.notes[? @.category=="summary" || @.type=="summary"].text`
|
||||
)
|
||||
|
||||
type extraction struct {
|
||||
id string
|
||||
title string
|
||||
publisher *csaf.CSAFPublisher
|
||||
initialReleaseDate time.Time
|
||||
currentReleaseDate time.Time
|
||||
summary string
|
||||
tlpLabel string
|
||||
}
|
||||
|
||||
func newExtraction(content interface{}) (*extraction, error) {
|
||||
|
||||
builder := gval.Full(jsonpath.Language())
|
||||
|
||||
e := new(extraction)
|
||||
|
||||
for _, fn := range []func(*gval.Language, interface{}) error{
|
||||
extractText(idExpr, &e.id),
|
||||
extractText(titleExpr, &e.title),
|
||||
extractTime(currentReleaseDateExpr, &e.currentReleaseDate),
|
||||
extractTime(initialReleaseDateExpr, &e.initialReleaseDate),
|
||||
extractText(summaryExpr, &e.summary),
|
||||
extractText(tlpLabelExpr, &e.tlpLabel),
|
||||
e.extractPublisher,
|
||||
} {
|
||||
if err := fn(&builder, content); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return e, nil
|
||||
}
|
||||
|
||||
func extractText(
|
||||
expr string,
|
||||
store *string,
|
||||
) func(*gval.Language, interface{}) error {
|
||||
return func(builder *gval.Language, content interface{}) error {
|
||||
eval, err := builder.NewEvaluable(expr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s, err := eval(context.Background(), content)
|
||||
if text, ok := s.(string); ok && err == nil {
|
||||
*store = text
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func extractTime(
|
||||
expr string,
|
||||
store *time.Time,
|
||||
) func(*gval.Language, interface{}) error {
|
||||
return func(builder *gval.Language, content interface{}) error {
|
||||
eval, err := builder.NewEvaluable(expr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s, err := eval(context.Background(), content)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
text, ok := s.(string)
|
||||
if !ok {
|
||||
return errors.New("not a string")
|
||||
}
|
||||
date, err := time.Parse(dateFormat, text)
|
||||
if err == nil {
|
||||
*store = date.UTC()
|
||||
}
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
func (e *extraction) extractPublisher(
|
||||
builder *gval.Language,
|
||||
content interface{},
|
||||
) error {
|
||||
eval, err := builder.NewEvaluable(publisherExpr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p, err := eval(context.Background(), content)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// XXX: It's a bit cumbersome to serialize and deserialize
|
||||
// it into our own structure.
|
||||
var buf bytes.Buffer
|
||||
enc := json.NewEncoder(&buf)
|
||||
if err := enc.Encode(p); err != nil {
|
||||
return err
|
||||
}
|
||||
e.publisher = new(csaf.CSAFPublisher)
|
||||
if err := json.Unmarshal(buf.Bytes(), e.publisher); err != nil {
|
||||
return err
|
||||
}
|
||||
return e.publisher.Validate()
|
||||
}
|
||||
158
cmd/csaf_provider/indices.go
Normal file
158
cmd/csaf_provider/indices.go
Normal file
|
|
@ -0,0 +1,158 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/csv"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"time"
|
||||
)
|
||||
|
||||
func updateIndex(dir, fname string) error {
|
||||
|
||||
index := filepath.Join(dir, "index.txt")
|
||||
|
||||
lines, err := func() ([]string, error) {
|
||||
f, err := os.Open(index)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return []string{fname}, nil
|
||||
} else {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
defer f.Close()
|
||||
var lines []string
|
||||
scanner := bufio.NewScanner(f)
|
||||
for scanner.Scan() {
|
||||
// stop scanning when we found it.
|
||||
if line := scanner.Text(); line != fname {
|
||||
lines = append(lines, line)
|
||||
} else {
|
||||
return nil, nil
|
||||
}
|
||||
}
|
||||
return append(lines, fname), nil
|
||||
}()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(lines) == 0 {
|
||||
return nil
|
||||
}
|
||||
// Create new to break hard link.
|
||||
f, err := os.Create(index)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sort.Strings(lines)
|
||||
out := bufio.NewWriter(f)
|
||||
for _, line := range lines {
|
||||
fmt.Fprintln(out, line)
|
||||
}
|
||||
if err := out.Flush(); err != nil {
|
||||
f.Close()
|
||||
return err
|
||||
}
|
||||
return f.Close()
|
||||
}
|
||||
|
||||
func updateChanges(dir, fname string, releaseDate time.Time) error {
|
||||
|
||||
type change struct {
|
||||
time time.Time
|
||||
path string
|
||||
}
|
||||
|
||||
changes := filepath.Join(dir, "changes.csv")
|
||||
|
||||
chs, err := func() ([]change, error) {
|
||||
f, err := os.Open(changes)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return []change{{releaseDate, fname}}, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
defer f.Close()
|
||||
var chs []change
|
||||
r := csv.NewReader(f)
|
||||
r.FieldsPerRecord = 2
|
||||
r.ReuseRecord = true
|
||||
replaced := false
|
||||
for {
|
||||
record, err := r.Read()
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Check if new is already in.
|
||||
if record[1] == fname {
|
||||
// Identical -> no change at all.
|
||||
if record[0] == releaseDate.Format(dateFormat) {
|
||||
return nil, nil
|
||||
}
|
||||
// replace old entry
|
||||
replaced = true
|
||||
chs = append(chs, change{releaseDate, fname})
|
||||
continue
|
||||
}
|
||||
t, err := time.Parse(dateFormat, record[0])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
chs = append(chs, change{t, record[1]})
|
||||
}
|
||||
if !replaced {
|
||||
chs = append(chs, change{releaseDate, fname})
|
||||
}
|
||||
return chs, nil
|
||||
}()
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(chs) == 0 {
|
||||
return nil
|
||||
}
|
||||
// Sort descending
|
||||
sort.Slice(chs, func(i, j int) bool {
|
||||
return chs[j].time.Before(chs[i].time)
|
||||
})
|
||||
// Create new to break hard link.
|
||||
o, err := os.Create(changes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c := csv.NewWriter(o)
|
||||
record := make([]string, 2)
|
||||
for _, ch := range chs {
|
||||
record[0] = ch.time.Format(dateFormat)
|
||||
record[1] = ch.path
|
||||
if err := c.Write(record); err != nil {
|
||||
o.Close()
|
||||
return err
|
||||
}
|
||||
}
|
||||
c.Flush()
|
||||
err1 := c.Error()
|
||||
err2 := o.Close()
|
||||
if err1 != nil {
|
||||
return err1
|
||||
}
|
||||
return err2
|
||||
}
|
||||
|
||||
func updateIndices(dir, fname string, releaseDate time.Time) error {
|
||||
|
||||
if err := updateIndex(dir, fname); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return updateChanges(dir, fname, releaseDate)
|
||||
}
|
||||
24
cmd/csaf_provider/main.go
Normal file
24
cmd/csaf_provider/main.go
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http/cgi"
|
||||
)
|
||||
|
||||
func main() {
|
||||
cfg, err := loadConfig()
|
||||
if err != nil {
|
||||
log.Fatalf("error: %v\n", err)
|
||||
}
|
||||
|
||||
c, err := newController(cfg)
|
||||
if err != nil {
|
||||
log.Fatalf("error: %v\n", err)
|
||||
}
|
||||
pim := newPathInfoMux()
|
||||
c.bind(pim)
|
||||
|
||||
if err := cgi.Serve(pim); err != nil {
|
||||
log.Fatalf("error: %v\n", err)
|
||||
}
|
||||
}
|
||||
38
cmd/csaf_provider/mux.go
Normal file
38
cmd/csaf_provider/mux.go
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type pathInfoMux struct {
|
||||
routes map[string]http.Handler
|
||||
}
|
||||
|
||||
func newPathInfoMux() *pathInfoMux {
|
||||
return &pathInfoMux{routes: map[string]http.Handler{}}
|
||||
}
|
||||
|
||||
func (pim *pathInfoMux) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
||||
pi := os.Getenv("PATH_INFO")
|
||||
if h, ok := pim.routes[pi]; ok {
|
||||
h.ServeHTTP(rw, req)
|
||||
return
|
||||
}
|
||||
for k, v := range pim.routes {
|
||||
if strings.HasPrefix(k, pi) {
|
||||
v.ServeHTTP(rw, req)
|
||||
return
|
||||
}
|
||||
}
|
||||
http.NotFound(rw, req)
|
||||
}
|
||||
|
||||
func (pim *pathInfoMux) handle(pattern string, handler http.Handler) {
|
||||
pim.routes[pattern] = handler
|
||||
}
|
||||
|
||||
func (pim *pathInfoMux) handleFunc(pattern string, handler func(http.ResponseWriter, *http.Request)) {
|
||||
pim.handle(pattern, http.HandlerFunc(handler))
|
||||
}
|
||||
16
cmd/csaf_provider/tmpl/create.html
Normal file
16
cmd/csaf_provider/tmpl/create.html
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta description="CSAF-Provider - Directory structure created">
|
||||
<title>CSAF-Provider - Directory structure created</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>CSAF-Provider - Directory structure created</h1>
|
||||
{{ if .Error }}
|
||||
<strong>Error: <tt>{{ .Error }}.</tt></strong>
|
||||
{{ else }}
|
||||
Everything is setup fine now.
|
||||
{{ end }}
|
||||
</body>
|
||||
</html>
|
||||
41
cmd/csaf_provider/tmpl/index.html
Normal file
41
cmd/csaf_provider/tmpl/index.html
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta description="CSAF-Provider - CSAF upload">
|
||||
<title>CSAF-Provider - CSAF upload</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>CSAF-Provider - CSAF upload</h1>
|
||||
<form action="/cgi-bin/csaf_provider.go/upload" method="post" enctype="multipart/form-data">
|
||||
<fieldset>
|
||||
<legend>Select your CSAF file</legend>
|
||||
<label for="csaf">CSAF file:</label>
|
||||
<input name="csaf" id="csaf" type="file" size="50" accept="application/json" required="required">
|
||||
<br>
|
||||
{{ if eq (len .Config.TLPs) 1 }}
|
||||
<input type="hidden" value="{{ index .Config.TLPs 0 }}" id="tlp" name="tlp">
|
||||
{{ else }}
|
||||
<label for="tlp">TLP:</label>
|
||||
<select name="tlp" id="tlp">
|
||||
{{ range .Config.TLPs }}
|
||||
<option value="{{ . }}">{{ . }}</option>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
</select>
|
||||
<br>
|
||||
{{ if .Config.UploadSignature }}
|
||||
<label for="signature">Signature:</label>
|
||||
<br>
|
||||
<textarea name="signature" id="signature" required="required" cols="65" rows="20"
|
||||
placeholder="Insert ASCII armored signature here ..."></textarea>
|
||||
{{ else }}
|
||||
<label for="passphrase">Key passphrase:</label>
|
||||
<input name="passphrase" type="password" id="passphrase">
|
||||
{{ end }}
|
||||
<br>
|
||||
<input type="submit" value="Upload">
|
||||
</fieldset>
|
||||
</form>
|
||||
</body>
|
||||
</html>
|
||||
21
cmd/csaf_provider/tmpl/upload.html
Normal file
21
cmd/csaf_provider/tmpl/upload.html
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta description="CSAF-Provider - CSAF uploaded">
|
||||
<title>CSAF-Provider - CSAF uploaded</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>CSAF-Provider - CSAF uploaded</h1>
|
||||
{{ if .Error }}
|
||||
<strong>Error: <tt>{{ .Error }}.</tt></strong>
|
||||
{{ else }}
|
||||
<table>
|
||||
<tr><td>CSAF file:</td><td><tt>{{ .Name }}</tt></td></tr>
|
||||
<tr><td>Release date:</td><td><tt>{{ .ReleaseDate }}</tt></td></tr>
|
||||
</table>
|
||||
{{ end }}
|
||||
<br>
|
||||
<a href="/cgi-bin/csaf_provider.go/">Back</a>:
|
||||
</body>
|
||||
</html>
|
||||
349
csaf/models.go
Normal file
349
csaf/models.go
Normal file
|
|
@ -0,0 +1,349 @@
|
|||
package csaf
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type TLPLabel string
|
||||
|
||||
const (
|
||||
TLPLabelUnlabeled = "UNLABELED"
|
||||
TLPLabelWhite = "WHITE"
|
||||
TLPLabelGreen = "GREEN"
|
||||
TLPLabelAmber = "AMBER"
|
||||
TLPLabelRed = "RED"
|
||||
)
|
||||
|
||||
var tlpLabelPattern = alternativesUnmarshal(
|
||||
string("UNLABELED"),
|
||||
string("WHITE"),
|
||||
string("GREEN"),
|
||||
string("AMBER"),
|
||||
string("RED"))
|
||||
|
||||
type JsonURL string
|
||||
|
||||
var jsonURLPattern = patternUnmarshal(`\.json$`)
|
||||
|
||||
type Feed struct {
|
||||
Summary string `json:"summary"`
|
||||
TLPLabel *TLPLabel `json:"tlp_label"` // required
|
||||
URL *JsonURL `json:"url"` // required
|
||||
}
|
||||
|
||||
type ROLIE struct {
|
||||
Categories []JsonURL `json:"categories"`
|
||||
Feeds []Feed `json:"feeds"` // required
|
||||
}
|
||||
|
||||
type Distribution struct {
|
||||
DirectoryURL string `json:"directory_url"`
|
||||
Rolie []ROLIE `json:"rolie"`
|
||||
}
|
||||
|
||||
type TimeStamp time.Time
|
||||
|
||||
type Fingerprint string
|
||||
|
||||
var fingerprintPattern = patternUnmarshal(`^[0-9a-fA-F]{40,}$`)
|
||||
|
||||
type PGPKey struct {
|
||||
Fingerprint Fingerprint `json:"fingerprint,omitempty"`
|
||||
URL *string `json:"url"` // required
|
||||
}
|
||||
|
||||
type CSAFCategory string
|
||||
|
||||
const (
|
||||
CSAFCategoryCoordinator CSAFCategory = "coordinator"
|
||||
CSAFCategoryDiscoverer CSAFCategory = "discoverer"
|
||||
CSAFCategoryOther CSAFCategory = "other"
|
||||
CSAFCategoryTranslator CSAFCategory = "translator"
|
||||
CSAFCategoryUser CSAFCategory = "user"
|
||||
CSAFCategoryVendor CSAFCategory = "vendor"
|
||||
)
|
||||
|
||||
var csafCategoryPattern = alternativesUnmarshal(
|
||||
string(CSAFCategoryCoordinator),
|
||||
string(CSAFCategoryDiscoverer),
|
||||
string(CSAFCategoryOther),
|
||||
string(CSAFCategoryTranslator),
|
||||
string(CSAFCategoryUser),
|
||||
string(CSAFCategoryVendor))
|
||||
|
||||
type CSAFPublisher struct {
|
||||
Category *CSAFCategory `json:"category"` // required
|
||||
Name *string `json:"name"` // required
|
||||
Namespace *string `json:"namespace"` // required
|
||||
ContactDetails string `json:"contact_details,omitempty"`
|
||||
IssuingAuthority string `json:"issuing_authority,omitempty"`
|
||||
}
|
||||
|
||||
type MetadataVersion string
|
||||
|
||||
const MetadataVersion20 MetadataVersion = "2.0"
|
||||
|
||||
var metadataVersionPattern = alternativesUnmarshal(string(MetadataVersion20))
|
||||
|
||||
type MetadataRole string
|
||||
|
||||
const (
|
||||
MetadataRolePublisher MetadataRole = "csaf_publisher"
|
||||
MetadataRoleProvider MetadataRole = "csaf_provider"
|
||||
MetadataRoleTrustedProvider MetadataRole = "csaf_trusted_provider"
|
||||
)
|
||||
|
||||
var metadataRolePattern = alternativesUnmarshal(
|
||||
string(MetadataRolePublisher),
|
||||
string(MetadataRoleProvider),
|
||||
string(MetadataRoleTrustedProvider))
|
||||
|
||||
type ProviderURL string
|
||||
|
||||
var providerURLPattern = patternUnmarshal(`/provider-metadata\.json$`)
|
||||
|
||||
type ProviderMetadata struct {
|
||||
CanonicalURL *ProviderURL `json:"canonical_url"` // required
|
||||
Distributions []Distribution `json:"distributions,omitempty"`
|
||||
LastUpdated *TimeStamp `json:"last_updated"` // required
|
||||
ListOnCSAFAggregators *bool `json:"list_on_CSAF_aggregators"`
|
||||
MetadataVersion *MetadataVersion `json:"metadata_version"` // required
|
||||
MirrorOnCSAFAggregators *bool `json:"mirror_on_CSAF_aggregators"` // required
|
||||
PGPKeys []PGPKey `json:"pgp_keys,omitempty"`
|
||||
Publisher *CSAFPublisher `json:"publisher"` // required
|
||||
Role *MetadataRole `json:"role"` // required
|
||||
}
|
||||
|
||||
func patternUnmarshal(pattern string) func([]byte) (string, error) {
|
||||
r := regexp.MustCompile(pattern)
|
||||
return func(data []byte) (string, error) {
|
||||
s := string(data)
|
||||
if !r.MatchString(s) {
|
||||
return "", fmt.Errorf("%s does not match %v", s, r)
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
}
|
||||
|
||||
func alternativesUnmarshal(alternatives ...string) func([]byte) (string, error) {
|
||||
return func(data []byte) (string, error) {
|
||||
s := string(data)
|
||||
for _, alt := range alternatives {
|
||||
if alt == s {
|
||||
return s, nil
|
||||
}
|
||||
}
|
||||
return "", fmt.Errorf("%s not in [%s]", s, strings.Join(alternatives, "|"))
|
||||
}
|
||||
}
|
||||
|
||||
func (tl *TLPLabel) UnmarshalText(data []byte) error {
|
||||
s, err := tlpLabelPattern(data)
|
||||
if err == nil {
|
||||
*tl = TLPLabel(s)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (ju *JsonURL) UnmarshalText(data []byte) error {
|
||||
s, err := jsonURLPattern(data)
|
||||
if err == nil {
|
||||
*ju = JsonURL(s)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (pu *ProviderURL) UnmarshalText(data []byte) error {
|
||||
s, err := providerURLPattern(data)
|
||||
if err == nil {
|
||||
*pu = ProviderURL(s)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (cc *CSAFCategory) UnmarshalText(data []byte) error {
|
||||
s, err := csafCategoryPattern(data)
|
||||
if err == nil {
|
||||
*cc = CSAFCategory(s)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (fp *Fingerprint) UnmarshalText(data []byte) error {
|
||||
s, err := fingerprintPattern(data)
|
||||
if err == nil {
|
||||
*fp = Fingerprint(s)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (ts *TimeStamp) UnmarshalText(data []byte) error {
|
||||
t, err := time.Parse(time.RFC3339, string(data))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*ts = TimeStamp(t)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ts TimeStamp) MarshalText() ([]byte, error) {
|
||||
return []byte(time.Time(ts).Format(time.RFC3339)), nil
|
||||
}
|
||||
|
||||
func (pmd *ProviderMetadata) Defaults() {
|
||||
if pmd.Role == nil {
|
||||
role := MetadataRoleProvider
|
||||
pmd.Role = &role
|
||||
}
|
||||
if pmd.ListOnCSAFAggregators == nil {
|
||||
t := true
|
||||
pmd.ListOnCSAFAggregators = &t
|
||||
}
|
||||
if pmd.MirrorOnCSAFAggregators == nil {
|
||||
t := true
|
||||
pmd.MirrorOnCSAFAggregators = &t
|
||||
}
|
||||
if pmd.MetadataVersion == nil {
|
||||
mdv := MetadataVersion20
|
||||
pmd.MetadataVersion = &mdv
|
||||
}
|
||||
}
|
||||
|
||||
func (f *Feed) Validate() error {
|
||||
switch {
|
||||
case f.TLPLabel == nil:
|
||||
return errors.New("feed[].tlp_label is mandatory")
|
||||
case f.URL == nil:
|
||||
return errors.New("feed[].url is mandatory")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *ROLIE) Validate() error {
|
||||
if len(r.Feeds) < 1 {
|
||||
return errors.New("ROLIE needs at least one feed")
|
||||
}
|
||||
for i := range r.Feeds {
|
||||
if err := r.Feeds[i].Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cp *CSAFPublisher) Validate() error {
|
||||
switch {
|
||||
case cp == nil:
|
||||
return errors.New("publisher is mandatory")
|
||||
case cp.Category == nil:
|
||||
return errors.New("publisher.category is mandatory")
|
||||
case cp.Name == nil:
|
||||
return errors.New("publisher.name is mandatory")
|
||||
case cp.Namespace == nil:
|
||||
return errors.New("publisher.namespace is mandatory")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (pk *PGPKey) Validate() error {
|
||||
if pk.URL == nil {
|
||||
return errors.New("pgp_key[].url is mandatory")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Distribution) Validate() error {
|
||||
for i := range d.Rolie {
|
||||
if err := d.Rolie[i].Validate(); err != nil {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (pmd *ProviderMetadata) Validate() error {
|
||||
|
||||
switch {
|
||||
case pmd.CanonicalURL == nil:
|
||||
return errors.New("canonical_url is mandatory")
|
||||
case pmd.LastUpdated == nil:
|
||||
return errors.New("last_updated is mandatory")
|
||||
case pmd.MetadataVersion == nil:
|
||||
return errors.New("metadata_version is mandatory")
|
||||
}
|
||||
|
||||
if err := pmd.Publisher.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for i := range pmd.PGPKeys {
|
||||
if err := pmd.PGPKeys[i].Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
for i := range pmd.Distributions {
|
||||
if err := pmd.Distributions[i].Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (pmd *ProviderMetadata) SetLastUpdated(t time.Time) {
|
||||
ts := TimeStamp(t.UTC())
|
||||
pmd.LastUpdated = &ts
|
||||
}
|
||||
|
||||
func (pmd *ProviderMetadata) SetPGP(fingerprint, url string) {
|
||||
for i := range pmd.PGPKeys {
|
||||
if pmd.PGPKeys[i].Fingerprint == Fingerprint(fingerprint) {
|
||||
pmd.PGPKeys[i].URL = &url
|
||||
return
|
||||
}
|
||||
}
|
||||
pmd.PGPKeys = append(pmd.PGPKeys, PGPKey{
|
||||
Fingerprint: Fingerprint(fingerprint),
|
||||
URL: &url,
|
||||
})
|
||||
}
|
||||
|
||||
func NewProviderMetadata(canonicalURL string) *ProviderMetadata {
|
||||
pmd := new(ProviderMetadata)
|
||||
pmd.Defaults()
|
||||
pmd.SetLastUpdated(time.Now())
|
||||
cu := ProviderURL(canonicalURL)
|
||||
pmd.CanonicalURL = &cu
|
||||
return pmd
|
||||
}
|
||||
|
||||
func (pm *ProviderMetadata) Save(w io.Writer) error {
|
||||
enc := json.NewEncoder(w)
|
||||
enc.SetIndent("", " ")
|
||||
return enc.Encode(pm)
|
||||
}
|
||||
|
||||
func LoadProviderMetadata(r io.Reader) (*ProviderMetadata, error) {
|
||||
|
||||
var pmd ProviderMetadata
|
||||
dec := json.NewDecoder(r)
|
||||
if err := dec.Decode(&pmd); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := pmd.Validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Set defaults.
|
||||
pmd.Defaults()
|
||||
|
||||
return &pmd, nil
|
||||
}
|
||||
21
go.mod
Normal file
21
go.mod
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
module github.com/intevation/csaf_trusted
|
||||
|
||||
go 1.17
|
||||
|
||||
require (
|
||||
github.com/BurntSushi/toml v0.4.1
|
||||
github.com/PaesslerAG/gval v1.1.2
|
||||
github.com/PaesslerAG/jsonpath v0.1.1
|
||||
github.com/ProtonMail/gopenpgp/v2 v2.3.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/ProtonMail/go-crypto v0.0.0-20211112122917-428f8eabeeb3 // indirect
|
||||
github.com/ProtonMail/go-mime v0.0.0-20190923161245-9b5a4261663a // indirect
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/sirupsen/logrus v1.4.2 // indirect
|
||||
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2 // indirect
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 // indirect
|
||||
golang.org/x/text v0.3.3 // indirect
|
||||
)
|
||||
68
go.sum
Normal file
68
go.sum
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
github.com/BurntSushi/toml v0.4.1 h1:GaI7EiDXDRfa8VshkTj7Fym7ha+y8/XxIgD2okUIjLw=
|
||||
github.com/BurntSushi/toml v0.4.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
|
||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||
github.com/PaesslerAG/gval v1.0.0/go.mod h1:y/nm5yEyTeX6av0OfKJNp9rBNj2XrGhAf5+v24IBN1I=
|
||||
github.com/PaesslerAG/gval v1.1.2 h1:EROKxV4/fAKWb0Qoj7NOxmHZA7gcpjOV9XgiRZMRCUU=
|
||||
github.com/PaesslerAG/gval v1.1.2/go.mod h1:Fa8gfkCmUsELXgayr8sfL/sw+VzCVoa03dcOcR/if2w=
|
||||
github.com/PaesslerAG/jsonpath v0.1.0/go.mod h1:4BzmtoM/PI8fPO4aQGIusjGxGir2BzcV0grWtFzq1Y8=
|
||||
github.com/PaesslerAG/jsonpath v0.1.1 h1:c1/AToHQMVsduPAa4Vh6xp2U0evy4t8SWp8imEsylIk=
|
||||
github.com/PaesslerAG/jsonpath v0.1.1/go.mod h1:lVboNxFGal/VwW6d9JzIy56bUsYAP6tH/x80vjnCseY=
|
||||
github.com/ProtonMail/go-crypto v0.0.0-20211112122917-428f8eabeeb3 h1:XcF0cTDJeiuZ5NU8w7WUDge0HRwwNRmxj/GGk6KSA6g=
|
||||
github.com/ProtonMail/go-crypto v0.0.0-20211112122917-428f8eabeeb3/go.mod h1:z4/9nQmJSSwwds7ejkxaJwO37dru3geImFUdJlaLzQo=
|
||||
github.com/ProtonMail/go-mime v0.0.0-20190923161245-9b5a4261663a h1:W6RrgN/sTxg1msqzFFb+G80MFmpjMw61IU+slm+wln4=
|
||||
github.com/ProtonMail/go-mime v0.0.0-20190923161245-9b5a4261663a/go.mod h1:NYt+V3/4rEeDuaev/zw1zCq8uqVEuPHzDPo3OZrlGJ4=
|
||||
github.com/ProtonMail/gopenpgp/v2 v2.3.0 h1:eniutitHk02Yn3GtaDfJTVm/Ca1e8s6zkS0SpeaocXI=
|
||||
github.com/ProtonMail/gopenpgp/v2 v2.3.0/go.mod h1:F62x0m3akQuisX36pOgAtKOHZ1E7/MpnX8bZWCK+5dA=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
|
||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
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/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
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-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2 h1:It14KIkyBFYkHkwZ7k45minvA9aorojkyjGk9KJ5B/w=
|
||||
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
|
||||
golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56/go.mod h1:JhuoJpWY28nO4Vef9tZUw9qufEGTyX1+7lmHxV5q5G4=
|
||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
|
||||
golang.org/x/mobile v0.0.0-20200801112145-973feb4309de/go.mod h1:skQtrUTUwhdJvXM/2KKJzY8pDgNr9I/FOMqDVRPBUS4=
|
||||
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
|
||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/mod v0.1.1-0.20191209134235-331c550502dd/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
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-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 h1:nxC68pudNYkKU6jWhgrqdreuFiOQWj1Fs7T3VrH4Pjw=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20200117012304-6edc0a871e69/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
Loading…
Add table
Add a link
Reference in a new issue