mirror of
https://github.com/lunchcat/sif.git
synced 2026-06-12 11:01:24 -07:00
8078978a44
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.
75 lines
3.1 KiB
Go
75 lines
3.1 KiB
Go
/*
|
|
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
|
: :
|
|
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
|
: ▄█ █ █▀ · BSD 3-Clause License :
|
|
: :
|
|
: (c) 2022-2026 vmfunc, xyzeva, :
|
|
: lunchcat alumni & contributors :
|
|
: :
|
|
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
|
*/
|
|
|
|
package notify
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"strings"
|
|
|
|
"github.com/dropalldatabases/sif/internal/finding"
|
|
"github.com/dropalldatabases/sif/internal/httpx"
|
|
)
|
|
|
|
// contentTypeJSON is the body type every provider POSTs; all four speak json.
|
|
const contentTypeJSON = "application/json"
|
|
|
|
// messageHeader prefixes the rendered finding block. kept terse - chat sinks
|
|
// truncate, so the count and lead-in carry the signal.
|
|
const messageHeader = "sif found %d finding(s):"
|
|
|
|
// renderFindings turns a batch into a single plain-text block, one finding per
|
|
// line in the same "[severity] target module title" shape as the -silent sink so
|
|
// a reader sees identical lines across stdout and chat. a strings.Builder keeps
|
|
// the per-line concat to one allocation path.
|
|
func renderFindings(findings []finding.Finding) string {
|
|
var b strings.Builder
|
|
fmt.Fprintf(&b, messageHeader, len(findings))
|
|
b.WriteByte('\n')
|
|
for i := 0; i < len(findings); i++ {
|
|
b.WriteString(findings[i].Line())
|
|
b.WriteByte('\n')
|
|
}
|
|
return b.String()
|
|
}
|
|
|
|
// postJSON marshals payload and POSTs it to url through the shared client. it
|
|
// drains+closes the response so the conn returns to httpx's pool, and treats any
|
|
// non-2xx as a delivery failure so a 4xx from a bad webhook surfaces loudly.
|
|
func postJSON(ctx context.Context, client *http.Client, url string, payload any) error {
|
|
body, err := json.Marshal(payload)
|
|
if err != nil {
|
|
return fmt.Errorf("marshal payload: %w", err)
|
|
}
|
|
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(body))
|
|
if err != nil {
|
|
return fmt.Errorf("build request: %w", err)
|
|
}
|
|
req.Header.Set("Content-Type", contentTypeJSON)
|
|
|
|
resp, err := client.Do(req) //nolint:bodyclose // drained and closed via httpx.DrainClose
|
|
if err != nil {
|
|
return fmt.Errorf("post: %w", err)
|
|
}
|
|
defer httpx.DrainClose(resp)
|
|
|
|
if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices {
|
|
return fmt.Errorf("unexpected status %d", resp.StatusCode)
|
|
}
|
|
return nil
|
|
}
|