Files
BaaRai 809436e111 notify: make the summary and asset limits configurable
The IM renderers cut the summary of a single push at 120 characters and
listed at most 3 assets, as fixed constants. Both are now global settings
in the settings table, read by the delivery engine when it renders:

  notify_summary_max_runes   0-4000, default 120
  notify_assets_max_shown    0-50,   default 3

0 means no truncation and an unset key keeps 120 / 3. PUT /api/settings
answers 400 for a value outside the bounds. The limits travel on
notify.Message, so the notify package still does not touch the database.

With no truncation, nothing bounded a single item in a Feishu card, and
Feishu rejects an oversized request as a whole. feishuCard now truncates
an item's text as the final backstop, the role TruncateBytes plays in
markdownBody for DingTalk and WeCom. feishuMaxCardBytes goes from 24000
to 16000: Feishu custom bots cap the request body at 20 KB, which the old
value exceeded.

Refs #168.
2026-11-10 16:05:31 +01:00

107 lines
4.2 KiB
Go

package notify
// Snapshot is the contract for the notification_events.snapshot JSONB column.
// The writer is the vulnerability persistence transaction in the db layer; the
// readers are the delivery engine and filter matching in the server layer. The
// definition lives in this package because it is the payload of the
// "notification domain": db only serializes it and does not understand the fields.
//
// Why store vulnerability fields redundantly instead of looking them up at render
// time: a vulnerability can be renamed, re-severitied, or re-statused after the
// fact, while a pushed message must reflect the conclusion **at the moment of the
// event** — a lookup would produce the dangerously misleading "later downgraded to
// low" content. As a bonus, fan-out and rendering do not need to JOIN the
// findings/tasks/assets tables.
type Snapshot struct {
// Event kind: finding_created / finding_status_changed
Kind string `json:"kind"`
FindingID int64 `json:"finding_id"`
TaskID int64 `json:"task_id"`
VulnClass string `json:"vulnclass"`
Name string `json:"name"`
Severity string `json:"severity"`
Summary string `json:"summary"`
AssetIDs []int64 `json:"asset_ids"`
// Non-empty only when kind=finding_status_changed.
FromStatus string `json:"from_status,omitempty"`
ToStatus string `json:"to_status,omitempty"`
}
// Item is a single vulnerability to be pushed, rendered by channels.
type Item struct {
FindingID int64
Name string
VulnClass string
Severity string
Summary string
// Assets holds the resolved display names of the assets (e.g. domain/IP).
// Filled by the server layer — this package never touches the database
// and has no way to look up names.
Assets []string
// DetailURL is the backlink to the vulnerability details; empty means
// public_base_url is not configured and rendering omits it.
DetailURL string
// Only for status-change events; when both are non-empty, rendered as
// "Pending → Fixed".
FromStatus string
ToStatus string
}
// IsStatusChange reports whether this item is a status-change event.
func (i Item) IsStatusChange() bool { return i.FromStatus != "" || i.ToStatus != "" }
// Title returns the display title of the item: prefer the human-assigned name,
// fall back to the vulnerability class vulnclass, and use a placeholder when both
// are empty — never emit an empty title.
func (i Item) Title() string {
if i.Name != "" {
return i.Name
}
if i.VulnClass != "" {
return i.VulnClass
}
return "(unnamed vulnerability)"
}
// Message is the complete content of one channel send.
type Message struct {
// Length is 1 for a single push; a whole batch for a digest push.
// An empty slice is illegal; callers must guarantee at least one item.
Items []Item
// Batch=true renders as a digest message (different title, with time window
// and item count).
Batch bool
// WindowMinutes is the digest period (in minutes), used only with Batch=true
// for the "in the last N minutes" wording. Deliberately passed in explicitly
// from configuration instead of computing time.Since at render time: keeping
// rendering deterministic makes it testable.
WindowMinutes int
// HomeURL is the platform dashboard address (global public_base_url); empty
// means no dashboard entry is included.
HomeURL string
// Limits are the truncation limits to render with; nil means the defaults.
// A pointer because the zero value is meaningful (no truncation).
Limits *Limits
}
// Limits are the render-time truncation limits of a message, configured
// globally (see the server's notify_summary_max_runes / notify_assets_max_shown
// settings). In both fields 0 means no truncation; each channel's own length
// cap remains the final guard.
type Limits struct {
// SummaryRunes is how many characters the summary of a single push is
// compressed to.
SummaryRunes int
// AssetsShown is the maximum number of assets listed per vulnerability.
AssetsShown int
}
// limits returns the limits to render with, falling back to the defaults when
// none were passed in.
func (m Message) limits() Limits {
if m.Limits == nil {
return Limits{SummaryRunes: DefaultMaxSummaryRunes, AssetsShown: DefaultMaxAssetsShown}
}
return *m.Limits
}