Skip to content

Commit

Permalink
refact: cscli papi (#3222)
Browse files Browse the repository at this point in the history
* cscli papi status - fix nil deref + func test

* cscli papi: extract methods status(), sync()

* papi status -> stdout

* fix nil deref

* cscli support dump: include papi status

* lint
  • Loading branch information
mmetc authored Sep 10, 2024
1 parent c8750f6 commit 4d10e9d
Show file tree
Hide file tree
Showing 4 changed files with 129 additions and 66 deletions.
144 changes: 81 additions & 63 deletions cmd/crowdsec-cli/clipapi/papi.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ package clipapi

import (
"fmt"
"io"
"time"

"github.com/fatih/color"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"gopkg.in/tomb.v2"
Expand All @@ -13,9 +15,10 @@ import (
"github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/require"
"github.com/crowdsecurity/crowdsec/pkg/apiserver"
"github.com/crowdsecurity/crowdsec/pkg/csconfig"
"github.com/crowdsecurity/crowdsec/pkg/database"
)

type configGetter func() *csconfig.Config
type configGetter = func() *csconfig.Config

type cliPapi struct {
cfg configGetter
Expand Down Expand Up @@ -46,104 +49,119 @@ func (cli *cliPapi) NewCommand() *cobra.Command {
},
}

cmd.AddCommand(cli.NewStatusCmd())
cmd.AddCommand(cli.NewSyncCmd())
cmd.AddCommand(cli.newStatusCmd())
cmd.AddCommand(cli.newSyncCmd())

return cmd
}

func (cli *cliPapi) NewStatusCmd() *cobra.Command {
func (cli *cliPapi) Status(out io.Writer, db *database.Client) error {
cfg := cli.cfg()

apic, err := apiserver.NewAPIC(cfg.API.Server.OnlineClient, db, cfg.API.Server.ConsoleConfig, cfg.API.Server.CapiWhitelists)
if err != nil {
return fmt.Errorf("unable to initialize API client: %w", err)
}

papi, err := apiserver.NewPAPI(apic, db, cfg.API.Server.ConsoleConfig, log.GetLevel())
if err != nil {
return fmt.Errorf("unable to initialize PAPI client: %w", err)
}

perms, err := papi.GetPermissions()
if err != nil {
return fmt.Errorf("unable to get PAPI permissions: %w", err)
}

lastTimestampStr, err := db.GetConfigItem(apiserver.PapiPullKey)
if err != nil {
lastTimestampStr = ptr.Of("never")
}

// both can and did happen
if lastTimestampStr == nil || *lastTimestampStr == "0001-01-01T00:00:00Z" {
lastTimestampStr = ptr.Of("never")
}

fmt.Fprint(out, "You can successfully interact with Polling API (PAPI)\n")
fmt.Fprintf(out, "Console plan: %s\n", perms.Plan)
fmt.Fprintf(out, "Last order received: %s\n", *lastTimestampStr)
fmt.Fprint(out, "PAPI subscriptions:\n")

for _, sub := range perms.Categories {
fmt.Fprintf(out, " - %s\n", sub)
}

return nil
}

func (cli *cliPapi) newStatusCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "status",
Short: "Get status of the Polling API",
Args: cobra.MinimumNArgs(0),
DisableAutoGenTag: true,
RunE: func(cmd *cobra.Command, _ []string) error {
var err error
cfg := cli.cfg()
db, err := require.DBClient(cmd.Context(), cfg.DbConfig)
if err != nil {
return err
}

apic, err := apiserver.NewAPIC(cfg.API.Server.OnlineClient, db, cfg.API.Server.ConsoleConfig, cfg.API.Server.CapiWhitelists)
if err != nil {
return fmt.Errorf("unable to initialize API client: %w", err)
}
return cli.Status(color.Output, db)
},
}

papi, err := apiserver.NewPAPI(apic, db, cfg.API.Server.ConsoleConfig, log.GetLevel())
if err != nil {
return fmt.Errorf("unable to initialize PAPI client: %w", err)
}
return cmd
}

