- Implement tests for ImportSuccessModal to verify rendering and functionality. - Update AuthContext to store authentication token in localStorage and manage token state. - Modify useImport hook to capture and expose commit results, preventing unnecessary refetches. - Enhance useCertificates hook to support optional refetch intervals. - Update Dashboard to conditionally poll certificates based on pending status. - Integrate ImportSuccessModal into ImportCaddy for user feedback on import completion. - Adjust Login component to utilize returned token for authentication. - Refactor CrowdSecConfig tests for improved readability and reliability. - Add debug_db.py script for inspecting the SQLite database. - Update integration and test scripts for better configuration and error handling. - Introduce Trivy scan script for vulnerability assessment of Docker images.
266 lines
8.3 KiB
Go
266 lines
8.3 KiB
Go
package crowdsec
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io/fs"
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/Wikid82/charon/backend/internal/logger"
|
|
"github.com/Wikid82/charon/backend/internal/util"
|
|
)
|
|
|
|
var (
|
|
ErrCacheMiss = errors.New("cache miss")
|
|
ErrCacheExpired = errors.New("cache expired")
|
|
)
|
|
|
|
// CachedPreset captures metadata about a pulled preset bundle.
|
|
type CachedPreset struct {
|
|
Slug string `json:"slug"`
|
|
CacheKey string `json:"cache_key"`
|
|
Etag string `json:"etag"`
|
|
Source string `json:"source"`
|
|
RetrievedAt time.Time `json:"retrieved_at"`
|
|
PreviewPath string `json:"preview_path"`
|
|
ArchivePath string `json:"archive_path"`
|
|
SizeBytes int64 `json:"size_bytes"`
|
|
}
|
|
|
|
// HubCache persists pulled bundles on disk with TTL-based eviction.
|
|
type HubCache struct {
|
|
baseDir string
|
|
ttl time.Duration
|
|
nowFn func() time.Time
|
|
}
|
|
|
|
var slugPattern = regexp.MustCompile(`^[A-Za-z0-9./_-]+$`)
|
|
|
|
// NewHubCache constructs a cache rooted at baseDir with the provided TTL.
|
|
func NewHubCache(baseDir string, ttl time.Duration) (*HubCache, error) {
|
|
if baseDir == "" {
|
|
return nil, fmt.Errorf("baseDir required")
|
|
}
|
|
if err := os.MkdirAll(baseDir, 0o755); err != nil {
|
|
return nil, fmt.Errorf("create cache dir: %w", err)
|
|
}
|
|
return &HubCache{baseDir: baseDir, ttl: ttl, nowFn: time.Now}, nil
|
|
}
|
|
|
|
// TTL returns the configured time-to-live for cached entries.
|
|
func (c *HubCache) TTL() time.Duration {
|
|
return c.ttl
|
|
}
|
|
|
|
// Store writes the bundle archive and preview to disk and returns the cache metadata.
|
|
func (c *HubCache) Store(ctx context.Context, slug, etag, source, preview string, archive []byte) (CachedPreset, error) {
|
|
if err := ctx.Err(); err != nil {
|
|
return CachedPreset{}, err
|
|
}
|
|
cleanSlug := sanitizeSlug(slug)
|
|
if cleanSlug == "" {
|
|
return CachedPreset{}, fmt.Errorf("invalid slug")
|
|
}
|
|
dir := filepath.Join(c.baseDir, cleanSlug)
|
|
logger.Log().WithField("slug", util.SanitizeForLog(cleanSlug)).WithField("cache_dir", util.SanitizeForLog(dir)).WithField("archive_size", len(archive)).Debug("storing preset in cache")
|
|
|
|
if err := os.MkdirAll(dir, 0o755); err != nil {
|
|
logger.Log().WithError(err).WithField("dir", util.SanitizeForLog(dir)).Error("failed to create cache directory")
|
|
return CachedPreset{}, fmt.Errorf("create slug dir: %w", err)
|
|
}
|
|
|
|
ts := c.nowFn().UTC()
|
|
cacheKey := fmt.Sprintf("%s-%d", cleanSlug, ts.Unix())
|
|
|
|
archivePath := filepath.Join(dir, "bundle.tgz")
|
|
if err := os.WriteFile(archivePath, archive, 0o640); err != nil {
|
|
return CachedPreset{}, fmt.Errorf("write archive: %w", err)
|
|
}
|
|
previewPath := filepath.Join(dir, "preview.yaml")
|
|
if err := os.WriteFile(previewPath, []byte(preview), 0o640); err != nil {
|
|
return CachedPreset{}, fmt.Errorf("write preview: %w", err)
|
|
}
|
|
|
|
meta := CachedPreset{
|
|
Slug: cleanSlug,
|
|
CacheKey: cacheKey,
|
|
Etag: etag,
|
|
Source: source,
|
|
RetrievedAt: ts,
|
|
PreviewPath: previewPath,
|
|
ArchivePath: archivePath,
|
|
SizeBytes: int64(len(archive)),
|
|
}
|
|
metaPath := filepath.Join(dir, "metadata.json")
|
|
raw, err := json.Marshal(meta)
|
|
if err != nil {
|
|
return CachedPreset{}, fmt.Errorf("marshal metadata: %w", err)
|
|
}
|
|
if err := os.WriteFile(metaPath, raw, 0o640); err != nil {
|
|
logger.Log().WithError(err).WithField("meta_path", util.SanitizeForLog(metaPath)).Error("failed to write metadata file")
|
|
return CachedPreset{}, fmt.Errorf("write metadata: %w", err)
|
|
}
|
|
|
|
logger.Log().WithField("slug", util.SanitizeForLog(cleanSlug)).WithField("cache_key", cacheKey).WithField("archive_path", util.SanitizeForLog(archivePath)).WithField("preview_path", util.SanitizeForLog(previewPath)).WithField("meta_path", util.SanitizeForLog(metaPath)).Info("preset successfully stored in cache")
|
|
|
|
return meta, nil
|
|
}
|
|
|
|
// Load returns cached preset metadata, enforcing TTL.
|
|
func (c *HubCache) Load(ctx context.Context, slug string) (CachedPreset, error) {
|
|
if err := ctx.Err(); err != nil {
|
|
return CachedPreset{}, err
|
|
}
|
|
cleanSlug := sanitizeSlug(slug)
|
|
if cleanSlug == "" {
|
|
return CachedPreset{}, fmt.Errorf("invalid slug")
|
|
}
|
|
metaPath := filepath.Join(c.baseDir, cleanSlug, "metadata.json")
|
|
logger.Log().WithField("slug", util.SanitizeForLog(cleanSlug)).WithField("meta_path", util.SanitizeForLog(metaPath)).Debug("attempting to load cached preset")
|
|
|
|
data, err := os.ReadFile(metaPath)
|
|
if err != nil {
|
|
if errors.Is(err, os.ErrNotExist) {
|
|
logger.Log().WithField("slug", util.SanitizeForLog(cleanSlug)).WithField("meta_path", util.SanitizeForLog(metaPath)).Debug("preset not found in cache (cache miss)")
|
|
return CachedPreset{}, ErrCacheMiss
|
|
}
|
|
logger.Log().WithError(err).WithField("slug", util.SanitizeForLog(cleanSlug)).WithField("meta_path", util.SanitizeForLog(metaPath)).Error("failed to read cached preset metadata")
|
|
return CachedPreset{}, err
|
|
}
|
|
var meta CachedPreset
|
|
if err := json.Unmarshal(data, &meta); err != nil {
|
|
logger.Log().WithError(err).WithField("slug", util.SanitizeForLog(cleanSlug)).Error("failed to unmarshal cached preset metadata")
|
|
return CachedPreset{}, fmt.Errorf("unmarshal metadata: %w", err)
|
|
}
|
|
|
|
if c.ttl > 0 && c.nowFn().After(meta.RetrievedAt.Add(c.ttl)) {
|
|
logger.Log().WithField("slug", util.SanitizeForLog(cleanSlug)).WithField("retrieved_at", meta.RetrievedAt).WithField("ttl", c.ttl).Debug("cached preset expired")
|
|
return CachedPreset{}, ErrCacheExpired
|
|
}
|
|
|
|
logger.Log().WithField("slug", util.SanitizeForLog(meta.Slug)).WithField("cache_key", meta.CacheKey).WithField("archive_path", util.SanitizeForLog(meta.ArchivePath)).Debug("successfully loaded cached preset")
|
|
return meta, nil
|
|
}
|
|
|
|
// LoadPreview returns the preview contents for a cached preset.
|
|
func (c *HubCache) LoadPreview(ctx context.Context, slug string) (string, error) {
|
|
meta, err := c.Load(ctx, slug)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
data, err := os.ReadFile(meta.PreviewPath)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return string(data), nil
|
|
}
|
|
|
|
// List returns cached presets that have not expired.
|
|
func (c *HubCache) List(ctx context.Context) ([]CachedPreset, error) {
|
|
if err := ctx.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
results := make([]CachedPreset, 0)
|
|
err := filepath.WalkDir(c.baseDir, func(path string, d fs.DirEntry, err error) error {
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
if d.IsDir() || d.Name() != "metadata.json" {
|
|
return nil
|
|
}
|
|
rel, err := filepath.Rel(c.baseDir, path)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
slug := filepath.Dir(rel)
|
|
meta, err := c.Load(ctx, slug)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
results = append(results, meta)
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return results, nil
|
|
}
|
|
|
|
// Evict removes cached data for the given slug.
|
|
func (c *HubCache) Evict(ctx context.Context, slug string) error {
|
|
if err := ctx.Err(); err != nil {
|
|
return err
|
|
}
|
|
cleanSlug := sanitizeSlug(slug)
|
|
if cleanSlug == "" {
|
|
return fmt.Errorf("invalid slug")
|
|
}
|
|
return os.RemoveAll(filepath.Join(c.baseDir, cleanSlug))
|
|
}
|
|
|
|
// sanitizeSlug guards against traversal and unsupported characters.
|
|
func sanitizeSlug(slug string) string {
|
|
trimmed := strings.TrimSpace(slug)
|
|
if trimmed == "" {
|
|
return ""
|
|
}
|
|
cleaned := filepath.Clean(trimmed)
|
|
cleaned = strings.ReplaceAll(cleaned, "\\", "/")
|
|
if strings.HasPrefix(cleaned, "..") || strings.Contains(cleaned, string(os.PathSeparator)+"..") || strings.HasPrefix(cleaned, string(os.PathSeparator)) {
|
|
return ""
|
|
}
|
|
if !slugPattern.MatchString(cleaned) {
|
|
return ""
|
|
}
|
|
return cleaned
|
|
}
|
|
|
|
// Exists returns true when a non-expired cache entry is present.
|
|
func (c *HubCache) Exists(ctx context.Context, slug string) bool {
|
|
if _, err := c.Load(ctx, slug); err == nil {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
// Touch updates the timestamp to extend TTL; noop when missing.
|
|
func (c *HubCache) Touch(ctx context.Context, slug string) error {
|
|
meta, err := c.Load(ctx, slug)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
meta.RetrievedAt = c.nowFn().UTC()
|
|
raw, err := json.Marshal(meta)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
metaPath := filepath.Join(c.baseDir, meta.Slug, "metadata.json")
|
|
return os.WriteFile(metaPath, raw, 0o640)
|
|
}
|
|
|
|
// Size returns aggregated size of cached archives (best effort).
|
|
func (c *HubCache) Size(ctx context.Context) int64 {
|
|
var total int64
|
|
_ = filepath.WalkDir(c.baseDir, func(path string, d fs.DirEntry, err error) error {
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
if d.IsDir() {
|
|
return nil
|
|
}
|
|
info, err := d.Info()
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
total += info.Size()
|
|
return nil
|
|
})
|
|
return total
|
|
}
|