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
135 lines
3.9 KiB
Go
135 lines
3.9 KiB
Go
package db
|
|
|
|
import (
|
|
"net"
|
|
"net/url"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"golang.org/x/net/publicsuffix"
|
|
)
|
|
|
|
// 归一化自然键 (nkey):移植自旧 graph/id.go,去掉 StableID 哈希(PG 用 BIGSERIAL 主键 +
|
|
// UNIQUE(type, nkey) 去重)。子资产的 nkey 内嵌父资产的 int64 id,把层级编码进键。
|
|
|
|
func DomainKey(fqdn string) string {
|
|
return strings.TrimSuffix(strings.ToLower(strings.TrimSpace(fqdn)), ".")
|
|
}
|
|
|
|
func IPKey(ip string) string { return strings.TrimSpace(ip) }
|
|
|
|
// RootDomain returns the registrable domain (eTLD+1) for a host and whether the
|
|
// host itself IS that apex (§3.1). Edge cases (§3.1 边界处理): an IP literal or a
|
|
// host publicsuffix can't classify (localhost / internal / non-ICANN TLD) is
|
|
// returned unchanged as its own root with isApex=true — best-effort, never treated
|
|
// as a subdomain.
|
|
func RootDomain(host string) (root string, isApex bool) {
|
|
h := DomainKey(host)
|
|
if h == "" || net.ParseIP(h) != nil {
|
|
return h, true
|
|
}
|
|
etld1, err := publicsuffix.EffectiveTLDPlusOne(h)
|
|
if err != nil || etld1 == "" {
|
|
return h, true
|
|
}
|
|
return etld1, h == etld1
|
|
}
|
|
|
|
func PortKey(ipID int64, proto string, port int) string {
|
|
return itoa(ipID) + "|" + strings.ToLower(proto) + "|" + strconv.Itoa(port)
|
|
}
|
|
|
|
func ServiceKey(portID int64, svcName string) string {
|
|
return itoa(portID) + "|" + strings.ToLower(svcName)
|
|
}
|
|
|
|
func SiteKey(scheme, host string, port int) string {
|
|
return strings.ToLower(scheme) + "|" + strings.ToLower(host) + "|" + strconv.Itoa(port)
|
|
}
|
|
|
|
func EndpointKey(siteID int64, method, urlTemplate string) string {
|
|
return itoa(siteID) + "|" + strings.ToUpper(method) + "|" + urlTemplate
|
|
}
|
|
|
|
func ParameterKey(endpointID int64, location, name string) string {
|
|
return itoa(endpointID) + "|" + strings.ToLower(location) + "|" + name
|
|
}
|
|
|
|
// NormalizeParamName 归一化参数名(endpoint.params 元素的「相同引用」判定)。
|
|
// 规则:lower + trim,不做同义词合并(userId/user_id/uid 视为不同)。写入与查询共享此实现,
|
|
// 保证「按参数名查同公司接口」可复现。
|
|
func NormalizeParamName(name string) string {
|
|
return strings.ToLower(strings.TrimSpace(name))
|
|
}
|
|
|
|
func TechKey(name, version string) string {
|
|
return strings.ToLower(name) + "|" + version
|
|
}
|
|
|
|
func itoa(n int64) string { return strconv.FormatInt(n, 10) }
|
|
|
|
var (
|
|
reNumeric = regexp.MustCompile(`^\d+$`)
|
|
reUUID = regexp.MustCompile(`^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$`)
|
|
reHex = regexp.MustCompile(`^[0-9a-fA-F]{16,}$`)
|
|
reLong = regexp.MustCompile(`^[A-Za-z0-9_-]{24,}$`)
|
|
)
|
|
|
|
// TemplatePath collapses high-cardinality path segments into placeholders so the
|
|
// graph is not flooded by instances: /user/123 -> /user/{id}.
|
|
func TemplatePath(path string) string {
|
|
if path == "" {
|
|
return "/"
|
|
}
|
|
segs := strings.Split(path, "/")
|
|
for i, s := range segs {
|
|
switch {
|
|
case s == "":
|
|
continue
|
|
case reNumeric.MatchString(s):
|
|
segs[i] = "{id}"
|
|
case reUUID.MatchString(s):
|
|
segs[i] = "{uuid}"
|
|
case reHex.MatchString(s):
|
|
segs[i] = "{hex}"
|
|
case reLong.MatchString(s):
|
|
segs[i] = "{token}"
|
|
}
|
|
}
|
|
return strings.Join(segs, "/")
|
|
}
|
|
|
|
// SplitURL parses a raw URL into scheme/host/port/urlTemplate/params for building
|
|
// site/endpoint/param keys.
|
|
func SplitURL(raw, method string) (scheme, host string, port int, urlTemplate string, params []string, err error) {
|
|
u, err := url.Parse(raw)
|
|
if err != nil {
|
|
return "", "", 0, "", nil, err
|
|
}
|
|
scheme = strings.ToLower(u.Scheme)
|
|
host = strings.ToLower(u.Hostname())
|
|
port = defaultPort(scheme, u.Port())
|
|
urlTemplate = TemplatePath(u.EscapedPath())
|
|
for k := range u.Query() {
|
|
params = append(params, k)
|
|
}
|
|
return scheme, host, port, urlTemplate, params, nil
|
|
}
|
|
|
|
func defaultPort(scheme, p string) int {
|
|
if p != "" {
|
|
if n, err := strconv.Atoi(p); err == nil {
|
|
return n
|
|
}
|
|
}
|
|
switch scheme {
|
|
case "https":
|
|
return 443
|
|
case "http":
|
|
return 80
|
|
default:
|
|
return 0
|
|
}
|
|
}
|