155 lines
3.1 KiB
Go
155 lines
3.1 KiB
Go
/*
|
|
©AngelaMos | 2026
|
|
baseline.go
|
|
|
|
Baseline subcommand for save and diff operations
|
|
|
|
The save subcommand runs a full scan and saves findings as a JSON
|
|
baseline snapshot. The diff subcommand runs a fresh scan and shows
|
|
only findings that are new since the baseline was taken.
|
|
*/
|
|
|
|
package cli
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"time"
|
|
|
|
"github.com/spf13/cobra"
|
|
|
|
"github.com/CarterPerez-dev/sentinel/internal/baseline"
|
|
"github.com/CarterPerez-dev/sentinel/internal/config"
|
|
"github.com/CarterPerez-dev/sentinel/internal/report"
|
|
"github.com/CarterPerez-dev/sentinel/internal/scanner"
|
|
"github.com/CarterPerez-dev/sentinel/internal/ui"
|
|
"github.com/CarterPerez-dev/sentinel/pkg/types"
|
|
)
|
|
|
|
var flagBaselinePath string
|
|
|
|
func init() {
|
|
baselineCmd.PersistentFlags().StringVar(
|
|
&flagBaselinePath, "file", "sentinel-baseline.json",
|
|
"path to baseline snapshot file",
|
|
)
|
|
baselineCmd.AddCommand(baselineSaveCmd)
|
|
baselineCmd.AddCommand(baselineDiffCmd)
|
|
rootCmd.AddCommand(baselineCmd)
|
|
}
|
|
|
|
var baselineCmd = &cobra.Command{
|
|
Use: "baseline",
|
|
Short: "Manage baseline snapshots",
|
|
}
|
|
|
|
var baselineSaveCmd = &cobra.Command{
|
|
Use: "save",
|
|
Short: "Save current state as baseline",
|
|
RunE: runBaselineSave,
|
|
}
|
|
|
|
var baselineDiffCmd = &cobra.Command{
|
|
Use: "diff",
|
|
Short: "Show changes since baseline",
|
|
RunE: runBaselineDiff,
|
|
}
|
|
|
|
func runBaselineSave(_ *cobra.Command, _ []string) error {
|
|
hostname, err := os.Hostname()
|
|
if err != nil {
|
|
hostname = fallbackHostname
|
|
}
|
|
|
|
var sp *ui.Spinner
|
|
if !flagJSON {
|
|
sp = ui.NewSpinner("Scanning for baseline...")
|
|
sp.Start()
|
|
}
|
|
|
|
findings := scanner.RunAll(flagRoot)
|
|
|
|
if sp != nil {
|
|
sp.Stop()
|
|
}
|
|
|
|
if err := baseline.Save(
|
|
flagBaselinePath, findings, hostname,
|
|
); err != nil {
|
|
return err
|
|
}
|
|
|
|
fmt.Printf(
|
|
" %s Baseline saved to %s (%d findings)\n\n",
|
|
ui.GreenBold(ui.Check),
|
|
ui.Cyan(flagBaselinePath),
|
|
len(findings),
|
|
)
|
|
return nil
|
|
}
|
|
|
|
func runBaselineDiff(_ *cobra.Command, _ []string) error {
|
|
snap, err := baseline.Load(flagBaselinePath)
|
|
if err != nil {
|
|
return fmt.Errorf(
|
|
"load baseline %s: %w",
|
|
flagBaselinePath, err,
|
|
)
|
|
}
|
|
|
|
hostname, err := os.Hostname()
|
|
if err != nil {
|
|
hostname = fallbackHostname
|
|
}
|
|
minSev := types.ParseSeverity(flagMinSeverity)
|
|
|
|
var sp *ui.Spinner
|
|
if !flagJSON {
|
|
sp = ui.NewSpinner("Scanning and comparing to baseline...")
|
|
sp.Start()
|
|
}
|
|
|
|
start := time.Now()
|
|
current := scanner.RunAll(flagRoot)
|
|
duration := time.Since(start)
|
|
|
|
if sp != nil {
|
|
sp.Stop()
|
|
}
|
|
|
|
ignoreList, err := config.LoadIgnoreFile(flagIgnoreFile)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
newFindings := baseline.Diff(snap, current)
|
|
newFindings = ignoreList.Filter(newFindings)
|
|
filtered := filterBySeverity(newFindings, minSev)
|
|
|
|
result := types.ScanResult{
|
|
Version: types.Version,
|
|
ScanTime: start,
|
|
Hostname: hostname,
|
|
Findings: filtered,
|
|
Summary: types.Tally(filtered),
|
|
DurationMs: duration.Milliseconds(),
|
|
}
|
|
|
|
if !flagJSON {
|
|
fmt.Printf(
|
|
" %s Baseline: %d known %s New: %d findings\n\n",
|
|
ui.CyanBold(ui.Diamond),
|
|
len(snap.Findings),
|
|
ui.Dim("|"),
|
|
len(filtered),
|
|
)
|
|
}
|
|
|
|
if flagJSON {
|
|
return report.PrintJSON(result)
|
|
}
|
|
|
|
report.PrintTerminal(result)
|
|
return nil
|
|
}
|