Files
ARTEX/db/asset_intercept_match.go
BaaRai e233c02314 Translate all Chinese text to English
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
2026-10-08 23:14:34 +02:00

262 lines
7.6 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package db
import (
"fmt"
"net"
"net/url"
"strings"
)
// Matching/enforcement layer for asset interception rules. asset_intercept.go only
// stores rules; this file matches a target asset's domain/IP/URL candidates against
// the enabled rules. Called by agent tools (add_intent, insert_assets) before
// dispatching an intent / inserting assets; a hit means rejection.
// AssetInterceptKindLabel returns the human-readable label for kind, used in
// explanatory messages to the agent.
func AssetInterceptKindLabel(kind string) string {
switch kind {
case "exact_domain":
return "Domain (exact)"
case "exact_ip":
return "IP (exact)"
case "exact_url":
return "URL (exact)"
case "fuzzy_domain":
return "Domain (fuzzy)"
case "fuzzy_ip":
return "IP (fuzzy)"
case "fuzzy_url":
return "URL (fuzzy)"
case "cidr":
return "CIDR range"
}
return kind
}
// Reason returns a readable hit reason, e.g.: Matched asset interception rule [Domain (fuzzy): .gov.cn] (note).
func (r AssetInterceptRule) Reason() string {
s := fmt.Sprintf("Matched asset interception rule [%s: %s]", AssetInterceptKindLabel(r.Kind), r.Pattern)
if note := strings.TrimSpace(r.Note); note != "" {
s += " (" + note + ")"
}
return s
}
// matchOne checks whether a single enabled rule matches the given domain/IP/URL
// candidates, and returns the concrete matched value.
func matchOne(r AssetInterceptRule, domains, ips, urls []string) (string, bool) {
p := strings.TrimSpace(r.Pattern)
if p == "" {
return "", false
}
switch r.Kind {
case "exact_domain":
for _, d := range domains {
if strings.EqualFold(strings.TrimSpace(d), p) {
return d, true
}
}
case "exact_ip":
for _, ip := range ips {
if strings.TrimSpace(ip) == p {
return ip, true
}
}
case "exact_url":
for _, u := range urls {
if strings.TrimSpace(u) == p {
return u, true
}
}
case "fuzzy_domain":
lp := strings.ToLower(p)
for _, d := range domains {
if d != "" && strings.Contains(strings.ToLower(d), lp) {
return d, true
}
}
case "fuzzy_ip":
for _, ip := range ips {
if ip != "" && strings.Contains(ip, p) {
return ip, true
}
}
case "fuzzy_url":
lp := strings.ToLower(p)
for _, u := range urls {
if u != "" && strings.Contains(strings.ToLower(u), lp) {
return u, true
}
}
case "cidr":
_, ipnet, err := net.ParseCIDR(p)
if err != nil {
return "", false
}
for _, ip := range ips {
if pip := net.ParseIP(strings.TrimSpace(ip)); pip != nil && ipnet.Contains(pip) {
return ip, true
}
}
}
return "", false
}
// MatchAssetInterceptRules returns the first enabled rule matching the given
// domain/IP/URL candidates, plus the concrete matched value.
// Used by insert_assets to match raw input (assetInputItem not yet persisted).
func MatchAssetInterceptRules(rules []AssetInterceptRule, domains, ips, urls []string) (AssetInterceptRule, string, bool) {
for _, r := range rules {
if !r.Enabled {
continue
}
if v, ok := matchOne(r, domains, ips, urls); ok {
return r, v, true
}
}
return AssetInterceptRule{}, "", false
}
// interceptCandidates extracts a persisted asset's domain/IP/URL candidates for
// interception matching. A URL's host is split out and classified, so service
// assets that only carry a URL can still be hit by domain/IP rules.
func (a *Asset) interceptCandidates() (domains, ips, urls []string) {
add := func(dst *[]string, s string) {
if s = strings.TrimSpace(s); s != "" {
*dst = append(*dst, s)
}
}
add(&domains, a.Domain)
add(&domains, a.RootDomain)
for _, d := range a.BoundDomains {
add(&domains, d)
}
add(&ips, a.IP)
add(&urls, a.URL)
if a.URL != "" {
if u, err := url.Parse(a.URL); err == nil {
if h := u.Hostname(); h != "" {
if net.ParseIP(h) != nil {
add(&ips, h)
} else {
add(&domains, h)
}
}
}
}
return domains, ips, urls
}
// InterceptLabel returns a short identifier for the asset, used in explanatory
// messages to the agent.
func (a *Asset) InterceptLabel() string {
var target string
switch {
case a.Domain != "":
target = a.Domain
case a.URL != "":
target = a.URL
case a.IP != "":
target = a.IP
default:
target = fmt.Sprintf("#%d", a.ID)
}
return fmt.Sprintf("Asset #%d[%s] %s", a.ID, a.Type, target)
}
// hasEnabledRule reports whether the rule set contains any enabled rule.
func hasEnabledRule(rules []AssetInterceptRule) bool {
for _, r := range rules {
if r.Enabled {
return true
}
}
return false
}
// AssetGateDecision is the result of the "intercept first, then allow" gate over
// a set of candidate strings.
type AssetGateDecision struct {
Allowed bool
Reason string // rejection reason (without the asset identifier); empty when Allowed=true
}
// EvaluateAssetGate performs the task-level gate decision:
// 1. Any enabled blockRules hit → reject (interception reason).
// 2. Otherwise, if allowRules has enabled entries and none match → reject (outside the allowed scope).
// 3. Otherwise allow.
//
// When allowRules is empty / has no enabled entries, the allow gate is inert
// (i.e. no whitelist, everything is allowed), so that "no allow rules
// configured" does not block every asset.
func EvaluateAssetGate(blockRules, allowRules []AssetInterceptRule, domains, ips, urls []string) AssetGateDecision {
if rule, _, ok := MatchAssetInterceptRules(blockRules, domains, ips, urls); ok {
return AssetGateDecision{Allowed: false, Reason: rule.Reason()}
}
if hasEnabledRule(allowRules) {
if _, _, ok := MatchAssetInterceptRules(allowRules, domains, ips, urls); !ok {
return AssetGateDecision{Allowed: false, Reason: "Not in the task's allowed (whitelist) scope; testing is not permitted"}
}
}
return AssetGateDecision{Allowed: true}
}
// AssetInterceptHit describes an asset rejected by the gate (interception hit or
// outside the allowed scope).
type AssetInterceptHit struct {
Asset *Asset
Reason string // readable reason
}
// Describe returns a readable explanation: asset info + reason.
func (h AssetInterceptHit) Describe() string {
return fmt.Sprintf("%s → %s", h.Asset.InterceptLabel(), h.Reason)
}
// ListAssetInterceptRules is a pass-through of the *DB method of the same name,
// so callers that only hold an AssetStore (e.g. agent tools) can also read rules.
func (s *AssetStore) ListAssetInterceptRules() ([]AssetInterceptRule, error) {
return s.db.ListAssetInterceptRules()
}
// CheckAssetsIntercept loads assets by id, runs the "intercept first, then allow"
// gate on each, and returns all rejected assets. Interception rules = global ∪
// task-level block; allow rules = task-level allow (this task only).
// Returns fast when there are no ids. Uses the global GetByIDs (not filtered by
// task scope) so interception is never weakened by scope.
func (s *AssetStore) CheckAssetsIntercept(taskID int64, ids []int64) ([]AssetInterceptHit, error) {
if len(ids) == 0 {
return nil, nil
}
blockRules, err := s.db.ListAssetInterceptRules()
if err != nil {
return nil, err
}
var allowRules []AssetInterceptRule
if taskID > 0 {
tb, ta, err := s.TaskInterceptRulesSplit(taskID)
if err != nil {
return nil, err
}
blockRules = append(blockRules, tb...)
allowRules = ta
}
// No interception rules and no enabled allow rules → nothing to decide, allow all.
if len(blockRules) == 0 && !hasEnabledRule(allowRules) {
return nil, nil
}
assets, err := s.GetByIDs(ids)
if err != nil {
return nil, err
}
var hits []AssetInterceptHit
for _, a := range assets {
domains, ips, urls := a.interceptCandidates()
if d := EvaluateAssetGate(blockRules, allowRules, domains, ips, urls); !d.Allowed {
hits = append(hits, AssetInterceptHit{Asset: a, Reason: d.Reason})
}
}
return hits, nil
}