machines.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431
  1. package main
  2. import (
  3. saferand "crypto/rand"
  4. "encoding/csv"
  5. "encoding/json"
  6. "fmt"
  7. "io"
  8. "math/big"
  9. "os"
  10. "strings"
  11. "time"
  12. "github.com/AlecAivazis/survey/v2"
  13. "github.com/fatih/color"
  14. "github.com/go-openapi/strfmt"
  15. "github.com/google/uuid"
  16. log "github.com/sirupsen/logrus"
  17. "github.com/spf13/cobra"
  18. "golang.org/x/exp/slices"
  19. "gopkg.in/yaml.v2"
  20. "github.com/crowdsecurity/machineid"
  21. "github.com/crowdsecurity/crowdsec/pkg/csconfig"
  22. "github.com/crowdsecurity/crowdsec/pkg/database"
  23. "github.com/crowdsecurity/crowdsec/pkg/database/ent"
  24. "github.com/crowdsecurity/crowdsec/pkg/types"
  25. )
  26. var (
  27. passwordLength = 64
  28. )
  29. func generatePassword(length int) string {
  30. upper := "ABCDEFGHIJKLMNOPQRSTUVWXY"
  31. lower := "abcdefghijklmnopqrstuvwxyz"
  32. digits := "0123456789"
  33. charset := upper + lower + digits
  34. charsetLength := len(charset)
  35. buf := make([]byte, length)
  36. for i := 0; i < length; i++ {
  37. rInt, err := saferand.Int(saferand.Reader, big.NewInt(int64(charsetLength)))
  38. if err != nil {
  39. log.Fatalf("failed getting data from prng for password generation : %s", err)
  40. }
  41. buf[i] = charset[rInt.Int64()]
  42. }
  43. return string(buf)
  44. }
  45. // Returns a unique identifier for each crowdsec installation, using an
  46. // identifier of the OS installation where available, otherwise a random
  47. // string.
  48. func generateIDPrefix() (string, error) {
  49. prefix, err := machineid.ID()
  50. if err == nil {
  51. return prefix, nil
  52. }
  53. log.Debugf("failed to get machine-id with usual files: %s", err)
  54. bId, err := uuid.NewRandom()
  55. if err == nil {
  56. return bId.String(), nil
  57. }
  58. return "", fmt.Errorf("generating machine id: %w", err)
  59. }
  60. // Generate a unique identifier, composed by a prefix and a random suffix.
  61. // The prefix can be provided by a parameter to use in test environments.
  62. func generateID(prefix string) (string, error) {
  63. var err error
  64. if prefix == "" {
  65. prefix, err = generateIDPrefix()
  66. }
  67. if err != nil {
  68. return "", err
  69. }
  70. prefix = strings.ReplaceAll(prefix, "-", "")[:32]
  71. suffix := generatePassword(16)
  72. return prefix + suffix, nil
  73. }
  74. // getLastHeartbeat returns the last heartbeat timestamp of a machine
  75. // and a boolean indicating if the machine is considered active or not.
  76. func getLastHeartbeat(m *ent.Machine) (string, bool) {
  77. if m.LastHeartbeat == nil {
  78. return "-", false
  79. }
  80. elapsed := time.Now().UTC().Sub(*m.LastHeartbeat)
  81. hb := elapsed.Truncate(time.Second).String()
  82. if elapsed > 2*time.Minute {
  83. return hb, false
  84. }
  85. return hb, true
  86. }
  87. func getAgents(out io.Writer, dbClient *database.Client) error {
  88. machines, err := dbClient.ListMachines()
  89. if err != nil {
  90. return fmt.Errorf("unable to list machines: %s", err)
  91. }
  92. if csConfig.Cscli.Output == "human" {
  93. getAgentsTable(out, machines)
  94. } else if csConfig.Cscli.Output == "json" {
  95. enc := json.NewEncoder(out)
  96. enc.SetIndent("", " ")
  97. if err := enc.Encode(machines); err != nil {
  98. return fmt.Errorf("failed to marshal")
  99. }
  100. return nil
  101. } else if csConfig.Cscli.Output == "raw" {
  102. csvwriter := csv.NewWriter(out)
  103. err := csvwriter.Write([]string{"machine_id", "ip_address", "updated_at", "validated", "version", "auth_type", "last_heartbeat"})
  104. if err != nil {
  105. return fmt.Errorf("failed to write header: %s", err)
  106. }
  107. for _, m := range machines {
  108. var validated string
  109. if m.IsValidated {
  110. validated = "true"
  111. } else {
  112. validated = "false"
  113. }
  114. hb, _ := getLastHeartbeat(m)
  115. err := csvwriter.Write([]string{m.MachineId, m.IpAddress, m.UpdatedAt.Format(time.RFC3339), validated, m.Version, m.AuthType, hb})
  116. if err != nil {
  117. return fmt.Errorf("failed to write raw output: %w", err)
  118. }
  119. }
  120. csvwriter.Flush()
  121. } else {
  122. log.Errorf("unknown output '%s'", csConfig.Cscli.Output)
  123. }
  124. return nil
  125. }
  126. func NewMachinesListCmd() *cobra.Command {
  127. cmdMachinesList := &cobra.Command{
  128. Use: "list",
  129. Short: "List machines",
  130. Long: `List `,
  131. Example: `cscli machines list`,
  132. Args: cobra.MaximumNArgs(1),
  133. DisableAutoGenTag: true,
  134. PreRunE: func(cmd *cobra.Command, args []string) error {
  135. var err error
  136. dbClient, err = database.NewClient(csConfig.DbConfig)
  137. if err != nil {
  138. return fmt.Errorf("unable to create new database client: %s", err)
  139. }
  140. return nil
  141. },
  142. RunE: func(cmd *cobra.Command, args []string) error {
  143. err := getAgents(color.Output, dbClient)
  144. if err != nil {
  145. return fmt.Errorf("unable to list machines: %s", err)
  146. }
  147. return nil
  148. },
  149. }
  150. return cmdMachinesList
  151. }
  152. func NewMachinesAddCmd() *cobra.Command {
  153. cmdMachinesAdd := &cobra.Command{
  154. Use: "add",
  155. Short: "add machine to the database.",
  156. DisableAutoGenTag: true,
  157. Long: `Register a new machine in the database. cscli should be on the same machine as LAPI.`,
  158. Example: `
  159. cscli machines add --auto
  160. cscli machines add MyTestMachine --auto
  161. cscli machines add MyTestMachine --password MyPassword
  162. `,
  163. PreRunE: func(cmd *cobra.Command, args []string) error {
  164. var err error
  165. dbClient, err = database.NewClient(csConfig.DbConfig)
  166. if err != nil {
  167. return fmt.Errorf("unable to create new database client: %s", err)
  168. }
  169. return nil
  170. },
  171. RunE: runMachinesAdd,
  172. }
  173. flags := cmdMachinesAdd.Flags()
  174. flags.StringP("password", "p", "", "machine password to login to the API")
  175. flags.StringP("file", "f", "", "output file destination (defaults to "+csconfig.DefaultConfigPath("local_api_credentials.yaml")+")")
  176. flags.StringP("url", "u", "", "URL of the local API")
  177. flags.BoolP("interactive", "i", false, "interfactive mode to enter the password")
  178. flags.BoolP("auto", "a", false, "automatically generate password (and username if not provided)")
  179. flags.Bool("force", false, "will force add the machine if it already exist")
  180. return cmdMachinesAdd
  181. }
  182. func runMachinesAdd(cmd *cobra.Command, args []string) error {
  183. var dumpFile string
  184. var err error
  185. flags := cmd.Flags()
  186. machinePassword, err := flags.GetString("password")
  187. if err != nil {
  188. return err
  189. }
  190. outputFile, err := flags.GetString("file")
  191. if err != nil {
  192. return err
  193. }
  194. apiURL, err := flags.GetString("url")
  195. if err != nil {
  196. return err
  197. }
  198. interactive, err := flags.GetBool("interactive")
  199. if err != nil {
  200. return err
  201. }
  202. autoAdd, err := flags.GetBool("auto")
  203. if err != nil {
  204. return err
  205. }
  206. forceAdd, err := flags.GetBool("force")
  207. if err != nil {
  208. return err
  209. }
  210. var machineID string
  211. // create machineID if not specified by user
  212. if len(args) == 0 {
  213. if !autoAdd {
  214. printHelp(cmd)
  215. return nil
  216. }
  217. machineID, err = generateID("")
  218. if err != nil {
  219. return fmt.Errorf("unable to generate machine id: %s", err)
  220. }
  221. } else {
  222. machineID = args[0]
  223. }
  224. /*check if file already exists*/
  225. if outputFile != "" {
  226. dumpFile = outputFile
  227. } else if csConfig.API.Client != nil && csConfig.API.Client.CredentialsFilePath != "" {
  228. dumpFile = csConfig.API.Client.CredentialsFilePath
  229. }
  230. // create a password if it's not specified by user
  231. if machinePassword == "" && !interactive {
  232. if !autoAdd {
  233. printHelp(cmd)
  234. return nil
  235. }
  236. machinePassword = generatePassword(passwordLength)
  237. } else if machinePassword == "" && interactive {
  238. qs := &survey.Password{
  239. Message: "Please provide a password for the machine",
  240. }
  241. survey.AskOne(qs, &machinePassword)
  242. }
  243. password := strfmt.Password(machinePassword)
  244. _, err = dbClient.CreateMachine(&machineID, &password, "", true, forceAdd, types.PasswordAuthType)
  245. if err != nil {
  246. return fmt.Errorf("unable to create machine: %s", err)
  247. }
  248. log.Infof("Machine '%s' successfully added to the local API", machineID)
  249. if apiURL == "" {
  250. if csConfig.API.Client != nil && csConfig.API.Client.Credentials != nil && csConfig.API.Client.Credentials.URL != "" {
  251. apiURL = csConfig.API.Client.Credentials.URL
  252. } else if csConfig.API.Server != nil && csConfig.API.Server.ListenURI != "" {
  253. apiURL = "http://" + csConfig.API.Server.ListenURI
  254. } else {
  255. return fmt.Errorf("unable to dump an api URL. Please provide it in your configuration or with the -u parameter")
  256. }
  257. }
  258. apiCfg := csconfig.ApiCredentialsCfg{
  259. Login: machineID,
  260. Password: password.String(),
  261. URL: apiURL,
  262. }
  263. apiConfigDump, err := yaml.Marshal(apiCfg)
  264. if err != nil {
  265. return fmt.Errorf("unable to marshal api credentials: %s", err)
  266. }
  267. if dumpFile != "" && dumpFile != "-" {
  268. err = os.WriteFile(dumpFile, apiConfigDump, 0644)
  269. if err != nil {
  270. return fmt.Errorf("write api credentials in '%s' failed: %s", dumpFile, err)
  271. }
  272. log.Printf("API credentials dumped to '%s'", dumpFile)
  273. } else {
  274. fmt.Printf("%s\n", string(apiConfigDump))
  275. }
  276. return nil
  277. }
  278. func NewMachinesDeleteCmd() *cobra.Command {
  279. cmdMachinesDelete := &cobra.Command{
  280. Use: "delete [machine_name]...",
  281. Short: "delete machines",
  282. Example: `cscli machines delete "machine1" "machine2"`,
  283. Args: cobra.MinimumNArgs(1),
  284. Aliases: []string{"remove"},
  285. DisableAutoGenTag: true,
  286. PreRunE: func(cmd *cobra.Command, args []string) error {
  287. var err error
  288. dbClient, err = database.NewClient(csConfig.DbConfig)
  289. if err != nil {
  290. return fmt.Errorf("unable to create new database client: %s", err)
  291. }
  292. return nil
  293. },
  294. ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
  295. var err error
  296. dbClient, err = getDBClient()
  297. if err != nil {
  298. cobra.CompError("unable to create new database client: " + err.Error())
  299. return nil, cobra.ShellCompDirectiveNoFileComp
  300. }
  301. machines, err := dbClient.ListMachines()
  302. if err != nil {
  303. cobra.CompError("unable to list machines " + err.Error())
  304. }
  305. ret := make([]string, 0)
  306. for _, machine := range machines {
  307. if strings.Contains(machine.MachineId, toComplete) && !slices.Contains(args, machine.MachineId) {
  308. ret = append(ret, machine.MachineId)
  309. }
  310. }
  311. return ret, cobra.ShellCompDirectiveNoFileComp
  312. },
  313. RunE: runMachinesDelete,
  314. }
  315. return cmdMachinesDelete
  316. }
  317. func runMachinesDelete(cmd *cobra.Command, args []string) error {
  318. for _, machineID := range args {
  319. err := dbClient.DeleteWatcher(machineID)
  320. if err != nil {
  321. log.Errorf("unable to delete machine '%s': %s", machineID, err)
  322. return nil
  323. }
  324. log.Infof("machine '%s' deleted successfully", machineID)
  325. }
  326. return nil
  327. }
  328. func NewMachinesValidateCmd() *cobra.Command {
  329. cmdMachinesValidate := &cobra.Command{
  330. Use: "validate",
  331. Short: "validate a machine to access the local API",
  332. Long: `validate a machine to access the local API.`,
  333. Example: `cscli machines validate "machine_name"`,
  334. Args: cobra.ExactArgs(1),
  335. DisableAutoGenTag: true,
  336. PreRunE: func(cmd *cobra.Command, args []string) error {
  337. var err error
  338. dbClient, err = database.NewClient(csConfig.DbConfig)
  339. if err != nil {
  340. return fmt.Errorf("unable to create new database client: %s", err)
  341. }
  342. return nil
  343. },
  344. RunE: func(cmd *cobra.Command, args []string) error {
  345. machineID := args[0]
  346. if err := dbClient.ValidateMachine(machineID); err != nil {
  347. return fmt.Errorf("unable to validate machine '%s': %s", machineID, err)
  348. }
  349. log.Infof("machine '%s' validated successfully", machineID)
  350. return nil
  351. },
  352. }
  353. return cmdMachinesValidate
  354. }
  355. func NewMachinesCmd() *cobra.Command {
  356. var cmdMachines = &cobra.Command{
  357. Use: "machines [action]",
  358. Short: "Manage local API machines [requires local API]",
  359. Long: `To list/add/delete/validate machines.
  360. Note: This command requires database direct access, so is intended to be run on the local API machine.
  361. `,
  362. Example: `cscli machines [action]`,
  363. DisableAutoGenTag: true,
  364. Aliases: []string{"machine"},
  365. PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
  366. if err := csConfig.LoadAPIServer(); err != nil || csConfig.DisableAPI {
  367. if err != nil {
  368. log.Errorf("local api : %s", err)
  369. }
  370. return fmt.Errorf("local API is disabled, please run this command on the local API machine")
  371. }
  372. return nil
  373. },
  374. }
  375. cmdMachines.AddCommand(NewMachinesListCmd())
  376. cmdMachines.AddCommand(NewMachinesAddCmd())
  377. cmdMachines.AddCommand(NewMachinesDeleteCmd())
  378. cmdMachines.AddCommand(NewMachinesValidateCmd())
  379. return cmdMachines
  380. }