github.com/crowdsecurity/crowdsec@v1.6.1/cmd/crowdsec-cli/lapi.go (about)

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