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
118 changes: 9 additions & 109 deletions cmd/bagel/scan.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,117 +115,17 @@ func runScan(cmd *cobra.Command, args []string) error {
return nil
}

// initializeProbes creates and configures all probes
// initializeProbes builds the default probe set and keeps only the enabled ones.
// The collector also filters on IsEnabled at execution time, but filtering here
// preserves the historical probe slice (and probe_count log) exactly.
func initializeProbes(cfg *models.Config) []probe.Probe {
var probes []probe.Probe

// Create detector registry and register all secret detectors
registry := detector.NewRegistry()
registry.Register(detector.NewGitHubPATDetector())
registry.Register(detector.NewNPMTokenDetector())
registry.Register(detector.NewSSHPrivateKeyDetector())
registry.Register(detector.NewAIServiceDetector())
registry.Register(detector.NewHTTPAuthDetector())
registry.Register(detector.NewCloudCredentialsDetector())
registry.Register(detector.NewVaultTokenDetector())
registry.Register(detector.NewPyPITokenDetector())
registry.Register(detector.NewWireGuardKeyDetector())
registry.Register(detector.NewSplunkTokenDetector())
registry.Register(detector.NewDatabaseConnectionDetector())
registry.Register(detector.NewSlackTokenDetector())
registry.Register(detector.NewStripeKeyDetector())
registry.Register(detector.NewTwilioKeyDetector())
registry.Register(detector.NewGenericAPIKeyDetector())
registry.Register(detector.NewJWTDetector())
// Add more detectors here as they are implemented:
// registry.Register(detector.NewSlackTokenDetector())
// etc.

// Git probe
if cfg.Probes.Git.Enabled {
probes = append(probes, probe.NewGitProbe(cfg.Probes.Git, registry))
}

// Environment variable probe
if cfg.Probes.Env.Enabled {
probes = append(probes, probe.NewEnvProbe(cfg.Probes.Env, registry))
}

// NPM probe
if cfg.Probes.NPM.Enabled {
probes = append(probes, probe.NewNPMProbe(cfg.Probes.NPM, registry))
}

// SSH probe
if cfg.Probes.SSH.Enabled {
probes = append(probes, probe.NewSSHProbe(cfg.Probes.SSH, registry))
}

// Shell history probe
if cfg.Probes.ShellHistory.Enabled {
probes = append(probes, probe.NewShellHistoryProbe(cfg.Probes.ShellHistory, registry))
}

// Cloud credentials probe
if cfg.Probes.Cloud.Enabled {
probes = append(probes, probe.NewCloudProbe(cfg.Probes.Cloud, registry))
}

// JetBrains probe
if cfg.Probes.JetBrains.Enabled {
probes = append(probes, probe.NewJetBrainsProbe(cfg.Probes.JetBrains, registry))
}

// GitHub CLI probe
if cfg.Probes.GH.Enabled {
probes = append(probes, probe.NewGHProbe(cfg.Probes.GH, registry))
}

// AI credentials probe (auth/oauth files; scan only — scrub leaves these alone)
if cfg.Probes.AICredentials.Enabled {
probes = append(probes, probe.NewAICredentialsProbe(cfg.Probes.AICredentials, registry))
}

// AI chats probe (conversation history)
if cfg.Probes.AIChats.Enabled {
probes = append(probes, probe.NewAIChatsProbe(cfg.Probes.AIChats, registry))
}

// WireGuard probe
if cfg.Probes.WireGuard.Enabled {
probes = append(probes, probe.NewWireGuardProbe(cfg.Probes.WireGuard, registry))
}

// PyPI probe
if cfg.Probes.PyPI.Enabled {
probes = append(probes, probe.NewPyPIProbe(cfg.Probes.PyPI, registry))
}

// Kubernetes probe — credential extraction from kubeconfig
if cfg.Probes.Kube.Enabled {
probes = append(probes, probe.NewKubeProbe(cfg.Probes.Kube, registry))
}
registry := detector.NewDefaultRegistry()

// Docker/Podman probe — inline registry credentials
if cfg.Probes.Docker.Enabled {
probes = append(probes, probe.NewDockerProbe(cfg.Probes.Docker, registry))
}

// Infrastructure-as-Code probe — Terraform + Helm credential discovery
if cfg.Probes.IaC.Enabled {
probes = append(probes, probe.NewIaCProbe(cfg.Probes.IaC, registry))
}

// AI MCP probe — credentials in MCP server configs (scan-only;
// scrub would break agents by replacing real tokens with markers).
if cfg.Probes.AIMCP.Enabled {
probes = append(probes, probe.NewMCPProbe(cfg.Probes.AIMCP, registry))
}

// AI context/memory probe — CLAUDE.md / AGENTS.md scanning.
if cfg.Probes.AIContext.Enabled {
probes = append(probes, probe.NewContextProbe(cfg.Probes.AIContext, registry))
var probes []probe.Probe
for _, p := range probe.DefaultProbes(cfg, registry) {
if p.IsEnabled() {
probes = append(probes, p)
}
}

return probes
}
20 changes: 15 additions & 5 deletions pkg/collector/collector.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ type Collector struct {
config *models.Config
noCache bool
noProgress bool
cacheStore *cache.Store // Reused across load/save operations
cacheStore *cache.Store // Reused across load/save operations
fileIndex *fileindex.FileIndex // Prebuilt index; when non-nil, Collect skips building/caching its own
}

// NewInput holds the parameters for creating a new Collector
Expand All @@ -38,6 +39,10 @@ type NewInput struct {
Config *models.Config
NoCache bool
NoProgress bool
// FileIndex, when non-nil, is used as-is; Collect skips building and
// caching its own index. Lets a caller own the crawl (e.g. share one
// index across bagel's probes and its own).
FileIndex *fileindex.FileIndex
}

// New creates a new Collector
Expand All @@ -53,6 +58,7 @@ func New(input NewInput) *Collector {
noCache: input.NoCache,
noProgress: input.NoProgress,
cacheStore: store,
fileIndex: input.FileIndex,
}
}

