284 lines
6.6 KiB
Go
284 lines
6.6 KiB
Go
/*
|
|
©AngelaMos | 2026
|
|
main.go
|
|
|
|
CLI entry point for docksec with scan, version, and benchmark subcommands
|
|
|
|
Builds a cobra command tree with three subcommands: scan (the primary
|
|
path), version, and benchmark (list/show CIS controls). Signal handling
|
|
via signal.NotifyContext ensures graceful shutdown on SIGINT/SIGTERM.
|
|
Scan flags map directly to config.Config fields.
|
|
|
|
Connects to:
|
|
config/config.go - Config struct populated from cobra flags
|
|
scanner/scanner.go - Scanner constructed and Run() called from runScan
|
|
benchmark/controls.go - All() and Get() used by benchmark subcommands
|
|
*/
|
|
|
|
package main
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"os/signal"
|
|
"sort"
|
|
"syscall"
|
|
|
|
"github.com/CarterPerez-dev/docksec/internal/benchmark"
|
|
"github.com/CarterPerez-dev/docksec/internal/config"
|
|
"github.com/CarterPerez-dev/docksec/internal/scanner"
|
|
"github.com/spf13/cobra"
|
|
)
|
|
|
|
var (
|
|
version = "dev"
|
|
commit = "none"
|
|
buildDate = "unknown"
|
|
)
|
|
|
|
func main() {
|
|
os.Exit(run())
|
|
}
|
|
|
|
func run() int {
|
|
ctx, cancel := signal.NotifyContext(
|
|
context.Background(),
|
|
syscall.SIGINT,
|
|
syscall.SIGTERM,
|
|
)
|
|
defer cancel()
|
|
|
|
if err := newRootCmd().ExecuteContext(ctx); err != nil {
|
|
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
|
return 1
|
|
}
|
|
return 0
|
|
}
|
|
|
|
func newRootCmd() *cobra.Command {
|
|
cfg := config.New()
|
|
|
|
root := &cobra.Command{
|
|
Use: "docksec",
|
|
Short: "Docker security audit tool",
|
|
Long: `docksec scans Docker environments for security misconfigurations,
|
|
validates against CIS Docker Benchmark controls, and generates
|
|
actionable remediation reports.`,
|
|
SilenceUsage: true,
|
|
SilenceErrors: true,
|
|
}
|
|
|
|
root.AddCommand(
|
|
newScanCmd(cfg),
|
|
newVersionCmd(),
|
|
newBenchmarkCmd(),
|
|
)
|
|
|
|
return root
|
|
}
|
|
|
|
func newScanCmd(cfg *config.Config) *cobra.Command {
|
|
cmd := &cobra.Command{
|
|
Use: "scan",
|
|
Short: "Scan Docker environment for security issues",
|
|
Long: `Scan running containers, images, Dockerfiles, and docker-compose files
|
|
for security misconfigurations and CIS Docker Benchmark violations.`,
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
return runScan(cmd.Context(), cfg)
|
|
},
|
|
}
|
|
|
|
flags := cmd.Flags()
|
|
|
|
flags.StringSliceVarP(&cfg.Targets, "target", "t", []string{"all"},
|
|
"Scan targets: all, containers, daemon, images")
|
|
|
|
flags.StringSliceVarP(&cfg.Files, "file", "f", nil,
|
|
"Dockerfile or docker-compose.yml files to scan")
|
|
|
|
flags.StringVarP(&cfg.Output, "output", "o", "terminal",
|
|
"Output format: terminal, json, sarif, junit")
|
|
|
|
flags.StringVar(&cfg.OutputFile, "output-file", "",
|
|
"Write output to file instead of stdout")
|
|
|
|
flags.StringSliceVar(&cfg.Severity, "severity", nil,
|
|
"Filter by severity: info, low, medium, high, critical")
|
|
|
|
flags.StringSliceVar(&cfg.CISControls, "cis", nil,
|
|
"Filter by specific CIS control IDs (e.g., 5.4,5.31)")
|
|
|
|
flags.StringSliceVar(&cfg.ExcludeContainers, "exclude-container", nil,
|
|
"Exclude containers by name pattern")
|
|
|
|
flags.StringSliceVar(&cfg.IncludeContainers, "include-container", nil,
|
|
"Include only containers matching name pattern")
|
|
|
|
flags.StringVar(
|
|
&cfg.FailOn,
|
|
"fail-on",
|
|
"",
|
|
"Exit with code 1 if findings at or above severity: low, medium, high, critical",
|
|
)
|
|
|
|
flags.BoolVarP(&cfg.Quiet, "quiet", "q", false,
|
|
"Minimal output (counts only)")
|
|
|
|
flags.BoolVarP(&cfg.Verbose, "verbose", "v", false,
|
|
"Verbose output for debugging")
|
|
|
|
flags.IntVar(&cfg.Workers, "workers", config.DefaultWorkerCount,
|
|
"Number of concurrent workers")
|
|
|
|
return cmd
|
|
}
|
|
|
|
func newVersionCmd() *cobra.Command {
|
|
return &cobra.Command{
|
|
Use: "version",
|
|
Short: "Print version information",
|
|
Run: func(cmd *cobra.Command, args []string) {
|
|
fmt.Printf("docksec %s\n", version)
|
|
fmt.Printf(" commit: %s\n", commit)
|
|
fmt.Printf(" built: %s\n", buildDate)
|
|
},
|
|
}
|
|
}
|
|
|
|
func newBenchmarkCmd() *cobra.Command {
|
|
cmd := &cobra.Command{
|
|
Use: "benchmark",
|
|
Short: "CIS Docker Benchmark information",
|
|
}
|
|
|
|
cmd.AddCommand(
|
|
&cobra.Command{
|
|
Use: "list",
|
|
Short: "List all available CIS controls",
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
return listBenchmarkControls()
|
|
},
|
|
},
|
|
&cobra.Command{
|
|
Use: "show [control-id]",
|
|
Short: "Show details of a specific CIS control",
|
|
Args: cobra.ExactArgs(1),
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
return showBenchmarkControl(args[0])
|
|
},
|
|
},
|
|
)
|
|
|
|
return cmd
|
|
}
|
|
|
|
func runScan(ctx context.Context, cfg *config.Config) (err error) {
|
|
s, err := scanner.New(cfg)
|
|
if err != nil {
|
|
return fmt.Errorf("initializing scanner: %w", err)
|
|
}
|
|
defer func() {
|
|
if closeErr := s.Close(); closeErr != nil && err == nil {
|
|
err = fmt.Errorf("closing scanner: %w", closeErr)
|
|
}
|
|
}()
|
|
|
|
return s.Run(ctx)
|
|
}
|
|
|
|
func listBenchmarkControls() error {
|
|
controls := benchmark.All()
|
|
if len(controls) == 0 {
|
|
fmt.Println("No CIS controls registered.")
|
|
return nil
|
|
}
|
|
|
|
sort.Slice(controls, func(i, j int) bool {
|
|
return controls[i].ID < controls[j].ID
|
|
})
|
|
|
|
grouped := make(map[string][]benchmark.Control)
|
|
for _, c := range controls {
|
|
grouped[c.Section] = append(grouped[c.Section], c)
|
|
}
|
|
|
|
sections := make([]string, 0, len(grouped))
|
|
for section := range grouped {
|
|
sections = append(sections, section)
|
|
}
|
|
sort.Strings(sections)
|
|
|
|
fmt.Println("CIS Docker Benchmark Controls")
|
|
fmt.Println("==============================")
|
|
fmt.Println()
|
|
|
|
for _, section := range sections {
|
|
fmt.Printf("[%s]\n", section)
|
|
for _, c := range grouped[section] {
|
|
levelStr := fmt.Sprintf("L%d", c.Level)
|
|
scoredStr := " "
|
|
if c.Scored {
|
|
scoredStr = "S "
|
|
}
|
|
fmt.Printf(
|
|
" %s %-6s %s %s\n",
|
|
scoredStr,
|
|
c.ID,
|
|
levelStr,
|
|
c.Title,
|
|
)
|
|
}
|
|
fmt.Println()
|
|
}
|
|
|
|
fmt.Printf("Total: %d controls\n", len(controls))
|
|
fmt.Println()
|
|
fmt.Println("Legend: S = Scored, L1/L2 = CIS Level")
|
|
|
|
return nil
|
|
}
|
|
|
|
func showBenchmarkControl(id string) error {
|
|
control, found := benchmark.Get(id)
|
|
if !found {
|
|
return fmt.Errorf("control %q not found", id)
|
|
}
|
|
|
|
fmt.Printf("CIS Control: %s\n", control.ID)
|
|
fmt.Println("============" + repeatChar('=', len(control.ID)))
|
|
fmt.Println()
|
|
|
|
fmt.Printf("Section: %s\n", control.Section)
|
|
fmt.Printf("Title: %s\n", control.Title)
|
|
fmt.Printf("Severity: %s\n", control.Severity.String())
|
|
fmt.Printf("Level: %d\n", control.Level)
|
|
fmt.Printf("Scored: %t\n", control.Scored)
|
|
fmt.Println()
|
|
|
|
fmt.Println("Description:")
|
|
fmt.Printf(" %s\n", control.Description)
|
|
fmt.Println()
|
|
|
|
fmt.Println("Remediation:")
|
|
fmt.Printf(" %s\n", control.Remediation)
|
|
fmt.Println()
|
|
|
|
if len(control.References) > 0 {
|
|
fmt.Println("References:")
|
|
for _, ref := range control.References {
|
|
fmt.Printf(" - %s\n", ref)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func repeatChar(c rune, n int) string {
|
|
result := make([]rune, n)
|
|
for i := range result {
|
|
result[i] = c
|
|
}
|
|
return string(result)
|
|
}
|