mirror of
https://github.com/BaaRai/ARTEX.git
synced 2026-11-13 11:40:53 +08:00
Comments, doc comments, UI labels, log and error messages, LLM prompt strings, markdown docs, test data and assertions. Identifiers, enum keys, routes, placeholders and code structure are unchanged. Notes: - docs/漏洞流量证据.md renamed to docs/vulnerability-traffic-evidence.md - tests that exercised CJK specific handling (GBK zip names, Chinese full text search) now use ASCII equivalents; go build passes and the test suite matches the pre translation baseline (remaining failures need a PostgreSQL DSN and fail identically on the original) - frontend mention syntax now uses English kinds (finding, asset, ...) matching the backend, chat mentions tests pass
225 lines
7.6 KiB
Go
225 lines
7.6 KiB
Go
package notify
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
"unicode/utf8"
|
|
)
|
|
|
|
// This file covers the "pack whole items" fix: when a digest message exceeds the
|
|
// channel's length limit, it must be truncated **item by item** and honestly
|
|
// report how many items did not fit, so the caller marks only the ones actually
|
|
// delivered.
|
|
//
|
|
// The previous approach rendered the whole thing, truncated it, and then marked
|
|
// the whole batch as delivered: the second half of the message vanished into
|
|
// thin air while the delivery history showed all succeeded — vulnerabilities
|
|
// were simply lost, and nowhere could you find out.
|
|
|
|
func TestMarkdownBodyPacksWholeItemsWithinByteLimit(t *testing.T) {
|
|
// 200 digest items are guaranteed to far exceed WeCom's 4096 bytes.
|
|
m := batchMsg(200)
|
|
body, kept := markdownBody(m, weComMarkdownLimit)
|
|
|
|
if len(body) > weComMarkdownLimit {
|
|
t.Fatalf("body of %d bytes exceeds the limit of %d", len(body), weComMarkdownLimit)
|
|
}
|
|
if !utf8.ValidString(body) {
|
|
t.Fatal("body is not valid UTF-8")
|
|
}
|
|
if kept <= 0 || kept >= len(m.Items) {
|
|
t.Fatalf("only a part should fit (0 < kept < %d), got %d", len(m.Items), kept)
|
|
}
|
|
// The header must honestly state how many items this message contains and how
|
|
// many remain — otherwise readers take the number in the header for the whole
|
|
// batch.
|
|
if !strings.Contains(body, "remaining") || !strings.Contains(body, "next message") {
|
|
t.Fatalf("the header should state how many items are not included in this message:\n%s", body[:minInt(400, len(body))])
|
|
}
|
|
// Only the first kept items should be included.
|
|
for i := 0; i < kept; i++ {
|
|
if !strings.Contains(body, "vuln"+itoa(i+1)) {
|
|
t.Fatalf("item %d should be in this message:\n%s", i+1, body)
|
|
}
|
|
}
|
|
if strings.Contains(body, "vuln"+itoa(kept+1)) {
|
|
t.Fatalf("item %d should not appear (it belongs to the next batch)", kept+1)
|
|
}
|
|
}
|
|
|
|
func TestMarkdownBodyKeepsEverythingWhenUnderLimit(t *testing.T) {
|
|
m := batchMsg(3)
|
|
body, kept := markdownBody(m, 0) // 0 = unlimited
|
|
if kept != len(m.Items) {
|
|
t.Fatalf("everything should be kept when length is unlimited, got kept=%d", kept)
|
|
}
|
|
if strings.Contains(body, "remaining") {
|
|
t.Fatalf("no truncation notice should appear when nothing was truncated:\n%s", body)
|
|
}
|
|
}
|
|
|
|
func TestMarkdownBodyAlwaysKeepsAtLeastOneItem(t *testing.T) {
|
|
// When the budget is too small to fit even one item, still send one (with
|
|
// final truncation as the backstop). Otherwise a single oversized
|
|
// vulnerability would permanently jam the whole batch in place: never fitting
|
|
// on every claim, never sent.
|
|
m := batchMsg(5)
|
|
_, kept := markdownBody(m, 50)
|
|
if kept != 1 {
|
|
t.Fatalf("at least 1 item should be kept, got %d", kept)
|
|
}
|
|
}
|
|
|
|
func TestMarkdownBodySingleReturnsOne(t *testing.T) {
|
|
_, kept := markdownBody(singleMsg(), 4096)
|
|
if kept != 1 {
|
|
t.Fatalf("a single message should report 1 delivered, got %d", kept)
|
|
}
|
|
// An empty message has no items to deliver.
|
|
if _, k := markdownBody(Message{}, 4096); k != 0 {
|
|
t.Fatalf("an empty message should report 0 items, got %d", k)
|
|
}
|
|
}
|
|
|
|
func TestTelegramPackingUsesRuneBudget(t *testing.T) {
|
|
m := batchMsg(200)
|
|
text, kept := telegramHTML(m)
|
|
// Telegram limits by **character count**; measuring in bytes would cram a
|
|
// CJK message down to a third.
|
|
if n := utf8.RuneCountInString(text); n > telegramTextLimit {
|
|
t.Fatalf("body of %d characters exceeds the limit of %d", n, telegramTextLimit)
|
|
}
|
|
if kept <= 0 || kept >= len(m.Items) {
|
|
t.Fatalf("only a part should fit, got %d", kept)
|
|
}
|
|
if !strings.Contains(text, "next message") {
|
|
t.Fatalf("should state that more items are not included:\n%.300s", text)
|
|
}
|
|
}
|
|
|
|
func TestFeishuPackingReportsKept(t *testing.T) {
|
|
m := batchMsg(2000)
|
|
_, kept := feishuCard(m)
|
|
if kept <= 0 || kept >= len(m.Items) {
|
|
t.Fatalf("the card should only fit a part, got %d", kept)
|
|
}
|
|
}
|
|
|
|
func TestWebhookAndEmailReportAllItems(t *testing.T) {
|
|
// These two channels do not truncate the body; the whole batch counts as
|
|
// delivered.
|
|
m := batchMsg(7)
|
|
if n := len(m.Items); n != 7 {
|
|
t.Fatal("precondition does not hold")
|
|
}
|
|
// Confirm indirectly via the renderer's return value: markdownBody(0) keeps
|
|
// everything when unlimited.
|
|
if _, k := markdownBody(m, 0); k != len(m.Items) {
|
|
t.Fatalf("everything should be used when length is unlimited, got %d", k)
|
|
}
|
|
}
|
|
|
|
// TestMarkdownEscapesUntrustedContent is the regression test for "untrusted
|
|
// content must not change message structure". Titles and summaries come from
|
|
// model output (the model reads the tested target's responses); asset names come
|
|
// from the tested target's URLs.
|
|
func TestMarkdownEscapesUntrustedContent(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
item Item
|
|
must []string // must appear in the result (escaped form)
|
|
wrong []string // must not appear in the result (unescaped form)
|
|
}{
|
|
{
|
|
name: "newline + external link in the title",
|
|
item: Item{
|
|
Severity: "high",
|
|
Name: "SQL injection on login page\n[URGENT: click to verify your account](http://attacker.tld)",
|
|
},
|
|
// The newline must be collapsed (otherwise it can forge a new list
|
|
// item / quote block); the square brackets and parentheses must be
|
|
// escaped (otherwise it is a clickable external link).
|
|
must: []string{`\[URGENT: click to verify your account\]`, `\(http://attacker.tld\)`},
|
|
wrong: []string{"\n[URGENT", "\n\n[URGENT"},
|
|
},
|
|
{
|
|
name: "image beacon in the title",
|
|
item: Item{
|
|
Severity: "high",
|
|
Name: "Vuln ",
|
|
},
|
|
must: []string{`\!`, `\(http://attacker.tld/beacon\)`},
|
|
wrong: []string{" {
|
|
m := Message{Items: []Item{tc.item}}
|
|
// writeItem in single mode is the rendering path shared by all three
|
|
// markdown channels.
|
|
var b strings.Builder
|
|
writeItem(&b, tc.item, "", true)
|
|
got := b.String()
|
|
for _, want := range tc.must {
|
|
if !strings.Contains(got, want) {
|
|
t.Errorf("missing escaped form %q:\n%s", want, got)
|
|
}
|
|
}
|
|
for _, bad := range tc.wrong {
|
|
if strings.Contains(got, bad) {
|
|
t.Errorf("unescaped form %q appeared (can be used to inject structure or external links):\n%s", bad, got)
|
|
}
|
|
}
|
|
_ = m
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestMarkdownEscapeBackslashFirst locks the escape order: the backslash must be
|
|
// handled first, otherwise the backslashes added later get another layer and the
|
|
// output ends up with double backslashes.
|
|
func TestMarkdownEscapeBackslashFirst(t *testing.T) {
|
|
if got := markdownEscape(`a\b*c`); got != `a\\b\*c` {
|
|
t.Fatalf("wrong escape order, got %q", got)
|
|
}
|
|
}
|
|
|
|
// TestTelegramTitleHasNoMarkdownEscapes locks a specific regression: markdown
|
|
// escapes must not leak into Telegram's HTML output (an escape was once added to
|
|
// the shared title function, and Telegram messages ended up showing visible
|
|
// backslashes like `\(1\)`).
|
|
func TestTelegramTitleHasNoMarkdownEscapes(t *testing.T) {
|
|
m := Message{Items: []Item{{Severity: "high", Name: "alert(1) *important*"}}}
|
|
text, _ := telegramHTML(m)
|
|
if strings.Contains(text, `\(`) || strings.Contains(text, `\*`) {
|
|
t.Fatalf("markdown backslash escapes appeared in the Telegram body:\n%s", text)
|
|
}
|
|
}
|
|
|
|
func minInt(a, b int) int {
|
|
if a < b {
|
|
return a
|
|
}
|
|
return b
|
|
}
|