f99010fae1
CI / lint (push) Failing after 1s
CI / frontend (push) Failing after 1s
CI / scripts (push) Failing after 1s
CI / Go Test (ubuntu-latest) (push) Failing after 0s
CI / frontend-node-25 (push) Failing after 1s
CI / docs (push) Failing after 0s
CI / coverage (push) Failing after 0s
CI / e2e (push) Failing after 0s
Docker / build-and-push (push) Failing after 1s
CI / integration (push) Failing after 4m43s
CI / Go Test (windows-latest) (push) Has been cancelled
CI / Desktop Unit Tests (Windows) (push) Has been cancelled
Desktop Artifacts / Desktop Build (Linux (arm64)) (push) Has been cancelled
Desktop Artifacts / Desktop Build (Linux) (push) Has been cancelled
Desktop Artifacts / Desktop Build (Windows) (push) Has been cancelled
Desktop Artifacts (macOS) / Desktop Build (macOS (aarch64)) (push) Has been cancelled
Desktop Artifacts (macOS) / Desktop Build (macOS (x86_64)) (push) Has been cancelled
177 lines
5.8 KiB
Go
177 lines
5.8 KiB
Go
// ABOUTME: `secrets` command group: scan for and list detected secret leaks
|
|
// ABOUTME: across sessions, redacted by default.
|
|
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
|
|
"github.com/spf13/cobra"
|
|
"go.kenn.io/agentsview/internal/secrets"
|
|
"go.kenn.io/agentsview/internal/service"
|
|
)
|
|
|
|
func newSecretsCommand() *cobra.Command {
|
|
cmd := &cobra.Command{
|
|
Use: "secrets",
|
|
Short: "Scan for and list detected secret leaks",
|
|
GroupID: groupData,
|
|
SilenceUsage: true,
|
|
Args: cobra.NoArgs,
|
|
RunE: func(cmd *cobra.Command, _ []string) error {
|
|
return cmd.Help()
|
|
},
|
|
}
|
|
registerFormatFlags(cmd.PersistentFlags())
|
|
cmd.AddCommand(newSecretsListCommand())
|
|
cmd.AddCommand(newSecretsScanCommand())
|
|
return cmd
|
|
}
|
|
|
|
func newSecretsScanCommand() *cobra.Command {
|
|
var (
|
|
backfill bool
|
|
project, agent, dateFrom, dateTo string
|
|
)
|
|
cmd := &cobra.Command{
|
|
Use: "scan",
|
|
Short: "Run a full-ruleset scan (definite + candidate rules) and persist findings",
|
|
Long: `Inline sync runs only the fast, well-anchored definite rules. This command
|
|
runs the full ruleset, including candidate-tier detectors (generic
|
|
high-entropy env values, JWTs, basic-auth URLs).
|
|
|
|
By default every matching session is rescanned. Use --backfill to skip
|
|
sessions already at the current ruleset version — the efficient choice
|
|
after a binary upgrade.
|
|
|
|
Findings from candidate rules are hidden by 'secrets list' unless you pass
|
|
--confidence candidate or --confidence all.`,
|
|
Args: cobra.NoArgs,
|
|
SilenceUsage: true,
|
|
RunE: func(cmd *cobra.Command, _ []string) error {
|
|
// Defense in depth: main enables this for normal binaries, but
|
|
// command-level wiring keeps ad-hoc command execution from scanning
|
|
// agentsview's own fixtures as leaks.
|
|
secrets.EnableFixtureDeny()
|
|
svc, cleanup, err := resolveWritableService(cmd)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer cleanup()
|
|
jsonOut := outputFormat(cmd) == "json"
|
|
sum, err := svc.ScanSecrets(cmd.Context(), service.SecretScanInput{
|
|
Backfill: backfill, Project: project, Agent: agent,
|
|
DateFrom: dateFrom, DateTo: dateTo,
|
|
}, func(p service.SecretScanProgress) {
|
|
if !jsonOut {
|
|
fmt.Fprintf(cmd.ErrOrStderr(),
|
|
"\rscanned %d / %d", p.Scanned, p.Total)
|
|
}
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !jsonOut {
|
|
fmt.Fprintln(cmd.ErrOrStderr())
|
|
}
|
|
if jsonOut {
|
|
return json.NewEncoder(cmd.OutOrStdout()).Encode(sum)
|
|
}
|
|
fmt.Fprintf(cmd.OutOrStdout(),
|
|
"Scanned %d sessions; %d with definite leaks; "+
|
|
"%d findings (%d definite, %d candidate).\n",
|
|
sum.Scanned, sum.WithSecrets, sum.TotalFindings,
|
|
sum.DefiniteFindings, sum.CandidateFindings)
|
|
if sum.CandidateFindings > 0 {
|
|
fmt.Fprintln(cmd.OutOrStdout(),
|
|
"\nCandidate findings are hidden by default in 'secrets list'. "+
|
|
"To see them:")
|
|
fmt.Fprintln(cmd.OutOrStdout(),
|
|
" agentsview secrets list --confidence all")
|
|
}
|
|
return nil
|
|
},
|
|
}
|
|
flags := cmd.Flags()
|
|
flags.BoolVar(&backfill, "backfill", false,
|
|
"Skip sessions already scanned at the current ruleset version")
|
|
flags.StringVar(&project, "project", "", "Limit to a project")
|
|
flags.StringVar(&agent, "agent", "", "Limit to an agent")
|
|
flags.StringVar(&dateFrom, "date-from", "", "Sessions on or after YYYY-MM-DD")
|
|
flags.StringVar(&dateTo, "date-to", "", "Sessions on or before YYYY-MM-DD")
|
|
return cmd
|
|
}
|
|
|
|
func newSecretsListCommand() *cobra.Command {
|
|
var (
|
|
project, agent, dateFrom, dateTo string
|
|
rule, confidence string
|
|
reveal bool
|
|
limit, cursor int
|
|
)
|
|
cmd := &cobra.Command{
|
|
Use: "list",
|
|
Short: "List detected secret findings (redacted by default)",
|
|
Args: cobra.NoArgs,
|
|
SilenceUsage: true,
|
|
RunE: func(cmd *cobra.Command, _ []string) error {
|
|
svc, cleanup, err := resolveService(cmd)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer cleanup()
|
|
res, err := svc.ListSecrets(cmd.Context(), service.SecretListFilter{
|
|
Project: project, Agent: agent,
|
|
DateFrom: dateFrom, DateTo: dateTo,
|
|
Rule: rule, Confidence: confidence, Reveal: reveal,
|
|
Limit: limit, Cursor: cursor,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if reveal {
|
|
fmt.Fprintln(cmd.ErrOrStderr(),
|
|
"WARNING: --reveal prints full secret values; "+
|
|
"this terminal/session may itself be recorded.")
|
|
}
|
|
if outputFormat(cmd) == "json" {
|
|
return json.NewEncoder(cmd.OutOrStdout()).Encode(res)
|
|
}
|
|
return printSecretFindingsHuman(cmd.OutOrStdout(), res)
|
|
},
|
|
}
|
|
flags := cmd.Flags()
|
|
flags.StringVar(&project, "project", "", "Filter by project")
|
|
flags.StringVar(&agent, "agent", "", "Filter by agent")
|
|
flags.StringVar(&dateFrom, "date-from", "", "Sessions on or after YYYY-MM-DD")
|
|
flags.StringVar(&dateTo, "date-to", "", "Sessions on or before YYYY-MM-DD")
|
|
flags.StringVar(&rule, "rule", "", "Filter by rule name")
|
|
flags.StringVar(&confidence, "confidence", "",
|
|
"Filter by confidence: definite, candidate, or all "+
|
|
"(default definite; candidates are opt-in)")
|
|
flags.BoolVar(&reveal, "reveal", false, "Show full secret values (unredacted)")
|
|
flags.IntVar(&limit, "limit", 0, "Max findings (default 50, max 500)")
|
|
flags.IntVar(&cursor, "cursor", 0, "Pagination cursor")
|
|
return cmd
|
|
}
|
|
|
|
func printSecretFindingsHuman(w io.Writer, res *service.SecretFindingList) error {
|
|
if len(res.Findings) == 0 {
|
|
fmt.Fprintln(w, "(no findings)")
|
|
return nil
|
|
}
|
|
fmt.Fprintf(w, "%-40s %-16s %-22s %-18s %s\n",
|
|
"SESSION", "PROJECT", "RULE", "LOCATION", "VALUE")
|
|
for _, f := range res.Findings {
|
|
fmt.Fprintf(w, "%-40s %-16s %-22s %-18s %s\n",
|
|
sanitizeTerminal(f.SessionID), sanitizeTerminal(f.Project),
|
|
sanitizeTerminal(f.RuleName), sanitizeTerminal(f.LocationKind),
|
|
sanitizeTerminal(f.RedactedMatch))
|
|
}
|
|
if res.NextCursor != 0 {
|
|
fmt.Fprintf(w, "\nMore results: --cursor %d\n", res.NextCursor)
|
|
}
|
|
return nil
|
|
}
|