mirror of
https://github.com/RuoJi6/ARTEX.git
synced 2026-11-13 22:03:50 +08:00
feat: make task global asset sharing opt-in
This commit is contained in:
1 parent
d36e1c3295
commit
080a46eb2d
25 files changed
+838
-328
No files matched your search
+2
-2
@@ -309,10 +309,10 @@ const plannerDefaultTmpl = `你是一个网络安全平台授权渗透测试系
|
||||
**每次唤醒的决策流程**:
|
||||
|
||||
1. **完整态势已附在本提示下方**(就是 graph_overview 的返回,无需再调它):task(原始标题+目标/根节点)、资产计数、goals+状态、open/running/recent_done 意图、sites_without_endpoints(无端点的站点,提示可能待探的方向)、facts(探索事实数,与漏洞是两类)、recent_facts({id,summary,confidence?})。
|
||||
- **范围**:探索节点(goals/意图/facts/findings)只含本任务;**资产图全局共享**(多任务同一份,资产计数是全局在范围内的、非本任务独有)——出现非本任务相关的资产时忽略。
|
||||
- **范围**:探索节点(goals/意图/facts/findings)只含本任务;**资产默认按任务隔离**(仅本任务及明确关联任务;用户开启「共享全局资产」后可查询全库)——出现非本任务相关的资产时忽略。
|
||||
- **血缘**:每个意图带 parents(上游:派生自哪些事实/意图)和 yields(下游:产生了哪些事实/发现),recent_facts 每条带 from_intent;据此理解"哪些事实来自哪个方向、能否综合出新方向"。
|
||||
- **否定/存疑观察**(recent_facts 里"端口关闭/不可注入"等)是 worker 的观察、不是定论:采信前先 node_detail(id) 看 evidence——evidence 扎实、confidence=observed 且手段已穷尽的才视为该方向暂时封住;evidence 缺失、只是"看起来像/只探一次"、或 confidence=inferred 的,按【尚未探明】处理,若在范围内且无其它意图覆盖,默认派一条复核意图去证实或推翻(**同一否定方向至多复核一次**;复核后仍为否定、且证据合理,就尊重该结论、不再派)。
|
||||
- **要更深细节才按需调**:list_facts(分页,最新在前,默认 20,可 q 过滤、before 翻页,带 total/has_more)、list_findings(全部漏洞)、node_detail(id)(完整证据/详情;列表/recent_facts 只给摘要)、list_assets(pull:q 搜索、type/company_id/task_id 过滤、分页,或 id/ids 直取)、asset_neighbors。资产全局共享,别默认拉全量。
|
||||
- **要更深细节才按需调**:list_facts(分页,最新在前,默认 20,可 q 过滤、before 翻页,带 total/has_more)、list_findings(全部漏洞)、node_detail(id)(完整证据/详情;列表/recent_facts 只给摘要)、list_assets(pull:q 搜索、type/company_id/task_id 过滤、分页,或 id/ids 直取)、asset_neighbors。资产是否共享由用户设置决定,不能自行开启;共享不代表扩大测试授权。
|
||||
|
||||
2. **判目标(核心职责)**:goals 字段已含目标与状态;对已被某发现/事实证明的未达成目标,调 prove_goal(goal_id, evidence_id, reason) 标 met。**当你标记的恰是最后一个未完成目标时,系统自动判定整个任务完成**——收官只由逐个 prove_goal 驱动,没有别的"一键完成"手段。
|
||||
- ⚠️ **量化验收核对(严禁提前盖章)**:目标含可量化条件(覆盖度达 X%、达到指定数量、获得某权限)时,prove_goal 前【必须】核对上方 graph_overview 的实测值(coverage.pct、findings_total 计数等):未达标就【禁止】prove_goal,改派意图补差;不得以"大体达成/核心已拿下"为由提前标 met。例:要求覆盖度 100% 而实测 coverage.pct=40% → 未达成,继续派补测意图。
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/Autumn-27/artex/db"
|
||||
)
|
||||
|
||||
func TestTaskAssetSharingLiveToolPolicy(t *testing.T) {
|
||||
d := testDB(t)
|
||||
defer d.Close()
|
||||
task, err := d.CreateTask("tool sharing", "goal", nil, 0, 0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer d.DeleteTask(task.ID)
|
||||
assets := d.Assets()
|
||||
company, _, err := d.Companies().UpsertCompany(fmt.Sprintf("sharing-tool-%d", task.ID), "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer d.Companies().DeleteCompany(company)
|
||||
foreign, err := assets.UpsertRootDomain(db.UpsertRootDomainReq{Domain: fmt.Sprintf("sharing-tool-%d.invalid", task.ID)})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer assets.DeleteByIDs([]int64{foreign})
|
||||
if _, err = d.Exec(`UPDATE assets SET company_id=$2 WHERE id=$1`, foreign, company); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
tools := NewToolSet(d.Exploration(task.ExplorationID), "planner")
|
||||
tools.SetTaskID(task.ID)
|
||||
tools.SetAssetStore(assets, d.Companies())
|
||||
idJSON, _ := json.Marshal(foreign)
|
||||
intent := intentItem{Summary: "reference foreign target", AssetIDs: []json.RawMessage{idJSON, idJSON}}
|
||||
if _, err := tools.addOneIntent(intent); err == nil {
|
||||
t.Fatal("foreign intent should fail while sharing off")
|
||||
}
|
||||
query := fmt.Sprintf(`{"id":%d}`, foreign)
|
||||
if got := callReadJSON(t, tools.listAssets(), query); strings.Contains(fmt.Sprint(got), fmt.Sprintf("sharing-tool-%d.invalid", task.ID)) {
|
||||
t.Fatalf("foreign asset leaked: %v", got)
|
||||
}
|
||||
for _, enabled := range []bool{true, false} {
|
||||
if _, err := d.UpdateTask(task.ID, db.TaskPatch{ShareGlobalAssets: &enabled}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got := callReadJSON(t, tools.listAssets(), query)
|
||||
visible := strings.Contains(fmt.Sprint(got), fmt.Sprintf("sharing-tool-%d.invalid", task.ID))
|
||||
if visible != enabled {
|
||||
t.Fatalf("live list_assets enabled=%v result=%v", enabled, got)
|
||||
}
|
||||
companies, err := d.Companies().ListCompaniesForTask(task.ID)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
found := false
|
||||
for _, c := range companies {
|
||||
if c.ID == company {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
if found != enabled {
|
||||
t.Fatalf("live company policy enabled=%v found=%v", enabled, found)
|
||||
}
|
||||
}
|
||||
enabled := true
|
||||
if _, err := d.UpdateTask(task.ID, db.TaskPatch{ShareGlobalAssets: &enabled}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := tools.addOneIntent(intent); err != nil {
|
||||
t.Fatalf("explicit opt-in should allow target and duplicate IDs: %v", err)
|
||||
}
|
||||
}
|
||||
+21
-4
@@ -226,13 +226,12 @@ func (t *ToolSet) AddHintTool() actool.CoreTool { return t.addHint()
|
||||
func (t *ToolSet) SetEnrich(e EnrichTrigger) { t.enrich = e }
|
||||
|
||||
// SetOwnerNode sets the exploration node that writes anchor to (worker: its
|
||||
// intent node; planner/main: the begin root). Assets created/referenced while
|
||||
// ownerNode is set are anchored to it as lineage (not visibility).
|
||||
// intent node; planner/main: the begin root). Anchors preserve explicit asset
|
||||
// references and make them available in this task context.
|
||||
func (t *ToolSet) SetOwnerNode(id int64) { t.ownerNode = id }
|
||||
|
||||
// anchorOwner records a lineage edge from this run's owner node to an asset
|
||||
// (no-op if unset). Provenance only — the asset graph is global and shared, so
|
||||
// this no longer affects which assets a task can read.
|
||||
// (no-op if unset). These explicit references also contribute to task visibility.
|
||||
func (t *ToolSet) anchorOwner(assetID int64) {
|
||||
if t.ts != nil && t.ownerNode > 0 && assetID > 0 {
|
||||
_ = t.ts.Anchor(t.ownerNode, assetID)
|
||||
@@ -1054,6 +1053,24 @@ func (t *ToolSet) addOneIntent(it intentItem) (int64, error) {
|
||||
priority = 5
|
||||
}
|
||||
anchors := pidList(it.AssetIDs)
|
||||
seenAnchors := map[int64]bool{}
|
||||
uniqueAnchors := anchors[:0]
|
||||
for _, id := range anchors {
|
||||
if !seenAnchors[id] {
|
||||
seenAnchors[id] = true
|
||||
uniqueAnchors = append(uniqueAnchors, id)
|
||||
}
|
||||
}
|
||||
anchors = uniqueAnchors
|
||||
if t.as != nil && t.taskID > 0 && len(anchors) > 0 {
|
||||
visible, err := t.as.GetByIDsInScope(t.taskID, anchors)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("资产可见范围校验失败:%w", err)
|
||||
}
|
||||
if len(visible) != len(anchors) {
|
||||
return 0, fmt.Errorf("目标资产不属于本任务或直接关联任务;如需使用全局资产,请由用户开启「共享全局资产」")
|
||||
}
|
||||
}
|
||||
// 资产拦截:意图绑定的资产若命中系统资产拦截规则,则禁止下发该意图。
|
||||
if t.as != nil && len(anchors) > 0 {
|
||||
hits, err := t.as.CheckAssetsIntercept(t.taskID, anchors)
|
||||
|
||||
@@ -539,7 +539,7 @@ func (t *ToolSet) listUntestedAssets() actool.CoreTool {
|
||||
func (t *ToolSet) listAssets() actool.CoreTool {
|
||||
return readTool(
|
||||
"list_assets",
|
||||
"查询资产库:DSL 表达式搜索,或按 id/ids 直取;支持分页。只返回【本任务及直接关联任务】测试范围内的资产。\n"+
|
||||
"查询资产库:DSL 表达式搜索,或按 id/ids 直取;支持分页。默认只返回本任务及直接关联任务的资产;用户开启「共享全局资产」后可查询全局资产。\n"+
|
||||
"DSL:field=value 模糊(ILIKE) | field==value 精确 | field!=value 排除 | 数字字段支持 > >= < <= | 裸词=全文模糊;AND/OR 组合(AND 优先级高),可用括号分组。资产类型用独立 type 参数,不写进 DSL。\n"+
|
||||
"未传 id/ids 时 dsl 必须非空(不允许无条件全量查询)。\n"+
|
||||
"可用字段:domain(根/子/服务域名)、root_domain、ip、url、page_title、icp、service_name、app_name、method(如 GET/POST)、service_type(http|other)、record_type(如 A/CNAME)、technology(数组,=模糊 ==精确)、port/status_code/company_id(整数)。\n"+
|
||||
@@ -596,7 +596,7 @@ func (t *ToolSet) listAssets() actool.CoreTool {
|
||||
func (t *ToolSet) listCompanies() actool.CoreTool {
|
||||
return readTool(
|
||||
"list_companies",
|
||||
"列出资产库中的【企业/公司】及其资产范围(scope)与已归属资产数。用于查看有哪些公司、"+
|
||||
"列出当前任务可见的【企业/公司】及其资产范围(scope)与可见资产数;开启共享全局资产后可查询全局企业。用于查看有哪些公司、"+
|
||||
"拿到 company_id(insert_assets 关联 app、list_assets 按 company_id 过滤时用)。"+
|
||||
"可选 search 按公司名模糊过滤(不区分大小写),留空返回全部。",
|
||||
obj(map[string]any{
|
||||
@@ -610,7 +610,7 @@ func (t *ToolSet) listCompanies() actool.CoreTool {
|
||||
Search string `json:"search"`
|
||||
}
|
||||
_ = json.Unmarshal(in, &a)
|
||||
cos, err := t.cs.ListCompanies()
|
||||
cos, err := t.cs.ListCompaniesForTask(t.taskID)
|
||||
if err != nil {
|
||||
return actool.Errorf("查询公司失败: " + err.Error()), nil
|
||||
}
|
||||
|
||||
+1
-1
@@ -398,7 +398,7 @@ func (w *Worker) execute(ctx context.Context, name string, taskID int64, as *db.
|
||||
sysBody += renderIntentTask(intent)
|
||||
if as != nil {
|
||||
if ids := intentAssetIDs(intent); len(ids) > 0 {
|
||||
if assets, err := as.GetByIDs(ids); err == nil && len(assets) > 0 {
|
||||
if assets, err := as.GetByIDsInScope(taskID, ids); err == nil && len(assets) > 0 {
|
||||
if b, err := json.Marshal(assets); err == nil {
|
||||
sysBody += "\n\n本意图 asset_ids 对应的目标资产:\n" + string(b)
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user