fix(assets): ip 字段拒收主机名,并告诉调用方该怎么改
上一次修复让非法 ip 不再打挂企业归属重算,但脏数据仍在持续产生。这次从源头
拦住:assets.ip 会被转成 inet 参与网段归属,存主机名的资产对所有 IP/CIDR
范围规则都是隐形的。
- db 层新增 ValidateAssetIP + ErrAssetIPInvalid,在 UpsertIP /
UpsertHTTPService / UpsertOtherService / UpsertEndpoint 四个入口校验。
放在 db 层是因为 Agent 工具和资产 API 都经由它们,不必写两遍;内部调用方
(enrich 的解析结果、task_assets 的 ParseCIDR 输出、RecordValue 已 ParseIP
过滤的分支)传的本就是合法地址,不受影响。
- 错误信息给的是改法而不只是「非法」:带上收到的原值,并指出主机名应改用
type=subdomain 填 domain,或先解析 A/AAAA 记录再填 ip。insert_assets 的
错误是逐条返回的({index, error}),同批其余资产照常入库,Agent 拿到后能
定位到具体那条并在下一轮自行修正。
- 同步收紧 insert_assets 的工具描述与 ip 字段说明,在提示层先挡一道。
- 空 ip 仍然合法:service / endpoint 的 ip 是可选字段。
验证: 新增 db 层四类型拒收测试与 agent 层「一批混入一条非法、其余照常入库」
测试,均连 PG 实跑通过;干净库 go test -p 1 ./... 无新增失败(仅剩既有的
TestCompanyScopeInvalid)。
This commit is contained in:
1 parent
9e98407528
commit
dfbbb33069
5 files changed
+147
-2
No files matched your search
@@ -3,6 +3,7 @@ package agent
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/Autumn-27/artex/db"
|
||||
@@ -394,3 +395,56 @@ func TestInsertAssetsDedup(t *testing.T) {
|
||||
t.Errorf("dedup: want same ID on double insert, got %v vs %v", id1, id2)
|
||||
}
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// TestInsertAssetsRejectsHostnameIPPerItem
|
||||
// 一批里混入 ip 填了主机名的一条 → 只有那条失败,其余照常入库,
|
||||
// 且错误里带得上 index 和改正方法,Agent 下一轮能自己修好。
|
||||
// =====================================================================
|
||||
func TestInsertAssetsRejectsHostnameIPPerItem(t *testing.T) {
|
||||
d := testDB(t)
|
||||
defer d.Close()
|
||||
|
||||
ts := NewToolSet(nil, "")
|
||||
ts.SetAssetStore(d.Assets(), d.Companies())
|
||||
defer d.Exec(`DELETE FROM assets WHERE ip IN ('198.51.100.23','cdn.badip-test.com') OR domain='badip-test.com'`)
|
||||
|
||||
out := callInsertAssets(t, ts, map[string]any{
|
||||
"assets": []any{
|
||||
map[string]any{"type": "root_domain", "domain": "badip-test.com"},
|
||||
map[string]any{"type": "ip", "ip": "cdn.badip-test.com"},
|
||||
map[string]any{"type": "ip", "ip": "198.51.100.23"},
|
||||
},
|
||||
})
|
||||
|
||||
// The two valid entries must survive the bad one — a whole-batch failure
|
||||
// would make the agent re-send assets that were already fine.
|
||||
results, _ := out["results"].([]any)
|
||||
if len(results) != 2 {
|
||||
t.Fatalf("results=%v, want the 2 valid assets", out["results"])
|
||||
}
|
||||
|
||||
errsRaw, _ := out["errors"].([]any)
|
||||
if len(errsRaw) != 1 {
|
||||
t.Fatalf("errors=%v, want exactly the invalid entry", out["errors"])
|
||||
}
|
||||
entry, _ := errsRaw[0].(map[string]any)
|
||||
if index, _ := entry["index"].(float64); int(index) != 1 {
|
||||
t.Fatalf("error index=%v, want 1", entry["index"])
|
||||
}
|
||||
message, _ := entry["error"].(string)
|
||||
for _, want := range []string{"cdn.badip-test.com", "type=subdomain", "A/AAAA"} {
|
||||
if !strings.Contains(message, want) {
|
||||
t.Fatalf("error message %q lacks %q — agent cannot act on it", message, want)
|
||||
}
|
||||
}
|
||||
|
||||
// The rejected value must not have reached the table.
|
||||
var stored int
|
||||
if err := d.QueryRow(`SELECT count(*) FROM assets WHERE ip='cdn.badip-test.com'`).Scan(&stored); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if stored != 0 {
|
||||
t.Fatalf("rejected hostname still stored in assets.ip (%d rows)", stored)
|
||||
}
|
||||
}
|
||||
@@ -79,7 +79,7 @@ func (t *ToolSet) insertAssets() actool.CoreTool {
|
||||
"发现新资产时立即批量插入资产到资产库。支持一次提交多种资产类型:root_domain/ip/subdomain/app/service/endpoint。\n"+
|
||||
"每条资产的字段说明:\n"+
|
||||
"• root_domain:domain(根域名, 必填)、icp(备案, 可选)\n"+
|
||||
"• ip:ip(IP地址, 必填)、bound_domains([域名])、open_ports([{port,service}])\n"+
|
||||
"• ip:ip(IP地址, 必填, 必须是 IPv4/IPv6 地址而非主机名)、bound_domains([域名])、open_ports([{port,service}])\n"+
|
||||
"• subdomain:domain(子域名, 必填)、record_type(A/AAAA/CNAME等)、record_value(记录值)、icp(可选)\n"+
|
||||
"• app:app_name(应用名, 必填)、bundle_id(bundle id, 可选)、category、description、app_icp、company_id(归属企业 id, 可选)\n"+
|
||||
"• service(http):url(必填)、technologies([指纹])、status_code、content_length、page_title、favicon_mmh3、auth([{type,username,password,...}])、service_ip\n"+
|
||||
@@ -108,7 +108,7 @@ func (t *ToolSet) insertAssets() actool.CoreTool {
|
||||
"description": "DNS 解析值列表(subdomain 可选,如 [\"1.2.3.4\",\"2.3.4.5\"])",
|
||||
},
|
||||
// ip
|
||||
"ip": str("IP 地址(ip 类型必填;service/endpoint 类型可填,用于关联 IP)"),
|
||||
"ip": str("IP 地址,必须是 IPv4/IPv6 地址,不能填主机名(主机名请用 type=subdomain 的 domain 字段);ip 类型必填;service/endpoint 类型可填,用于关联 IP"),
|
||||
"bound_domains": map[string]any{
|
||||
"type": "array",
|
||||
"items": map[string]any{"type": "string"},
|
||||
|
||||
Reference in new issue
Block a user