lapi.go 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578
  1. package main
  2. import (
  3. "context"
  4. "errors"
  5. "fmt"
  6. "net/url"
  7. "os"
  8. "sort"
  9. "strings"
  10. "github.com/go-openapi/strfmt"
  11. log "github.com/sirupsen/logrus"
  12. "github.com/spf13/cobra"
  13. "gopkg.in/yaml.v2"
  14. "slices"
  15. "github.com/crowdsecurity/go-cs-lib/version"
  16. "github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/require"
  17. "github.com/crowdsecurity/crowdsec/pkg/alertcontext"
  18. "github.com/crowdsecurity/crowdsec/pkg/apiclient"
  19. "github.com/crowdsecurity/crowdsec/pkg/csconfig"
  20. "github.com/crowdsecurity/crowdsec/pkg/cwhub"
  21. "github.com/crowdsecurity/crowdsec/pkg/exprhelpers"
  22. "github.com/crowdsecurity/crowdsec/pkg/models"
  23. "github.com/crowdsecurity/crowdsec/pkg/parser"
  24. )
  25. const LAPIURLPrefix = "v1"
  26. type cliLapi struct {
  27. cfg configGetter
  28. }
  29. func NewCLILapi(cfg configGetter) *cliLapi {
  30. return &cliLapi{
  31. cfg: cfg,
  32. }
  33. }
  34. func (cli *cliLapi) status() error {
  35. cfg := cli.cfg()
  36. password := strfmt.Password(cfg.API.Client.Credentials.Password)
  37. apiurl, err := url.Parse(cfg.API.Client.Credentials.URL)
  38. login := cfg.API.Client.Credentials.Login
  39. if err != nil {
  40. return fmt.Errorf("parsing api url: %w", err)
  41. }
  42. hub, err := require.Hub(cfg, nil, nil)
  43. if err != nil {
  44. return err
  45. }
  46. scenarios, err := hub.GetInstalledItemNames(cwhub.SCENARIOS)
  47. if err != nil {
  48. return fmt.Errorf("failed to get scenarios: %w", err)
  49. }
  50. Client, err = apiclient.NewDefaultClient(apiurl,
  51. LAPIURLPrefix,
  52. fmt.Sprintf("crowdsec/%s", version.String()),
  53. nil)
  54. if err != nil {
  55. return fmt.Errorf("init default client: %w", err)
  56. }
  57. t := models.WatcherAuthRequest{
  58. MachineID: &login,
  59. Password: &password,
  60. Scenarios: scenarios,
  61. }
  62. log.Infof("Loaded credentials from %s", cfg.API.Client.CredentialsFilePath)
  63. log.Infof("Trying to authenticate with username %s on %s", login, apiurl)
  64. _, _, err = Client.Auth.AuthenticateWatcher(context.Background(), t)
  65. if err != nil {
  66. return fmt.Errorf("failed to authenticate to Local API (LAPI): %w", err)
  67. }
  68. log.Infof("You can successfully interact with Local API (LAPI)")
  69. return nil
  70. }
  71. func (cli *cliLapi) register(apiURL string, outputFile string, machine string) error {
  72. var err error
  73. lapiUser := machine
  74. cfg := cli.cfg()
  75. if lapiUser == "" {
  76. lapiUser, err = generateID("")
  77. if err != nil {
  78. return fmt.Errorf("unable to generate machine id: %w", err)
  79. }
  80. }
  81. password := strfmt.Password(generatePassword(passwordLength))
  82. if apiURL == "" {
  83. if cfg.API.Client == nil || cfg.API.Client.Credentials == nil || cfg.API.Client.Credentials.URL == "" {
  84. return fmt.Errorf("no Local API URL. Please provide it in your configuration or with the -u parameter")
  85. }
  86. apiURL = cfg.API.Client.Credentials.URL
  87. }
  88. /*URL needs to end with /, but user doesn't care*/
  89. if !strings.HasSuffix(apiURL, "/") {
  90. apiURL += "/"
  91. }
  92. /*URL needs to start with http://, but user doesn't care*/
  93. if !strings.HasPrefix(apiURL, "http://") && !strings.HasPrefix(apiURL, "https://") {
  94. apiURL = "http://" + apiURL
  95. }
  96. apiurl, err := url.Parse(apiURL)
  97. if err != nil {
  98. return fmt.Errorf("parsing api url: %w", err)
  99. }
  100. _, err = apiclient.RegisterClient(&apiclient.Config{
  101. MachineID: lapiUser,
  102. Password: password,
  103. UserAgent: fmt.Sprintf("crowdsec/%s", version.String()),
  104. URL: apiurl,
  105. VersionPrefix: LAPIURLPrefix,
  106. }, nil)
  107. if err != nil {
  108. return fmt.Errorf("api client register: %w", err)
  109. }
  110. log.Printf("Successfully registered to Local API (LAPI)")
  111. var dumpFile string
  112. if outputFile != "" {
  113. dumpFile = outputFile
  114. } else if cfg.API.Client.CredentialsFilePath != "" {
  115. dumpFile = cfg.API.Client.CredentialsFilePath
  116. } else {
  117. dumpFile = ""
  118. }
  119. apiCfg := csconfig.ApiCredentialsCfg{
  120. Login: lapiUser,
  121. Password: password.String(),
  122. URL: apiURL,
  123. }
  124. apiConfigDump, err := yaml.Marshal(apiCfg)
  125. if err != nil {
  126. return fmt.Errorf("unable to marshal api credentials: %w", err)
  127. }
  128. if dumpFile != "" {
  129. err = os.WriteFile(dumpFile, apiConfigDump, 0o600)
  130. if err != nil {
  131. return fmt.Errorf("write api credentials to '%s' failed: %w", dumpFile, err)
  132. }
  133. log.Printf("Local API credentials written to '%s'", dumpFile)
  134. } else {
  135. fmt.Printf("%s\n", string(apiConfigDump))
  136. }
  137. log.Warning(ReloadMessage())
  138. return nil
  139. }
  140. func (cli *cliLapi) newStatusCmd() *cobra.Command {
  141. cmdLapiStatus := &cobra.Command{
  142. Use: "status",
  143. Short: "Check authentication to Local API (LAPI)",
  144. Args: cobra.MinimumNArgs(0),
  145. DisableAutoGenTag: true,
  146. RunE: func(cmd *cobra.Command, args []string) error {
  147. return cli.status()
  148. },
  149. }
  150. return cmdLapiStatus
  151. }
  152. func (cli *cliLapi) newRegisterCmd() *cobra.Command {
  153. var (
  154. apiURL string
  155. outputFile string
  156. machine string
  157. )
  158. cmd := &cobra.Command{
  159. Use: "register",
  160. Short: "Register a machine to Local API (LAPI)",
  161. Long: `Register your machine to the Local API (LAPI).
  162. Keep in mind the machine needs to be validated by an administrator on LAPI side to be effective.`,
  163. Args: cobra.MinimumNArgs(0),
  164. DisableAutoGenTag: true,
  165. RunE: func(_ *cobra.Command, _ []string) error {
  166. return cli.register(apiURL, outputFile, machine)
  167. },
  168. }
  169. flags := cmd.Flags()
  170. flags.StringVarP(&apiURL, "url", "u", "", "URL of the API (ie. http://127.0.0.1)")
  171. flags.StringVarP(&outputFile, "file", "f", "", "output file destination")
  172. flags.StringVar(&machine, "machine", "", "Name of the machine to register with")
  173. return cmd
  174. }
  175. func (cli *cliLapi) NewCommand() *cobra.Command {
  176. cmd := &cobra.Command{
  177. Use: "lapi [action]",
  178. Short: "Manage interaction with Local API (LAPI)",
  179. Args: cobra.MinimumNArgs(1),
  180. DisableAutoGenTag: true,
  181. PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
  182. if err := cli.cfg().LoadAPIClient(); err != nil {
  183. return fmt.Errorf("loading api client: %w", err)
  184. }
  185. return nil
  186. },
  187. }
  188. cmd.AddCommand(cli.newRegisterCmd())
  189. cmd.AddCommand(cli.newStatusCmd())
  190. cmd.AddCommand(cli.newContextCmd())
  191. return cmd
  192. }
  193. func AddContext(key string, values []string) error {
  194. if err := alertcontext.ValidateContextExpr(key, values); err != nil {
  195. return fmt.Errorf("invalid context configuration :%s", err)
  196. }
  197. if _, ok := csConfig.Crowdsec.ContextToSend[key]; !ok {
  198. csConfig.Crowdsec.ContextToSend[key] = make([]string, 0)
  199. log.Infof("key '%s' added", key)
  200. }
  201. data := csConfig.Crowdsec.ContextToSend[key]
  202. for _, val := range values {
  203. if !slices.Contains(data, val) {
  204. log.Infof("value '%s' added to key '%s'", val, key)
  205. data = append(data, val)
  206. }
  207. csConfig.Crowdsec.ContextToSend[key] = data
  208. }
  209. if err := csConfig.Crowdsec.DumpContextConfigFile(); err != nil {
  210. return err
  211. }
  212. return nil
  213. }
  214. func (cli *cliLapi) newContextAddCmd() *cobra.Command {
  215. var keyToAdd string
  216. var valuesToAdd []string
  217. cmd := &cobra.Command{
  218. Use: "add",
  219. Short: "Add context to send with alerts. You must specify the output key with the expr value you want",
  220. Example: `cscli lapi context add --key source_ip --value evt.Meta.source_ip
  221. cscli lapi context add --key file_source --value evt.Line.Src
  222. cscli lapi context add --value evt.Meta.source_ip --value evt.Meta.target_user
  223. `,
  224. DisableAutoGenTag: true,
  225. RunE: func(cmd *cobra.Command, args []string) error {
  226. hub, err := require.Hub(cli.cfg(), nil, nil)
  227. if err != nil {
  228. return err
  229. }
  230. if err = alertcontext.LoadConsoleContext(cli.cfg(), hub); err != nil {
  231. return fmt.Errorf("while loading context: %w", err)
  232. }
  233. if keyToAdd != "" {
  234. if err := AddContext(keyToAdd, valuesToAdd); err != nil {
  235. return err
  236. }
  237. return nil
  238. }
  239. for _, v := range valuesToAdd {
  240. keySlice := strings.Split(v, ".")
  241. key := keySlice[len(keySlice)-1]
  242. value := []string{v}
  243. if err := AddContext(key, value); err != nil {
  244. return err
  245. }
  246. }
  247. return nil
  248. },
  249. }
  250. flags := cmd.Flags()
  251. flags.StringVarP(&keyToAdd, "key", "k", "", "The key of the different values to send")
  252. flags.StringSliceVar(&valuesToAdd, "value", []string{}, "The expr fields to associate with the key")
  253. cmd.MarkFlagRequired("value")
  254. return cmd
  255. }
  256. func (cli *cliLapi) newContextCmd() *cobra.Command {
  257. cmd := &cobra.Command{
  258. Use: "context [command]",
  259. Short: "Manage context to send with alerts",
  260. DisableAutoGenTag: true,
  261. PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
  262. if err := csConfig.LoadCrowdsec(); err != nil {
  263. fileNotFoundMessage := fmt.Sprintf("failed to open context file: open %s: no such file or directory", csConfig.Crowdsec.ConsoleContextPath)
  264. if err.Error() != fileNotFoundMessage {
  265. return fmt.Errorf("unable to load CrowdSec agent configuration: %w", err)
  266. }
  267. }
  268. if csConfig.DisableAgent {
  269. return errors.New("agent is disabled and lapi context can only be used on the agent")
  270. }
  271. return nil
  272. },
  273. Run: func(cmd *cobra.Command, args []string) {
  274. printHelp(cmd)
  275. },
  276. }
  277. cmd.AddCommand(cli.newContextAddCmd())
  278. cmdContextStatus := &cobra.Command{
  279. Use: "status",
  280. Short: "List context to send with alerts",
  281. DisableAutoGenTag: true,
  282. RunE: func(cmd *cobra.Command, args []string) error {
  283. hub, err := require.Hub(csConfig, nil, nil)
  284. if err != nil {
  285. return err
  286. }
  287. if err = alertcontext.LoadConsoleContext(csConfig, hub); err != nil {
  288. return fmt.Errorf("while loading context: %w", err)
  289. }
  290. if len(csConfig.Crowdsec.ContextToSend) == 0 {
  291. fmt.Println("No context found on this agent. You can use 'cscli lapi context add' to add context to your alerts.")
  292. return nil
  293. }
  294. dump, err := yaml.Marshal(csConfig.Crowdsec.ContextToSend)
  295. if err != nil {
  296. return fmt.Errorf("unable to show context status: %w", err)
  297. }
  298. fmt.Print(string(dump))
  299. return nil
  300. },
  301. }
  302. cmd.AddCommand(cmdContextStatus)
  303. var detectAll bool
  304. cmdContextDetect := &cobra.Command{
  305. Use: "detect",
  306. Short: "Detect available fields from the installed parsers",
  307. Example: `cscli lapi context detect --all
  308. cscli lapi context detect crowdsecurity/sshd-logs
  309. `,
  310. DisableAutoGenTag: true,
  311. RunE: func(cmd *cobra.Command, args []string) error {
  312. if !detectAll && len(args) == 0 {
  313. log.Infof("Please provide parsers to detect or --all flag.")
  314. printHelp(cmd)
  315. }
  316. // to avoid all the log.Info from the loaders functions
  317. log.SetLevel(log.WarnLevel)
  318. if err := exprhelpers.Init(nil); err != nil {
  319. return fmt.Errorf("failed to init expr helpers: %w", err)
  320. }
  321. hub, err := require.Hub(csConfig, nil, nil)
  322. if err != nil {
  323. return err
  324. }
  325. csParsers := parser.NewParsers(hub)
  326. if csParsers, err = parser.LoadParsers(csConfig, csParsers); err != nil {
  327. return fmt.Errorf("unable to load parsers: %w", err)
  328. }
  329. fieldByParsers := make(map[string][]string)
  330. for _, node := range csParsers.Nodes {
  331. if !detectAll && !slices.Contains(args, node.Name) {
  332. continue
  333. }
  334. if !detectAll {
  335. args = removeFromSlice(node.Name, args)
  336. }
  337. fieldByParsers[node.Name] = make([]string, 0)
  338. fieldByParsers[node.Name] = detectNode(node, *csParsers.Ctx)
  339. subNodeFields := detectSubNode(node, *csParsers.Ctx)
  340. for _, field := range subNodeFields {
  341. if !slices.Contains(fieldByParsers[node.Name], field) {
  342. fieldByParsers[node.Name] = append(fieldByParsers[node.Name], field)
  343. }
  344. }
  345. }
  346. fmt.Printf("Acquisition :\n\n")
  347. fmt.Printf(" - evt.Line.Module\n")
  348. fmt.Printf(" - evt.Line.Raw\n")
  349. fmt.Printf(" - evt.Line.Src\n")
  350. fmt.Println()
  351. parsersKey := make([]string, 0)
  352. for k := range fieldByParsers {
  353. parsersKey = append(parsersKey, k)
  354. }
  355. sort.Strings(parsersKey)
  356. for _, k := range parsersKey {
  357. if len(fieldByParsers[k]) == 0 {
  358. continue
  359. }
  360. fmt.Printf("%s :\n\n", k)
  361. values := fieldByParsers[k]
  362. sort.Strings(values)
  363. for _, value := range values {
  364. fmt.Printf(" - %s\n", value)
  365. }
  366. fmt.Println()
  367. }
  368. if len(args) > 0 {
  369. for _, parserNotFound := range args {
  370. log.Errorf("parser '%s' not found, can't detect fields", parserNotFound)
  371. }
  372. }
  373. return nil
  374. },
  375. }
  376. cmdContextDetect.Flags().BoolVarP(&detectAll, "all", "a", false, "Detect evt field for all installed parser")
  377. cmd.AddCommand(cmdContextDetect)
  378. cmdContextDelete := &cobra.Command{
  379. Use: "delete",
  380. DisableAutoGenTag: true,
  381. RunE: func(_ *cobra.Command, _ []string) error {
  382. filePath := csConfig.Crowdsec.ConsoleContextPath
  383. if filePath == "" {
  384. filePath = "the context file"
  385. }
  386. fmt.Printf("Command \"delete\" is deprecated, please manually edit %s.", filePath)
  387. return nil
  388. },
  389. }
  390. cmd.AddCommand(cmdContextDelete)
  391. return cmd
  392. }
  393. func detectStaticField(GrokStatics []parser.ExtraField) []string {
  394. ret := make([]string, 0)
  395. for _, static := range GrokStatics {
  396. if static.Parsed != "" {
  397. fieldName := fmt.Sprintf("evt.Parsed.%s", static.Parsed)
  398. if !slices.Contains(ret, fieldName) {
  399. ret = append(ret, fieldName)
  400. }
  401. }
  402. if static.Meta != "" {
  403. fieldName := fmt.Sprintf("evt.Meta.%s", static.Meta)
  404. if !slices.Contains(ret, fieldName) {
  405. ret = append(ret, fieldName)
  406. }
  407. }
  408. if static.TargetByName != "" {
  409. fieldName := static.TargetByName
  410. if !strings.HasPrefix(fieldName, "evt.") {
  411. fieldName = "evt." + fieldName
  412. }
  413. if !slices.Contains(ret, fieldName) {
  414. ret = append(ret, fieldName)
  415. }
  416. }
  417. }
  418. return ret
  419. }
  420. func detectNode(node parser.Node, parserCTX parser.UnixParserCtx) []string {
  421. ret := make([]string, 0)
  422. if node.Grok.RunTimeRegexp != nil {
  423. for _, capturedField := range node.Grok.RunTimeRegexp.Names() {
  424. fieldName := fmt.Sprintf("evt.Parsed.%s", capturedField)
  425. if !slices.Contains(ret, fieldName) {
  426. ret = append(ret, fieldName)
  427. }
  428. }
  429. }
  430. if node.Grok.RegexpName != "" {
  431. grokCompiled, err := parserCTX.Grok.Get(node.Grok.RegexpName)
  432. // ignore error (parser does not exist?)
  433. if err == nil {
  434. for _, capturedField := range grokCompiled.Names() {
  435. fieldName := fmt.Sprintf("evt.Parsed.%s", capturedField)
  436. if !slices.Contains(ret, fieldName) {
  437. ret = append(ret, fieldName)
  438. }
  439. }
  440. }
  441. }
  442. if len(node.Grok.Statics) > 0 {
  443. staticsField := detectStaticField(node.Grok.Statics)
  444. for _, staticField := range staticsField {
  445. if !slices.Contains(ret, staticField) {
  446. ret = append(ret, staticField)
  447. }
  448. }
  449. }
  450. if len(node.Statics) > 0 {
  451. staticsField := detectStaticField(node.Statics)
  452. for _, staticField := range staticsField {
  453. if !slices.Contains(ret, staticField) {
  454. ret = append(ret, staticField)
  455. }
  456. }
  457. }
  458. return ret
  459. }
  460. func detectSubNode(node parser.Node, parserCTX parser.UnixParserCtx) []string {
  461. var ret = make([]string, 0)
  462. for _, subnode := range node.LeavesNodes {
  463. if subnode.Grok.RunTimeRegexp != nil {
  464. for _, capturedField := range subnode.Grok.RunTimeRegexp.Names() {
  465. fieldName := fmt.Sprintf("evt.Parsed.%s", capturedField)
  466. if !slices.Contains(ret, fieldName) {
  467. ret = append(ret, fieldName)
  468. }
  469. }
  470. }
  471. if subnode.Grok.RegexpName != "" {
  472. grokCompiled, err := parserCTX.Grok.Get(subnode.Grok.RegexpName)
  473. if err == nil {
  474. // ignore error (parser does not exist?)
  475. for _, capturedField := range grokCompiled.Names() {
  476. fieldName := fmt.Sprintf("evt.Parsed.%s", capturedField)
  477. if !slices.Contains(ret, fieldName) {
  478. ret = append(ret, fieldName)
  479. }
  480. }
  481. }
  482. }
  483. if len(subnode.Grok.Statics) > 0 {
  484. staticsField := detectStaticField(subnode.Grok.Statics)
  485. for _, staticField := range staticsField {
  486. if !slices.Contains(ret, staticField) {
  487. ret = append(ret, staticField)
  488. }
  489. }
  490. }
  491. if len(subnode.Statics) > 0 {
  492. staticsField := detectStaticField(subnode.Statics)
  493. for _, staticField := range staticsField {
  494. if !slices.Contains(ret, staticField) {
  495. ret = append(ret, staticField)
  496. }
  497. }
  498. }
  499. }
  500. return ret
  501. }