1
0
Fork 0
mirror of https://github.com/gocsaf/csaf.git synced 2025-12-22 11:55:40 +01:00

Merge pull request #10 from csaf-poc/csaf-uploader

CSAF uploader
This commit is contained in:
Fadi Abbud 2021-12-07 16:55:58 +01:00 committed by GitHub
commit 9d0ed98a17
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 328 additions and 0 deletions

319
cmd/csaf_uploader/main.go Normal file
View file

@ -0,0 +1,319 @@
package main
import (
"bytes"
"encoding/json"
"fmt"
"log"
"mime/multipart"
"net/http"
"os"
"path/filepath"
"github.com/ProtonMail/gopenpgp/v2/crypto"
"github.com/jessevdk/go-flags"
"github.com/mitchellh/go-homedir"
"golang.org/x/crypto/bcrypt"
"golang.org/x/crypto/ssh/terminal"
)
type options struct {
Action string `short:"a" long:"action" choice:"upload" choice:"create" default:"upload" description:"Action to perform"`
URL string `short:"u" long:"url" description:"URL of the CSAF provider" default:"https://localhost/cgi-bin/csaf_provider.go" value-name:"URL"`
TLP string `short:"t" long:"tlp" choice:"csaf" choice:"white" choice:"green" choice:"amber" choice:"red" default:"csaf" description:"TLP of the feed"`
Key *string `short:"k" long:"key" description:"OpenPGP key to sign the CSAF files" value-name:"KEY-FILE"`
Password *string `short:"p" long:"password" description:"Authentication password for accessing the CSAF provider" value-name:"PASSWORD"`
Passphrase *string `short:"P" long:"passphrase" description:"Passphrase to unlock the OpenPGP key" value-name:"PASSPHRASE"`
PasswordInteractive bool `short:"i" long:"password-interactive" description:"Enter password interactively" no-ini:"true"`
PassphraseInteractive bool `short:"I" long:"passphrase-interacive" description:"Enter passphrase interactively" no-ini:"true"`
Config *string `short:"c" long:"config" description:"Path to config ini file" value-name:"INI-FILE" no-ini:"true"`
}
type processor struct {
opts *options
cachedAuth string
keyRing *crypto.KeyRing
}
var iniPaths = []string{
"~/.config/csaf/uploader.ini",
"~/.csaf_uploader.ini",
"csaf_uploader.ini",
}
func loadKey(filename string) (*crypto.Key, error) {
f, err := os.Open(filename)
if err != nil {
return nil, err
}
defer f.Close()
return crypto.NewKeyFromArmoredReader(f)
}
func newProcessor(opts *options) (*processor, error) {
p := processor{
opts: opts,
}
if opts.Action == "upload" {
if opts.Key != nil {
var err error
var key *crypto.Key
if key, err = loadKey(*opts.Key); err != nil {
return nil, err
}
if opts.Passphrase != nil {
if key, err = key.Unlock([]byte(*opts.Passphrase)); err != nil {
return nil, err
}
}
if p.keyRing, err = crypto.NewKeyRing(key); err != nil {
return nil, err
}
}
}
// pre-calc the auth header
if opts.Password != nil {
hash, err := bcrypt.GenerateFromPassword(
[]byte(*opts.Password), bcrypt.DefaultCost)
if err != nil {
return nil, err
}
p.cachedAuth = string(hash)
}
return &p, nil
}
func (p *processor) create() error {
req, err := http.NewRequest(http.MethodGet, p.opts.URL+"/api/create", nil)
if err != nil {
return err
}
req.Header.Set("X-CSAF-PROVIDER-AUTH", p.cachedAuth)
resp, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
log.Printf("Create failed: %s\n", resp.Status)
}
var result struct {
Message string `json:"message"`
Errors []string `json:"errors"`
}
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return err
}
if result.Message != "" {
fmt.Printf("\t%s\n", result.Message)
}
if len(result.Errors) > 0 {
fmt.Println("Errors:")
for _, err := range result.Errors {
fmt.Printf("\t%s\n", err)
}
}
return nil
}
func (p *processor) uploadRequest(filename string) (*http.Request, error) {
data, err := os.ReadFile(filename)
if err != nil {
return nil, err
}
body := new(bytes.Buffer)
writer := multipart.NewWriter(body)
part, err := writer.CreateFormFile("csaf", filepath.Base(filename))
if err != nil {
return nil, err
}
if _, err := part.Write(data); err != nil {
return nil, err
}
if err := writer.WriteField("tlp", p.opts.TLP); err != nil {
return nil, err
}
if p.keyRing == nil && p.opts.Passphrase != nil {
if err := writer.WriteField("passphrase", *p.opts.Passphrase); err != nil {
return nil, err
}
}
if p.keyRing != nil {
sig, err := p.keyRing.SignDetached(crypto.NewPlainMessage(data))
if err != nil {
return nil, err
}
armored, err := sig.GetArmored()
if err != nil {
return nil, err
}
if err := writer.WriteField("signature", armored); err != nil {
return nil, err
}
}
if err := writer.Close(); err != nil {
return nil, err
}
req, err := http.NewRequest(http.MethodPost, p.opts.URL+"/api/upload", body)
if err != nil {
return nil, err
}
req.Header.Set("X-CSAF-PROVIDER-AUTH", p.cachedAuth)
req.Header.Set("Content-Type", writer.FormDataContentType())
return req, nil
}
func (p *processor) process(filename string) error {
req, err := p.uploadRequest(filename)
if err != nil {
return err
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
log.Printf("Upload failed: %s\n", resp.Status)
}
var result struct {
Name string `json:"name"`
ReleaseDate string `json:"release_date"`
Warnings []string `json:"warnings"`
Errors []string `json:"errors"`
}
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return err
}
if result.Name != "" {
fmt.Printf("Name: %s\n", result.Name)
}
if result.ReleaseDate != "" {
fmt.Printf("Release date: %s\n", result.ReleaseDate)
}
if len(result.Warnings) > 0 {
fmt.Println("Warnings:")
for _, warning := range result.Warnings {
fmt.Printf("\t%s\n", warning)
}
}
if len(result.Errors) > 0 {
fmt.Println("Errors:")
for _, err := range result.Errors {
fmt.Printf("\t%s\n", err)
}
}
return nil
}
func findIniFile() string {
for _, f := range iniPaths {
name, err := homedir.Expand(f)
if err != nil {
log.Printf("warn: %v\n", err)
continue
}
if _, err := os.Stat(name); err == nil {
return name
}
}
return ""
}
func readInteractive(prompt string, pw **string) error {
fmt.Print(prompt)
p, err := terminal.ReadPassword(int(os.Stdin.Fd()))
if err != nil {
return err
}
ps := string(p)
*pw = &ps
return nil
}
func check(err error) {
if err != nil {
log.Fatalf("error: %v\n", err)
}
}
func checkParser(err error) {
if err != nil {
if e, ok := err.(*flags.Error); ok && e.Type == flags.ErrHelp {
os.Exit(0)
}
os.Exit(1)
}
}
func main() {
var opts options
parser := flags.NewParser(&opts, flags.Default)
args, err := parser.Parse()
checkParser(err)
if opts.Config != nil {
iniParser := flags.NewIniParser(parser)
iniParser.ParseAsDefaults = true
name, err := homedir.Expand(*opts.Config)
check(err)
checkParser(iniParser.ParseFile(name))
} else if iniFile := findIniFile(); iniFile != "" {
iniParser := flags.NewIniParser(parser)
iniParser.ParseAsDefaults = true
checkParser(iniParser.ParseFile(iniFile))
}
if opts.PasswordInteractive {
check(readInteractive("Enter auth password: ", &opts.Password))
}
if opts.PassphraseInteractive {
check(readInteractive("Enter OpenPGP passphrase: ", &opts.Passphrase))
}
p, err := newProcessor(&opts)
check(err)
if opts.Action == "create" {
check(p.create())
return
}
for _, arg := range args {
check(p.process(arg))
}
}

