Skip to content
Open
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
4 changes: 4 additions & 0 deletions cmd/bumblebee/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ type scanOpts struct {
httpGzip bool

deviceIDEnv string
quiet bool
}

func registerScanFlags(fs *flag.FlagSet, o *scanOpts) {
Expand Down Expand Up @@ -161,6 +162,8 @@ func registerScanFlags(fs *flag.FlagSet, o *scanOpts) {

fs.StringVar(&o.deviceIDEnv, "device-id-env", "",
"env var holding a stable opaque endpoint/device id (e.g. set by MDM, EDR, or a provisioning script); populates endpoint.device_id when set")
fs.BoolVar(&o.quiet, "quiet", false, "suppress info and warning level diagnostic output to stderr")
fs.BoolVar(&o.quiet, "q", false, "suppress info and warning level diagnostic output to stderr (shorthand)")
}

// runScan executes the scan subcommand. Returns the process exit code.
Expand Down Expand Up @@ -221,6 +224,7 @@ func runScan(args []string) int {

runID := newRunID()
emitter := output.New(recordsW, os.Stderr, runID)
emitter.Quiet = o.quiet

for _, n := range diagNotes {
emitter.Diag("info", "", n)
Expand Down
109 changes: 109 additions & 0 deletions internal/ecosystem/cargo/cargo.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
package cargo

import (
"bufio"
"bytes"
"errors"
"fmt"
"io"
"os"
"path/filepath"
"strings"

"github.com/perplexityai/bumblebee/internal/model"
)

const Ecosystem = model.EcosystemCargo

type Scanner struct {
MaxFileSize int64
Emit func(model.Record)
Diag func(level, path, msg string)
}

func IsCargoLock(base string) bool { return base == "Cargo.lock" }

func (s *Scanner) ScanCargoLock(path string, base model.Record) error {
data, err := s.readBounded(path)
if err != nil {
return err
}
projectPath := filepath.Dir(path)

sc := bufio.NewScanner(bytes.NewReader(data))
sc.Buffer(make([]byte, 0, 64*1024), 4*1024*1024)

var inPackage bool
var name, version string

emitCurrent := func() {
if inPackage && name != "" && version != "" {
r := base
r.Ecosystem = Ecosystem
r.PackageName = name
r.NormalizedName = strings.ToLower(name)
r.Version = version
r.ProjectPath = projectPath
r.PackageManager = "cargo"
r.SourceType = "cargo-lock"
r.SourceFile = path
r.Confidence = "high"
s.Emit(r)
}
inPackage = false
name = ""
version = ""
}

for sc.Scan() {
line := strings.TrimSpace(sc.Text())
if line == "[[package]]" {
emitCurrent()
inPackage = true
continue
}
if !inPackage {
continue
}
if strings.HasPrefix(line, "name =") {
name = unquote(strings.TrimSpace(strings.TrimPrefix(line, "name =")))
} else if strings.HasPrefix(line, "version =") {
version = unquote(strings.TrimSpace(strings.TrimPrefix(line, "version =")))
}
}
emitCurrent()

return nil
}

func unquote(s string) string {
if len(s) >= 2 && s[0] == '"' && s[len(s)-1] == '"' {
return s[1 : len(s)-1]
}
if len(s) >= 2 && s[0] == '\'' && s[len(s)-1] == '\'' {
return s[1 : len(s)-1]
}
return s
}

func (s *Scanner) readBounded(path string) ([]byte, error) {
f, err := os.Open(path)
if err != nil {
return nil, err
}
defer f.Close()
info, err := f.Stat()
if err != nil {
return nil, err
}
if !info.Mode().IsRegular() {
return nil, errors.New("not a regular file")
}
if s.MaxFileSize > 0 && info.Size() > s.MaxFileSize {
if s.Diag != nil {
s.Diag("warn", path, fmt.Sprintf("skipping: size %d exceeds max %d", info.Size(), s.MaxFileSize))
}
return nil, fmt.Errorf("file %s exceeds max size %d", path, s.MaxFileSize)
}
return io.ReadAll(f)
}
12 changes: 12 additions & 0 deletions internal/ecosystem/mcp/mcp.go
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,18 @@ func (s *Scanner) emitServers(servers map[string]serverEntry, base model.Record,
sort.Strings(ids)
for _, id := range ids {
srv := servers[id]

if s.Diag != nil {
for envKey, envVal := range srv.Env {
if strVal, ok := envVal.(string); ok && strVal != "" && !looksUnresolvedShellVar(strVal) {
uk := strings.ToUpper(envKey)
if strings.Contains(uk, "KEY") || strings.Contains(uk, "TOKEN") || strings.Contains(uk, "SECRET") || strings.Contains(uk, "PASSWORD") || strings.Contains(uk, "CRED") {
s.Diag("warn", sourcePath, fmt.Sprintf("MCP server %q specifies potential plaintext credential in env var %q", id, envKey))
}
}
}
}

r := base
r.Ecosystem = Ecosystem
r.PackageManager = "mcp"
Expand Down
3 changes: 3 additions & 0 deletions internal/model/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ const (
EcosystemBrowserExtension = "browser-extension"
EcosystemHomebrew = "homebrew"
EcosystemAgentSkill = "agent-skill"
EcosystemCargo = "cargo"
)

var supportedEcosystems = map[string]struct{}{
Expand All @@ -58,6 +59,7 @@ var supportedEcosystems = map[string]struct{}{
EcosystemBrowserExtension: {},
EcosystemHomebrew: {},
EcosystemAgentSkill: {},
EcosystemCargo: {},
}

var supportedEcosystemOrder = []string{
Expand All @@ -71,6 +73,7 @@ var supportedEcosystemOrder = []string{
EcosystemBrowserExtension,
EcosystemHomebrew,
EcosystemAgentSkill,
EcosystemCargo,
}

// SupportedEcosystems returns the emitted ecosystem values supported by v0.1.
Expand Down
7 changes: 7 additions & 0 deletions internal/output/output.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ type Emitter struct {
RecordsEmitted int
Duplicates int
Diagnostics int

Quiet bool
}

func New(records, diags io.Writer, runID string) *Emitter {
Expand Down Expand Up @@ -131,6 +133,11 @@ func (e *Emitter) EmitSummary(s model.ScanSummary) error {
func (e *Emitter) Diag(level, path, msg string) {
e.mu.Lock()
defer e.mu.Unlock()

if e.Quiet && (level == "info" || level == "warn" || level == "warning") {
return
}

e.Diagnostics++
d := model.Diagnostic{
RecordType: model.RecordTypeDiagnostic,
Expand Down
6 changes: 6 additions & 0 deletions internal/scanner/scanner.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (

"github.com/perplexityai/bumblebee/internal/ecosystem/browserext"
"github.com/perplexityai/bumblebee/internal/ecosystem/bun"
"github.com/perplexityai/bumblebee/internal/ecosystem/cargo"
"github.com/perplexityai/bumblebee/internal/ecosystem/composer"
"github.com/perplexityai/bumblebee/internal/ecosystem/editorext"
"github.com/perplexityai/bumblebee/internal/ecosystem/gomod"
Expand Down Expand Up @@ -253,6 +254,7 @@ func Run(ctx context.Context, cfg Config) (Result, error) {
extS := &editorext.Scanner{MaxFileSize: cfg.MaxFileSize, Emit: emit, Diag: diag}
bxS := &browserext.Scanner{MaxFileSize: cfg.MaxFileSize, Emit: emit, Diag: diag}
hbS := &homebrew.Scanner{MaxFileSize: cfg.MaxFileSize, Emit: emit, Diag: diag}
cargoS := &cargo.Scanner{MaxFileSize: cfg.MaxFileSize, Emit: emit, Diag: diag}

type job struct {
kind string
Expand Down Expand Up @@ -298,6 +300,8 @@ func Run(ctx context.Context, cfg Config) (Result, error) {
err = yarnS.ScanLockfile(j.path, cfg.BaseRecord)
case "bun-lock":
err = bunS.ScanTextLockfile(j.path, cfg.BaseRecord)
case "cargo-lock":
err = cargoS.ScanCargoLock(j.path, cfg.BaseRecord)
case "go-sum":
err = goS.ScanGoSum(j.path, cfg.BaseRecord)
case "go-mod":
Expand Down Expand Up @@ -414,6 +418,8 @@ func Run(ctx context.Context, cfg Config) (Result, error) {
send(job{kind: "bun-lock", path: path})
case enabled(model.EcosystemNPM) && bun.IsBinaryLockfile(base):
bunS.NoteBinaryLockfile(path)
case enabled(model.EcosystemCargo) && cargo.IsCargoLock(base):
send(job{kind: "cargo-lock", path: path})
case enabled(model.EcosystemGo) && gomod.IsGoSum(base):
send(job{kind: "go-sum", path: path})
case enabled(model.EcosystemGo) && gomod.IsGoMod(base):
Expand Down