From 9fb23892b6d2c17efd28706ccc2a7b3eb9497e5e Mon Sep 17 00:00:00 2001 From: "Sascha L. Teichmann" Date: Tue, 7 Dec 2021 02:50:05 +0100 Subject: [PATCH 1/8] Started with uploader. --- cmd/csaf_uploader/main.go | 81 +++++++++++++++++++++++++++++++++++++++ go.mod | 2 + go.sum | 5 +++ 3 files changed, 88 insertions(+) create mode 100644 cmd/csaf_uploader/main.go diff --git a/cmd/csaf_uploader/main.go b/cmd/csaf_uploader/main.go new file mode 100644 index 0000000..34adbb2 --- /dev/null +++ b/cmd/csaf_uploader/main.go @@ -0,0 +1,81 @@ +package main + +import ( + "log" + "os" + + "github.com/jessevdk/go-flags" + "github.com/mitchellh/go-homedir" +) + +type options struct { + URL string `short:"u" long:"url" description:"URL of the CSAF provider" default:"https://localhost/cgi-bin/csaf_provider.go" value-name:"URL"` + Password *string `short:"P" long:"password" description:"Authentication password for accessing the CSAF provider" value-name:"PASSWORD"` + Key *string `short:"k" long:"key" description:"OpenPGP key to sign the CSAF files" value-name:"KEY-FILE"` + Passphrase *string `short:"p" long:"passphrase" description:"Passphrase to unlock the OpenPGP key" value-name:"PASSPHRASE"` + Action string `short:"a" long:"action" choice:"upload" choice:"create" default:"upload" description:"Action to perform"` + Config *string `short:"c" long:"config" description:"Path to config ini file" value-name:"INI-FILE"` +} + +var iniPaths = []string{ + "~/.config/csaf/uploader.ini", + "~/.csaf_uploader.ini", + "csaf_uploader.ini", +} + +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 main() { + var opts options + + parser := flags.NewParser(&opts, flags.Default) + + args, err := parser.Parse() + if err != nil { + if e, ok := err.(*flags.Error); ok && e.Type == flags.ErrHelp { + os.Exit(0) + } + os.Exit(1) + } + + if opts.Config != nil { + iniParser := flags.NewIniParser(parser) + iniParser.ParseAsDefaults = true + name, err := homedir.Expand(*opts.Config) + if err != nil { + log.Fatalf("error: %v\n", err) + } + if err := iniParser.ParseFile(name); err != nil { + os.Exit(1) + } + } else if iniFile := findIniFile(); iniFile != "" { + iniParser := flags.NewIniParser(parser) + iniParser.ParseAsDefaults = true + if err := iniParser.ParseFile(iniFile); err != nil { + os.Exit(1) + } + } + + if opts.Key != nil { + log.Printf("key: %s\n", *opts.Key) + } + + log.Printf("url: %s\n", opts.URL) + log.Printf("action: %s\n", opts.Action) + + for _, arg := range args { + log.Printf("arg: %s\n", arg) + } +} diff --git a/go.mod b/go.mod index 8814c8e..a62b230 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,8 @@ require ( github.com/PaesslerAG/gval v1.1.2 github.com/PaesslerAG/jsonpath v0.1.1 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 golang.org/x/crypto v0.0.0-20211202192323-5770296d904e ) diff --git a/go.sum b/go.sum index 83173d4..b3ff493 100644 --- a/go.sum +++ b/go.sum @@ -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.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 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/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/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -55,6 +59,7 @@ 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-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-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-20210615035016-665e8c7367d1 h1:SrN+KX8Art/Sf4HNj6Zcz06G7VEz+7w9tdXTPOZ7+l4= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= From 19b4f1dfb2b5de7a6db54e1bf8a130c46622158c Mon Sep 17 00:00:00 2001 From: "Sascha L. Teichmann" Date: Tue, 7 Dec 2021 11:38:14 +0100 Subject: [PATCH 2/8] Added support for entering passwords interactively. --- cmd/csaf_uploader/main.go | 73 +++++++++++++++++++++++++++++---------- go.mod | 1 + go.sum | 1 + 3 files changed, 56 insertions(+), 19 deletions(-) diff --git a/cmd/csaf_uploader/main.go b/cmd/csaf_uploader/main.go index 34adbb2..bcb1adc 100644 --- a/cmd/csaf_uploader/main.go +++ b/cmd/csaf_uploader/main.go @@ -1,20 +1,24 @@ package main import ( + "fmt" "log" "os" "github.com/jessevdk/go-flags" "github.com/mitchellh/go-homedir" + "golang.org/x/crypto/ssh/terminal" ) type options struct { - URL string `short:"u" long:"url" description:"URL of the CSAF provider" default:"https://localhost/cgi-bin/csaf_provider.go" value-name:"URL"` - Password *string `short:"P" long:"password" description:"Authentication password for accessing the CSAF provider" value-name:"PASSWORD"` - Key *string `short:"k" long:"key" description:"OpenPGP key to sign the CSAF files" value-name:"KEY-FILE"` - Passphrase *string `short:"p" long:"passphrase" description:"Passphrase to unlock the OpenPGP key" value-name:"PASSPHRASE"` - Action string `short:"a" long:"action" choice:"upload" choice:"create" default:"upload" description:"Action to perform"` - Config *string `short:"c" long:"config" description:"Path to config ini file" value-name:"INI-FILE"` + URL string `short:"u" long:"url" description:"URL of the CSAF provider" default:"https://localhost/cgi-bin/csaf_provider.go" value-name:"URL"` + Password *string `short:"p" long:"password" description:"Authentication password for accessing the CSAF provider" value-name:"PASSWORD"` + Key *string `short:"k" long:"key" description:"OpenPGP key to sign the CSAF files" value-name:"KEY-FILE"` + Passphrase *string `short:"P" long:"passphrase" description:"Passphrase to unlock the OpenPGP key" value-name:"PASSPHRASE"` + Action string `short:"a" long:"action" choice:"upload" choice:"create" default:"upload" description:"Action to perform"` + Config *string `short:"c" long:"config" description:"Path to config ini file" value-name:"INI-FILE"` + 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"` } var iniPaths = []string{ @@ -37,35 +41,50 @@ func findIniFile() string { return "" } -func main() { - var opts options +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 +} - parser := flags.NewParser(&opts, flags.Default) +func check(err error) { + if err != nil { + log.Fatalf("error: %v\n", err) + } +} - args, err := parser.Parse() +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) - if err != nil { - log.Fatalf("error: %v\n", err) - } - if err := iniParser.ParseFile(name); err != nil { - os.Exit(1) - } + check(err) + checkParser(iniParser.ParseFile(name)) } else if iniFile := findIniFile(); iniFile != "" { iniParser := flags.NewIniParser(parser) iniParser.ParseAsDefaults = true - if err := iniParser.ParseFile(iniFile); err != nil { - os.Exit(1) - } + checkParser(iniParser.ParseFile(iniFile)) } if opts.Key != nil { @@ -75,6 +94,22 @@ func main() { log.Printf("url: %s\n", opts.URL) log.Printf("action: %s\n", opts.Action) + if opts.PasswordInteractive { + check(readInteractive("Enter auth password: ", &opts.Password)) + } + + if opts.PassphraseInteractive { + check(readInteractive("Enter OpenPGP passphrase: ", &opts.Passphrase)) + } + + if opts.Password != nil { + log.Printf("password: '%s'\n", *opts.Password) + } + + if opts.Passphrase != nil { + log.Printf("passphrase: '%s'\n", *opts.Passphrase) + } + for _, arg := range args { log.Printf("arg: %s\n", arg) } diff --git a/go.mod b/go.mod index a62b230..4508260 100644 --- a/go.mod +++ b/go.mod @@ -20,5 +20,6 @@ require ( github.com/pkg/errors v0.9.1 // indirect github.com/sirupsen/logrus v1.4.2 // 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 ) diff --git a/go.sum b/go.sum index b3ff493..c52e3a5 100644 --- a/go.sum +++ b/go.sum @@ -63,6 +63,7 @@ golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7w 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/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/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= From f069593f8784f4d2c8f139e64c69f3322cc55704 Mon Sep 17 00:00:00 2001 From: "Sascha L. Teichmann" Date: Tue, 7 Dec 2021 12:12:52 +0100 Subject: [PATCH 3/8] Started with upload processor. --- cmd/csaf_uploader/main.go | 45 +++++++++++++++++++++++++++++++++------ 1 file changed, 39 insertions(+), 6 deletions(-) diff --git a/cmd/csaf_uploader/main.go b/cmd/csaf_uploader/main.go index bcb1adc..d0e5b38 100644 --- a/cmd/csaf_uploader/main.go +++ b/cmd/csaf_uploader/main.go @@ -7,6 +7,7 @@ import ( "github.com/jessevdk/go-flags" "github.com/mitchellh/go-homedir" + "golang.org/x/crypto/bcrypt" "golang.org/x/crypto/ssh/terminal" ) @@ -21,12 +22,44 @@ type options struct { PassphraseInteractive bool `short:"I" long:"passphrase-interacive" description:"Enter passphrase interactively" no-ini:"true"` } +type processor struct { + opts *options + cachedAuth string +} + var iniPaths = []string{ "~/.config/csaf/uploader.ini", "~/.csaf_uploader.ini", "csaf_uploader.ini", } +func newProcessor(opts *options) (*processor, error) { + p := processor{ + opts: opts, + } + + // 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 { + // TODO: Implement me! + return nil +} + +func (p *processor) process(filename string) error { + // TODO: Implement me! + return nil +} + func findIniFile() string { for _, f := range iniPaths { name, err := homedir.Expand(f) @@ -102,15 +135,15 @@ func main() { check(readInteractive("Enter OpenPGP passphrase: ", &opts.Passphrase)) } - if opts.Password != nil { - log.Printf("password: '%s'\n", *opts.Password) - } + p, err := newProcessor(&opts) + check(err) - if opts.Passphrase != nil { - log.Printf("passphrase: '%s'\n", *opts.Passphrase) + if opts.Action == "create" { + check(p.create()) + return } for _, arg := range args { - log.Printf("arg: %s\n", arg) + check(p.process(arg)) } } From 9e52dfff8038a727ea56606482c821463db4eec4 Mon Sep 17 00:00:00 2001 From: "Sascha L. Teichmann" Date: Tue, 7 Dec 2021 12:18:18 +0100 Subject: [PATCH 4/8] Added no-ini to config option. --- cmd/csaf_uploader/main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/csaf_uploader/main.go b/cmd/csaf_uploader/main.go index d0e5b38..066c406 100644 --- a/cmd/csaf_uploader/main.go +++ b/cmd/csaf_uploader/main.go @@ -17,9 +17,9 @@ type options struct { Key *string `short:"k" long:"key" description:"OpenPGP key to sign the CSAF files" value-name:"KEY-FILE"` Passphrase *string `short:"P" long:"passphrase" description:"Passphrase to unlock the OpenPGP key" value-name:"PASSPHRASE"` Action string `short:"a" long:"action" choice:"upload" choice:"create" default:"upload" description:"Action to perform"` - Config *string `short:"c" long:"config" description:"Path to config ini file" value-name:"INI-FILE"` 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 { From edaa6c38384b9406e676f8ad8634402a3d5e697f Mon Sep 17 00:00:00 2001 From: "Sascha L. Teichmann" Date: Tue, 7 Dec 2021 12:20:54 +0100 Subject: [PATCH 5/8] Moved action option to front. --- cmd/csaf_uploader/main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/csaf_uploader/main.go b/cmd/csaf_uploader/main.go index 066c406..1a1c989 100644 --- a/cmd/csaf_uploader/main.go +++ b/cmd/csaf_uploader/main.go @@ -12,11 +12,11 @@ import ( ) 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"` Password *string `short:"p" long:"password" description:"Authentication password for accessing the CSAF provider" value-name:"PASSWORD"` Key *string `short:"k" long:"key" description:"OpenPGP key to sign the CSAF files" value-name:"KEY-FILE"` Passphrase *string `short:"P" long:"passphrase" description:"Passphrase to unlock the OpenPGP key" value-name:"PASSPHRASE"` - Action string `short:"a" long:"action" choice:"upload" choice:"create" default:"upload" description:"Action to perform"` 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"` From 662c729ba2614f2ccb9f62239dd91f61a8350f8f Mon Sep 17 00:00:00 2001 From: "Sascha L. Teichmann" Date: Tue, 7 Dec 2021 14:01:44 +0100 Subject: [PATCH 6/8] load crypto key and sign files --- cmd/csaf_uploader/main.go | 47 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/cmd/csaf_uploader/main.go b/cmd/csaf_uploader/main.go index 1a1c989..b016a9c 100644 --- a/cmd/csaf_uploader/main.go +++ b/cmd/csaf_uploader/main.go @@ -5,6 +5,7 @@ import ( "log" "os" + "github.com/ProtonMail/gopenpgp/v2/crypto" "github.com/jessevdk/go-flags" "github.com/mitchellh/go-homedir" "golang.org/x/crypto/bcrypt" @@ -25,6 +26,7 @@ type options struct { type processor struct { opts *options cachedAuth string + keyRing *crypto.KeyRing } var iniPaths = []string{ @@ -33,11 +35,38 @@ var iniPaths = []string{ "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( @@ -47,6 +76,7 @@ func newProcessor(opts *options) (*processor, error) { } p.cachedAuth = string(hash) } + return &p, nil } @@ -56,7 +86,24 @@ func (p *processor) create() error { } func (p *processor) process(filename string) error { + + data, err := os.ReadFile(filename) + if err != nil { + return err + } + + var armored string + if p.keyRing != nil { + sig, err := p.keyRing.SignDetached(crypto.NewPlainMessage(data)) + if err != nil { + return err + } + if armored, err = sig.GetArmored(); err != nil { + return err + } + } // TODO: Implement me! + _ = armored return nil } From c536f728edad210ff48c84efc6ab2010180479ba Mon Sep 17 00:00:00 2001 From: "Sascha L. Teichmann" Date: Tue, 7 Dec 2021 16:14:37 +0100 Subject: [PATCH 7/8] Implemented CSAF upload. --- cmd/csaf_uploader/main.go | 141 +++++++++++++++++++++++++++++++------- 1 file changed, 115 insertions(+), 26 deletions(-) diff --git a/cmd/csaf_uploader/main.go b/cmd/csaf_uploader/main.go index b016a9c..98f283d 100644 --- a/cmd/csaf_uploader/main.go +++ b/cmd/csaf_uploader/main.go @@ -1,9 +1,14 @@ 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" @@ -13,14 +18,18 @@ import ( ) 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"` - Password *string `short:"p" long:"password" description:"Authentication password for accessing the CSAF provider" value-name:"PASSWORD"` - Key *string `short:"k" long:"key" description:"OpenPGP key to sign the CSAF files" value-name:"KEY-FILE"` - 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"` + 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 { @@ -85,25 +94,112 @@ func (p *processor) create() error { 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("POST", 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 { - data, err := os.ReadFile(filename) + req, err := p.uploadRequest(filename) if err != nil { return err } - var armored string - if p.keyRing != nil { - sig, err := p.keyRing.SignDetached(crypto.NewPlainMessage(data)) - if err != nil { - return err - } - if armored, err = sig.GetArmored(); 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) } } - // TODO: Implement me! - _ = armored + + if len(result.Errors) > 0 { + fmt.Println("Errors:") + for _, err := range result.Errors { + fmt.Printf("\t%s\n", err) + } + } + return nil } @@ -167,13 +263,6 @@ func main() { checkParser(iniParser.ParseFile(iniFile)) } - if opts.Key != nil { - log.Printf("key: %s\n", *opts.Key) - } - - log.Printf("url: %s\n", opts.URL) - log.Printf("action: %s\n", opts.Action) - if opts.PasswordInteractive { check(readInteractive("Enter auth password: ", &opts.Password)) } From e4ed65db894209f9438d29878344a8d0f67f8106 Mon Sep 17 00:00:00 2001 From: "Sascha L. Teichmann" Date: Tue, 7 Dec 2021 16:29:43 +0100 Subject: [PATCH 8/8] Implemented create request. --- cmd/csaf_uploader/main.go | 38 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 36 insertions(+), 2 deletions(-) diff --git a/cmd/csaf_uploader/main.go b/cmd/csaf_uploader/main.go index 98f283d..86a57dd 100644 --- a/cmd/csaf_uploader/main.go +++ b/cmd/csaf_uploader/main.go @@ -90,7 +90,41 @@ func newProcessor(opts *options) (*processor, error) { } func (p *processor) create() error { - // TODO: Implement me! + 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 } @@ -140,7 +174,7 @@ func (p *processor) uploadRequest(filename string) (*http.Request, error) { return nil, err } - req, err := http.NewRequest("POST", p.opts.URL+"/api/upload", body) + req, err := http.NewRequest(http.MethodPost, p.opts.URL+"/api/upload", body) if err != nil { return nil, err }