Ctrl K
ComunidadGuías…

internal/baseline/store.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package baseline

import (
	"bytes"
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"io/fs"
	"os"
	"path/filepath"
	"sync"

	"github.com/sophotechlabs/spinoza/internal/atomicfile"
	"github.com/sophotechlabs/spinoza/internal/checks"
	"github.com/sophotechlabs/spinoza/internal/filetx"
)

const maxKeys = 50_000

const maxBytes = 8 << 20

const maxKeyValueBytes = 4 << 20

const maxChecks = 512

const maxCounts = 512

const nameLength = 16

var ErrRead = errors.New("baselines: could not be read")

type errorReader struct {
	reader io.Reader
	err    error
}

func (r *errorReader) Read(body []byte) (int, error) {
	read, err := r.reader.Read(body)
	if err != nil && !errors.Is(err, io.EOF) {
		r.err = err
	}
	return read, err
}

type stored struct {
	Cluster string            `json:"cluster,omitempty"`
	TakenAt string            `json:"takenAt"`
	Checks  []string          `json:"checks"`
	Counts  map[string]int    `json:"counts"`
	Scanned int               `json:"scanned,omitempty"`
	Keys    map[string]string `json:"keys"`
}

type Store struct {
	mu  sync.Mutex
	dir string
}

func Open(dir string) *Store {
	return &Store{dir: dir}
}

func DefaultDir() (string, error) {
	dir, err := os.UserConfigDir()
	if err != nil {
		return "", fmt.Errorf("baselines: %w", err)
	}
	return filepath.Join(dir, "spinoza", "baselines"), nil
}

func (s *Store) fileFor(cluster string) string {
	sum := sha256.Sum256([]byte(cluster))
	return filepath.Join(s.dir, hex.EncodeToString(sum[:])[:nameLength]+".json")
}

func (s *Store) Load(cluster string) (checks.Baseline, bool) {
	s.mu.Lock()
	defer s.mu.Unlock()
	if s.dir == "" {
		return checks.Baseline{}, false
	}
	body, err := filetx.Read(s.fileFor(cluster), maxBytes)
	if err != nil {
		return checks.Baseline{}, false
	}
	taken, err := Decode(body)
	if err != nil {
		return checks.Baseline{}, false
	}
	return taken, true
}

func (s *Store) Save(cluster string, taken checks.Baseline) error {
	s.mu.Lock()
	defer s.mu.Unlock()
	if s.dir == "" {
		return nil
	}
	if err := os.MkdirAll(s.dir, 0o700); err != nil {
		return fmt.Errorf("baselines: %w", err)
	}
	body, err := Encode(taken)
	if err != nil {
		return err
	}
	if saveErr := atomicfile.Save(s.fileFor(cluster), "baseline-*.json", body); saveErr != nil {
		return fmt.Errorf("baselines: %w", saveErr)
	}
	return nil
}

func (s *Store) Clear(cluster string) error {
	s.mu.Lock()
	defer s.mu.Unlock()
	if s.dir == "" {
		return nil
	}
	err := os.Remove(s.fileFor(cluster))
	if errors.Is(err, fs.ErrNotExist) {
		return nil
	}
	if err != nil {
		return fmt.Errorf("baselines: %w", err)
	}
	return nil
}

func flatten(taken checks.Baseline) stored {
	return stored{
		Cluster: taken.Cluster,
		TakenAt: taken.TakenAt,
		Checks:  taken.Checks,
		Counts:  taken.Counts,
		Scanned: taken.Scanned,
		Keys:    taken.Keys,
	}
}

func Encode(taken checks.Baseline) ([]byte, error) {
	if err := validate(taken); err != nil {
		return nil, err
	}
	body, err := json.Marshal(flatten(taken))
	if err != nil {
		return nil, fmt.Errorf("baselines: %w", err)
	}
	if len(body) > maxBytes {
		return nil, fmt.Errorf("baselines: %d bytes is more than one baseline holds", len(body))
	}
	return body, nil
}

func Decode(body []byte) (checks.Baseline, error) {
	if len(body) > maxBytes {
		return checks.Baseline{}, fmt.Errorf("baselines: %d bytes is more than one baseline holds", len(body))
	}
	return DecodeReader(bytes.NewReader(body))
}

func DecodeReader(reader io.Reader) (checks.Baseline, error) {
	tracked := &errorReader{reader: reader}
	limited := &io.LimitedReader{R: tracked, N: maxBytes + 1}
	held, err := decodeStored(json.NewDecoder(limited))
	if tracked.err != nil {
		return checks.Baseline{}, fmt.Errorf("%w: %w", ErrRead, tracked.err)
	}
	if limited.N == 0 {
		return checks.Baseline{}, fmt.Errorf("baselines: more than %d bytes is more than one baseline holds", maxBytes)
	}
	if err != nil {
		return checks.Baseline{}, fmt.Errorf("baselines: this is not a baseline: %w", err)
	}
	return asBaseline(held)
}

func decodeStored(decoder *json.Decoder) (stored, error) {
	opening, err := decoder.Token()
	if err != nil {
		return stored{}, err
	}
	delim, isDelim := opening.(json.Delim)
	if !isDelim || delim != '{' {
		return stored{}, errors.New("the top-level value is not an object")
	}
	var held stored
	for decoder.More() {
		field, fieldErr := decoder.Token()
		if fieldErr != nil {
			return stored{}, fieldErr
		}
		name, isString := field.(string)
		if !isString {
			return stored{}, errors.New("a baseline field name is not a string")
		}
		if decodeErr := decodeStoredField(decoder, &held, name); decodeErr != nil {
			return stored{}, decodeErr
		}
	}
	if _, closingErr := decoder.Token(); closingErr != nil {
		return stored{}, closingErr
	}
	var trailing any
	trailingErr := decoder.Decode(&trailing)
	if !errors.Is(trailingErr, io.EOF) {
		if trailingErr == nil {
			return stored{}, errors.New("the baseline has a trailing value")
		}
		return stored{}, trailingErr
	}
	return held, nil
}

