2020-11-30 09:37:17 +00:00
package main
import (
2021-02-02 13:15:13 +00:00
saferand "crypto/rand"
2021-12-29 13:08:47 +00:00
"encoding/csv"
2020-11-30 09:37:17 +00:00
"encoding/json"
"fmt"
2021-02-02 13:15:13 +00:00
"math/big"
2022-09-06 11:55:03 +00:00
"os"
2020-11-30 09:37:17 +00:00
"strings"
"time"
"github.com/AlecAivazis/survey/v2"
2022-10-13 10:28:24 +00:00
"github.com/fatih/color"
2020-11-30 09:37:17 +00:00
"github.com/go-openapi/strfmt"
2022-06-06 16:20:10 +00:00
"github.com/google/uuid"
2020-11-30 09:37:17 +00:00
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
2023-12-13 14:43:46 +00:00
"gopkg.in/yaml.v3"
"slices"
2022-10-07 09:05:35 +00:00
"github.com/crowdsecurity/machineid"
2023-12-13 14:43:46 +00:00
"github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/require"
2022-10-07 09:05:35 +00:00
"github.com/crowdsecurity/crowdsec/pkg/csconfig"
"github.com/crowdsecurity/crowdsec/pkg/database"
"github.com/crowdsecurity/crowdsec/pkg/database/ent"
"github.com/crowdsecurity/crowdsec/pkg/types"
2020-11-30 09:37:17 +00:00
)
2023-12-04 22:06:01 +00:00
const passwordLength = 64
2020-11-30 09:37:17 +00:00
func generatePassword ( length int ) string {
2023-05-31 10:39:22 +00:00
upper := "ABCDEFGHIJKLMNOPQRSTUVWXY"
lower := "abcdefghijklmnopqrstuvwxyz"
2022-12-30 09:13:52 +00:00
digits := "0123456789"
2020-11-30 09:37:17 +00:00
charset := upper + lower + digits
2021-02-02 13:15:13 +00:00
charsetLength := len ( charset )
2020-11-30 09:37:17 +00:00
buf := make ( [ ] byte , length )
2023-12-04 21:59:52 +00:00
2021-02-02 13:15:13 +00:00
for i := 0 ; i < length ; i ++ {
rInt , err := saferand . Int ( saferand . Reader , big . NewInt ( int64 ( charsetLength ) ) )
if err != nil {
log . Fatalf ( "failed getting data from prng for password generation : %s" , err )
}
buf [ i ] = charset [ rInt . Int64 ( ) ]
2020-11-30 09:37:17 +00:00
}
return string ( buf )
}
2022-03-24 10:07:54 +00:00
// Returns a unique identifier for each crowdsec installation, using an
// identifier of the OS installation where available, otherwise a random
// string.
func generateIDPrefix ( ) ( string , error ) {
prefix , err := machineid . ID ( )
if err == nil {
return prefix , nil
2020-11-30 09:37:17 +00:00
}
2022-03-24 10:07:54 +00:00
log . Debugf ( "failed to get machine-id with usual files: %s" , err )
2023-12-13 14:43:46 +00:00
bID , err := uuid . NewRandom ( )
2022-03-24 10:07:54 +00:00
if err == nil {
2023-12-13 14:43:46 +00:00
return bID . String ( ) , nil
2022-03-24 10:07:54 +00:00
}
2023-02-20 14:05:42 +00:00
return "" , fmt . Errorf ( "generating machine id: %w" , err )
2022-03-24 10:07:54 +00:00
}
// Generate a unique identifier, composed by a prefix and a random suffix.
// The prefix can be provided by a parameter to use in test environments.
func generateID ( prefix string ) ( string , error ) {
var err error
if prefix == "" {
prefix , err = generateIDPrefix ( )
}
if err != nil {
return "" , err
2020-11-30 09:37:17 +00:00
}
2022-03-24 10:07:54 +00:00
prefix = strings . ReplaceAll ( prefix , "-" , "" ) [ : 32 ]
suffix := generatePassword ( 16 )
return prefix + suffix , nil
2020-11-30 09:37:17 +00:00
}
2023-06-08 13:08:51 +00:00
// getLastHeartbeat returns the last heartbeat timestamp of a machine
// and a boolean indicating if the machine is considered active or not.
func getLastHeartbeat ( m * ent . Machine ) ( string , bool ) {
if m . LastHeartbeat == nil {
return "-" , false
}
2022-07-07 13:29:30 +00:00
2023-06-08 13:08:51 +00:00
elapsed := time . Now ( ) . UTC ( ) . Sub ( * m . LastHeartbeat )
hb := elapsed . Truncate ( time . Second ) . String ( )
if elapsed > 2 * time . Minute {
return hb , false
2022-07-07 13:29:30 +00:00
}
2023-06-08 13:08:51 +00:00
return hb , true
2022-07-07 13:29:30 +00:00
}
2024-02-01 16:22:52 +00:00
type cliMachines struct {
db * database . Client
cfg configGetter
}
func NewCLIMachines ( getconfig configGetter ) * cliMachines {
return & cliMachines {
cfg : getconfig ,
}
}
func ( cli * cliMachines ) NewCommand ( ) * cobra . Command {
cmd := & cobra . Command {
Use : "machines [action]" ,
Short : "Manage local API machines [requires local API]" ,
Long : ` To list / add / delete / validate / prune machines .
Note : This command requires database direct access , so is intended to be run on the local API machine .
` ,
Example : ` cscli machines [action] ` ,
DisableAutoGenTag : true ,
Aliases : [ ] string { "machine" } ,
PersistentPreRunE : func ( _ * cobra . Command , _ [ ] string ) error {
var err error
if err = require . LAPI ( cli . cfg ( ) ) ; err != nil {
return err
}
cli . db , err = database . NewClient ( cli . cfg ( ) . DbConfig )
if err != nil {
return fmt . Errorf ( "unable to create new database client: %s" , err )
}
return nil
} ,
}
cmd . AddCommand ( cli . newListCmd ( ) )
cmd . AddCommand ( cli . newAddCmd ( ) )
cmd . AddCommand ( cli . newDeleteCmd ( ) )
cmd . AddCommand ( cli . newValidateCmd ( ) )
cmd . AddCommand ( cli . newPruneCmd ( ) )
return cmd
}
func ( cli * cliMachines ) list ( ) error {
out := color . Output
machines , err := cli . db . ListMachines ( )
2022-08-18 09:54:01 +00:00
if err != nil {
2022-10-07 09:05:35 +00:00
return fmt . Errorf ( "unable to list machines: %s" , err )
2022-08-18 09:54:01 +00:00
}
2023-12-13 14:43:46 +00:00
2024-02-01 16:22:52 +00:00
switch cli . cfg ( ) . Cscli . Output {
2023-12-13 14:43:46 +00:00
case "human" :
2022-10-07 09:05:35 +00:00
getAgentsTable ( out , machines )
2023-12-13 14:43:46 +00:00
case "json" :
2022-10-07 09:05:35 +00:00
enc := json . NewEncoder ( out )
enc . SetIndent ( "" , " " )
2024-02-01 16:22:52 +00:00
2022-10-07 09:05:35 +00:00
if err := enc . Encode ( machines ) ; err != nil {
2023-02-20 14:05:42 +00:00
return fmt . Errorf ( "failed to marshal" )
2022-08-18 09:54:01 +00:00
}
2024-02-01 16:22:52 +00:00
2022-10-07 09:05:35 +00:00
return nil
2023-12-13 14:43:46 +00:00
case "raw" :
2022-10-07 09:05:35 +00:00
csvwriter := csv . NewWriter ( out )
2024-02-01 16:22:52 +00:00
2022-08-18 09:54:01 +00:00
err := csvwriter . Write ( [ ] string { "machine_id" , "ip_address" , "updated_at" , "validated" , "version" , "auth_type" , "last_heartbeat" } )
if err != nil {
2023-02-20 14:05:42 +00:00
return fmt . Errorf ( "failed to write header: %s" , err )
2022-08-18 09:54:01 +00:00
}
2024-02-01 16:22:52 +00:00
2022-10-07 09:05:35 +00:00
for _ , m := range machines {
2023-12-13 14:43:46 +00:00
validated := "false"
2022-10-07 09:05:35 +00:00
if m . IsValidated {
2022-08-18 09:54:01 +00:00
validated = "true"
}
2024-02-01 16:22:52 +00:00
2023-06-08 13:08:51 +00:00
hb , _ := getLastHeartbeat ( m )
2024-02-01 16:22:52 +00:00
if err := csvwriter . Write ( [ ] string { m . MachineId , m . IpAddress , m . UpdatedAt . Format ( time . RFC3339 ) , validated , m . Version , m . AuthType , hb } ) ; err != nil {
2023-06-08 13:08:51 +00:00
return fmt . Errorf ( "failed to write raw output: %w" , err )
2022-08-18 09:54:01 +00:00
}
}
2023-12-07 13:36:35 +00:00
2024-02-01 16:22:52 +00:00
csvwriter . Flush ( )
2023-12-07 13:36:35 +00:00
}
2024-02-01 16:22:52 +00:00
return nil
2023-12-07 13:36:35 +00:00
}
2024-02-01 16:22:52 +00:00
func ( cli * cliMachines ) newListCmd ( ) * cobra . Command {
2023-12-07 13:36:35 +00:00
cmd := & cobra . Command {
2021-08-31 13:03:47 +00:00
Use : "list" ,
2023-07-28 14:23:47 +00:00
Short : "list all machines in the database" ,
Long : ` list all machines in the database with their status and last heartbeat ` ,
2021-08-31 13:03:47 +00:00
Example : ` cscli machines list ` ,
2023-07-28 14:23:47 +00:00
Args : cobra . NoArgs ,
2021-08-31 13:03:47 +00:00
DisableAutoGenTag : true ,
2023-12-13 14:43:46 +00:00
RunE : func ( _ * cobra . Command , _ [ ] string ) error {
2024-02-01 16:22:52 +00:00
return cli . list ( )
2020-11-30 09:37:17 +00:00
} ,
}
2023-12-07 13:36:35 +00:00
return cmd
2022-12-30 09:13:52 +00:00
}
2024-02-01 16:22:52 +00:00
func ( cli * cliMachines ) newAddCmd ( ) * cobra . Command {
var (
password MachinePassword
dumpFile string
apiURL string
interactive bool
autoAdd bool
force bool
)
2023-12-07 13:36:35 +00:00
cmd := & cobra . Command {
2021-08-31 13:03:47 +00:00
Use : "add" ,
2023-07-28 14:23:47 +00:00
Short : "add a single machine to the database" ,
2021-08-31 13:03:47 +00:00
DisableAutoGenTag : true ,
Long : ` Register a new machine in the database. cscli should be on the same machine as LAPI. ` ,
2024-02-01 16:22:52 +00:00
Example : ` cscli machines add -- auto
2020-11-30 09:37:17 +00:00
cscli machines add MyTestMachine -- auto
cscli machines add MyTestMachine -- password MyPassword
2024-02-01 16:22:52 +00:00
cscli machines add - f - -- auto > / tmp / mycreds . yaml ` ,
RunE : func ( _ * cobra . Command , args [ ] string ) error {
return cli . add ( args , string ( password ) , dumpFile , apiURL , interactive , autoAdd , force )
} ,
2022-12-30 09:13:52 +00:00
}
2020-11-30 09:37:17 +00:00
2023-12-07 13:36:35 +00:00
flags := cmd . Flags ( )
2024-02-01 16:22:52 +00:00
flags . VarP ( & password , "password" , "p" , "machine password to login to the API" )
flags . StringVarP ( & dumpFile , "file" , "f" , "" , "output file destination (defaults to " + csconfig . DefaultConfigPath ( "local_api_credentials.yaml" ) + ")" )
flags . StringVarP ( & apiURL , "url" , "u" , "" , "URL of the local API" )
flags . BoolVarP ( & interactive , "interactive" , "i" , false , "interfactive mode to enter the password" )
flags . BoolVarP ( & autoAdd , "auto" , "a" , false , "automatically generate password (and username if not provided)" )
flags . BoolVar ( & force , "force" , false , "will force add the machine if it already exist" )
2020-11-30 09:37:17 +00:00
2023-12-07 13:36:35 +00:00
return cmd
2022-12-30 09:13:52 +00:00
}
2020-11-30 09:37:17 +00:00
2024-02-01 16:22:52 +00:00
func ( cli * cliMachines ) add ( args [ ] string , machinePassword string , dumpFile string , apiURL string , interactive bool , autoAdd bool , force bool ) error {
var (
err error
machineID string
)
2022-12-30 09:13:52 +00:00
// create machineID if not specified by user
if len ( args ) == 0 {
if ! autoAdd {
2024-02-01 16:22:52 +00:00
return fmt . Errorf ( "please specify a machine name to add, or use --auto" )
2022-12-30 09:13:52 +00:00
}
2024-02-01 16:22:52 +00:00
2022-12-30 09:13:52 +00:00
machineID , err = generateID ( "" )
if err != nil {
2023-02-20 14:05:42 +00:00
return fmt . Errorf ( "unable to generate machine id: %s" , err )
2022-12-30 09:13:52 +00:00
}
} else {
machineID = args [ 0 ]
}
2024-02-01 16:22:52 +00:00
clientCfg := cli . cfg ( ) . API . Client
serverCfg := cli . cfg ( ) . API . Server
2022-12-30 09:13:52 +00:00
/*check if file already exists*/
2024-02-01 16:22:52 +00:00
if dumpFile == "" && clientCfg != nil && clientCfg . CredentialsFilePath != "" {
credFile := clientCfg . CredentialsFilePath
2023-12-04 21:59:52 +00:00
// use the default only if the file does not exist
2023-12-13 14:43:46 +00:00
_ , err = os . Stat ( credFile )
2023-12-04 21:59:52 +00:00
switch {
case os . IsNotExist ( err ) || force :
2024-02-01 16:22:52 +00:00
dumpFile = credFile
2023-12-04 21:59:52 +00:00
case err != nil :
return fmt . Errorf ( "unable to stat '%s': %s" , credFile , err )
default :
return fmt . Errorf ( ` credentials file '%s' already exists: please remove it, use "--force" or specify a different file with "-f" ("-f -" for standard output) ` , credFile )
}
}
if dumpFile == "" {
return fmt . Errorf ( ` please specify a file to dump credentials to, with -f ("-f -" for standard output) ` )
2022-12-30 09:13:52 +00:00
}
// create a password if it's not specified by user
if machinePassword == "" && ! interactive {
if ! autoAdd {
2023-12-04 21:59:52 +00:00
return fmt . Errorf ( "please specify a password with --password or use --auto" )
2022-12-30 09:13:52 +00:00
}
machinePassword = generatePassword ( passwordLength )
} else if machinePassword == "" && interactive {
qs := & survey . Password {
2024-02-01 16:22:52 +00:00
Message : "Please provide a password for the machine:" ,
2022-12-30 09:13:52 +00:00
}
survey . AskOne ( qs , & machinePassword )
}
2024-02-01 16:22:52 +00:00
2022-12-30 09:13:52 +00:00
password := strfmt . Password ( machinePassword )
2024-02-01 16:22:52 +00:00
_ , err = cli . db . CreateMachine ( & machineID , & password , "" , true , force , types . PasswordAuthType )
2022-12-30 09:13:52 +00:00
if err != nil {
2023-02-20 14:05:42 +00:00
return fmt . Errorf ( "unable to create machine: %s" , err )
2022-12-30 09:13:52 +00:00
}
2024-02-01 16:22:52 +00:00
fmt . Fprintf ( os . Stderr , "Machine '%s' successfully added to the local API.\n" , machineID )
2022-12-30 09:13:52 +00:00
if apiURL == "" {
2024-02-01 16:22:52 +00:00
if clientCfg != nil && clientCfg . Credentials != nil && clientCfg . Credentials . URL != "" {
apiURL = clientCfg . Credentials . URL
} else if serverCfg != nil && serverCfg . ListenURI != "" {
apiURL = "http://" + serverCfg . ListenURI
2022-12-30 09:13:52 +00:00
} else {
2023-02-20 14:05:42 +00:00
return fmt . Errorf ( "unable to dump an api URL. Please provide it in your configuration or with the -u parameter" )
2022-12-30 09:13:52 +00:00
}
}
2024-02-01 16:22:52 +00:00
2022-12-30 09:13:52 +00:00
apiCfg := csconfig . ApiCredentialsCfg {
Login : machineID ,
Password : password . String ( ) ,
URL : apiURL ,
2020-11-30 09:37:17 +00:00
}
2024-02-01 16:22:52 +00:00
2022-12-30 09:13:52 +00:00
apiConfigDump , err := yaml . Marshal ( apiCfg )
if err != nil {
2023-02-20 14:05:42 +00:00
return fmt . Errorf ( "unable to marshal api credentials: %s" , err )
2022-12-30 09:13:52 +00:00
}
2024-02-01 16:22:52 +00:00
2022-12-30 09:13:52 +00:00
if dumpFile != "" && dumpFile != "-" {
2023-12-08 09:51:15 +00:00
err = os . WriteFile ( dumpFile , apiConfigDump , 0 o600 )
2022-12-30 09:13:52 +00:00
if err != nil {
2023-02-20 14:05:42 +00:00
return fmt . Errorf ( "write api credentials in '%s' failed: %s" , dumpFile , err )
2022-12-30 09:13:52 +00:00
}
2024-02-01 16:22:52 +00:00
fmt . Fprintf ( os . Stderr , "API credentials written to '%s'.\n" , dumpFile )
2022-12-30 09:13:52 +00:00
} else {
2024-02-01 16:22:52 +00:00
fmt . Print ( string ( apiConfigDump ) )
2022-12-30 09:13:52 +00:00
}
return nil
}
2024-02-01 16:22:52 +00:00
func ( cli * cliMachines ) deleteValid ( cmd * cobra . Command , args [ ] string , toComplete string ) ( [ ] string , cobra . ShellCompDirective ) {
machines , err := cli . db . ListMachines ( )
if err != nil {
cobra . CompError ( "unable to list machines " + err . Error ( ) )
}
ret := [ ] string { }
for _ , machine := range machines {
if strings . Contains ( machine . MachineId , toComplete ) && ! slices . Contains ( args , machine . MachineId ) {
ret = append ( ret , machine . MachineId )
}
}
return ret , cobra . ShellCompDirectiveNoFileComp
}
func ( cli * cliMachines ) delete ( machines [ ] string ) error {
for _ , machineID := range machines {
err := cli . db . DeleteWatcher ( machineID )
if err != nil {
log . Errorf ( "unable to delete machine '%s': %s" , machineID , err )
return nil
}
log . Infof ( "machine '%s' deleted successfully" , machineID )
}
return nil
}
func ( cli * cliMachines ) newDeleteCmd ( ) * cobra . Command {
2023-12-07 13:36:35 +00:00
cmd := & cobra . Command {
2022-12-23 16:13:20 +00:00
Use : "delete [machine_name]..." ,
2023-07-28 14:23:47 +00:00
Short : "delete machine(s) by name" ,
2022-12-23 16:13:20 +00:00
Example : ` cscli machines delete "machine1" "machine2" ` ,
2021-09-02 10:23:06 +00:00
Args : cobra . MinimumNArgs ( 1 ) ,
2022-04-20 13:44:48 +00:00
Aliases : [ ] string { "remove" } ,
2021-08-31 13:03:47 +00:00
DisableAutoGenTag : true ,
2024-02-01 16:22:52 +00:00
ValidArgsFunction : cli . deleteValid ,
RunE : func ( _ * cobra . Command , args [ ] string ) error {
return cli . delete ( args )
2022-12-05 14:39:54 +00:00
} ,
2020-11-30 09:37:17 +00:00
}
2023-12-07 13:36:35 +00:00
return cmd
2022-12-30 09:13:52 +00:00
}
2024-02-01 16:22:52 +00:00
func ( cli * cliMachines ) prune ( duration time . Duration , notValidOnly bool , force bool ) error {
if duration < 2 * time . Minute && ! notValidOnly {
if yes , err := askYesNo (
"The duration you provided is less than 2 minutes. " +
"This can break installations if the machines are only temporarily disconnected. Continue?" , false ) ; err != nil {
return err
} else if ! yes {
fmt . Println ( "User aborted prune. No changes were made." )
2022-12-30 09:13:52 +00:00
return nil
}
}
2024-02-01 16:22:52 +00:00
machines := [ ] * ent . Machine { }
if pending , err := cli . db . QueryPendingMachine ( ) ; err == nil {
machines = append ( machines , pending ... )
}
if ! notValidOnly {
if pending , err := cli . db . QueryLastValidatedHeartbeatLT ( time . Now ( ) . UTC ( ) . Add ( duration ) ) ; err == nil {
machines = append ( machines , pending ... )
}
}
if len ( machines ) == 0 {
fmt . Println ( "no machines to prune" )
return nil
}
getAgentsTable ( color . Output , machines )
if ! force {
if yes , err := askYesNo (
"You are about to PERMANENTLY remove the above machines from the database. " +
"These will NOT be recoverable. Continue?" , false ) ; err != nil {
return err
} else if ! yes {
fmt . Println ( "User aborted prune. No changes were made." )
return nil
}
}
deleted , err := cli . db . BulkDeleteWatchers ( machines )
if err != nil {
return fmt . Errorf ( "unable to prune machines: %s" , err )
}
fmt . Fprintf ( os . Stderr , "successfully delete %d machines\n" , deleted )
2022-12-30 09:13:52 +00:00
return nil
}
2024-02-01 16:22:52 +00:00
func ( cli * cliMachines ) newPruneCmd ( ) * cobra . Command {
var (
duration time . Duration
notValidOnly bool
force bool
)
const defaultDuration = 10 * time . Minute
2023-12-07 13:36:35 +00:00
cmd := & cobra . Command {
2023-07-28 14:23:47 +00:00
Use : "prune" ,
Short : "prune multiple machines from the database" ,
Long : ` prune multiple machines that are not validated or have not connected to the local API in a given duration. ` ,
Example : ` cscli machines prune
cscli machines prune -- duration 1 h
cscli machines prune -- not - validated - only -- force ` ,
Args : cobra . NoArgs ,
DisableAutoGenTag : true ,
2024-02-01 16:22:52 +00:00
RunE : func ( _ * cobra . Command , _ [ ] string ) error {
return cli . prune ( duration , notValidOnly , force )
2023-07-28 14:23:47 +00:00
} ,
}
2024-02-01 16:22:52 +00:00
flags := cmd . Flags ( )
flags . DurationVarP ( & duration , "duration" , "d" , defaultDuration , "duration of time since validated machine last heartbeat" )
flags . BoolVar ( & notValidOnly , "not-validated-only" , false , "only prune machines that are not validated" )
flags . BoolVar ( & force , "force" , false , "force prune without asking for confirmation" )
2023-07-28 14:23:47 +00:00
2023-12-07 13:36:35 +00:00
return cmd
2023-07-28 14:23:47 +00:00
}
2024-02-01 16:22:52 +00:00
func ( cli * cliMachines ) validate ( machineID string ) error {
if err := cli . db . ValidateMachine ( machineID ) ; err != nil {
return fmt . Errorf ( "unable to validate machine '%s': %s" , machineID , err )
}
log . Infof ( "machine '%s' validated successfully" , machineID )
return nil
}
func ( cli * cliMachines ) newValidateCmd ( ) * cobra . Command {
2023-12-07 13:36:35 +00:00
cmd := & cobra . Command {
2021-08-31 13:03:47 +00:00
Use : "validate" ,
Short : "validate a machine to access the local API" ,
Long : ` validate a machine to access the local API. ` ,
Example : ` cscli machines validate "machine_name" ` ,
Args : cobra . ExactArgs ( 1 ) ,
DisableAutoGenTag : true ,
2024-02-01 16:22:52 +00:00
RunE : func ( cmd * cobra . Command , args [ ] string ) error {
return cli . validate ( args [ 0 ] )
2020-11-30 09:37:17 +00:00
} ,
}
2022-12-30 09:13:52 +00:00
2023-12-07 13:36:35 +00:00
return cmd
2020-11-30 09:37:17 +00:00
}