Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions cmd/simrun/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ func main() {

// Create stores
runStore := db.NewRunStore(database.Pool)
scenarioStore := db.NewScenarioStore(database.Pool)
scenarioStore := db.NewAssessmentStore(database.Pool)
packStore := db.NewPackStore(database.Pool)
configStore := db.NewConfigStore(database.Pool)
secretStore := db.NewSecretStore(database.Pool)
Expand Down Expand Up @@ -130,8 +130,8 @@ func main() {
log.Warnf("Retention sweep: failed to load config: %v", err)
return
}
web.SweepRunLogs(bootstrap.DataDir, cfg.AssessmentLogRetentionEnabled, cfg.AssessmentLogRetentionDays)
web.SweepAssessments(ctx, runStore, bootstrap.DataDir, cfg.AssessmentRetentionEnabled, cfg.AssessmentRetentionDays)
web.SweepRunLogs(bootstrap.DataDir, cfg.RunLogRetentionEnabled, cfg.RunLogRetentionDays)
web.SweepRuns(ctx, runStore, bootstrap.DataDir, cfg.RunRetentionEnabled, cfg.RunRetentionDays)
}
sweep()
for {
Expand Down
2 changes: 1 addition & 1 deletion internal/collectors/collector.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import (
type Collector interface {
// Collect searches for logs matching the configured query and indicators,
// and writes them to the output file. This is called once at the end of
// scenario execution (when assertions pass or timeout is reached).
// scenario execution (when expectations pass or timeout is reached).
// It returns the number of documents collected and any error that occurred.
Collect(ctx context.Context, indicators map[string]string) (int, error)

Expand Down
32 changes: 16 additions & 16 deletions internal/config/appconfig.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,27 +7,27 @@ package config
// belongs in connectors, anything secret belongs in secret_groups, anything
// set at deploy belongs in Bootstrap.
type AppConfig struct {
Parallelism int `json:"parallelism"`
TerraformVersion string `json:"terraform_version"`
PackLogsEnabled bool `json:"pack_logs_enabled"`
SSHLoggingEnabled bool `json:"ssh_logging_enabled"`
AssessmentLogRetentionEnabled bool `json:"assessment_log_retention_enabled"`
AssessmentLogRetentionDays int `json:"assessment_log_retention_days"`
AssessmentRetentionEnabled bool `json:"assessment_retention_enabled"`
AssessmentRetentionDays int `json:"assessment_retention_days"`
Parallelism int `json:"parallelism"`
TerraformVersion string `json:"terraform_version"`
PackLogsEnabled bool `json:"pack_logs_enabled"`
SSHLoggingEnabled bool `json:"ssh_logging_enabled"`
RunLogRetentionEnabled bool `json:"run_log_retention_enabled"`
RunLogRetentionDays int `json:"run_log_retention_days"`
RunRetentionEnabled bool `json:"run_retention_enabled"`
RunRetentionDays int `json:"run_retention_days"`
}

// DefaultAppConfig returns the default values used when no row exists for
// a key. Keep these aligned with the migration that backfills app_config.
func DefaultAppConfig() AppConfig {
return AppConfig{
Parallelism: 5,
TerraformVersion: "",
PackLogsEnabled: true,
SSHLoggingEnabled: false,
AssessmentLogRetentionEnabled: true,
AssessmentLogRetentionDays: 7,
AssessmentRetentionEnabled: false,
AssessmentRetentionDays: 30,
Parallelism: 5,
TerraformVersion: "",
PackLogsEnabled: true,
SSHLoggingEnabled: false,
RunLogRetentionEnabled: true,
RunLogRetentionDays: 7,
RunRetentionEnabled: false,
RunRetentionDays: 30,
}
}
16 changes: 8 additions & 8 deletions internal/config/appconfig_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,13 @@ import (

func TestDefaultAppConfig(t *testing.T) {
assert.Equal(t, AppConfig{
Parallelism: 5,
TerraformVersion: "",
PackLogsEnabled: true,
SSHLoggingEnabled: false,
AssessmentLogRetentionEnabled: true,
AssessmentLogRetentionDays: 7,
AssessmentRetentionEnabled: false,
AssessmentRetentionDays: 30,
Parallelism: 5,
TerraformVersion: "",
PackLogsEnabled: true,
SSHLoggingEnabled: false,
RunLogRetentionEnabled: true,
RunLogRetentionDays: 7,
RunRetentionEnabled: false,
RunRetentionDays: 30,
}, DefaultAppConfig())
}
202 changes: 202 additions & 0 deletions internal/db/assessments.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
package db

import (
"context"
"strconv"
"strings"
"time"

"github.com/google/uuid"
"github.com/jackc/pgx/v5/pgxpool"
)

// AssessmentStore manages saved assessment (definition) YAML persistence.
type AssessmentStore interface {
Save(ctx context.Context, name, assessmentType, yaml, createdBy string) (*Assessment, error)
Get(ctx context.Context, id uuid.UUID) (*Assessment, error)
// GetByName returns the assessment with the given unique name.
GetByName(ctx context.Context, name string) (*Assessment, error)
// List returns a filtered, paginated slice of assessments for the UI.
List(ctx context.Context, filters ListAssessmentsFilters, limit, offset int) (AssessmentPage, error)
// ListAll returns every assessment in updated_at DESC order. For internal
// callers (e.g. coverage maps) that need the full set in one shot.
ListAll(ctx context.Context) ([]Assessment, error)
Update(ctx context.Context, id uuid.UUID, name, assessmentType, yaml, updatedBy string) error
Delete(ctx context.Context, id uuid.UUID) error
}

// Assessment represents a saved assessment definition (the saved YAML).
type Assessment struct {
ID uuid.UUID `json:"id"`
Name string `json:"name"`
Type string `json:"type"`
YAML string `json:"yaml"`
CreatedBy string `json:"createdBy"`
UpdatedBy string `json:"updatedBy"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}

// AssessmentPage is a paginated slice of assessments with the total row count.
type AssessmentPage struct {
Assessments []Assessment `json:"assessments"`
Total int `json:"total"`
}

// ListAssessmentsFilters narrows the result set for AssessmentStore.List. Zero
// values mean "no constraint on this dimension".
type ListAssessmentsFilters struct {
// Name is an ILIKE %name% match against assessments.name.
Name string
// Types restricts assessments.type to the listed values.
Types []string
// Since restricts assessments to updated_at >= Since.
Since *time.Time
}

type assessmentStore struct {
pool *pgxpool.Pool
}

// NewAssessmentStore creates a new AssessmentStore backed by PostgreSQL.
func NewAssessmentStore(pool *pgxpool.Pool) AssessmentStore {
return &assessmentStore{pool: pool}
}

func (s *assessmentStore) Save(ctx context.Context, name, assessmentType, yaml, createdBy string) (*Assessment, error) {
var sc Assessment
err := s.pool.QueryRow(ctx,
`INSERT INTO assessments (name, type, yaml, created_by, updated_by) VALUES ($1, $2, $3, $4, $4)
RETURNING id, name, type, yaml, created_by, updated_by, created_at, updated_at`,
name, assessmentType, yaml, createdBy,
).Scan(&sc.ID, &sc.Name, &sc.Type, &sc.YAML, &sc.CreatedBy, &sc.UpdatedBy, &sc.CreatedAt, &sc.UpdatedAt)
if err != nil {
return nil, err
}
return &sc, nil
}

func (s *assessmentStore) Get(ctx context.Context, id uuid.UUID) (*Assessment, error) {
var sc Assessment
err := s.pool.QueryRow(ctx,
`SELECT id, name, type, yaml, created_by, updated_by, created_at, updated_at FROM assessments WHERE id = $1`, id,
).Scan(&sc.ID, &sc.Name, &sc.Type, &sc.YAML, &sc.CreatedBy, &sc.UpdatedBy, &sc.CreatedAt, &sc.UpdatedAt)
if err != nil {
return nil, err
}
return &sc, nil
}

func (s *assessmentStore) GetByName(ctx context.Context, name string) (*Assessment, error) {
var sc Assessment
err := s.pool.QueryRow(ctx,
`SELECT id, name, type, yaml, created_by, updated_by, created_at, updated_at FROM assessments WHERE name = $1`, name,
).Scan(&sc.ID, &sc.Name, &sc.Type, &sc.YAML, &sc.CreatedBy, &sc.UpdatedBy, &sc.CreatedAt, &sc.UpdatedAt)
if err != nil {
return nil, err
}
return &sc, nil
}

func (s *assessmentStore) List(ctx context.Context, filters ListAssessmentsFilters, limit, offset int) (AssessmentPage, error) {
where, args := buildAssessmentsWhere(filters)
rows, err := s.pool.Query(ctx,
`SELECT id, name, type, yaml, created_by, updated_by, created_at, updated_at,
COUNT(*) OVER() AS total_count
FROM assessments
`+where+`
ORDER BY updated_at DESC
LIMIT $`+strconv.Itoa(len(args)+1)+` OFFSET $`+strconv.Itoa(len(args)+2),
append(args, limit, offset)...,
)
if err != nil {
return AssessmentPage{}, err
}
defer rows.Close()

page := AssessmentPage{Assessments: []Assessment{}}
for rows.Next() {
var sc Assessment
var total int
if err := rows.Scan(&sc.ID, &sc.Name, &sc.Type, &sc.YAML, &sc.CreatedBy, &sc.UpdatedBy, &sc.CreatedAt, &sc.UpdatedAt, &total); err != nil {
return AssessmentPage{}, err
}
page.Assessments = append(page.Assessments, sc)
page.Total = total
}
if err := rows.Err(); err != nil {
return AssessmentPage{}, err
}
if len(page.Assessments) == 0 {
// COUNT(*) OVER() collapses to no rows when LIMIT/OFFSET yields nothing.
// Re-run the same filter as a plain COUNT so the UI can show "of N".
countSQL := `SELECT COUNT(*) FROM assessments ` + where
if err := s.pool.QueryRow(ctx, countSQL, args...).Scan(&page.Total); err != nil {
return AssessmentPage{}, err
}
}
return page, nil
}

func (s *assessmentStore) ListAll(ctx context.Context) ([]Assessment, error) {
rows, err := s.pool.Query(ctx,
`SELECT id, name, type, yaml, created_by, updated_by, created_at, updated_at
FROM assessments ORDER BY updated_at DESC`,
)
if err != nil {
return nil, err
}
defer rows.Close()

assessments := []Assessment{}
for rows.Next() {
var sc Assessment
if err := rows.Scan(&sc.ID, &sc.Name, &sc.Type, &sc.YAML, &sc.CreatedBy, &sc.UpdatedBy, &sc.CreatedAt, &sc.UpdatedAt); err != nil {
return nil, err
}
assessments = append(assessments, sc)
}
return assessments, rows.Err()
}

// buildAssessmentsWhere returns a WHERE clause (or "") and its positional args
// for assessments. Placeholders are $1..$N in argument order.
func buildAssessmentsWhere(f ListAssessmentsFilters) (string, []any) {
var clauses []string
var args []any
if f.Name != "" {
args = append(args, "%"+f.Name+"%")
clauses = append(clauses, "name ILIKE $"+strconv.Itoa(len(args)))
}
if len(f.Types) > 0 {
placeholders := make([]string, len(f.Types))
for i, t := range f.Types {
args = append(args, t)
placeholders[i] = "$" + strconv.Itoa(len(args))
}
clauses = append(clauses, "type IN ("+strings.Join(placeholders, ",")+")")
}
if f.Since != nil {
args = append(args, *f.Since)
clauses = append(clauses, "updated_at >= $"+strconv.Itoa(len(args)))
}
if len(clauses) == 0 {
return "", args
}
return "WHERE " + strings.Join(clauses, " AND "), args
}

func (s *assessmentStore) Update(ctx context.Context, id uuid.UUID, name, assessmentType, yaml, updatedBy string) error {
_, err := s.pool.Exec(ctx,
`UPDATE assessments SET name = $2, type = $3, yaml = $4, updated_by = $5, updated_at = NOW() WHERE id = $1`,
id, name, assessmentType, yaml, updatedBy,
)
return err
}

func (s *assessmentStore) Delete(ctx context.Context, id uuid.UUID) error {
_, err := s.pool.Exec(ctx,
`DELETE FROM assessments WHERE id = $1`, id,
)
return err
}
24 changes: 12 additions & 12 deletions internal/db/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,28 +96,28 @@ func parseAppConfig(all map[string]json.RawMessage) config.AppConfig {
cfg.SSHLoggingEnabled = b
}
}
if v, ok := all["assessment_log_retention_enabled"]; ok {
if v, ok := all["run_log_retention_enabled"]; ok {
var b bool
if err := json.Unmarshal(v, &b); err == nil {
cfg.AssessmentLogRetentionEnabled = b
cfg.RunLogRetentionEnabled = b
}
}
if v, ok := all["assessment_log_retention_days"]; ok {
if v, ok := all["run_log_retention_days"]; ok {
var n int
if err := json.Unmarshal(v, &n); err == nil && n > 0 {
cfg.AssessmentLogRetentionDays = n
cfg.RunLogRetentionDays = n
}
}
if v, ok := all["assessment_retention_enabled"]; ok {
if v, ok := all["run_retention_enabled"]; ok {
var b bool
if err := json.Unmarshal(v, &b); err == nil {
cfg.AssessmentRetentionEnabled = b
cfg.RunRetentionEnabled = b
}
}
if v, ok := all["assessment_retention_days"]; ok {
if v, ok := all["run_retention_days"]; ok {
var n int
if err := json.Unmarshal(v, &n); err == nil && n > 0 {
cfg.AssessmentRetentionDays = n
cfg.RunRetentionDays = n
}
}

Expand All @@ -138,10 +138,10 @@ func appConfigKVs(c config.AppConfig) []appConfigKV {
{"terraform_version", c.TerraformVersion},
{"pack_logs_enabled", c.PackLogsEnabled},
{"ssh_logging_enabled", c.SSHLoggingEnabled},
{"assessment_log_retention_enabled", c.AssessmentLogRetentionEnabled},
{"assessment_log_retention_days", c.AssessmentLogRetentionDays},
{"assessment_retention_enabled", c.AssessmentRetentionEnabled},
{"assessment_retention_days", c.AssessmentRetentionDays},
{"run_log_retention_enabled", c.RunLogRetentionEnabled},
{"run_log_retention_days", c.RunLogRetentionDays},
{"run_retention_enabled", c.RunRetentionEnabled},
{"run_retention_days", c.RunRetentionDays},
}
}

Expand Down
Loading