mirror of
https://github.com/crowdsecurity/crowdsec.git
synced 2025-05-11 04:15:54 +02:00
254 lines
6.9 KiB
Go
254 lines
6.9 KiB
Go
package cwhub
|
|
|
|
// Install, upgrade and remove items from the hub to the local configuration
|
|
|
|
import (
|
|
"context"
|
|
"crypto"
|
|
"encoding/base64"
|
|
"encoding/hex"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
|
|
"github.com/sirupsen/logrus"
|
|
|
|
"github.com/crowdsecurity/go-cs-lib/downloader"
|
|
|
|
"github.com/crowdsecurity/crowdsec/pkg/emoji"
|
|
)
|
|
|
|
// Upgrade downloads and applies the last version of the item from the hub.
|
|
func (i *Item) Upgrade(ctx context.Context, force bool) (bool, error) {
|
|
if i.State.IsLocal() {
|
|
i.hub.logger.Infof("not upgrading %s: local item", i.Name)
|
|
return false, nil
|
|
}
|
|
|
|
if !i.State.Downloaded {
|
|
return false, fmt.Errorf("can't upgrade %s: not installed", i.Name)
|
|
}
|
|
|
|
if !i.State.Installed {
|
|
return false, fmt.Errorf("can't upgrade %s: downloaded but not installed", i.Name)
|
|
}
|
|
|
|
if i.State.UpToDate {
|
|
i.hub.logger.Infof("%s: up-to-date", i.Name)
|
|
|
|
if err := i.DownloadDataIfNeeded(ctx, force); err != nil {
|
|
return false, fmt.Errorf("%s: download failed: %w", i.Name, err)
|
|
}
|
|
|
|
if !force {
|
|
// no upgrade needed
|
|
return false, nil
|
|
}
|
|
}
|
|
|
|
if _, err := i.downloadLatest(ctx, force, true); err != nil {
|
|
return false, fmt.Errorf("%s: download failed: %w", i.Name, err)
|
|
}
|
|
|
|
if !i.State.UpToDate {
|
|
if i.State.Tainted {
|
|
i.hub.logger.Warningf("%v %s is tainted, --force to overwrite", emoji.Warning, i.Name)
|
|
}
|
|
|
|
return false, nil
|
|
}
|
|
|
|
// a check on stdout is used while scripting to know if the hub has been upgraded
|
|
// and a configuration reload is required
|
|
// TODO: use a better way to communicate this
|
|
fmt.Printf("updated %s\n", i.Name)
|
|
i.hub.logger.Infof("%v %s: updated", emoji.Package, i.Name)
|
|
|
|
return true, nil
|
|
}
|
|
|
|
// downloadLatest downloads the latest version of the item to the hub directory.
|
|
func (i *Item) downloadLatest(ctx context.Context, overwrite bool, updateOnly bool) (bool, error) {
|
|
i.hub.logger.Debugf("Downloading %s %s", i.Type, i.Name)
|
|
|
|
for _, sub := range i.SubItems() {
|
|
if !sub.State.Installed && updateOnly && sub.State.Downloaded {
|
|
i.hub.logger.Debugf("skipping upgrade of %s: not installed", i.Name)
|
|
continue
|
|
}
|
|
|
|
i.hub.logger.Debugf("Download %s sub-item: %s %s (%t -> %t)", i.Name, sub.Type, sub.Name, i.State.Installed, updateOnly)
|
|
|
|
// recurse as it's a collection
|
|
if sub.HasSubItems() {
|
|
i.hub.logger.Tracef("collection, recurse")
|
|
|
|
if _, err := sub.downloadLatest(ctx, overwrite, updateOnly); err != nil {
|
|
return false, err
|
|
}
|
|
}
|
|
|
|
downloaded := sub.State.Downloaded
|
|
|
|
if _, err := sub.download(ctx, overwrite); err != nil {
|
|
return false, err
|
|
}
|
|
|
|
// We need to enable an item when it has been added to a collection since latest release of the collection.
|
|
// We check if sub.Downloaded is false because maybe the item has been disabled by the user.
|
|
if !sub.State.Installed && !downloaded {
|
|
if err := sub.enable(); err != nil {
|
|
return false, fmt.Errorf("enabling '%s': %w", sub.Name, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
if !i.State.Installed && updateOnly && i.State.Downloaded && !overwrite {
|
|
i.hub.logger.Debugf("skipping upgrade of %s: not installed", i.Name)
|
|
return false, nil
|
|
}
|
|
|
|
return i.download(ctx, overwrite)
|
|
}
|
|
|
|
// FetchContentTo downloads the last version of the item's YAML file to the specified path.
|
|
func (i *Item) FetchContentTo(ctx context.Context, destPath string) (bool, string, error) {
|
|
wantHash := i.latestHash()
|
|
if wantHash == "" {
|
|
return false, "", errors.New("latest hash missing from index. The index file is invalid, please run 'cscli hub update' and try again")
|
|
}
|
|
|
|
// Use the embedded content if available
|
|
if i.Content != "" {
|
|
// the content was historically base64 encoded
|
|
content, err := base64.StdEncoding.DecodeString(i.Content)
|
|
if err != nil {
|
|
content = []byte(i.Content)
|
|
}
|
|
|
|
dir := filepath.Dir(destPath)
|
|
|
|
if err := os.MkdirAll(dir, 0o755); err != nil {
|
|
return false, "", fmt.Errorf("while creating %s: %w", dir, err)
|
|
}
|
|
|
|
// check sha256
|
|
hash := crypto.SHA256.New()
|
|
if _, err := hash.Write(content); err != nil {
|
|
return false, "", fmt.Errorf("while hashing %s: %w", i.Name, err)
|
|
}
|
|
|
|
gotHash := hex.EncodeToString(hash.Sum(nil))
|
|
if gotHash != wantHash {
|
|
return false, "", fmt.Errorf("hash mismatch: expected %s, got %s. The index file is invalid, please run 'cscli hub update' and try again", wantHash, gotHash)
|
|
}
|
|
|
|
if err := os.WriteFile(destPath, content, 0o600); err != nil {
|
|
return false, "", fmt.Errorf("while writing %s: %w", destPath, err)
|
|
}
|
|
|
|
i.hub.logger.Debugf("Wrote %s content from .index.json to %s", i.Name, destPath)
|
|
|
|
return true, fmt.Sprintf("(embedded in %s)", i.hub.local.HubIndexFile), nil
|
|
}
|
|
|
|
url, err := i.hub.remote.urlTo(i.RemotePath)
|
|
if err != nil {
|
|
return false, "", fmt.Errorf("failed to build request: %w", err)
|
|
}
|
|
|
|
d := downloader.
|
|
New().
|
|
WithHTTPClient(hubClient).
|
|
ToFile(destPath).
|
|
WithETagFn(downloader.SHA256).
|
|
WithMakeDirs(true).
|
|
WithLogger(logrus.WithField("url", url)).
|
|
CompareContent().
|
|
VerifyHash("sha256", wantHash)
|
|
|
|
// TODO: recommend hub update if hash does not match
|
|
|
|
downloaded, err := d.Download(ctx, url)
|
|
if err != nil {
|
|
return false, "", err
|
|
}
|
|
|
|
return downloaded, url, nil
|
|
}
|
|
|
|
// download downloads the item from the hub and writes it to the hub directory.
|
|
func (i *Item) download(ctx context.Context, overwrite bool) (bool, error) {
|
|
// ensure that target file is within target dir
|
|
finalPath, err := i.downloadPath()
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
if i.State.IsLocal() {
|
|
i.hub.logger.Warningf("%s is local, can't download", i.Name)
|
|
return false, nil
|
|
}
|
|
|
|
// if user didn't --force, don't overwrite local, tainted, up-to-date files
|
|
if !overwrite {
|
|
if i.State.Tainted {
|
|
i.hub.logger.Debugf("%s: tainted, not updated", i.Name)
|
|
return false, nil
|
|
}
|
|
|
|
if i.State.UpToDate {
|
|
// We still have to check if data files are present
|
|
i.hub.logger.Debugf("%s: up-to-date, not updated", i.Name)
|
|
}
|
|
}
|
|
|
|
downloaded, _, err := i.FetchContentTo(ctx, finalPath)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
if downloaded {
|
|
i.hub.logger.Infof("Downloaded %s", i.Name)
|
|
}
|
|
|
|
i.State.Downloaded = true
|
|
i.State.Tainted = false
|
|
i.State.UpToDate = true
|
|
|
|
// read content to get the list of data files
|
|
reader, err := os.Open(finalPath)
|
|
if err != nil {
|
|
return false, fmt.Errorf("while opening %s: %w", finalPath, err)
|
|
}
|
|
|
|
defer reader.Close()
|
|
|
|
if err = downloadDataSet(ctx, i.hub.local.InstallDataDir, overwrite, reader, i.hub.logger); err != nil {
|
|
return false, fmt.Errorf("while downloading data for %s: %w", i.FileName, err)
|
|
}
|
|
|
|
return true, nil
|
|
}
|
|
|
|
// DownloadDataIfNeeded downloads the data set for the item.
|
|
func (i *Item) DownloadDataIfNeeded(ctx context.Context, force bool) error {
|
|
itemFilePath, err := i.installPath()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
itemFile, err := os.Open(itemFilePath)
|
|
if err != nil {
|
|
return fmt.Errorf("while opening %s: %w", itemFilePath, err)
|
|
}
|
|
|
|
defer itemFile.Close()
|
|
|
|
if err = downloadDataSet(ctx, i.hub.local.InstallDataDir, force, itemFile, i.hub.logger); err != nil {
|
|
return fmt.Errorf("while downloading data for %s: %w", itemFilePath, err)
|
|
}
|
|
|
|
return nil
|
|
}
|