crowdsec/pkg/setup/install.go

263 lines
6.1 KiB
Go

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)
}