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
187 changes: 187 additions & 0 deletions cmd/digest.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
package cmd

import (
"context"
"fmt"
"log"

"github.com/compliance-framework/api/internal/config"
"github.com/compliance-framework/api/internal/service"
"github.com/compliance-framework/api/internal/service/digest"
"github.com/compliance-framework/api/internal/service/email"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"go.uber.org/zap"
)

var (
dryRun bool

DigestCmd = &cobra.Command{
Use: "digest",
Short: "Digest management commands",
}

digestTestCmd = &cobra.Command{
Use: "test",
Short: "Test the digest by sending it immediately to all subscribed users",
Run: runDigestTest,
}

digestPreviewCmd = &cobra.Command{
Use: "preview",
Short: "Preview the digest summary without sending emails",
Run: runDigestPreview,
}
)

func init() {
digestTestCmd.Flags().BoolVar(&dryRun, "dry-run", false, "Show what would be sent without sending emails")
DigestCmd.AddCommand(digestTestCmd)
DigestCmd.AddCommand(digestPreviewCmd)
}

func runDigestTest(cmd *cobra.Command, args []string) {
ctx := context.Background()

var sugar *zap.SugaredLogger
if viper.GetBool("use_dev_logger") {
sugar = zap.Must(zap.NewDevelopment()).Sugar()
} else {
sugar = zap.Must(zap.NewProduction()).Sugar()
}

defer func() {
if err := sugar.Sync(); err != nil {
log.Printf("failed to sync zap logger: %v", err)
}
}()

cfg := config.NewConfig(sugar)

// Check if this is production and add confirmation if not dry-run
if cfg.Environment == "production" && !dryRun {
fmt.Print("⚠️ WARNING: You are about to send digest emails to all subscribed users in PRODUCTION!\n")
fmt.Print("This will send emails to real users. Are you sure you want to continue? (type 'yes' to confirm): ")

var response string
_, err := fmt.Scanln(&response)
if err != nil {
sugar.Fatalw("Failed to read user input", "error", err)
}
if response != "yes" {
fmt.Println("Operation cancelled.")
return
}
}

db, err := service.ConnectSQLDb(ctx, cfg, sugar)
if err != nil {
sugar.Fatalw("Failed to connect to SQL database", "error", err)
}

emailService, err := email.NewService(cfg.Email, sugar)
if err != nil {
sugar.Fatalw("Failed to initialize email service", "error", err)
}

digestService := digest.NewService(db, emailService, cfg, sugar)

if dryRun {
sugar.Info("Running digest test in DRY-RUN mode (no emails will be sent)...")

// Get the digest summary without sending
summary, err := digestService.GetGlobalEvidenceSummary(ctx)
if err != nil {
sugar.Fatalw("Failed to get digest summary", "error", err)
}

sugar.Infow("Digest summary (dry-run)",
"total_evidence", summary.TotalCount,
"satisfied", summary.SatisfiedCount,
"not_satisfied", summary.NotSatisfiedCount,
"expired", summary.ExpiredCount,
"top_not_satisfied_count", len(summary.TopNotSatisfied),
"top_expired_count", len(summary.TopExpired),
)

sugar.Info("Dry-run completed successfully - no emails were sent")
return
}

sugar.Info("Running digest test...")
if err := digestService.SendGlobalDigest(ctx); err != nil {
sugar.Fatalw("Failed to send digest", "error", err)
}

sugar.Info("Digest test completed successfully")
}

