Skip to content
Open
Show file tree
Hide file tree
Changes from 4 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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,6 @@ go.work.sum

/bagel
dist/

# mise-en-place
mise.toml
3 changes: 2 additions & 1 deletion CLA_SIGNATURES.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ SUSTAPLE117 - Alexis-Maurer Fortin
fproulx-boostsecurity - François Proulx
Talgarr - Sebastien Graveline
julien-boost - Julien Champoux
GuillaumeRoss - Guillaume Ross
GuillaumeRoss - Guillaume Ross
jksolbakken - Jan-Kåre Solbakken
27 changes: 15 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

Bagel is a cross‑platform CLI that inspects developer workstations (macOS, Linux, Windows) and produces a structured report of:

* **Dev tool configurations and risky settings** across 9 probes: Git, SSH, npm, environment variables, shell history, cloud credentials (AWS/GCP/Azure), JetBrains IDEs, GitHub CLI, and AI CLI tools.
* **Dev tool configurations and risky settings** across 10 probes: Git, SSH, npm, environment variables, shell history, cloud credentials (AWS/GCP/Azure), JetBrains IDEs, GitHub CLI, AI CLI tools and Docker.
* **Secret locations (metadata only)**: presence of tokens, keys, and credentials in config files, env vars, and history—detected by 8 secret detectors—**never the secret values**.

For detailed documentation on each probe and detector, see the [Bagel docs site](https://boostsecurityio.github.io/bagel/).
Expand Down Expand Up @@ -173,6 +173,8 @@ probes:
enabled: true
ai_cli:
enabled: true
docker:
enabled: true
privacy:
redact_paths: []
exclude_env_prefixes: []
Expand Down Expand Up @@ -241,17 +243,18 @@ Each probe declares its scope (user/system), paths touched, env vars read, and r

### Current Probes

| Probe | Description | What it checks |
|-------|-------------|----------------|
| `git` | Git configuration security | SSL verification disabled, SSH config issues (StrictHostKeyChecking, UserKnownHostsFile), plaintext credential storage (`credential.helper=store`), dangerous protocols (ext, fd, file), fsck disabled, proxy settings, custom hooks path |
| `ssh` | SSH configuration and key security | `StrictHostKeyChecking=no`, `UserKnownHostsFile=/dev/null`, `ForwardAgent=yes`, private key file permissions, unencrypted private keys |
| `npm` | NPM/Yarn configuration | `.npmrc` and `.yarnrc` files: `strict-ssl=false`, HTTP (non-HTTPS) registries, `always-auth` settings |
| `env` | Environment variables and dotfiles | Environment variables, shell config files (`.bashrc`, `.zshrc`), `.env` files for embedded secrets |
| `shell_history` | Shell history files | `.bash_history`, `.zsh_history` for secrets in command history |
| `cloud` | Cloud provider credentials | AWS (`~/.aws/config`, `~/.aws/credentials`), GCP (`~/.config/gcloud/`), Azure config files |
| `jetbrains` | JetBrains IDE configuration | JetBrains IDE workspace files and configuration for embedded secrets |
| `gh` | GitHub CLI | GitHub CLI authentication tokens and configuration |
| `ai_cli` | AI CLI tools | Credential files and chat logs for Gemini, Codex, Claude, and OpenCode |
| Probe | Description | What it checks |
|-----------------|------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `git` | Git configuration security | SSL verification disabled, SSH config issues (StrictHostKeyChecking, UserKnownHostsFile), plaintext credential storage (`credential.helper=store`), dangerous protocols (ext, fd, file), fsck disabled, proxy settings, custom hooks path |
| `ssh` | SSH configuration and key security | `StrictHostKeyChecking=no`, `UserKnownHostsFile=/dev/null`, `ForwardAgent=yes`, private key file permissions, unencrypted private keys |
| `npm` | NPM/Yarn configuration | `.npmrc` and `.yarnrc` files: `strict-ssl=false`, HTTP (non-HTTPS) registries, `always-auth` settings |
| `env` | Environment variables and dotfiles | Environment variables, shell config files (`.bashrc`, `.zshrc`), `.env` files for embedded secrets |
| `shell_history` | Shell history files | `.bash_history`, `.zsh_history` for secrets in command history |
| `cloud` | Cloud provider credentials | AWS (`~/.aws/config`, `~/.aws/credentials`), GCP (`~/.config/gcloud/`), Azure config files |
| `jetbrains` | JetBrains IDE configuration | JetBrains IDE workspace files and configuration for embedded secrets |
| `gh` | GitHub CLI | GitHub CLI authentication tokens and configuration |
| `ai_cli` | AI CLI tools | Credential files and chat logs for Gemini, Codex, Claude, and OpenCode |
| `docker` | Docker | Docker config files with registry creds in cleartext |

### Current Detectors

Expand Down
5 changes: 5 additions & 0 deletions cmd/bagel/scan.go
Original file line number Diff line number Diff line change
Expand Up @@ -168,5 +168,10 @@ func initializeProbes(cfg *models.Config) []probe.Probe {
probes = append(probes, probe.NewAICliProbe(cfg.Probes.AICli, registry))
}

// Docker credentials probe
if cfg.Probes.DockerCreds.Enabled {
probes = append(probes, probe.NewDockerCredsProbe(cfg.Probes.DockerCreds))
}

return probes
}
6 changes: 5 additions & 1 deletion pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ func setDefaults(v *viper.Viper) {
v.SetDefault("probes.jetbrains.enabled", true)
v.SetDefault("probes.gh.enabled", true)
v.SetDefault("probes.ai_cli.enabled", true)
v.SetDefault("probes.docker.enabled", true)
v.SetDefault("output.include_file_hashes", false)
v.SetDefault("output.include_file_content", false)

Expand Down Expand Up @@ -142,7 +143,10 @@ func setDefaults(v *viper.Viper) {
}, "type": "glob"},

