Documentation
¶
Overview ¶
Package notify dispatches compliancekit Findings to operator- configured channels (Slack, Discord, Teams, email, generic webhook, GitHub PR comments, Jira, PagerDuty) per the v0.17 milestone.
Per ADR-006 the binary remains read-only end-to-end: a notification is *generation* — the package POSTs a payload an operator already asked to receive. The same constraints that apply to remediate + ingest apply here: no telemetry, no phone-home, no analytics. Every notification target is operator-configured via env vars and `compliancekit.yaml`. A sink with missing credentials reports Configured()=false and is skipped silently; one failing sink never blocks the others.
Package layout:
internal/notify/notify.go — this file: Notifier interface,
Registry, Notification + Result
types, severity gate, builder.
internal/notify/config.go — typed config block (`notify:`
in compliancekit.yaml).
internal/notify/<sink>.go — one file per sink: slack.go,
discord.go, teams.go, webhook.go,
email.go, github.go, jira.go,
pagerduty.go.
internal/notify/<sink>_test.go — per-sink httptest contract test.
CLI surface is `compliancekit notify --in=findings.json --config=compliancekit.yaml` (Phase 10).
Index ¶
- Variables
- func Register(n Notifier)
- type BuildOptions
- type Discord
- type DiscordConfig
- type Email
- type EmailConfig
- type GitHub
- type GitHubConfig
- type Jira
- type JiraConfig
- type Notification
- type Notifier
- type PagerDuty
- type PagerDutyConfig
- type Registry
- type Result
- type Slack
- type SlackConfig
- type Teams
- type TeamsConfig
- type Webhook
- type WebhookConfig
Constants ¶
This section is empty.
Variables ¶
var Default = NewRegistry()
Default is the process-wide registry every sink package registers against from init(). The CLI uses Default.Sinks(); tests build isolated registries via NewRegistry.
var ErrAuth = errors.New("notify: authentication failed")
ErrAuth is returned by sinks on 401/403. The CLI surfaces this with a clear "check your credentials" message.
var HTTPClient = &http.Client{Timeout: 30 * time.Second}
HTTPClient is the shared http.Client every webhook-based sink uses. Single connection pool + 30s timeout so a misconfigured sink can't hang the dispatch. Tests can substitute their own via the per-sink config struct.
Functions ¶
Types ¶
type BuildOptions ¶
type BuildOptions struct {
// URLPrefix is prepended to a per-finding deep-link path. Empty
// string means no URL is rendered. Operators with a hosted
// evidence pack point this at their dashboard.
URLPrefix string
// Project is the project identifier stamped into the title +
// tags so cross-project notifications stay distinguishable.
Project string
}
BuildOptions controls Notification rendering.
type Discord ¶
type Discord struct {
// contains filtered or unexported fields
}
Discord implements Notifier for Discord incoming webhooks.
func (*Discord) Configured ¶
Configured returns true when the webhook URL is set.
func (*Discord) Send ¶
Send dispatches the notifications via the webhook. Per-notification failures accumulate; top-level error only when every send failed.
func (*Discord) Threshold ¶
func (d *Discord) Threshold() compliancekit.Severity
Threshold returns the per-sink severity floor.
type DiscordConfig ¶
type DiscordConfig struct {
WebhookURL string
SeverityFloor compliancekit.Severity
HTTPClient *http.Client
}
DiscordConfig configures the Discord sink. Discord uses webhook URLs exclusively for outbound notifications; the bot-token API requires a Gateway connection which is out of scope.
Env: DISCORD_WEBHOOK_URL, DISCORD_THRESHOLD.
type Email ¶
type Email struct {
// contains filtered or unexported fields
}
Email implements Notifier for SMTP delivery.
func (*Email) Configured ¶
Configured returns true when host + from + at least one recipient are present. Username/password are optional; some operators run authenticated relays, others run unauthenticated relays inside trusted networks.
func (*Email) Send ¶
Send dispatches the notifications. One message per notification — keeps subject lines targeted and lets recipients filter per check.
func (*Email) Threshold ¶
func (e *Email) Threshold() compliancekit.Severity
Threshold returns the per-sink severity floor.
type EmailConfig ¶
type EmailConfig struct {
Host string
Port int
Username string
Password string
From string
To []string
TLSMode string // starttls | tls | none
SkipVerify bool // disable cert verification — test/dev only
SeverityFloor compliancekit.Severity
// contains filtered or unexported fields
}
EmailConfig configures the SMTP sink. Supports three TLS modes:
- "starttls" (default for port 587) — connect plain, upgrade via STARTTLS before AUTH.
- "tls" (default for port 465) — connect TLS immediately (implicit TLS / "SMTPS").
- "none" — plaintext, AUTH optional. Only safe inside trusted networks (Postfix on localhost, milter sidecar).
Env: SMTP_HOST, SMTP_PORT, SMTP_USERNAME, SMTP_PASSWORD, SMTP_FROM, SMTP_TO (comma-separated), SMTP_TLS (starttls|tls|none), SMTP_THRESHOLD.
type GitHub ¶
type GitHub struct {
// contains filtered or unexported fields
}
GitHub implements Notifier for GitHub PR comments.
func (*GitHub) Configured ¶
Configured returns true when token + repo + PR number are set.
func (*GitHub) Send ¶
Send posts a single summary comment to the PR. Each notification becomes a markdown bullet listing severity + title + link. The "one comment per dispatch" choice avoids the PR-comment spam that "one comment per finding" would produce on noisy scans.
func (*GitHub) Threshold ¶
func (g *GitHub) Threshold() compliancekit.Severity
Threshold returns the per-sink severity floor.
type GitHubConfig ¶
type GitHubConfig struct {
Token string
Repo string // owner/name
PRNumber int
APIURL string // defaults to https://api.github.com
SeverityFloor compliancekit.Severity
HTTPClient *http.Client
}
GitHubConfig configures the GitHub PR comment sink. Posts one summary comment per dispatch (NOT per finding) to avoid PR-comment spam — the comment lists every notification as a markdown bullet.
The token needs `pull_requests:write` (fine-grained) or `repo` (classic). For the GitHub Action use case, GITHUB_TOKEN already has it; for ad-hoc runs the operator generates a PAT.
Env: GITHUB_TOKEN, GITHUB_REPO ("owner/name"), GITHUB_PR_NUMBER, GITHUB_API_URL (override for GHES), GITHUB_THRESHOLD.
type Jira ¶
type Jira struct {
// contains filtered or unexported fields
}
Jira implements Notifier by adapting the v0.15 tickets.Jira client.
func NewJira ¶
func NewJira(cfg JiraConfig) *Jira
NewJira constructs a Jira sink. Holds a tickets.Jira instance internally so the per-call Send path stays small.
func (*Jira) Configured ¶
Configured proxies to the underlying ticket client.
func (*Jira) Send ¶
Send creates one Jira issue per notification. Per-notification failures accumulate in Result.Errors; top-level error only when every send failed.
func (*Jira) Threshold ¶
func (j *Jira) Threshold() compliancekit.Severity
Threshold returns the per-sink severity floor.
type JiraConfig ¶
type JiraConfig struct {
Host string
Email string
Token string
ProjectKey string
IssueType string
SeverityFloor compliancekit.Severity
HTTPClient *http.Client
}
JiraConfig configures the Jira notification sink. Reuses the v0.15 internal/remediate/tickets.Jira client wholesale — every Jira API concern (REST v3 endpoint, basic auth, ADF body, severity → priority mapping) is already covered there.
The difference between this sink and `compliancekit remediate --tickets` is intent: remediate files a ticket for every manual- remediation finding once; notify files for any actionable finding crossing the per-sink threshold every time `notify` runs. Dedup (Phase 10) prevents the same finding firing repeatedly.
Env: JIRA_NOTIFY_* envs (separate from JIRA_* used by remediate so an operator can route remediation-tickets and notification- tickets to different projects). Falls back to JIRA_* when the notify-prefixed ones aren't set.
type Notification ¶
type Notification struct {
// Finding is the upstream finding. Sinks needing fields beyond
// title/body (severity color, CheckID, framework refs) pull
// them from here.
Finding compliancekit.Finding
// Title is a single-line headline rendered for the sink (e.g.
// "[CRITICAL] aws-s3-public-access-block on prod-data-bucket").
Title string
// Body is the per-notification body in CommonMark (plain markdown).
// Each sink may convert: Discord + Slack render natively; Teams
// converts to MessageCard; email wraps in multipart MIME;
// webhook + PagerDuty pass as-is in a body field.
Body string
// URL points operators at where to act: usually the
// `compliancekit checks show <id>` URL, the GitHub PR, or the
// finding's evidence-pack URL. Optional — sinks omit when empty.
URL string
// Tags are sink-routing hints (e.g. "team:security",
// "service:checkout"). Sinks that support tagging surface them;
// sinks that don't (PagerDuty, email subject) ignore.
Tags []string
// Fingerprint is the dedup key. Built from
// Finding.Fingerprint()+sink-name so the same finding fires once
// per sink, not once per sink × notification window. Populated
// by the dispatcher (Phase 10), not by the sink itself.
Fingerprint string
}
Notification is the canonical shape every Notifier consumes. It carries the original Finding plus pre-rendered text (title + body) that sinks adapt to their wire format. Sinks needing rich shapes (PagerDuty event JSON, Slack blocks) build them from Finding + rendered text together.
func BuildNotifications ¶
func BuildNotifications(findings []compliancekit.Finding, opts BuildOptions) []Notification
BuildNotifications converts a Findings slice into the canonical Notification slice every sink consumes. Caller-provided Options control rendering (URL prefix for "checks show", baseline for only-new-findings mode — Phase 10).
Severity gating is applied per-sink inside Dispatch, not here — the same Notification slice may flow to a low-threshold sink (Slack: all-medium-plus) and a high-threshold one (PagerDuty: critical-only). Building once and gating per sink keeps the rendering cost amortized.
type Notifier ¶
type Notifier interface {
// Name returns a short, unique, kebab-case identifier for the
// sink. Used in error messages, `compliancekit doctor` output,
// the per-sink `notify:` config block, and the Result rendering.
Name() string
// Configured reports whether the sink has enough config to call
// its upstream. Missing credentials = not configured = caller
// skips silently. This is the same pattern as the v0.15 ticket
// providers.
Configured() bool
// Threshold returns the per-sink severity floor below which
// notifications are dropped. compliancekit.SeverityInfo means "send
// everything". The CLI / config layer can override per-sink.
Threshold() compliancekit.Severity
// Send dispatches the slice of Notification objects already
// filtered by the global severity gate. Returns a Result tally;
// transport errors return the second return value but never
// abort other Notifications in the slice — sinks should partial-
// succeed and report partial-error counts.
Send(ctx context.Context, notifications []Notification) (Result, error)
}
Notifier is the contract every channel implements. Implementations must be stateless: the same instance is reused across notify invocations and across goroutines. Per-call state flows through the Notification slice + the context.
type PagerDuty ¶
type PagerDuty struct {
// contains filtered or unexported fields
}
PagerDuty implements Notifier for PagerDuty Events v2.
func NewPagerDuty ¶
func NewPagerDuty(cfg PagerDutyConfig) *PagerDuty
NewPagerDuty constructs a PagerDuty sink with sensible operational defaults: Critical-only severity, "compliancekit" source.
func (*PagerDuty) Configured ¶
Configured returns true when the integration key is set.
func (*PagerDuty) Send ¶
Send dispatches one Events v2 enqueue per notification. PagerDuty dedups on dedup_key — we use the notification.Fingerprint so a re-firing finding updates the existing incident instead of opening a new one. Severity → PD severity mapping:
critical → critical high → error medium → warning low/info → info
func (*PagerDuty) Threshold ¶
func (p *PagerDuty) Threshold() compliancekit.Severity
Threshold returns the per-sink severity floor.
type PagerDutyConfig ¶
type PagerDutyConfig struct {
// IntegrationKey is the Events API v2 routing key (32 hex chars).
// Generated per-service in the PagerDuty UI.
IntegrationKey string
// Source identifies the producer in the PagerDuty event (default
// "compliancekit"). Helps on-call distinguish multiple
// notifications from the same service.
Source string
// EventsURL overrides the canonical https://events.pagerduty.com
// endpoint. Tests inject a stub here.
EventsURL string
SeverityFloor compliancekit.Severity
HTTPClient *http.Client
}
PagerDutyConfig configures the PagerDuty Events v2 sink. PagerDuty is the canonical operational escalation channel — pages humans at 3 AM — so this sink defaults to a high severity floor (Critical only) to avoid waking on-call for non-actionable medium findings. Operators can lower the threshold if they want broader paging.
Env: PAGERDUTY_INTEGRATION_KEY, PAGERDUTY_THRESHOLD, PAGERDUTY_EVENTS_URL (override for testing).
type Registry ¶
type Registry struct {
// contains filtered or unexported fields
}
Registry holds every registered Notifier. Sinks self-register via notify.Register(...) from their init(). The CLI side-effect imports each sink subpackage so importing this package is enough to make every shipped sink available.
func NewRegistry ¶
func NewRegistry() *Registry
NewRegistry returns an empty Registry. Tests use this for isolation; production code goes through Default.
type Result ¶
type Result struct {
Sent int // delivered to the upstream successfully
Skipped int // under the sink's threshold OR deduped
Errors int // permanent failures (4xx, parse errors)
Messages []string // one human-readable line per notification's outcome
}
Result reports what a sink did with the notifications it was sent. Returned per Send call; the dispatcher aggregates across sinks.
func Dispatch ¶
Dispatch fans the notifications out to every Configured sink in the registry whose Threshold permits them. Errors from individual sinks are reported in the aggregated Result.Messages; a failing sink never blocks others.
Returns the aggregated Result + the slice of per-sink (sinkName, error) pairs for the CLI to format. Per-sink errors are wrapped with the sink name so the operator knows which channel failed.
type Slack ¶
type Slack struct {
// contains filtered or unexported fields
}
Slack implements Notifier.
func NewSlack ¶
func NewSlack(cfg SlackConfig) *Slack
NewSlack constructs a Slack sink. The returned sink reports Configured()=false when both WebhookURL and (BotToken+Channel) are empty — caller can still pass it to Default safely and the dispatcher will skip it.
func (*Slack) Configured ¶
Configured returns true when either delivery path has credentials.
func (*Slack) Send ¶
Send dispatches the notifications. Per-notification failures accumulate in Result.Errors + Result.Messages; the call returns nil error unless every send failed (transport / auth).
func (*Slack) Threshold ¶
func (s *Slack) Threshold() compliancekit.Severity
Threshold returns the per-sink severity floor.
type SlackConfig ¶
type SlackConfig struct {
// WebhookURL is the canonical incoming-webhook URL. Used when
// the operator only needs notifications to land in one channel.
WebhookURL string
// BotToken (xoxb-…) is the API token for chat.postMessage.
// Used when the operator needs per-channel routing or thread
// replies. Requires the chat:write scope.
BotToken string
// Channel is the destination when using BotToken. Accepts "#name"
// or a channel ID. Ignored when WebhookURL is set.
Channel string
// SeverityFloor is the per-sink threshold. Notifications below
// this severity are dropped. Defaults to SeverityInfo (everything
// actionable passes) when zero-value.
SeverityFloor compliancekit.Severity
// HTTPClient overrides the package HTTPClient. Tests set this.
HTTPClient *http.Client
}
SlackConfig configures the Slack sink. Two delivery paths, both supported simultaneously: webhook (no auth header, no channel selection) and bot-token (PostMessage API with per-channel control). Env vars: SLACK_WEBHOOK_URL, SLACK_BOT_TOKEN, SLACK_CHANNEL, SLACK_THRESHOLD (severity string).
type Teams ¶
type Teams struct {
// contains filtered or unexported fields
}
Teams implements Notifier for Microsoft Teams incoming-webhook connectors (MessageCard payload).
func (*Teams) Configured ¶
Configured returns true when WebhookURL is set.
func (*Teams) Send ¶
Send dispatches the notifications. Per-notification failures accumulate; top-level error only when every send failed.
func (*Teams) Threshold ¶
func (t *Teams) Threshold() compliancekit.Severity
Threshold returns the per-sink severity floor.
type TeamsConfig ¶
type TeamsConfig struct {
WebhookURL string
SeverityFloor compliancekit.Severity
HTTPClient *http.Client
}
TeamsConfig configures the Microsoft Teams sink. Microsoft has two webhook flavors:
- Incoming Webhook connectors — the legacy MessageCard format. Wide deployment; deprecated by Microsoft but still supported.
- Workflows-based webhooks — Adaptive Card payload.
We ship MessageCard at v0.17 because the deprecation timeline (October 2026) leaves room to migrate, and most enterprise deployments still use legacy connectors. Adaptive Card support is a v0.18+ enhancement.
Env: TEAMS_WEBHOOK_URL, TEAMS_THRESHOLD.
type Webhook ¶
type Webhook struct {
// contains filtered or unexported fields
}
Webhook implements Notifier for generic HTTP POST sinks.
func (*Webhook) Configured ¶
Configured returns true when URL is set.
func (*Webhook) Send ¶
Send dispatches the notifications. One POST per notification (not a batch) so the receiver can correlate request ↔ finding without having to parse a list.
func (*Webhook) Threshold ¶
func (w *Webhook) Threshold() compliancekit.Severity
Threshold returns the per-sink severity floor.
type WebhookConfig ¶
type WebhookConfig struct {
URL string
Secret string
SeverityFloor compliancekit.Severity
HTTPClient *http.Client
}
WebhookConfig configures the generic webhook sink. POSTs every notification as JSON to the configured URL; optional HMAC-SHA256 signing header lets the receiver verify authenticity.
Payload shape (stable since v0.17.0, governed by SemVer once v1.0 freezes the API):
{
"schema": "compliancekit.notification.v1",
"fingerprint": "abc123...",
"title": "[CRITICAL] aws-s3-public-access-block on prod",
"body": "<CommonMark>",
"url": "https://...",
"tags": ["s3", "data-exposure"],
"finding": { ...compliancekit.Finding... }
}
When Secret is set, the request includes an X-CompliancekitSignature header: `sha256=<hex(HMAC-SHA256(secret, body))>`. Same format GitHub webhook receivers expect; receiver code reused across hooks.
Env: COMPLIANCEKIT_WEBHOOK_URL, COMPLIANCEKIT_WEBHOOK_SECRET, COMPLIANCEKIT_WEBHOOK_THRESHOLD.