Expand All @@ -74,10 +80,14 @@ func (c *Collector) Collect(ctx context.Context) (*models.ScanResult, error) {
return nil, fmt.Errorf("failed to get host info: %w", err)
}

// Build file index if enabled
fileIdx, err := c.buildFileIndex(ctx)
if err != nil {
return nil, fmt.Errorf("failed to build file index: %w", err)
// Use a caller-supplied index as-is; otherwise build (and cache) our own.
fileIdx := c.fileIndex
if fileIdx == nil {
var err error
fileIdx, err = c.buildFileIndex(ctx)
if err != nil {
return nil, fmt.Errorf("failed to build file index: %w", err)
}
}

// Compute fingerprint salt from host identity and propagate to probes
Expand Down
34 changes: 34 additions & 0 deletions pkg/collector/collector_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"testing"
"time"

"github.com/boostsecurityio/bagel/pkg/fileindex"
"github.com/boostsecurityio/bagel/pkg/models"
"github.com/boostsecurityio/bagel/pkg/probe"
"github.com/stretchr/testify/assert"
Expand Down Expand Up @@ -159,6 +160,39 @@ func TestCollect_ProbeErrors(t *testing.T) {
assert.Equal(t, "finding1", result.Findings[0].ID, "expected finding1")
}

// fileIndexProbe records the file index handed to it via SetFileIndex.
type fileIndexProbe struct {
mockProbe
got *fileindex.FileIndex
}

func (p *fileIndexProbe) SetFileIndex(idx *fileindex.FileIndex) { p.got = idx }

// TestCollect_InjectedFileIndex confirms a caller-supplied index is used as-is:
// it reaches FileIndexAware probes and Collect never builds its own (the probe
// receives the exact pointer we injected, which a fresh build could not produce).
func TestCollect_InjectedFileIndex(t *testing.T) {
t.Parallel()

injected, err := fileindex.BuildIndex(context.TODO(), fileindex.BuildIndexInput{
BaseDirs: []string{t.TempDir()},
})
require.NoError(t, err)

prb := &fileIndexProbe{mockProbe: mockProbe{name: "fi", enabled: true}}

// NoCache:false would normally build + touch the cache; injection must skip both.
col := New(NewInput{
Probes: []probe.Probe{prb},
Config: &models.Config{Version: 1},
FileIndex: injected,
})

_, err = col.Collect(context.TODO())
require.NoError(t, err)
assert.Same(t, injected, prb.got, "probe should receive the injected index, not a freshly built one")
}

func TestExecuteProbes_ConcurrentExecution(t *testing.T) {
t.Parallel()

Expand Down
Loading
Loading