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"
2022-10-07 09:05:35 +00:00
"io"
2021-02-02 13:15:13 +00:00
"math/big"
2022-09-06 11:55:03 +00:00
"os"
2023-10-04 11:01:57 +00:00
"slices"
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"
"gopkg.in/yaml.v2"
2022-10-07 09:05:35 +00:00
"github.com/crowdsecurity/machineid"
"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"
2023-07-27 15:02:20 +00:00
"github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/require"
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 )
2022-06-06 16:20:10 +00:00
bId , err := uuid . NewRandom ( )
2022-03-24 10:07:54 +00:00
if err == nil {
2022-06-06 16:20:10 +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
}
2022-10-07 09:05:35 +00:00
func getAgents ( out io . Writer , dbClient * database . Client ) error {
2022-08-18 09:54:01 +00:00
machines , err := dbClient . ListMachines ( )
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
}
if csConfig . Cscli . Output == "human" {
2022-10-07 09:05:35 +00:00
getAgentsTable ( out , machines )
2022-08-18 09:54:01 +00:00
} else if csConfig . Cscli . Output == "json" {
2022-10-07 09:05:35 +00:00
enc := json . NewEncoder ( out )
enc . SetIndent ( "" , " " )
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
}
2022-10-07 09:05:35 +00:00
return nil
2022-08-18 09:54:01 +00:00
} else if csConfig . Cscli . Output == "raw" {
2022-10-07 09:05:35 +00:00
csvwriter := csv . NewWriter ( out )
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
}
2022-10-07 09:05:35 +00:00
for _ , m := range machines {
2022-08-18 09:54:01 +00:00
var validated string
2022-10-07 09:05:35 +00:00
if m . IsValidated {
2022-08-18 09:54:01 +00:00
validated = "true"
} else {
validated = "false"
}
2023-06-08 13:08:51 +00:00
hb , _ := getLastHeartbeat ( m )
err := csvwriter . Write ( [ ] string { m . MachineId , m . IpAddress , m . UpdatedAt . Format ( time . RFC3339 ) , validated , m . Version , m . AuthType , hb } )
2022-08-18 09:54:01 +00:00
if 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
}
}
csvwriter . Flush ( )
} else {
log . Errorf ( "unknown output '%s'" , csConfig . Cscli . Output )
}
2022-10-07 09:05:35 +00:00
return nil
2022-08-18 09:54:01 +00:00
}
2023-12-07 13:36:35 +00:00
type cliMachines struct { }
func NewCLIMachines ( ) * cliMachines {
return & cliMachines { }
}
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 ( cmd * cobra . Command , args [ ] string ) error {
var err error
if err := require . LAPI ( csConfig ) ; err != nil {
return err
}
dbClient , err = database . NewClient ( csConfig . 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 ) NewListCmd ( ) * cobra . Command {
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-02-20 14:05:42 +00:00
RunE : func ( cmd * cobra . Command , args [ ] string ) error {
2022-10-13 10:28:24 +00:00
err := getAgents ( color . Output , dbClient )
2020-11-30 09:37:17 +00:00
if err != nil {
2023-02-20 14:05:42 +00:00
return fmt . Errorf ( "unable to list machines: %s" , err )
2020-11-30 09:37:17 +00:00
}
2023-02-20 14:05:42 +00:00
return nil
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
}
2023-12-07 13:36:35 +00:00
func ( cli cliMachines ) NewAddCmd ( ) * cobra . Command {
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. ` ,
2020-11-30 09:37:17 +00:00
Example : `
cscli machines add -- auto
cscli machines add MyTestMachine -- auto
cscli machines add MyTestMachine -- password MyPassword
` ,
2023-12-07 13:36:35 +00:00
RunE : cli . add ,
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 ( )
2022-12-30 09:13:52 +00:00
flags . StringP ( "password" , "p" , "" , "machine password to login to the API" )
2023-03-03 10:06:27 +00:00
flags . StringP ( "file" , "f" , "" , "output file destination (defaults to " + csconfig . DefaultConfigPath ( "local_api_credentials.yaml" ) + ")" )
2022-12-30 09:13:52 +00:00
flags . StringP ( "url" , "u" , "" , "URL of the local API" )
flags . BoolP ( "interactive" , "i" , false , "interfactive mode to enter the password" )
flags . BoolP ( "auto" , "a" , false , "automatically generate password (and username if not provided)" )
flags . Bool ( "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
2023-12-07 13:36:35 +00:00
func ( cli cliMachines ) add ( cmd * cobra . Command , args [ ] string ) error {
2022-12-30 09:13:52 +00:00
flags := cmd . Flags ( )
machinePassword , err := flags . GetString ( "password" )
if err != nil {
return err
}
2023-12-04 21:59:52 +00:00
dumpFile , err := flags . GetString ( "file" )
2022-12-30 09:13:52 +00:00
if err != nil {
return err
}
apiURL , err := flags . GetString ( "url" )
if err != nil {
return err
}
interactive , err := flags . GetBool ( "interactive" )
if err != nil {
return err
}
autoAdd , err := flags . GetBool ( "auto" )
if err != nil {
return err
}
2023-12-04 21:59:52 +00:00
force , err := flags . GetBool ( "force" )
2022-12-30 09:13:52 +00:00
if err != nil {
return err
}
var machineID string
// create machineID if not specified by user
if len ( args ) == 0 {
if ! autoAdd {
printHelp ( cmd )
return nil
}
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 ]
}
/*check if file already exists*/
2023-12-04 21:59:52 +00:00
if dumpFile == "" && csConfig . API . Client != nil && csConfig . API . Client . CredentialsFilePath != "" {
credFile := csConfig . API . Client . CredentialsFilePath
// use the default only if the file does not exist
_ , err := os . Stat ( credFile )
switch {
case os . IsNotExist ( err ) || force :
dumpFile = csConfig . API . Client . CredentialsFilePath
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 {
Message : "Please provide a password for the machine" ,
}
survey . AskOne ( qs , & machinePassword )
}
password := strfmt . Password ( machinePassword )
2023-12-04 21:59:52 +00:00
_ , err = dbClient . 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
}
log . Infof ( "Machine '%s' successfully added to the local API" , machineID )
if apiURL == "" {
if csConfig . API . Client != nil && csConfig . API . Client . Credentials != nil && csConfig . API . Client . Credentials . URL != "" {
apiURL = csConfig . API . Client . Credentials . URL
} else if csConfig . API . Server != nil && csConfig . API . Server . ListenURI != "" {
apiURL = "http://" + csConfig . API . Server . ListenURI
} 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
}
}
apiCfg := csconfig . ApiCredentialsCfg {
Login : machineID ,
Password : password . String ( ) ,
URL : apiURL ,
2020-11-30 09:37:17 +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
}
if dumpFile != "" && dumpFile != "-" {
err = os . WriteFile ( dumpFile , apiConfigDump , 0644 )
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
}
2023-12-04 21:59:52 +00:00
log . Printf ( "API credentials written to '%s'" , dumpFile )
2022-12-30 09:13:52 +00:00
} else {
fmt . Printf ( "%s\n" , string ( apiConfigDump ) )
}
return nil
}
2023-12-07 13:36:35 +00:00
func ( cli cliMachines ) NewDeleteCmd ( ) * cobra . Command {
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 ,
2022-12-05 14:39:54 +00:00
ValidArgsFunction : func ( cmd * cobra . Command , args [ ] string , toComplete string ) ( [ ] string , cobra . ShellCompDirective ) {
machines , err := dbClient . ListMachines ( )
if err != nil {
cobra . CompError ( "unable to list machines " + err . Error ( ) )
}
ret := make ( [ ] string , 0 )
for _ , machine := range machines {
2023-05-31 10:39:22 +00:00
if strings . Contains ( machine . MachineId , toComplete ) && ! slices . Contains ( args , machine . MachineId ) {
2022-12-05 14:39:54 +00:00
ret = append ( ret , machine . MachineId )
}
}
return ret , cobra . ShellCompDirectiveNoFileComp
} ,
2023-12-07 13:36:35 +00:00
RunE : cli . delete ,
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
}
2023-12-07 13:36:35 +00:00
func ( cli cliMachines ) delete ( cmd * cobra . Command , args [ ] string ) error {
2022-12-30 09:13:52 +00:00
for _ , machineID := range args {
err := dbClient . 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
}
2023-12-07 13:36:35 +00:00
func ( cli cliMachines ) NewPruneCmd ( ) * cobra . Command {
2023-07-28 14:23:47 +00:00
var parsedDuration time . Duration
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 ,
PreRunE : func ( cmd * cobra . Command , args [ ] string ) error {
dur , _ := cmd . Flags ( ) . GetString ( "duration" )
var err error
parsedDuration , err = time . ParseDuration ( fmt . Sprintf ( "-%s" , dur ) )
if err != nil {
return fmt . Errorf ( "unable to parse duration '%s': %s" , dur , err )
}
return nil
} ,
RunE : func ( cmd * cobra . Command , args [ ] string ) error {
notValidOnly , _ := cmd . Flags ( ) . GetBool ( "not-validated-only" )
force , _ := cmd . Flags ( ) . GetBool ( "force" )
if parsedDuration >= 0 - 60 * time . Second && ! notValidOnly {
var answer bool
prompt := & survey . Confirm {
Message : "The duration you provided is less than or equal 60 seconds this can break installations do you want to continue ?" ,
Default : false ,
}
if err := survey . AskOne ( prompt , & answer ) ; err != nil {
return fmt . Errorf ( "unable to ask about prune check: %s" , err )
}
if ! answer {
fmt . Println ( "user aborted prune no changes were made" )
return nil
}
}
machines := make ( [ ] * ent . Machine , 0 )
if pending , err := dbClient . QueryPendingMachine ( ) ; err == nil {
machines = append ( machines , pending ... )
}
if ! notValidOnly {
if pending , err := dbClient . QueryLastValidatedHeartbeatLT ( time . Now ( ) . UTC ( ) . Add ( parsedDuration ) ) ; err == nil {
machines = append ( machines , pending ... )
}
}
if len ( machines ) == 0 {
fmt . Println ( "no machines to prune" )
return nil
}
getAgentsTable ( color . Output , machines )
if ! force {
var answer bool
prompt := & survey . Confirm {
Message : "You are about to PERMANENTLY remove the above machines from the database these will NOT be recoverable, continue ?" ,
Default : false ,
}
if err := survey . AskOne ( prompt , & answer ) ; err != nil {
return fmt . Errorf ( "unable to ask about prune check: %s" , err )
}
if ! answer {
fmt . Println ( "user aborted prune no changes were made" )
return nil
}
}
nbDeleted , err := dbClient . BulkDeleteWatchers ( machines )
if err != nil {
return fmt . Errorf ( "unable to prune machines: %s" , err )
}
fmt . Printf ( "successfully delete %d machines\n" , nbDeleted )
return nil
} ,
}
2023-12-07 13:36:35 +00:00
cmd . Flags ( ) . StringP ( "duration" , "d" , "10m" , "duration of time since validated machine last heartbeat" )
cmd . Flags ( ) . Bool ( "not-validated-only" , false , "only prune machines that are not validated" )
cmd . Flags ( ) . Bool ( "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
}
2023-12-07 13:36:35 +00:00
func ( cli cliMachines ) NewValidateCmd ( ) * cobra . Command {
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 ,
2023-02-20 14:05:42 +00:00
RunE : func ( cmd * cobra . Command , args [ ] string ) error {
2022-12-30 09:13:52 +00:00
machineID := args [ 0 ]
2020-11-30 09:37:17 +00:00
if err := dbClient . ValidateMachine ( machineID ) ; err != nil {
2023-02-20 14:05:42 +00:00
return fmt . Errorf ( "unable to validate machine '%s': %s" , machineID , err )
2020-11-30 09:37:17 +00:00
}
2021-12-06 16:29:23 +00:00
log . Infof ( "machine '%s' validated successfully" , machineID )
2023-02-20 14:05:42 +00:00
return nil
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
}