- Add telego v1.8.0 dependency for Telegram Bot API - Create pkg/bot package with Bot struct holding engine, verifier, recon, storage, registry deps - Implement 8 command handlers: /help, /scan, /verify, /recon, /status, /stats, /providers, /key - /key enforced private-chat-only for security (never exposes unmasked keys in groups) - All other commands use masked keys only - Handler registration via telego's BotHandler with CommandEqual predicates
141 lines
3.9 KiB
Go
141 lines
3.9 KiB
Go
// Package bot implements the Telegram bot interface for KeyHunter.
|
|
// It wraps existing scan, verify, recon, and storage functionality,
|
|
// exposing them through Telegram command handlers via the telego library.
|
|
package bot
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/mymmrac/telego"
|
|
th "github.com/mymmrac/telego/telegohandler"
|
|
|
|
"github.com/salvacybersec/keyhunter/pkg/engine"
|
|
"github.com/salvacybersec/keyhunter/pkg/providers"
|
|
"github.com/salvacybersec/keyhunter/pkg/recon"
|
|
"github.com/salvacybersec/keyhunter/pkg/storage"
|
|
"github.com/salvacybersec/keyhunter/pkg/verify"
|
|
)
|
|
|
|
// Bot holds the Telegram bot instance and all dependencies needed
|
|
// to process commands. It delegates to the existing KeyHunter engine,
|
|
// verifier, recon engine, and storage layer.
|
|
type Bot struct {
|
|
api *telego.Bot
|
|
handler *th.BotHandler
|
|
engine *engine.Engine
|
|
verifier *verify.HTTPVerifier
|
|
recon *recon.Engine
|
|
db *storage.DB
|
|
registry *providers.Registry
|
|
encKey []byte
|
|
|
|
mu sync.Mutex
|
|
startedAt time.Time
|
|
lastScan time.Time
|
|
}
|
|
|
|
// Deps bundles the dependencies required to construct a Bot.
|
|
type Deps struct {
|
|
Engine *engine.Engine
|
|
Verifier *verify.HTTPVerifier
|
|
Recon *recon.Engine
|
|
DB *storage.DB
|
|
Registry *providers.Registry
|
|
EncKey []byte
|
|
}
|
|
|
|
// New creates a Bot backed by the given telego API client and dependencies.
|
|
// Call RegisterHandlers to wire up command handlers before starting the update loop.
|
|
func New(api *telego.Bot, deps Deps) *Bot {
|
|
return &Bot{
|
|
api: api,
|
|
engine: deps.Engine,
|
|
verifier: deps.Verifier,
|
|
recon: deps.Recon,
|
|
db: deps.DB,
|
|
registry: deps.Registry,
|
|
encKey: deps.EncKey,
|
|
startedAt: time.Now(),
|
|
}
|
|
}
|
|
|
|
// RegisterHandlers wires all command handlers into a BotHandler that processes
|
|
// updates from the Telegram API. The caller must call Start() on the returned
|
|
// BotHandler to begin processing.
|
|
func (b *Bot) RegisterHandlers(updates <-chan telego.Update) *th.BotHandler {
|
|
bh, _ := th.NewBotHandler(b.api, updates)
|
|
|
|
bh.HandleMessage(b.handleHelp, th.CommandEqual("help"))
|
|
bh.HandleMessage(b.handleScan, th.CommandEqual("scan"))
|
|
bh.HandleMessage(b.handleVerify, th.CommandEqual("verify"))
|
|
bh.HandleMessage(b.handleRecon, th.CommandEqual("recon"))
|
|
bh.HandleMessage(b.handleStatus, th.CommandEqual("status"))
|
|
bh.HandleMessage(b.handleStats, th.CommandEqual("stats"))
|
|
bh.HandleMessage(b.handleProviders, th.CommandEqual("providers"))
|
|
bh.HandleMessage(b.handleKey, th.CommandEqual("key"))
|
|
|
|
b.handler = bh
|
|
return bh
|
|
}
|
|
|
|
// reply sends a text message back to the chat that originated msg.
|
|
func (b *Bot) reply(ctx context.Context, msg *telego.Message, text string) {
|
|
_, _ = b.api.SendMessage(ctx, &telego.SendMessageParams{
|
|
ChatID: telego.ChatID{ID: msg.Chat.ID},
|
|
Text: text,
|
|
})
|
|
}
|
|
|
|
// isPrivateChat returns true if the message was sent in a private (1:1) chat.
|
|
func isPrivateChat(msg *telego.Message) bool {
|
|
return msg.Chat.Type == "private"
|
|
}
|
|
|
|
// runScan executes a scan against the given path and returns findings.
|
|
// Findings are collected synchronously; the caller formats the output.
|
|
func (b *Bot) runScan(ctx context.Context, path string) ([]engine.Finding, error) {
|
|
src, err := selectBotSource(path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
cfg := engine.ScanConfig{
|
|
Workers: 0, // auto
|
|
Verify: false,
|
|
Unmask: false,
|
|
}
|
|
|
|
ch, err := b.engine.Scan(ctx, src, cfg)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("starting scan: %w", err)
|
|
}
|
|
|
|
var findings []engine.Finding
|
|
for f := range ch {
|
|
findings = append(findings, f)
|
|
}
|
|
|
|
// Persist findings
|
|
for _, f := range findings {
|
|
sf := storage.Finding{
|
|
ProviderName: f.ProviderName,
|
|
KeyValue: f.KeyValue,
|
|
KeyMasked: f.KeyMasked,
|
|
Confidence: f.Confidence,
|
|
SourcePath: f.Source,
|
|
SourceType: f.SourceType,
|
|
LineNumber: f.LineNumber,
|
|
}
|
|
_, _ = b.db.SaveFinding(sf, b.encKey)
|
|
}
|
|
|
|
b.mu.Lock()
|
|
b.lastScan = time.Now()
|
|
b.mu.Unlock()
|
|
|
|
return findings, nil
|
|
}
|