Files
sif/internal/notify/config.go
T
vmfunc 8078978a44 feat: notify integrations (slack, discord, telegram, webhook)
ship findings to chat/webhook sinks after a scan so continuous recon can
alert on what it turns up. each provider is one POST through httpx.Client,
so the global proxy/rate-limit/header config applies and there's no extra
http stack. config resolves env-first (SLACK_WEBHOOK_URL, DISCORD_WEBHOOK_URL,
TELEGRAM_BOT_TOKEN/TELEGRAM_CHAT_ID, NOTIFY_WEBHOOK_URL), overridable by a
notify-compatible yaml file so existing projectdiscovery/notify configs port
over. -notify enables it, -notify-severity gates on the finding severity
ladder (default medium), -notify-config points at the yaml. wired after the
scan loop on the severity-filtered finding set; no provider configured is a
silent no-op.
2026-06-10 16:40:14 -07:00

120 lines
4.7 KiB
Go

/*
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
: :
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
: ▄█ █ █▀ · BSD 3-Clause License :
: :
: (c) 2022-2026 vmfunc, xyzeva, :
: lunchcat alumni & contributors :
: :
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
*/
package notify
import (
"fmt"
"os"
"gopkg.in/yaml.v3"
)
// env var names notify reads, env-first. these mirror the conventional names so
// an operator who already exports them for other tooling gets notify for free.
const (
envSlackWebhook = "SLACK_WEBHOOK_URL"
envDiscordWebhook = "DISCORD_WEBHOOK_URL"
// the name of the env var holding the bot token, not the token itself.
envTelegramToken = "TELEGRAM_BOT_TOKEN" //nolint:gosec // env var name, not a secret
envTelegramChat = "TELEGRAM_CHAT_ID"
envWebhookURL = "NOTIFY_WEBHOOK_URL"
)
// config holds resolved destinations for every provider. yaml tags use
// projectdiscovery/notify-compatible key names so an existing notify config file
// ports over verbatim; env supplies the same values and yaml overrides it.
type config struct {
SlackWebhook string `yaml:"slack_webhook_url"`
DiscordWebhook string `yaml:"discord_webhook_url"`
// telegram needs both a bot token and a chat id. notify spells the token
// "telegram_api_key", so accept that key for drop-in compatibility.
TelegramToken string `yaml:"telegram_api_key"`
TelegramChat string `yaml:"telegram_chat_id"`
WebhookURL string `yaml:"webhook_url"`
}
// loadConfig resolves notify destinations env-first, then overlays a yaml file
// when path is non-empty. yaml wins per-field so a file value overrides the
// matching env var; an unset yaml field leaves the env value intact. an empty
// path means env-only. a missing/unparseable file is an error - if the operator
// pointed -notify-config somewhere, a typo should fail loud, not silently drop.
func loadConfig(path string) (config, error) {
cfg := config{
SlackWebhook: os.Getenv(envSlackWebhook),
DiscordWebhook: os.Getenv(envDiscordWebhook),
TelegramToken: os.Getenv(envTelegramToken),
TelegramChat: os.Getenv(envTelegramChat),
WebhookURL: os.Getenv(envWebhookURL),
}
if path == "" {
return cfg, nil
}
data, err := os.ReadFile(path)
if err != nil {
return config{}, fmt.Errorf("read config %q: %w", path, err)
}
// decode into a separate value so only the keys present in the file overlay
// the env-derived defaults; a zero field in the yaml must not blank an env var.
var file config
if err := yaml.Unmarshal(data, &file); err != nil {
return config{}, fmt.Errorf("parse config %q: %w", path, err)
}
overlay(&cfg, &file)
return cfg, nil
}
// overlay copies non-empty fields from src onto dst. used to let a yaml file
// override env without an empty yaml key wiping out a populated env value.
func overlay(dst, src *config) {
if src.SlackWebhook != "" {
dst.SlackWebhook = src.SlackWebhook
}
if src.DiscordWebhook != "" {
dst.DiscordWebhook = src.DiscordWebhook
}
if src.TelegramToken != "" {
dst.TelegramToken = src.TelegramToken
}
if src.TelegramChat != "" {
dst.TelegramChat = src.TelegramChat
}
if src.WebhookURL != "" {
dst.WebhookURL = src.WebhookURL
}
}
// providers builds the live provider list from the resolved config: a provider
// is included only when its destination is fully specified. telegram needs both
// token and chat id, so a half-configured telegram is dropped rather than POSTing
// to a broken endpoint.
func (c *config) providers() []provider {
var out []provider
if c.SlackWebhook != "" {
out = append(out, &slackProvider{webhook: c.SlackWebhook})
}
if c.DiscordWebhook != "" {
out = append(out, &discordProvider{webhook: c.DiscordWebhook})
}
if c.TelegramToken != "" && c.TelegramChat != "" {
out = append(out, &telegramProvider{token: c.TelegramToken, chatID: c.TelegramChat})
}
if c.WebhookURL != "" {
out = append(out, &webhookProvider{url: c.WebhookURL})
}
return out
}