perms, err := papi.GetPermissions()
if err != nil {
return fmt.Errorf("unable to get PAPI permissions: %w", err)
}
var lastTimestampStr *string
lastTimestampStr, err = db.GetConfigItem(apiserver.PapiPullKey)
if err != nil {
lastTimestampStr = ptr.Of("never")
}
log.Infof("You can successfully interact with Polling API (PAPI)")
log.Infof("Console plan: %s", perms.Plan)
log.Infof("Last order received: %s", *lastTimestampStr)
func (cli *cliPapi) sync(out io.Writer, db *database.Client) error {
cfg := cli.cfg()
t := tomb.Tomb{}

log.Infof("PAPI subscriptions:")
for _, sub := range perms.Categories {
log.Infof(" - %s", sub)
}
apic, err := apiserver.NewAPIC(cfg.API.Server.OnlineClient, db, cfg.API.Server.ConsoleConfig, cfg.API.Server.CapiWhitelists)
if err != nil {
return fmt.Errorf("unable to initialize API client: %w", err)
}

return nil
},
t.Go(apic.Push)

papi, err := apiserver.NewPAPI(apic, db, cfg.API.Server.ConsoleConfig, log.GetLevel())
if err != nil {
return fmt.Errorf("unable to initialize PAPI client: %w", err)
}

return cmd
t.Go(papi.SyncDecisions)

err = papi.PullOnce(time.Time{}, true)
if err != nil {
return fmt.Errorf("unable to sync decisions: %w", err)
}

log.Infof("Sending acknowledgements to CAPI")

apic.Shutdown()
papi.Shutdown()
t.Wait()
time.Sleep(5 * time.Second) // FIXME: the push done by apic.Push is run inside a sub goroutine, sleep to make sure it's done

return nil
}

func (cli *cliPapi) NewSyncCmd() *cobra.Command {
func (cli *cliPapi) newSyncCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "sync",
Short: "Sync with the Polling API, pulling all non-expired orders for the instance",
Args: cobra.MinimumNArgs(0),
DisableAutoGenTag: true,
RunE: func(cmd *cobra.Command, _ []string) error {
var err error
cfg := cli.cfg()
t := tomb.Tomb{}

db, err := require.DBClient(cmd.Context(), cfg.DbConfig)
if err != nil {
return err
}

apic, err := apiserver.NewAPIC(cfg.API.Server.OnlineClient, db, cfg.API.Server.ConsoleConfig, cfg.API.Server.CapiWhitelists)
if err != nil {
return fmt.Errorf("unable to initialize API client: %w", err)
}

t.Go(apic.Push)

papi, err := apiserver.NewPAPI(apic, db, cfg.API.Server.ConsoleConfig, log.GetLevel())
if err != nil {
return fmt.Errorf("unable to initialize PAPI client: %w", err)
}

t.Go(papi.SyncDecisions)

err = papi.PullOnce(time.Time{}, true)
if err != nil {
return fmt.Errorf("unable to sync decisions: %w", err)
}

log.Infof("Sending acknowledgements to CAPI")

apic.Shutdown()
papi.Shutdown()
t.Wait()
time.Sleep(5 * time.Second) // FIXME: the push done by apic.Push is run inside a sub goroutine, sleep to make sure it's done

return nil
return cli.sync(color.Output, db)
},
}

