1
0
Fork 0
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:
Sascha L. Teichmann 2021-11-16 13:58:54 +01:00
parent c2a483fc95
commit fed66c4e27
13 changed files with 1529 additions and 0 deletions

View 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
}

View 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
View 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
}

View 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()
}

View 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
View 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
View 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))
}

View 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>

View 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>

View 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
View 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
View 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
View 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=