From fed66c4e27c1512d62f225f2f7103ff92d3f32dd Mon Sep 17 00:00:00 2001 From: "Sascha L. Teichmann" Date: Tue, 16 Nov 2021 13:58:54 +0100 Subject: [PATCH] Added files from the first prototype. --- cmd/csaf_provider/config.go | 92 +++++++ cmd/csaf_provider/controller.go | 399 +++++++++++++++++++++++++++++ cmd/csaf_provider/dir.go | 176 +++++++++++++ cmd/csaf_provider/extract.go | 126 +++++++++ cmd/csaf_provider/indices.go | 158 ++++++++++++ cmd/csaf_provider/main.go | 24 ++ cmd/csaf_provider/mux.go | 38 +++ cmd/csaf_provider/tmpl/create.html | 16 ++ cmd/csaf_provider/tmpl/index.html | 41 +++ cmd/csaf_provider/tmpl/upload.html | 21 ++ csaf/models.go | 349 +++++++++++++++++++++++++ go.mod | 21 ++ go.sum | 68 +++++ 13 files changed, 1529 insertions(+) create mode 100644 cmd/csaf_provider/config.go create mode 100644 cmd/csaf_provider/controller.go create mode 100644 cmd/csaf_provider/dir.go create mode 100644 cmd/csaf_provider/extract.go create mode 100644 cmd/csaf_provider/indices.go create mode 100644 cmd/csaf_provider/main.go create mode 100644 cmd/csaf_provider/mux.go create mode 100644 cmd/csaf_provider/tmpl/create.html create mode 100644 cmd/csaf_provider/tmpl/index.html create mode 100644 cmd/csaf_provider/tmpl/upload.html create mode 100644 csaf/models.go create mode 100644 go.mod create mode 100644 go.sum diff --git a/cmd/csaf_provider/config.go b/cmd/csaf_provider/config.go new file mode 100644 index 0000000..c048721 --- /dev/null +++ b/cmd/csaf_provider/config.go @@ -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 +} diff --git a/cmd/csaf_provider/controller.go b/cmd/csaf_provider/controller.go new file mode 100644 index 0000000..58e4864 --- /dev/null +++ b/cmd/csaf_provider/controller.go @@ -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) +} diff --git a/cmd/csaf_provider/dir.go b/cmd/csaf_provider/dir.go new file mode 100644 index 0000000..89484e5 --- /dev/null +++ b/cmd/csaf_provider/dir.go @@ -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 +} diff --git a/cmd/csaf_provider/extract.go b/cmd/csaf_provider/extract.go new file mode 100644 index 0000000..d2a0b4d --- /dev/null +++ b/cmd/csaf_provider/extract.go @@ -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() +} diff --git a/cmd/csaf_provider/indices.go b/cmd/csaf_provider/indices.go new file mode 100644 index 0000000..46d35f4 --- /dev/null +++ b/cmd/csaf_provider/indices.go @@ -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) +} diff --git a/cmd/csaf_provider/main.go b/cmd/csaf_provider/main.go new file mode 100644 index 0000000..d4b527a --- /dev/null +++ b/cmd/csaf_provider/main.go @@ -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) + } +} diff --git a/cmd/csaf_provider/mux.go b/cmd/csaf_provider/mux.go new file mode 100644 index 0000000..12b17ed --- /dev/null +++ b/cmd/csaf_provider/mux.go @@ -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)) +} diff --git a/cmd/csaf_provider/tmpl/create.html b/cmd/csaf_provider/tmpl/create.html new file mode 100644 index 0000000..37bfadc --- /dev/null +++ b/cmd/csaf_provider/tmpl/create.html @@ -0,0 +1,16 @@ + + + + + + CSAF-Provider - Directory structure created + + +

CSAF-Provider - Directory structure created

+ {{ if .Error }} + Error: {{ .Error }}. + {{ else }} + Everything is setup fine now. + {{ end }} + + diff --git a/cmd/csaf_provider/tmpl/index.html b/cmd/csaf_provider/tmpl/index.html new file mode 100644 index 0000000..c4d7add --- /dev/null +++ b/cmd/csaf_provider/tmpl/index.html @@ -0,0 +1,41 @@ + + + + + + CSAF-Provider - CSAF upload + + +

CSAF-Provider - CSAF upload

+
+
+ Select your CSAF file + + +
+ {{ if eq (len .Config.TLPs) 1 }} + + {{ else }} + + +
+ {{ if .Config.UploadSignature }} + +
+ + {{ else }} + + + {{ end }} +
+ +
+
+ + diff --git a/cmd/csaf_provider/tmpl/upload.html b/cmd/csaf_provider/tmpl/upload.html new file mode 100644 index 0000000..d2501ff --- /dev/null +++ b/cmd/csaf_provider/tmpl/upload.html @@ -0,0 +1,21 @@ + + + + + + CSAF-Provider - CSAF uploaded + + +

CSAF-Provider - CSAF uploaded

+ {{ if .Error }} + Error: {{ .Error }}. + {{ else }} + + + +
CSAF file:{{ .Name }}
Release date:{{ .ReleaseDate }}
+ {{ end }} +
+ Back: + + diff --git a/csaf/models.go b/csaf/models.go new file mode 100644 index 0000000..aafc7be --- /dev/null +++ b/csaf/models.go @@ -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 +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..b656b21 --- /dev/null +++ b/go.mod @@ -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 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..b37f394 --- /dev/null +++ b/go.sum @@ -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=