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
web / web (push) Waiting to run
docs / links (push) Canceled after 0s
detections / detections (push) Canceled after 0s
114 lines
4.4 KiB
Go
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
|
|
}
|