在 agent 下发意图(add_intent)与插入资产(insert_assets)两处接入资产闸门;
并新增任务级规则(拦截 block + 允许 allow 白名单),创建任务时录入、任务详情
总览里可编辑。
- 匹配/执行(db/asset_intercept_match.go):MatchAssetInterceptRules 纯匹配
(exact/fuzzy 的 域名·IP·URL + CIDR,URL host 会拆出归类);EvaluateAssetGate
「先拦截后允许」闸门——命中拦截即禁止;未命中但存在启用的允许规则且都不命中→
不允许测试;无启用允许规则则不启用白名单。附单元测试。
- 执行点:add_intent 命中即不下发意图;insert_assets 命中即跳过 Upsert 及
AddAutoScope 副作用。均合并 全局∪任务级 规则。
- 任务级规则:表 task_intercept_rules(task_id FK CASCADE + action block/allow,
幂等 ALTER 补旧库);db/task_intercept.go 提供 List/Split/CRUD/事务插入;
创建任务随 intercept_rules 提交;CRUD 路由 /api/tasks/{id}/intercept-rules。
- 前端:新建任务抽屉多行录入(拦截/允许 + 类型,用 NativeSelect 避免 Sheet 内
下拉 portal 导致误关抽屉);任务详情总览新增规则管理卡片(增删改+启用开关)。
130 lines
5.3 KiB
Go
130 lines
5.3 KiB
Go
package db
|
|
|
|
import "testing"
|
|
|
|
func rule(kind, pattern string, enabled bool) AssetInterceptRule {
|
|
return AssetInterceptRule{Kind: kind, Pattern: pattern, Enabled: enabled}
|
|
}
|
|
|
|
func TestMatchAssetInterceptRules(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
rules []AssetInterceptRule
|
|
domains []string
|
|
ips []string
|
|
urls []string
|
|
want bool
|
|
wantVal string
|
|
}{
|
|
{"内置模糊政府域名命中", []AssetInterceptRule{rule("fuzzy_domain", ".gov.cn", true)},
|
|
[]string{"www.beijing.gov.cn"}, nil, nil, true, "www.beijing.gov.cn"},
|
|
{"模糊教育域名命中", []AssetInterceptRule{rule("fuzzy_domain", ".edu", true)},
|
|
[]string{"mit.edu"}, nil, nil, true, "mit.edu"},
|
|
{"全等域名命中大小写不敏感", []AssetInterceptRule{rule("exact_domain", "Example.com", true)},
|
|
[]string{"example.com"}, nil, nil, true, "example.com"},
|
|
{"全等域名不命中子域", []AssetInterceptRule{rule("exact_domain", "example.com", true)},
|
|
[]string{"a.example.com"}, nil, nil, false, ""},
|
|
{"全等IP命中", []AssetInterceptRule{rule("exact_ip", "203.0.113.5", true)},
|
|
nil, []string{"203.0.113.5"}, nil, true, "203.0.113.5"},
|
|
{"模糊IP前缀命中", []AssetInterceptRule{rule("fuzzy_ip", "203.0.113.", true)},
|
|
nil, []string{"203.0.113.99"}, nil, true, "203.0.113.99"},
|
|
{"CIDR 命中", []AssetInterceptRule{rule("cidr", "192.168.0.0/16", true)},
|
|
nil, []string{"192.168.5.20"}, nil, true, "192.168.5.20"},
|
|
{"CIDR 不命中", []AssetInterceptRule{rule("cidr", "192.168.0.0/16", true)},
|
|
nil, []string{"10.0.0.1"}, nil, false, ""},
|
|
{"全等URL命中", []AssetInterceptRule{rule("exact_url", "https://a.gov.cn/login", true)},
|
|
nil, nil, []string{"https://a.gov.cn/login"}, true, "https://a.gov.cn/login"},
|
|
{"模糊URL命中路径", []AssetInterceptRule{rule("fuzzy_url", "/admin", true)},
|
|
nil, nil, []string{"https://x.com/admin/panel"}, true, "https://x.com/admin/panel"},
|
|
{"禁用规则不命中", []AssetInterceptRule{rule("fuzzy_domain", ".gov.cn", false)},
|
|
[]string{"www.gov.cn"}, nil, nil, false, ""},
|
|
{"无规则不命中", nil, []string{"www.gov.cn"}, nil, nil, false, ""},
|
|
{"空pattern不命中", []AssetInterceptRule{rule("fuzzy_domain", " ", true)},
|
|
[]string{"www.gov.cn"}, nil, nil, false, ""},
|
|
}
|
|
for _, c := range cases {
|
|
t.Run(c.name, func(t *testing.T) {
|
|
r, val, ok := MatchAssetInterceptRules(c.rules, c.domains, c.ips, c.urls)
|
|
if ok != c.want {
|
|
t.Fatalf("命中 = %v, 期望 %v (rule=%+v)", ok, c.want, r)
|
|
}
|
|
if ok && val != c.wantVal {
|
|
t.Fatalf("命中值 = %q, 期望 %q", val, c.wantVal)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestEvaluateAssetGate(t *testing.T) {
|
|
block := []AssetInterceptRule{rule("fuzzy_domain", ".gov.cn", true)}
|
|
allow := []AssetInterceptRule{rule("fuzzy_domain", "example.com", true)}
|
|
|
|
// 1. 命中拦截规则 → 拒绝(拦截原因优先)。
|
|
if d := EvaluateAssetGate(block, allow, []string{"www.gov.cn"}, nil, nil); d.Allowed {
|
|
t.Fatal("命中拦截规则应被拒绝")
|
|
}
|
|
|
|
// 2. 未命中拦截、有允许规则但不命中 → 拒绝(不允许)。
|
|
d := EvaluateAssetGate(block, allow, []string{"foo.other.com"}, nil, nil)
|
|
if d.Allowed {
|
|
t.Fatal("有白名单且不命中应被拒绝")
|
|
}
|
|
if d.Reason == "" {
|
|
t.Fatal("拒绝应带原因")
|
|
}
|
|
|
|
// 3. 未命中拦截、命中允许规则 → 放行。
|
|
if d := EvaluateAssetGate(block, allow, []string{"api.example.com"}, nil, nil); !d.Allowed {
|
|
t.Fatal("命中白名单应放行")
|
|
}
|
|
|
|
// 4. 无允许规则(白名单未启用)→ 未命中拦截即放行。
|
|
if d := EvaluateAssetGate(block, nil, []string{"foo.other.com"}, nil, nil); !d.Allowed {
|
|
t.Fatal("无白名单时未命中拦截应放行")
|
|
}
|
|
|
|
// 5. 允许规则全部禁用 → 视为白名单未启用,放行。
|
|
disabledAllow := []AssetInterceptRule{rule("fuzzy_domain", "example.com", false)}
|
|
if d := EvaluateAssetGate(nil, disabledAllow, []string{"foo.other.com"}, nil, nil); !d.Allowed {
|
|
t.Fatal("白名单全禁用时应放行")
|
|
}
|
|
|
|
// 6. 拦截优先于允许:同一目标既命中拦截又命中允许 → 拒绝。
|
|
if d := EvaluateAssetGate(
|
|
[]AssetInterceptRule{rule("fuzzy_domain", ".gov.cn", true)},
|
|
[]AssetInterceptRule{rule("fuzzy_domain", ".gov.cn", true)},
|
|
[]string{"www.gov.cn"}, nil, nil,
|
|
); d.Allowed {
|
|
t.Fatal("拦截应优先于允许")
|
|
}
|
|
}
|
|
|
|
func TestAssetInterceptCandidates(t *testing.T) {
|
|
// 只带 URL 的服务资产:host 应被拆出并归入域名候选,从而被 fuzzy_domain 命中。
|
|
a := &Asset{Type: "service", URL: "https://portal.beijing.gov.cn:8443/app"}
|
|
domains, _, urls := a.interceptCandidates()
|
|
if len(urls) != 1 || urls[0] != a.URL {
|
|
t.Fatalf("urls = %v", urls)
|
|
}
|
|
found := false
|
|
for _, d := range domains {
|
|
if d == "portal.beijing.gov.cn" {
|
|
found = true
|
|
}
|
|
}
|
|
if !found {
|
|
t.Fatalf("URL host 未拆入域名候选: %v", domains)
|
|
}
|
|
r, _, ok := MatchAssetInterceptRules([]AssetInterceptRule{rule("fuzzy_domain", ".gov.cn", true)}, domains, nil, urls)
|
|
if !ok {
|
|
t.Fatalf("仅带 URL 的政府服务资产应被 fuzzy_domain 命中, rule=%+v", r)
|
|
}
|
|
|
|
// URL host 是 IP 时应归入 IP 候选,可被 CIDR 命中。
|
|
b := &Asset{Type: "service", URL: "http://10.1.2.3/x"}
|
|
_, ips, _ := b.interceptCandidates()
|
|
if r, _, ok := MatchAssetInterceptRules([]AssetInterceptRule{rule("cidr", "10.0.0.0/8", true)}, nil, ips, nil); !ok {
|
|
t.Fatalf("URL 中的 IP 应被 CIDR 命中, ips=%v rule=%+v", ips, r)
|
|
}
|
|
}
|