internal/baseline/store.go
Rama main · réplica de lectura del repositorio original.
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
}