package setup import ( "bytes" "fmt" "os" "path/filepath" "strings" goccyyaml "github.com/goccy/go-yaml" "gopkg.in/yaml.v3" "github.com/crowdsecurity/crowdsec/pkg/cwhub" ) // AcquisDocument is created from a SetupItem. It represents a single YAML document, and can be part of a multi-document file. type AcquisDocument struct { AcquisFilename string DataSource map[string]interface{} } func decodeSetup(input []byte, fancyErrors bool) (Setup, error) { ret := Setup{} // parse with goccy to have better error messages in many cases dec := goccyyaml.NewDecoder(bytes.NewBuffer(input), goccyyaml.Strict()) if err := dec.Decode(&ret); err != nil { if fancyErrors { return ret, fmt.Errorf("%v", goccyyaml.FormatError(err, true, true)) } // XXX errors here are multiline, should we just print them to stderr instead of logging? return ret, fmt.Errorf("%v", err) } // parse again because goccy is not strict enough anyway dec2 := yaml.NewDecoder(bytes.NewBuffer(input)) dec2.KnownFields(true) if err := dec2.Decode(&ret); err != nil { return ret, fmt.Errorf("while unmarshaling setup file: %w", err) } return ret, nil } // InstallHubItems installs the objects recommended in a setup file. func InstallHubItems(hub *cwhub.Hub, input []byte, dryRun bool) error { setupEnvelope, err := decodeSetup(input, false) if err != nil { return err } for _, setupItem := range setupEnvelope.Setup { forceAction := false downloadOnly := false install := setupItem.Install if install == nil { continue } if len(install.Collections) > 0 { for _, collection := range setupItem.Install.Collections { item := hub.GetItem(cwhub.COLLECTIONS, collection) if item == nil { return fmt.Errorf("collection %s not found", collection) } if dryRun { fmt.Println("dry-run: would install collection", collection) continue } if err := item.Install(forceAction, downloadOnly); err != nil { return fmt.Errorf("while installing collection %s: %w", item.Name, err) } } } if len(install.Parsers) > 0 { for _, parser := range setupItem.Install.Parsers { if dryRun { fmt.Println("dry-run: would install parser", parser) continue } item := hub.GetItem(cwhub.PARSERS, parser) if item == nil { return fmt.Errorf("parser %s not found", parser) } if err := item.Install(forceAction, downloadOnly); err != nil { return fmt.Errorf("while installing parser %s: %w", item.Name, err) } } } if len(install.Scenarios) > 0 { for _, scenario := range setupItem.Install.Scenarios { if dryRun { fmt.Println("dry-run: would install scenario", scenario) continue } item := hub.GetItem(cwhub.SCENARIOS, scenario) if item == nil { return fmt.Errorf("scenario %s not found", scenario) } if err := item.Install(forceAction, downloadOnly); err != nil { return fmt.Errorf("while installing scenario %s: %w", item.Name, err) } } } if len(install.PostOverflows) > 0 { for _, postoverflow := range setupItem.Install.PostOverflows { if dryRun { fmt.Println("dry-run: would install postoverflow", postoverflow) continue } item := hub.GetItem(cwhub.POSTOVERFLOWS, postoverflow) if item == nil { return fmt.Errorf("postoverflow %s not found", postoverflow) } if err := item.Install(forceAction, downloadOnly); err != nil { return fmt.Errorf("while installing postoverflow %s: %w", item.Name, err) } } } } return nil } // marshalAcquisDocuments creates the monolithic file, or itemized files (if a directory is provided) with the acquisition documents. func marshalAcquisDocuments(ads []AcquisDocument, toDir string) (string, error) { var sb strings.Builder dashTerminator := false disclaimer := ` # # This file was automatically generated by "cscli setup datasources". # You can modify it by hand, but will be responsible for its maintenance. # To add datasources or logfiles, you can instead write a new configuration # in the directory defined by acquisition_dir. # ` if toDir == "" { sb.WriteString(disclaimer) } else { _, err := os.Stat(toDir) if os.IsNotExist(err) { return "", fmt.Errorf("directory %s does not exist", toDir) } } for _, ad := range ads { out, err := goccyyaml.MarshalWithOptions(ad.DataSource, goccyyaml.IndentSequence(true)) if err != nil { return "", fmt.Errorf("while encoding datasource: %w", err) } if toDir != "" { if ad.AcquisFilename == "" { return "", fmt.Errorf("empty acquis filename") } fname := filepath.Join(toDir, ad.AcquisFilename) fmt.Println("creating", fname) f, err := os.Create(fname) if err != nil { return "", fmt.Errorf("creating acquisition file: %w", err) } defer f.Close() _, err = f.WriteString(disclaimer) if err != nil { return "", fmt.Errorf("while writing to %s: %w", ad.AcquisFilename, err) } _, err = f.Write(out) if err != nil { return "", fmt.Errorf("while writing to %s: %w", ad.AcquisFilename, err) } f.Sync() continue } if dashTerminator { sb.WriteString("---\n") } sb.Write(out) dashTerminator = true } return sb.String(), nil } // Validate checks the validity of a setup file. func Validate(input []byte) error { _, err := decodeSetup(input, true) if err != nil { return err } return nil } // DataSources generates the acquisition documents from a setup file. func DataSources(input []byte, toDir string) (string, error) { setupEnvelope, err := decodeSetup(input, false) if err != nil { return "", err } ads := make([]AcquisDocument, 0) filename := func(basename string, ext string) string { if basename == "" { return basename } return basename + ext } for _, setupItem := range setupEnvelope.Setup { datasource := setupItem.DataSource basename := "" if toDir != "" { basename = "setup." + setupItem.DetectedService } if datasource == nil { continue } ad := AcquisDocument{ AcquisFilename: filename(basename, ".yaml"), DataSource: datasource, } ads = append(ads, ad) } return marshalAcquisDocuments(ads, toDir) }