feat(17-03): implement Telegram bot command handlers
- 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
This commit is contained in:
14
go.mod
14
go.mod
@@ -24,12 +24,17 @@ require (
|
|||||||
dario.cat/mergo v1.0.0 // indirect
|
dario.cat/mergo v1.0.0 // indirect
|
||||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||||
github.com/ProtonMail/go-crypto v1.1.6 // indirect
|
github.com/ProtonMail/go-crypto v1.1.6 // indirect
|
||||||
|
github.com/andybalholm/brotli v1.2.0 // indirect
|
||||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||||
|
github.com/bytedance/gopkg v0.1.3 // indirect
|
||||||
|
github.com/bytedance/sonic v1.15.0 // indirect
|
||||||
|
github.com/bytedance/sonic/loader v0.5.0 // indirect
|
||||||
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
|
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
|
||||||
github.com/charmbracelet/x/ansi v0.8.0 // indirect
|
github.com/charmbracelet/x/ansi v0.8.0 // indirect
|
||||||
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect
|
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect
|
||||||
github.com/charmbracelet/x/term v0.2.1 // indirect
|
github.com/charmbracelet/x/term v0.2.1 // indirect
|
||||||
github.com/cloudflare/circl v1.6.3 // indirect
|
github.com/cloudflare/circl v1.6.3 // indirect
|
||||||
|
github.com/cloudwego/base64x v0.1.6 // indirect
|
||||||
github.com/cyphar/filepath-securejoin v0.4.1 // indirect
|
github.com/cyphar/filepath-securejoin v0.4.1 // indirect
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||||
@@ -40,12 +45,16 @@ require (
|
|||||||
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
|
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
|
||||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
|
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
|
||||||
github.com/google/uuid v1.6.0 // indirect
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
|
github.com/grbit/go-json v0.11.0 // indirect
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
|
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
|
||||||
github.com/kevinburke/ssh_config v1.2.0 // indirect
|
github.com/kevinburke/ssh_config v1.2.0 // indirect
|
||||||
|
github.com/klauspost/compress v1.18.2 // indirect
|
||||||
|
github.com/klauspost/cpuid/v2 v2.2.9 // indirect
|
||||||
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
||||||
github.com/mattn/go-runewidth v0.0.16 // indirect
|
github.com/mattn/go-runewidth v0.0.16 // indirect
|
||||||
github.com/muesli/termenv v0.16.0 // indirect
|
github.com/muesli/termenv v0.16.0 // indirect
|
||||||
|
github.com/mymmrac/telego v1.8.0 // indirect
|
||||||
github.com/ncruces/go-strftime v1.0.0 // indirect
|
github.com/ncruces/go-strftime v1.0.0 // indirect
|
||||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||||
github.com/pjbgf/sha1cd v0.3.2 // indirect
|
github.com/pjbgf/sha1cd v0.3.2 // indirect
|
||||||
@@ -63,9 +72,14 @@ require (
|
|||||||
github.com/temoto/robotstxt v1.1.2 // indirect
|
github.com/temoto/robotstxt v1.1.2 // indirect
|
||||||
github.com/tidwall/match v1.1.1 // indirect
|
github.com/tidwall/match v1.1.1 // indirect
|
||||||
github.com/tidwall/pretty v1.2.0 // indirect
|
github.com/tidwall/pretty v1.2.0 // indirect
|
||||||
|
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||||
|
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||||
|
github.com/valyala/fasthttp v1.69.0 // indirect
|
||||||
|
github.com/valyala/fastjson v1.6.10 // indirect
|
||||||
github.com/xanzy/ssh-agent v0.3.3 // indirect
|
github.com/xanzy/ssh-agent v0.3.3 // indirect
|
||||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||||
|
golang.org/x/arch v0.0.0-20210923205945-b76863e36670 // indirect
|
||||||
golang.org/x/net v0.52.0 // indirect
|
golang.org/x/net v0.52.0 // indirect
|
||||||
golang.org/x/sync v0.20.0 // indirect
|
golang.org/x/sync v0.20.0 // indirect
|
||||||
golang.org/x/sys v0.42.0 // indirect
|
golang.org/x/sys v0.42.0 // indirect
|
||||||
|
|||||||
36
go.sum
36
go.sum
@@ -5,6 +5,8 @@ github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERo
|
|||||||
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||||
github.com/ProtonMail/go-crypto v1.1.6 h1:ZcV+Ropw6Qn0AX9brlQLAUXfqLBc7Bl+f/DmNxpLfdw=
|
github.com/ProtonMail/go-crypto v1.1.6 h1:ZcV+Ropw6Qn0AX9brlQLAUXfqLBc7Bl+f/DmNxpLfdw=
|
||||||
github.com/ProtonMail/go-crypto v1.1.6/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE=
|
github.com/ProtonMail/go-crypto v1.1.6/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE=
|
||||||
|
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
|
||||||
|
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
|
||||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
|
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
|
||||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
|
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
|
||||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
|
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
|
||||||
@@ -13,6 +15,12 @@ github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z
|
|||||||
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
|
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
|
||||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
|
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
|
||||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
|
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
|
||||||
|
github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=
|
||||||
|
github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
|
||||||
|
github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE=
|
||||||
|
github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k=
|
||||||
|
github.com/bytedance/sonic/loader v0.5.0 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiDd5VjlgE=
|
||||||
|
github.com/bytedance/sonic/loader v0.5.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
|
||||||
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs=
|
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs=
|
||||||
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk=
|
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk=
|
||||||
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
|
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
|
||||||
@@ -25,6 +33,8 @@ github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQ
|
|||||||
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
|
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
|
||||||
github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8=
|
github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8=
|
||||||
github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4=
|
github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4=
|
||||||
|
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
|
||||||
|
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||||
github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s=
|
github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s=
|
||||||
github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI=
|
github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI=
|
||||||
@@ -61,6 +71,8 @@ github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17k
|
|||||||
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
|
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/grbit/go-json v0.11.0 h1:bAbyMdYrYl/OjYsSqLH99N2DyQ291mHy726Mx+sYrnc=
|
||||||
|
github.com/grbit/go-json v0.11.0/go.mod h1:IYpHsdybQ386+6g3VE6AXQ3uTGa5mquBme5/ZWmtzek=
|
||||||
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
|
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
|
||||||
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||||
@@ -69,6 +81,10 @@ github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOl
|
|||||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
|
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
|
||||||
github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4=
|
github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4=
|
||||||
github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
|
github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
|
||||||
|
github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk=
|
||||||
|
github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8=
|
||||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||||
@@ -84,6 +100,8 @@ github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6T
|
|||||||
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||||
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
|
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
|
||||||
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
|
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
|
||||||
|
github.com/mymmrac/telego v1.8.0 h1:EvIprWo9Cn0MHgumvvqNXPAXO1yJj3pu2cdCCeDxbow=
|
||||||
|
github.com/mymmrac/telego v1.8.0/go.mod h1:pdLV346EgVuq7Xrh3kMggeBiazeHhsdEoK0RTEOPXRM=
|
||||||
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
|
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
|
||||||
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||||
github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k=
|
github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k=
|
||||||
@@ -129,9 +147,16 @@ github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3A
|
|||||||
github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU=
|
github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU=
|
||||||
github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY=
|
github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||||
|
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||||
|
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||||
|
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||||
|
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||||
|
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
||||||
@@ -144,12 +169,22 @@ github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
|||||||
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
|
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
|
||||||
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
|
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
|
||||||
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||||
|
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||||
|
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||||
|
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||||
|
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||||
|
github.com/valyala/fasthttp v1.69.0 h1:fNLLESD2SooWeh2cidsuFtOcrEi4uB4m1mPrkJMZyVI=
|
||||||
|
github.com/valyala/fasthttp v1.69.0/go.mod h1:4wA4PfAraPlAsJ5jMSqCE2ug5tqUPwKXxVj8oNECGcw=
|
||||||
|
github.com/valyala/fastjson v1.6.10 h1:/yjJg8jaVQdYR3arGxPE2X5z89xrlhS0eGXdv+ADTh4=
|
||||||
|
github.com/valyala/fastjson v1.6.10/go.mod h1:e6FubmQouUNP73jtMLmcbxS6ydWIpOfhz34TSfO3JaE=
|
||||||
github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
|
github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
|
||||||
github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
|
github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
|
||||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
||||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
||||||
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||||
|
golang.org/x/arch v0.0.0-20210923205945-b76863e36670 h1:18EFjUmQOcUvxNYSkA6jO9VAiXCnxFY6NyDX0bHDmkU=
|
||||||
|
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||||
golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
|
golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
|
||||||
golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
|
golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
|
||||||
@@ -190,6 +225,7 @@ gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
|
|||||||
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
|
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
|
||||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
modernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis=
|
modernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis=
|
||||||
|
|||||||
140
pkg/bot/bot.go
Normal file
140
pkg/bot/bot.go
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
// 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
|
||||||
|
}
|
||||||
377
pkg/bot/handlers.go
Normal file
377
pkg/bot/handlers.go
Normal file
@@ -0,0 +1,377 @@
|
|||||||
|
package bot
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/mymmrac/telego"
|
||||||
|
th "github.com/mymmrac/telego/telegohandler"
|
||||||
|
|
||||||
|
"github.com/salvacybersec/keyhunter/pkg/engine"
|
||||||
|
"github.com/salvacybersec/keyhunter/pkg/recon"
|
||||||
|
"github.com/salvacybersec/keyhunter/pkg/storage"
|
||||||
|
)
|
||||||
|
|
||||||
|
// commandHelp lists all available bot commands with descriptions.
|
||||||
|
var commandHelp = []struct {
|
||||||
|
Cmd string
|
||||||
|
Desc string
|
||||||
|
}{
|
||||||
|
{"/help", "Show this help message"},
|
||||||
|
{"/scan <path>", "Scan a file or directory for leaked API keys"},
|
||||||
|
{"/verify <id>", "Verify a stored finding by ID"},
|
||||||
|
{"/recon [--sources=x,y]", "Run OSINT recon sweep across sources"},
|
||||||
|
{"/status", "Show bot status and uptime"},
|
||||||
|
{"/stats", "Show provider and finding statistics"},
|
||||||
|
{"/providers", "List loaded provider definitions"},
|
||||||
|
{"/key <id>", "Show full key detail (private chat only)"},
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleHelp responds with a list of all available commands.
|
||||||
|
func (b *Bot) handleHelp(ctx *th.Context, msg telego.Message) error {
|
||||||
|
var sb strings.Builder
|
||||||
|
sb.WriteString("KeyHunter Bot Commands:\n\n")
|
||||||
|
for _, c := range commandHelp {
|
||||||
|
sb.WriteString(fmt.Sprintf("%s - %s\n", c.Cmd, c.Desc))
|
||||||
|
}
|
||||||
|
b.reply(ctx, &msg, sb.String())
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleScan triggers a scan on the given path and returns masked findings.
|
||||||
|
// Usage: /scan <path>
|
||||||
|
func (b *Bot) handleScan(ctx *th.Context, msg telego.Message) error {
|
||||||
|
path := extractArg(msg.Text)
|
||||||
|
if path == "" {
|
||||||
|
b.reply(ctx, &msg, "Usage: /scan <path>\nExample: /scan /tmp/myrepo")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
b.reply(ctx, &msg, fmt.Sprintf("Scanning %s ...", path))
|
||||||
|
|
||||||
|
scanCtx, cancel := context.WithTimeout(ctx, 5*time.Minute)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
findings, err := b.runScan(scanCtx, path)
|
||||||
|
if err != nil {
|
||||||
|
b.reply(ctx, &msg, fmt.Sprintf("Scan error: %s", err))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(findings) == 0 {
|
||||||
|
b.reply(ctx, &msg, "Scan complete. No API keys found.")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var sb strings.Builder
|
||||||
|
sb.WriteString(fmt.Sprintf("Scan complete. Found %d key(s):\n\n", len(findings)))
|
||||||
|
for i, f := range findings {
|
||||||
|
if i >= 20 {
|
||||||
|
sb.WriteString(fmt.Sprintf("\n... and %d more", len(findings)-20))
|
||||||
|
break
|
||||||
|
}
|
||||||
|
sb.WriteString(fmt.Sprintf("[%s] %s %s:%d\n", f.ProviderName, f.KeyMasked, f.Source, f.LineNumber))
|
||||||
|
}
|
||||||
|
b.reply(ctx, &msg, sb.String())
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleVerify verifies a stored finding by its database ID.
|
||||||
|
// Usage: /verify <id>
|
||||||
|
func (b *Bot) handleVerify(ctx *th.Context, msg telego.Message) error {
|
||||||
|
arg := extractArg(msg.Text)
|
||||||
|
if arg == "" {
|
||||||
|
b.reply(ctx, &msg, "Usage: /verify <id>\nExample: /verify 42")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
id, err := strconv.ParseInt(arg, 10, 64)
|
||||||
|
if err != nil || id <= 0 {
|
||||||
|
b.reply(ctx, &msg, "Invalid ID. Must be a positive integer.")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
f, err := b.db.GetFinding(id, b.encKey)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
|
b.reply(ctx, &msg, fmt.Sprintf("No finding with ID %d.", id))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
b.reply(ctx, &msg, fmt.Sprintf("Error: %s", err))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
ef := storageToEngine(*f)
|
||||||
|
results := b.verifier.VerifyAll(ctx, []engine.Finding{ef}, b.registry, 1)
|
||||||
|
r := <-results
|
||||||
|
for range results {
|
||||||
|
}
|
||||||
|
|
||||||
|
// Persist verification result.
|
||||||
|
var metaJSON interface{}
|
||||||
|
if r.Metadata != nil {
|
||||||
|
byt, _ := json.Marshal(r.Metadata)
|
||||||
|
metaJSON = string(byt)
|
||||||
|
} else {
|
||||||
|
metaJSON = sql.NullString{}
|
||||||
|
}
|
||||||
|
_, _ = b.db.SQL().Exec(
|
||||||
|
`UPDATE findings SET verified=1, verify_status=?, verify_http_code=?, verify_metadata_json=? WHERE id=?`,
|
||||||
|
r.Status, r.HTTPCode, metaJSON, id,
|
||||||
|
)
|
||||||
|
|
||||||
|
var sb strings.Builder
|
||||||
|
sb.WriteString(fmt.Sprintf("Verification for finding #%d:\n", id))
|
||||||
|
sb.WriteString(fmt.Sprintf("Provider: %s\n", f.ProviderName))
|
||||||
|
sb.WriteString(fmt.Sprintf("Key: %s\n", f.KeyMasked))
|
||||||
|
sb.WriteString(fmt.Sprintf("Status: %s\n", r.Status))
|
||||||
|
if r.HTTPCode != 0 {
|
||||||
|
sb.WriteString(fmt.Sprintf("HTTP Code: %d\n", r.HTTPCode))
|
||||||
|
}
|
||||||
|
if r.Error != "" {
|
||||||
|
sb.WriteString(fmt.Sprintf("Error: %s\n", r.Error))
|
||||||
|
}
|
||||||
|
if len(r.Metadata) > 0 {
|
||||||
|
sb.WriteString("Metadata:\n")
|
||||||
|
for k, v := range r.Metadata {
|
||||||
|
sb.WriteString(fmt.Sprintf(" %s: %s\n", k, v))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
b.reply(ctx, &msg, sb.String())
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleRecon runs a recon sweep and returns a summary.
|
||||||
|
// Usage: /recon [--sources=github,gitlab]
|
||||||
|
func (b *Bot) handleRecon(ctx *th.Context, msg telego.Message) error {
|
||||||
|
arg := extractArg(msg.Text)
|
||||||
|
|
||||||
|
b.reply(ctx, &msg, "Running recon sweep...")
|
||||||
|
|
||||||
|
cfg := recon.Config{
|
||||||
|
RespectRobots: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
eng := b.recon
|
||||||
|
// Parse optional --sources filter
|
||||||
|
if strings.HasPrefix(arg, "--sources=") {
|
||||||
|
filter := strings.TrimPrefix(arg, "--sources=")
|
||||||
|
names := strings.Split(filter, ",")
|
||||||
|
filtered := recon.NewEngine()
|
||||||
|
for _, name := range names {
|
||||||
|
name = strings.TrimSpace(name)
|
||||||
|
if src, ok := eng.Get(name); ok {
|
||||||
|
filtered.Register(src)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
eng = filtered
|
||||||
|
}
|
||||||
|
|
||||||
|
reconCtx, cancel := context.WithTimeout(ctx, 10*time.Minute)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
all, err := eng.SweepAll(reconCtx, cfg)
|
||||||
|
if err != nil {
|
||||||
|
b.reply(ctx, &msg, fmt.Sprintf("Recon error: %s", err))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
deduped := recon.Dedup(all)
|
||||||
|
|
||||||
|
if len(deduped) == 0 {
|
||||||
|
b.reply(ctx, &msg, fmt.Sprintf("Recon complete. Swept %d sources, no findings.", len(eng.List())))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var sb strings.Builder
|
||||||
|
sb.WriteString(fmt.Sprintf("Recon complete: %d sources, %d findings (%d after dedup)\n\n",
|
||||||
|
len(eng.List()), len(all), len(deduped)))
|
||||||
|
for i, f := range deduped {
|
||||||
|
if i >= 20 {
|
||||||
|
sb.WriteString(fmt.Sprintf("\n... and %d more", len(deduped)-20))
|
||||||
|
break
|
||||||
|
}
|
||||||
|
sb.WriteString(fmt.Sprintf("[%s] %s %s %s\n", f.SourceType, f.ProviderName, f.KeyMasked, f.Source))
|
||||||
|
}
|
||||||
|
b.reply(ctx, &msg, sb.String())
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleStatus returns bot uptime and last scan time.
|
||||||
|
func (b *Bot) handleStatus(ctx *th.Context, msg telego.Message) error {
|
||||||
|
b.mu.Lock()
|
||||||
|
startedAt := b.startedAt
|
||||||
|
lastScan := b.lastScan
|
||||||
|
b.mu.Unlock()
|
||||||
|
|
||||||
|
uptime := time.Since(startedAt).Truncate(time.Second)
|
||||||
|
|
||||||
|
var sb strings.Builder
|
||||||
|
sb.WriteString("KeyHunter Bot Status\n\n")
|
||||||
|
sb.WriteString(fmt.Sprintf("Uptime: %s\n", uptime))
|
||||||
|
sb.WriteString(fmt.Sprintf("Started: %s\n", startedAt.Format(time.RFC3339)))
|
||||||
|
if !lastScan.IsZero() {
|
||||||
|
sb.WriteString(fmt.Sprintf("Last scan: %s\n", lastScan.Format(time.RFC3339)))
|
||||||
|
} else {
|
||||||
|
sb.WriteString("Last scan: none\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
// DB stats
|
||||||
|
findings, err := b.db.ListFindingsFiltered(b.encKey, storage.Filters{Limit: 0})
|
||||||
|
if err == nil {
|
||||||
|
sb.WriteString(fmt.Sprintf("Total findings: %d\n", len(findings)))
|
||||||
|
}
|
||||||
|
|
||||||
|
sb.WriteString(fmt.Sprintf("Providers loaded: %d\n", len(b.registry.List())))
|
||||||
|
sb.WriteString(fmt.Sprintf("Recon sources: %d\n", len(b.recon.List())))
|
||||||
|
|
||||||
|
b.reply(ctx, &msg, sb.String())
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleStats returns provider and finding statistics.
|
||||||
|
func (b *Bot) handleStats(ctx *th.Context, msg telego.Message) error {
|
||||||
|
stats := b.registry.Stats()
|
||||||
|
|
||||||
|
var sb strings.Builder
|
||||||
|
sb.WriteString("KeyHunter Statistics\n\n")
|
||||||
|
sb.WriteString(fmt.Sprintf("Total providers: %d\n", stats.Total))
|
||||||
|
sb.WriteString("By tier:\n")
|
||||||
|
for tier := 1; tier <= 9; tier++ {
|
||||||
|
if count := stats.ByTier[tier]; count > 0 {
|
||||||
|
sb.WriteString(fmt.Sprintf(" Tier %d: %d\n", tier, count))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sb.WriteString("By confidence:\n")
|
||||||
|
for conf, count := range stats.ByConfidence {
|
||||||
|
sb.WriteString(fmt.Sprintf(" %s: %d\n", conf, count))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finding counts
|
||||||
|
findings, err := b.db.ListFindingsFiltered(b.encKey, storage.Filters{Limit: 0})
|
||||||
|
if err == nil {
|
||||||
|
verified := 0
|
||||||
|
for _, f := range findings {
|
||||||
|
if f.Verified {
|
||||||
|
verified++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sb.WriteString(fmt.Sprintf("\nTotal findings: %d\n", len(findings)))
|
||||||
|
sb.WriteString(fmt.Sprintf("Verified: %d\n", verified))
|
||||||
|
sb.WriteString(fmt.Sprintf("Unverified: %d\n", len(findings)-verified))
|
||||||
|
}
|
||||||
|
|
||||||
|
b.reply(ctx, &msg, sb.String())
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleProviders lists all loaded provider definitions.
|
||||||
|
func (b *Bot) handleProviders(ctx *th.Context, msg telego.Message) error {
|
||||||
|
provs := b.registry.List()
|
||||||
|
|
||||||
|
var sb strings.Builder
|
||||||
|
sb.WriteString(fmt.Sprintf("Loaded providers (%d):\n\n", len(provs)))
|
||||||
|
for i, p := range provs {
|
||||||
|
if i >= 50 {
|
||||||
|
sb.WriteString(fmt.Sprintf("\n... and %d more", len(provs)-50))
|
||||||
|
break
|
||||||
|
}
|
||||||
|
sb.WriteString(fmt.Sprintf("%-20s tier=%d patterns=%d\n", p.Name, p.Tier, len(p.Patterns)))
|
||||||
|
}
|
||||||
|
b.reply(ctx, &msg, sb.String())
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleKey shows full key detail. Only works in private chats for security.
|
||||||
|
// Usage: /key <id>
|
||||||
|
func (b *Bot) handleKey(ctx *th.Context, msg telego.Message) error {
|
||||||
|
if !isPrivateChat(&msg) {
|
||||||
|
b.reply(ctx, &msg, "The /key command is only available in private chats for security reasons.")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
arg := extractArg(msg.Text)
|
||||||
|
if arg == "" {
|
||||||
|
b.reply(ctx, &msg, "Usage: /key <id>\nExample: /key 42")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
id, err := strconv.ParseInt(arg, 10, 64)
|
||||||
|
if err != nil || id <= 0 {
|
||||||
|
b.reply(ctx, &msg, "Invalid ID. Must be a positive integer.")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
f, err := b.db.GetFinding(id, b.encKey)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
|
b.reply(ctx, &msg, fmt.Sprintf("No finding with ID %d.", id))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
b.reply(ctx, &msg, fmt.Sprintf("Error: %s", err))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var sb strings.Builder
|
||||||
|
sb.WriteString(fmt.Sprintf("Finding #%d\n\n", f.ID))
|
||||||
|
sb.WriteString(fmt.Sprintf("Provider: %s\n", f.ProviderName))
|
||||||
|
sb.WriteString(fmt.Sprintf("Confidence: %s\n", f.Confidence))
|
||||||
|
sb.WriteString(fmt.Sprintf("Key: %s\n", f.KeyValue))
|
||||||
|
sb.WriteString(fmt.Sprintf("Key Masked: %s\n", f.KeyMasked))
|
||||||
|
sb.WriteString(fmt.Sprintf("Source: %s\n", f.SourcePath))
|
||||||
|
sb.WriteString(fmt.Sprintf("Source Type: %s\n", f.SourceType))
|
||||||
|
sb.WriteString(fmt.Sprintf("Line: %d\n", f.LineNumber))
|
||||||
|
if !f.CreatedAt.IsZero() {
|
||||||
|
sb.WriteString(fmt.Sprintf("Created: %s\n", f.CreatedAt.Format(time.RFC3339)))
|
||||||
|
}
|
||||||
|
sb.WriteString(fmt.Sprintf("Verified: %t\n", f.Verified))
|
||||||
|
if f.VerifyStatus != "" {
|
||||||
|
sb.WriteString(fmt.Sprintf("Status: %s\n", f.VerifyStatus))
|
||||||
|
}
|
||||||
|
if f.VerifyHTTPCode != 0 {
|
||||||
|
sb.WriteString(fmt.Sprintf("HTTP Code: %d\n", f.VerifyHTTPCode))
|
||||||
|
}
|
||||||
|
if len(f.VerifyMetadata) > 0 {
|
||||||
|
sb.WriteString("Metadata:\n")
|
||||||
|
for k, v := range f.VerifyMetadata {
|
||||||
|
sb.WriteString(fmt.Sprintf(" %s: %s\n", k, v))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
b.reply(ctx, &msg, sb.String())
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// extractArg extracts the argument after the /command from message text.
|
||||||
|
// For "/scan /tmp/repo", returns "/tmp/repo".
|
||||||
|
// For "/help", returns "".
|
||||||
|
func extractArg(text string) string {
|
||||||
|
parts := strings.SplitN(text, " ", 2)
|
||||||
|
if len(parts) < 2 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return strings.TrimSpace(parts[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
// storageToEngine converts a storage.Finding to an engine.Finding for verification.
|
||||||
|
func storageToEngine(f storage.Finding) engine.Finding {
|
||||||
|
return engine.Finding{
|
||||||
|
ProviderName: f.ProviderName,
|
||||||
|
KeyValue: f.KeyValue,
|
||||||
|
KeyMasked: f.KeyMasked,
|
||||||
|
Confidence: f.Confidence,
|
||||||
|
Source: f.SourcePath,
|
||||||
|
SourceType: f.SourceType,
|
||||||
|
LineNumber: f.LineNumber,
|
||||||
|
DetectedAt: f.CreatedAt,
|
||||||
|
Verified: f.Verified,
|
||||||
|
VerifyStatus: f.VerifyStatus,
|
||||||
|
VerifyHTTPCode: f.VerifyHTTPCode,
|
||||||
|
VerifyMetadata: f.VerifyMetadata,
|
||||||
|
}
|
||||||
|
}
|
||||||
21
pkg/bot/source.go
Normal file
21
pkg/bot/source.go
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
package bot
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/salvacybersec/keyhunter/pkg/engine/sources"
|
||||||
|
)
|
||||||
|
|
||||||
|
// selectBotSource returns the appropriate Source for a bot scan request.
|
||||||
|
// Only file and directory paths are supported (no git, stdin, clipboard, URL).
|
||||||
|
func selectBotSource(path string) (sources.Source, error) {
|
||||||
|
info, err := os.Stat(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("stat %q: %w", path, err)
|
||||||
|
}
|
||||||
|
if info.IsDir() {
|
||||||
|
return sources.NewDirSource(path), nil
|
||||||
|
}
|
||||||
|
return sources.NewFileSource(path), nil
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user