Files
artex/db/chat_mentions.go
T
dela 0335d572de
ci / go (push) Waiting to run
ci / go-db (agent) (push) Waiting to run
ci / go-db (config) (push) Waiting to run
ci / go-db (db) (push) Waiting to run
ci / go-db (evidence) (push) Waiting to run
ci / go-db (llmrec) (push) Waiting to run
ci / go-db (server) (push) Waiting to run
detections / detections (push) Waiting to run
web / web (push) Waiting to run
docs / links (push) Canceled after 0s
First Commit
2026-10-09 08:38:16 +08:00

114 lines
4.4 KiB
Go

package db
import (
"context"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"strings"
)
// ChatMention is a lightweight search result. Details are read again on send.
type ChatMention struct {
Kind string `json:"kind"`
ID int64 `json:"id"`
Label string `json:"label"`
Description string `json:"description"`
}
type ChatMentionPage struct {
Items []ChatMention `json:"items"`
NextCursor string `json:"next_cursor,omitempty"`
}
var ErrInvalidChatMentionCursor = errors.New("分页位置无效,请重新搜索")
type chatMentionCursor struct {
ID int64 `json:"id"`
Kind string `json:"kind"`
Exact bool `json:"exact"`
Scope string `json:"scope"`
Query string `json:"query"`
}
func ValidChatMentionKind(kind string) bool {
switch kind {
case "finding", "company", "asset", "endpoint", "ip", "app", "root_domain", "subdomain", "service":
return true
}
return false
}
// SearchChatMentions searches the shared catalog, just like the asset/finding
// pages. Values remain SQL parameters; %, _ and backslash are literal search text.
func (d *DB) SearchChatMentions(ctx context.Context, kind, query string) ([]ChatMention, error) {
page, err := d.SearchChatMentionsPage(ctx, kind, query, "")
return page.Items, err
}
// SearchChatMentionsPage uses the last result's stable sort key rather than an
// offset, so loading later pages does not repeatedly skip all earlier results.
func (d *DB) SearchChatMentionsPage(ctx context.Context, kind, query, cursor string) (ChatMentionPage, error) {
page := ChatMentionPage{Items: make([]ChatMention, 0)}
if kind != "" && !ValidChatMentionKind(kind) {
return page, fmt.Errorf("不支持的引用类型")
}
var after chatMentionCursor
if cursor != "" {
if len(cursor) > 2048 {
return page, ErrInvalidChatMentionCursor
}
blob, err := base64.RawURLEncoding.DecodeString(cursor)
if err != nil || json.Unmarshal(blob, &after) != nil || after.ID <= 0 ||
!ValidChatMentionKind(after.Kind) || after.Scope != kind || after.Query != query {
return page, ErrInvalidChatMentionCursor
}
}
pattern := "%" + strings.NewReplacer(`\`, `\\`, "%", `\%`, "_", `\_`).Replace(query) + "%"
rows, err := d.QueryContext(ctx, `
SELECT kind, id, left(label, 160), left(description, 240) FROM (
(SELECT 'finding' AS kind, id, COALESCE(NULLIF(name,''), vulnclass) AS label,
concat_ws(' · ', severity, status, left(summary, 160)) AS description
FROM findings WHERE ($1='' OR $1='finding') AND
($2='' OR id::text=$2 OR concat_ws(' ',name,vulnclass,summary) ILIKE $3)
AND ($4::bigint=0 OR (id::text=$2)<$6 OR ((id::text=$2)=$6 AND (id<$4 OR (id=$4 AND 'finding'>$5))))
ORDER BY (id::text=$2) DESC, id DESC LIMIT 21)
UNION ALL
(SELECT 'company', id, name, nkey FROM companies
WHERE ($1='' OR $1='company') AND ($2='' OR id::text=$2 OR name ILIKE $3 OR nkey ILIKE $3)
AND ($4::bigint=0 OR (id::text=$2)<$6 OR ((id::text=$2)=$6 AND (id<$4 OR (id=$4 AND 'company'>$5))))
ORDER BY (id::text=$2) DESC, id DESC LIMIT 21)
UNION ALL
(SELECT type, id,
CASE WHEN type='endpoint' THEN concat_ws(' ',NULLIF(method,''),url)
ELSE COALESCE(NULLIF(app_name,''),NULLIF(url,''),NULLIF(domain,''),NULLIF(ip,''),NULLIF(bundle_id,''),'资产 #'||id::text) END,
concat_ws(' · ',type,NULLIF(page_title,''),NULLIF(service_name,''),NULLIF(bundle_id,''),NULLIF(ip,''),port::text)
FROM assets WHERE ($1='' OR $1='asset' OR type=$1) AND
($2='' OR id::text=$2 OR concat_ws(' ',domain,root_domain,ip,url,app_name,bundle_id,page_title,service_name,method) ILIKE $3)
AND ($4::bigint=0 OR (id::text=$2)<$6 OR ((id::text=$2)=$6 AND (id<$4 OR (id=$4 AND type>$5))))
ORDER BY (id::text=$2) DESC, id DESC LIMIT 21)
) matches ORDER BY (id::text=$2) DESC, id DESC, kind LIMIT 21`, kind, query, pattern, after.ID, after.Kind, after.Exact)
if err != nil {
return page, err
}
defer rows.Close()
for rows.Next() {
var item ChatMention
if err := rows.Scan(&item.Kind, &item.ID, &item.Label, &item.Description); err != nil {
return page, err
}
page.Items = append(page.Items, item)
}
if err := rows.Err(); err != nil {
return page, err
}
if len(page.Items) > 20 {
page.Items = page.Items[:20]
last := page.Items[19]
blob, _ := json.Marshal(chatMentionCursor{last.ID, last.Kind, fmt.Sprint(last.ID) == query, kind, query})
page.NextCursor = base64.RawURLEncoding.EncodeToString(blob)
}
return page, nil
}