func decodeStoredField(decoder *json.Decoder, held *stored, name string) error {
	var err error
	switch name {
	case "cluster":
		err = decoder.Decode(&held.Cluster)
	case "takenAt":
		err = decoder.Decode(&held.TakenAt)
	case "checks":
		held.Checks, err = decodeChecks(decoder)
	case "counts":
		held.Counts, err = decodeCounts(decoder)
	case "scanned":
		err = decoder.Decode(&held.Scanned)
	case "keys":
		held.Keys, err = decodeKeys(decoder)
	default:
		return fmt.Errorf("unknown field %q", name)
	}
	return err
}

func decodeChecks(decoder *json.Decoder) ([]string, error) {
	opening, err := decoder.Token()
	if err != nil {
		return nil, err
	}
	delim, isDelim := opening.(json.Delim)
	if !isDelim || delim != '[' {
		return nil, errors.New("baseline checks are not a list")
	}
	names := make([]string, 0, min(maxChecks, 16))
	for decoder.More() {
		if len(names) == maxChecks {
			return nil, fmt.Errorf("%d checks is more than one baseline holds", maxChecks+1)
		}
		var check string
		if err := decoder.Decode(&check); err != nil {
			return nil, err
		}
		names = append(names, check)
	}
	if _, err := decoder.Token(); err != nil {
		return nil, err
	}
	return names, nil
}

func decodeCounts(decoder *json.Decoder) (map[string]int, error) {
	opening, err := decoder.Token()
	if err != nil {
		return nil, err
	}
	if opening == nil {
		return map[string]int{}, nil
	}
	delim, isDelim := opening.(json.Delim)
	if !isDelim || delim != '{' {
		return nil, errors.New("baseline counts are not an object")
	}
	counts := make(map[string]int)
	for decoder.More() {
		if len(counts) == maxCounts {
			return nil, fmt.Errorf("%d counts is more than one baseline holds", maxCounts+1)
		}
		field, fieldErr := decoder.Token()
		if fieldErr != nil {
			return nil, fieldErr
		}
		check, isString := field.(string)
		if !isString {
			return nil, errors.New("a baseline count name is not a string")
		}
		var count int
		if err := decoder.Decode(&count); err != nil {
			return nil, err
		}
		counts[check] = count
	}
	if _, err := decoder.Token(); err != nil {
		return nil, err
	}
	return counts, nil
}

func decodeKeys(decoder *json.Decoder) (map[string]string, error) {
	opening, err := decoder.Token()
	if err != nil {
		return nil, err
	}
	if opening == nil {
		return map[string]string{}, nil
	}
	delim, isDelim := opening.(json.Delim)
	if !isDelim || delim != '{' {
		return nil, errors.New("baseline keys are not an object")
	}
	keys := make(map[string]string)
	contentBytes := 0
	for decoder.More() {
		if len(keys) == maxKeys {
			return nil, fmt.Errorf("%d findings is more than one baseline holds", maxKeys+1)
		}
		field, fieldErr := decoder.Token()
		if fieldErr != nil {
			return nil, fieldErr
		}
		key, isString := field.(string)
		if !isString {
			return nil, errors.New("a baseline key is not a string")
		}
		var value string
		if err := decoder.Decode(&value); err != nil {
			return nil, err
		}
		contentBytes += len(key) + len(value)
		if contentBytes > maxKeyValueBytes {
			return nil, fmt.Errorf("baseline finding keys and values exceed %d bytes", maxKeyValueBytes)
		}
		keys[key] = value
	}
	if _, err := decoder.Token(); err != nil {
		return nil, err
	}
	return keys, nil
}

func asBaseline(held stored) (checks.Baseline, error) {
	keys := held.Keys
	if keys == nil {
		keys = map[string]string{}
	}
	taken := checks.Baseline{
		TakenAt: held.TakenAt,
		Cluster: held.Cluster,
		Checks:  held.Checks,
		Counts:  held.Counts,
		Scanned: held.Scanned,
		Keys:    keys,
	}
	if err := validate(taken); err != nil {
		return checks.Baseline{}, err
	}
	return taken, nil
}

func validate(taken checks.Baseline) error {
	if taken.TakenAt == "" || len(taken.Checks) == 0 {
		return errors.New("baselines: this file names no checks and no day it was taken")
	}
	if len(taken.Keys) > maxKeys {
		return fmt.Errorf("baselines: %d findings is more than one baseline holds", len(taken.Keys))
	}
	if len(taken.Checks) > maxChecks {
		return fmt.Errorf("baselines: %d checks is more than one baseline holds", len(taken.Checks))
	}
	if len(taken.Counts) > maxCounts {
		return fmt.Errorf("baselines: %d counts is more than one baseline holds", len(taken.Counts))
	}
	contentBytes := 0
	for key, value := range taken.Keys {
		contentBytes += len(key) + len(value)
		if contentBytes > maxKeyValueBytes {
			return fmt.Errorf("baselines: finding keys and values exceed %d bytes", maxKeyValueBytes)
		}
	}
	return nil
}