// Docker
{"name": "docker_config", "patterns": []string{".docker/config.json"}, "type": "glob"},
{"name": "docker_config", "patterns": []string{
".docker/config.json",
".config/containers/auth.json",
}, "type": "glob"},

// Kubernetes
{"name": "kubeconfig", "patterns": []string{".kube/config"}, "type": "glob"},
Expand Down
1 change: 1 addition & 0 deletions pkg/models/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ type ProbeConfig struct {
JetBrains ProbeSettings `yaml:"jetbrains" mapstructure:"jetbrains"`
GH ProbeSettings `yaml:"gh" mapstructure:"gh"`
AICli ProbeSettings `yaml:"ai_cli" mapstructure:"ai_cli"`
DockerCreds ProbeSettings `yaml:"docker" mapstructure:"docker"`
}

// ProbeSettings contains settings for a specific probe
Expand Down
108 changes: 108 additions & 0 deletions pkg/probe/docker_creds.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
// Copyright (C) 2026 boostsecurity.io
// SPDX-License-Identifier: GPL-3.0-or-later

package probe

import (
"context"
"encoding/json"
"fmt"
"os"

"github.com/boostsecurityio/bagel/pkg/fileindex"
"github.com/boostsecurityio/bagel/pkg/models"
"github.com/rs/zerolog/log"
)

// DockerCredsProbe checks for Docker registry creds in cleartext
type DockerCredsProbe struct {
enabled bool
config models.ProbeSettings
fileIndex *fileindex.FileIndex
}

// NewDockerCredsProbe creates a new cloud credentials probe
func NewDockerCredsProbe(config models.ProbeSettings) *DockerCredsProbe {
return &DockerCredsProbe{
enabled: config.Enabled,
config: config,
}
}

// Name returns the probe name
func (dcp *DockerCredsProbe) Name() string {
return "Docker credentials"
}

// IsEnabled returns whether the probe is enabled
func (dcp *DockerCredsProbe) IsEnabled() bool {
return dcp.enabled
}

// SetFileIndex sets the file index for this probe (implements FileIndexAware)
func (dcp *DockerCredsProbe) SetFileIndex(index *fileindex.FileIndex) {
dcp.fileIndex = index
}

// Execute runs the Docker credentials probe
func (dcp *DockerCredsProbe) Execute(ctx context.Context) ([]models.Finding, error) {
var findings []models.Finding

// If file index is not available, skip probe
if dcp.fileIndex == nil {
log.Ctx(ctx).Warn().
Str("probe", dcp.Name()).
Msg("File index not available, skipping Docker credentials probe")
return findings, nil
}

locations := dcp.fileIndex.Get("docker_config")
for _, location := range locations {
_, err := os.Stat(location)
if err != nil {
continue
}
fileContents, err := os.ReadFile(location)
if err != nil {
return findings, err
}
registries, err := regsWithCreds(fileContents)
if err != nil {
return findings, err
}
for _, registry := range registries {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry not being clear enough but the probe is used to access the files in the index but then you need to run the detectors on the content of your files to actually detect credentials. When credentials are actually detected then a finding can be created. The current implementation will just flag every docker and podman config files as having creds in clear when it is not necessarily the case

findings = append(findings, models.Finding{
ID: fmt.Sprintf("%s:%s", location, registry),
Probe: dcp.Name(),
Title: "Docker credentials found",
Message: fmt.Sprintf("%s contains cleartext credentials for %s. Consider using a credential helper.", location, registry),
Path: location,
Severity: "high",
Locations: locations,
})
}
}
return findings, nil
}

func regsWithCreds(fileContents []byte) ([]string, error) {
var registries []string
var config map[string]interface{}
if err := json.Unmarshal(fileContents, &config); err != nil {
return registries, err
}
auths, ok := config["auths"].(map[string]interface{})
if !ok {
return registries, nil
}
for registry, settings := range auths {
propsForRegistry, ok := settings.(map[string]interface{})
if !ok {
continue
}
if propsForRegistry["auth"] != nil {
registries = append(registries, registry)
}
}
return registries, nil
}
62 changes: 62 additions & 0 deletions pkg/probe/docker_creds_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
// Copyright (C) 2026 boostsecurity.io
// SPDX-License-Identifier: GPL-3.0-or-later

package probe

import (
"testing"

"github.com/boostsecurityio/bagel/pkg/models"
"github.com/stretchr/testify/assert"
)

var configFileWithCreds = `{
"auths": {
"dhi.io": {
"auth": "supersecret"
},
"docker.io": {},
"ghcr.io": {}
},
"credHelpers": {
"dhi.io": "osxkeychain",
"ghcr.io": "osxkeychain",
"docker.io": "osxkeychain"
}
}
`

var configFileWithoutCreds = `{
"auths": {
"dhi.io": {},
"docker.io": {},
"ghcr.io": {}
},
"credHelpers": {
"dhi.io": "osxkeychain",
"ghcr.io": "osxkeychain",
"docker.io": "osxkeychain"
}
}
`

var probeSettings = models.ProbeSettings{
Enabled: true,
}

func TestName(t *testing.T) {
probe := NewDockerCredsProbe(probeSettings)
assert.Equal(t, probe.Name(), "Docker credentials")
}

func TestParsingFileWithCreds(t *testing.T) {
expected := []string{"dhi.io"}
actual, _ := regsWithCreds([]byte(configFileWithCreds))
assert.Equal(t, expected, actual)
}

func TestParsingFileWithoutCreds(t *testing.T) {
var expected []string
actual, _ := regsWithCreds([]byte(configFileWithoutCreds))
assert.Equal(t, expected, actual)
}