Expand Down
28 changes: 26 additions & 2 deletions cmd/crowdsec-cli/clisupport/support.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import (
"github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/clilapi"
"github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/climachine"
"github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/climetrics"
"github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/clipapi"
"github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/require"
"github.com/crowdsecurity/crowdsec/pkg/csconfig"
"github.com/crowdsecurity/crowdsec/pkg/cwhub"
Expand All @@ -47,6 +48,7 @@ const (
SUPPORT_CROWDSEC_CONFIG_PATH = "config/crowdsec.yaml"
SUPPORT_LAPI_STATUS_PATH = "lapi_status.txt"
SUPPORT_CAPI_STATUS_PATH = "capi_status.txt"
SUPPORT_PAPI_STATUS_PATH = "papi_status.txt"
SUPPORT_ACQUISITION_DIR = "config/acquis/"
SUPPORT_CROWDSEC_PROFILE_PATH = "config/profiles.yaml"
SUPPORT_CRASH_DIR = "crash/"
Expand Down Expand Up @@ -195,9 +197,9 @@ func (cli *cliSupport) dumpBouncers(zw *zip.Writer, db *database.Client) error {
}

out := new(bytes.Buffer)
cm := clibouncer.New(cli.cfg)
cb := clibouncer.New(cli.cfg)

if err := cm.List(out, db); err != nil {
if err := cb.List(out, db); err != nil {
return err
}

Expand Down Expand Up @@ -265,6 +267,24 @@ func (cli *cliSupport) dumpCAPIStatus(zw *zip.Writer, hub *cwhub.Hub) error {
return nil
}

func (cli *cliSupport) dumpPAPIStatus(zw *zip.Writer, db *database.Client) error {
log.Info("Collecting PAPI status")

out := new(bytes.Buffer)
cp := clipapi.New(cli.cfg)

err := cp.Status(out, db)
if err != nil {
fmt.Fprintf(out, "%s\n", err)
}

stripped := stripAnsiString(out.String())

cli.writeToZip(zw, SUPPORT_PAPI_STATUS_PATH, time.Now(), strings.NewReader(stripped))

return nil
}

func (cli *cliSupport) dumpConfigYAML(zw *zip.Writer) error {
log.Info("Collecting crowdsec config")

Expand Down Expand Up @@ -517,6 +537,10 @@ func (cli *cliSupport) dump(ctx context.Context, outFile string) error {
if err = cli.dumpCAPIStatus(zipWriter, hub); err != nil {
log.Warnf("could not collect CAPI status: %s", err)
}

if err = cli.dumpPAPIStatus(zipWriter, db); err != nil {
log.Warnf("could not collect PAPI status: %s", err)
}
}

if !skipLAPI {
Expand Down
8 changes: 8 additions & 0 deletions cmd/crowdsec-cli/require/require.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,14 @@ func CAPI(c *csconfig.Config) error {
}

func PAPI(c *csconfig.Config) error {
if err := CAPI(c); err != nil {
return err
}

if err := CAPIRegistered(c); err != nil {
return err
}

if c.API.Server.OnlineClient.Credentials.PapiURL == "" {
return errors.New("no PAPI URL in configuration")
}
Expand Down
15 changes: 14 additions & 1 deletion test/bats/04_capi.bats
Original file line number Diff line number Diff line change
Expand Up @@ -46,13 +46,26 @@ setup() {
assert_stderr --regexp "no configuration for Central API \(CAPI\) in '$(echo $CONFIG_YAML|sed s#//#/#g)'"
}
@test "cscli capi status" {
@test "cscli {capi,papi} status" {
./instance-data load
config_enable_capi
# should not panic with no credentials, but return an error
rune -1 cscli papi status
assert_stderr --partial "the Central API (CAPI) must be configured with 'cscli capi register'"
rune -0 cscli capi register --schmilblick githubciXXXXXXXXXXXXXXXXXXXXXXXX
rune -1 cscli capi status
assert_stderr --partial "no scenarios or appsec-rules installed, abort"
rune -1 cscli papi status
assert_stderr --partial "no PAPI URL in configuration"
rune -0 cscli console enable console_management
rune -1 cscli papi status
assert_stderr --partial "unable to get PAPI permissions"
assert_stderr --partial "Forbidden for plan"
rune -0 cscli scenarios install crowdsecurity/ssh-bf
rune -0 cscli capi status
assert_output --partial "Loaded credentials from"
Expand Down

0 comments on commit 4d10e9d

Please sign in to comment.