3
go.mod
View file

@ -7,6 +7,8 @@ require (
github.com/PaesslerAG/gval v1.1.2 github.com/PaesslerAG/gval v1.1.2
github.com/PaesslerAG/jsonpath v0.1.1 github.com/PaesslerAG/jsonpath v0.1.1
github.com/ProtonMail/gopenpgp/v2 v2.3.0 github.com/ProtonMail/gopenpgp/v2 v2.3.0
github.com/jessevdk/go-flags v1.5.0
github.com/mitchellh/go-homedir v1.1.0
github.com/santhosh-tekuri/jsonschema/v5 v5.0.0 github.com/santhosh-tekuri/jsonschema/v5 v5.0.0
golang.org/x/crypto v0.0.0-20211202192323-5770296d904e golang.org/x/crypto v0.0.0-20211202192323-5770296d904e
) )
@ -18,5 +20,6 @@ require (
github.com/pkg/errors v0.9.1 // indirect github.com/pkg/errors v0.9.1 // indirect
github.com/sirupsen/logrus v1.4.2 // indirect github.com/sirupsen/logrus v1.4.2 // indirect
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 // indirect golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 // indirect
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 // indirect
golang.org/x/text v0.3.6 // indirect golang.org/x/text v0.3.6 // indirect
) )

6
go.sum
View file

@ -16,8 +16,12 @@ github.com/ProtonMail/gopenpgp/v2 v2.3.0/go.mod h1:F62x0m3akQuisX36pOgAtKOHZ1E7/
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/jessevdk/go-flags v1.5.0 h1:1jKYvbxEjfUl0fmqTCOfonvskHHXMjBySTLW4y9LFvc=
github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4=
github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk= 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/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
@ -55,9 +59,11 @@ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5h
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 h1:SrN+KX8Art/Sf4HNj6Zcz06G7VEz+7w9tdXTPOZ7+l4= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 h1:SrN+KX8Art/Sf4HNj6Zcz06G7VEz+7w9tdXTPOZ7+l4=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 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.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.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=