func runDigestPreview(cmd *cobra.Command, args []string) {
ctx := context.Background()

var sugar *zap.SugaredLogger
if viper.GetBool("use_dev_logger") {
sugar = zap.Must(zap.NewDevelopment()).Sugar()
} else {
sugar = zap.Must(zap.NewProduction()).Sugar()
}

defer func() {
if err := sugar.Sync(); err != nil {
log.Printf("failed to sync zap logger: %v", err)
}
}()

cfg := config.NewConfig(sugar)

db, err := service.ConnectSQLDb(ctx, cfg, sugar)
if err != nil {
sugar.Fatalw("Failed to connect to SQL database", "error", err)
}

emailService, err := email.NewService(cfg.Email, sugar)
if err != nil {
sugar.Warnw("Failed to initialize email service", "error", err)
}

digestService := digest.NewService(db, emailService, cfg, sugar)

summary, err := digestService.GetGlobalEvidenceSummary(ctx)
if err != nil {
sugar.Fatalw("Failed to get evidence summary", "error", err)
}

users, err := digestService.GetSubscribedUsers(ctx)
if err != nil {
sugar.Fatalw("Failed to get subscribed users", "error", err)
}

fmt.Println("\n=== Evidence Digest Preview ===")
fmt.Printf("Total Evidence: %d\n", summary.TotalCount)
fmt.Printf("Satisfied: %d\n", summary.SatisfiedCount)
fmt.Printf("Not Satisfied: %d\n", summary.NotSatisfiedCount)
fmt.Printf("Expired: %d\n", summary.ExpiredCount)
fmt.Printf("Other: %d\n", summary.OtherCount)
fmt.Printf("\nSubscribed Users: %d\n", len(users))

if len(summary.TopNotSatisfied) > 0 {
fmt.Println("\nTop Not Satisfied Evidence:")
for i, item := range summary.TopNotSatisfied {
fmt.Printf(" %d. %s (UUID: %s)\n", i+1, item.Title, item.UUID)
}
}

if len(summary.TopExpired) > 0 {
fmt.Println("\nTop Expired Evidence:")
for i, item := range summary.TopExpired {
fmt.Printf(" %d. %s (UUID: %s, Expired: %v)\n", i+1, item.Title, item.UUID, item.ExpiresAt)
}
}

if summary.NotSatisfiedCount == 0 && summary.ExpiredCount == 0 {
fmt.Println("\n✓ No issues found - digest would be skipped")
} else {
fmt.Println("\n✓ Digest would be sent to subscribed users")
}
}
7 changes: 7 additions & 0 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ func setDefaultEnvironmentVariables() {
viper.SetDefault("db_debug", "false")
viper.SetDefault("metrics_enabled", "true")
viper.SetDefault("metrics_port", ":9090")
viper.SetDefault("evidence_default_expiry_months", "1")
viper.SetDefault("digest_enabled", "true")
viper.SetDefault("digest_schedule", "@weekly")
}

func bindEnvironmentVariables() {
Expand All @@ -45,6 +48,9 @@ func bindEnvironmentVariables() {
viper.MustBindEnv("metrics_enabled")
viper.MustBindEnv("metrics_port")
viper.MustBindEnv("use_dev_logger")
viper.MustBindEnv("evidence_default_expiry_months")
viper.MustBindEnv("digest_enabled")
viper.MustBindEnv("digest_schedule")
}

func init() {
Expand All @@ -71,6 +77,7 @@ func init() {
rootCmd.AddCommand(seed.RootCmd)
rootCmd.AddCommand(newMigrateCMD())
rootCmd.AddCommand(dashboards.RootCmd)
rootCmd.AddCommand(DigestCmd)
}

func Execute() error {
Expand Down
43 changes: 42 additions & 1 deletion cmd/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,17 @@ package cmd
import (
"context"
"log"
"time"

"github.com/compliance-framework/api/internal/api"
"github.com/compliance-framework/api/internal/api/handler"
"github.com/compliance-framework/api/internal/api/handler/auth"
"github.com/compliance-framework/api/internal/api/handler/oscal"
"github.com/compliance-framework/api/internal/config"
"github.com/compliance-framework/api/internal/service"
"github.com/compliance-framework/api/internal/service/digest"
"github.com/compliance-framework/api/internal/service/email"
"github.com/compliance-framework/api/internal/service/scheduler"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"go.uber.org/zap"
Expand Down Expand Up @@ -51,9 +55,46 @@ func RunServer(cmd *cobra.Command, args []string) {
sugar.Fatalw("Failed to migrate database", "error", err)
}

// Initialize email service
emailService, err := email.NewService(cfg.Email, sugar)
if err != nil {
sugar.Warnw("Failed to initialize email service, digests will be disabled", "error", err)
}

// Initialize digest service
digestService := digest.NewService(db, emailService, cfg, sugar)

// Initialize scheduler
sched := scheduler.NewCronScheduler(sugar)

// Register digest job using config
if cfg.DigestEnabled {
digestJob := digest.NewGlobalDigestJob(digestService, sugar)
if err := sched.ScheduleCron(cfg.DigestSchedule, digestJob); err != nil {
sugar.Warnw("Failed to schedule digest job", "schedule", cfg.DigestSchedule, "error", err)
} else {
sugar.Debugw("Digest job scheduled", "schedule", cfg.DigestSchedule)
}
} else {
sugar.Debugw("Digest scheduler disabled")
}

// Start the scheduler
sched.Start()
defer func() {
stopCtx := sched.Stop()
// Wait for jobs to finish gracefully with a 10-second timeout
select {
case <-stopCtx.Done():
sugar.Debug("All scheduled jobs completed gracefully")
case <-time.After(10 * time.Second):
sugar.Warn("Scheduler shutdown timeout, some jobs may not have completed")
}
}()

metrics := api.NewMetricsHandler(ctx, sugar)
server := api.NewServer(ctx, sugar, cfg, metrics)
handler.RegisterHandlers(server, sugar, db, cfg)
handler.RegisterHandlers(server, sugar, db, cfg, digestService, sched)
oscal.RegisterHandlers(server, sugar, db, cfg)
auth.RegisterHandlers(server, sugar, db, cfg, metrics)

Expand Down
Loading
Loading