diff --git a/README.md b/README.md index e62cd3a..8e67fc8 100644 --- a/README.md +++ b/README.md @@ -627,4 +627,4 @@ https://github.com/oritera/Cairn Worker 可调用 `record_intelligence`,或在 `record_fact` 的单条/批量事实中附带 `intelligence` 数组一起提交。`insert_assets` 本次提交的服务 `auth` 自动登记为未验证凭据,避免重复上报。任务、执行节点由系统绑定,记录需要真实来源证据;获取凭据不等于登录成功。 -页面支持分类、标题/上报者搜索、状态筛选、分页、状态更新及来源查看。凭据和敏感数据的正文与证据默认不随列表返回,点击「显示敏感内容」后加载。直接关联任务的记录标明继承且只读;在资产页选择一项资产后点击「关联情报」可查看同一份记录。情报随任务归档、恢复和删除;新版归档格式为 v4,仍支持读取 v1–v3。此功能不自动补提取历史会话。 +页面支持分类、标题/上报者搜索、状态筛选、分页、状态更新及来源查看。凭据、敏感数据的正文和来源证据直接展示在列表及详情中,无需额外点击显示。直接关联任务的记录标明继承且只读;在资产页选择一项资产后点击「关联情报」可查看同一份记录。情报随任务归档、恢复和删除;新版归档格式为 v4,仍支持读取 v1–v3。此功能不自动补提取历史会话。 diff --git a/db/intelligence.go b/db/intelligence.go index b0f784f..54b7f0e 100644 --- a/db/intelligence.go +++ b/db/intelligence.go @@ -37,7 +37,6 @@ type Intelligence struct { CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` Inherited bool `json:"inherited"` - Redacted bool `json:"redacted"` } type IntelligenceFilter struct { TaskID int64 @@ -238,15 +237,14 @@ func (d *DB) ListIntelligence(ctx context.Context, f IntelligenceFilter) (*Intel if err != nil { return nil, err } - // Search metadata only: secrets are never searched or echoed through list endpoints. + // Search titles and reporters; return the recorded content directly in each row. where := scope + ` AND ($3='' OR kind=$3) AND ($4='' OR i.status=$4) AND ($5='' OR i.title ILIKE '%'||$5||'%' OR i.worker ILIKE '%'||$5||'%')` args = append(args, f.Kind, f.Status, f.Query) if err = d.QueryRowContext(ctx, `SELECT count(*)`+where, args...).Scan(&page.Total); err != nil { return nil, err } args = append(args, f.Limit, f.Offset) - // Do not load credential/data bodies into list responses at all. - cols := `i.id,i.task_id,i.node_id,i.kind,i.title,CASE WHEN kind='lead' THEN content ELSE '' END,CASE WHEN kind='lead' THEN evidence ELSE '' END,confidence,i.status,asset_ids,i.worker,i.created_at,i.updated_at` + cols := `i.id,i.task_id,i.node_id,i.kind,i.title,i.content,i.evidence,i.confidence,i.status,i.asset_ids,i.worker,i.created_at,i.updated_at` rows, err = d.QueryContext(ctx, `SELECT `+cols+where+` ORDER BY i.id DESC LIMIT $6 OFFSET $7`, args...) if err != nil { return nil, err @@ -258,7 +256,6 @@ func (d *DB) ListIntelligence(ctx context.Context, f IntelligenceFilter) (*Intel return nil, e } item.Inherited = f.TaskID > 0 && item.TaskID != f.TaskID - item.Redacted = item.Kind != "lead" page.Items = append(page.Items, item) } return page, rows.Err() diff --git a/db/intelligence_test.go b/db/intelligence_test.go index 2d8950f..9a1ef5e 100644 --- a/db/intelligence_test.go +++ b/db/intelligence_test.go @@ -29,7 +29,7 @@ func TestIntelligenceValidation(t *testing.T) { t.Fatalf("normalization failed: %+v %v", got, err) } } -func TestIntelligenceProvenanceDedupAndRedaction(t *testing.T) { +func TestIntelligenceProvenanceDedupAndDirectContent(t *testing.T) { d, err := Open(testDSN(t)) if err != nil { t.Fatal(err) @@ -68,8 +68,8 @@ func TestIntelligenceProvenanceDedupAndRedaction(t *testing.T) { t.Fatal(err) } raw, _ := json.Marshal(page) - if page.Total != 1 || !page.Items[0].Redacted || strings.Contains(string(raw), "local-fixture-secret") || strings.Contains(string(raw), "/tmp/fixture") { - t.Fatal("list leaked secret or lost record") + if page.Total != 1 || !strings.Contains(string(raw), "local-fixture-secret") || !strings.Contains(string(raw), "/tmp/fixture") { + t.Fatal("list omitted recorded content or evidence") } if err = d.UpdateIntelligenceStatus(t.Context(), second.ID, item.ID, "verified"); !errors.Is(err, ErrIntelligenceNotFound) { t.Fatalf("cross-task mutation accepted: %v", err) @@ -119,3 +119,25 @@ func TestIntelligenceArchiveAssetRemapPreservesManualTriage(t *testing.T) { t.Fatal("remap lost dedup or manual triage") } } + +func TestIntelligenceSensitiveDataListIncludesContent(t *testing.T) { + d, err := Open(testDSN(t)) + if err != nil { + t.Fatal(err) + } + defer d.Close() + task, err := d.CreateTask("direct sensitive data display", "local fixture", nil, 0, 0) + if err != nil { + t.Fatal(err) + } + defer d.DeleteTask(task.ID) + in := IntelligenceInput{Kind: "sensitive_data", Title: "fixture dataset", Content: "qa@example.invalid", Evidence: "/tmp/qa-users.json", Confidence: "observed"} + _, err = d.RecordIntelligence(t.Context(), task.ID, 0, "qa", in) + if err != nil { + t.Fatal(err) + } + page, err := d.ListIntelligence(t.Context(), IntelligenceFilter{TaskID: task.ID, Kind: "sensitive_data"}) + if err != nil || page.Total != 1 || page.Items[0].Content != in.Content || page.Items[0].Evidence != in.Evidence { + t.Fatalf("missing sensitive data content: %+v %v", page, err) + } +} diff --git a/server/intelligence_test.go b/server/intelligence_test.go index 1aaf48c..d3af77b 100644 --- a/server/intelligence_test.go +++ b/server/intelligence_test.go @@ -32,12 +32,12 @@ func TestIntelligenceHTTPSettingsAndScope(t *testing.T) { } base := fmt.Sprintf("/api/tasks/%d/intelligence", taskID) r := request("GET", base, "") - if r.Code != 200 || strings.Contains(r.Body.String(), in.Content) || strings.Contains(r.Body.String(), in.Evidence) { - t.Fatalf("list leaked secret: %d %s", r.Code, r.Body) + if r.Code != 200 || !strings.Contains(r.Body.String(), in.Content) || !strings.Contains(r.Body.String(), in.Evidence) { + t.Fatalf("list omitted content or evidence: %d %s", r.Code, r.Body) } r = request("GET", fmt.Sprintf("%s/%d", base, item.ID), "") if r.Code != 200 || !strings.Contains(r.Body.String(), in.Content) { - t.Fatal("explicit reveal failed", r.Code, r.Body) + t.Fatal("detail content missing", r.Code, r.Body) } if r = request("PATCH", fmt.Sprintf("%s/%d", base, item.ID), `{"status":"verified"}`); r.Code != 200 { t.Fatal(r.Code, r.Body) diff --git a/web/src/components/intelligence-panel.tsx b/web/src/components/intelligence-panel.tsx index 59c0990..cc91346 100644 --- a/web/src/components/intelligence-panel.tsx +++ b/web/src/components/intelligence-panel.tsx @@ -4,7 +4,7 @@ import * as React from "react"; import Link from "next/link"; -import { CopyIcon, EyeIcon, EyeOffIcon, RefreshCwIcon } from "lucide-react"; +import { CopyIcon, RefreshCwIcon } from "lucide-react"; import { toast } from "sonner"; import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; @@ -52,10 +52,7 @@ export function IntelligencePanel({ scope, scopeId }: { scope: "tasks" | "assets const [error, setError] = React.useState(""); const [refresh, setRefresh] = React.useState(0); const [selected, setSelected] = React.useState(null); - const [detail, setDetail] = React.useState(null); - const [revealing, setRevealing] = React.useState(false); const [saving, setSaving] = React.useState(false); - const detailRequest = React.useRef(0); React.useEffect(() => { const timer = setTimeout(() => { setQuery(search.trim()); @@ -95,38 +92,14 @@ export function IntelligencePanel({ scope, scopeId }: { scope: "tasks" | "assets clearInterval(timer); }; }, [scope, scopeId, kind, status, query, page, refresh]); - const open = (item: TaskIntelligence) => { - detailRequest.current++; - setSelected(item); - setDetail(item.redacted ? null : item); - setRevealing(false); - }; - const close = () => { - detailRequest.current++; - setSelected(null); - setDetail(null); - setRevealing(false); - }; - const reveal = async () => { - if (!selected) return; - const request = ++detailRequest.current; - setRevealing(true); - try { - const result = await api.intelligenceDetail(scope, scopeId, selected.id); - if (request === detailRequest.current) setDetail(result); - } catch (e) { - if (request === detailRequest.current) toast.error("读取失败:" + (e as Error).message); - } finally { - if (request === detailRequest.current) setRevealing(false); - } - }; + const open = (item: TaskIntelligence) => setSelected(item); + const close = () => setSelected(null); const update = async (value: string) => { if (!selected) return; setSaving(true); try { await api.updateIntelligence(selected.task_id, selected.id, value); setSelected((item) => (item ? { ...item, status: value } : null)); - setDetail((item) => (item ? { ...item, status: value } : null)); setRefresh((n) => n + 1); toast.success("情报状态已更新"); } catch (e) { @@ -155,7 +128,7 @@ export function IntelligencePanel({ scope, scopeId }: { scope: "tasks" | "assets {scope === "tasks" ? "任务情报" : "资产关联情报"} {scope === "tasks" ? "集中查看本次获取与直接关联任务继承的情报。" : "按来源任务查看此资产关联的情报。"}{" "} - 凭据与敏感数据按需显示,发现凭据不代表验证成功。 + 直接展示凭据、敏感数据及来源证据,发现凭据不代表验证成功。 @@ -228,6 +201,7 @@ export function IntelligencePanel({ scope, scopeId }: { scope: "tasks" | "assets 类别 / 标题 + 内容 / 来源证据 状态 来源 资产 @@ -242,9 +216,17 @@ export function IntelligencePanel({ scope, scopeId }: { scope: "tasks" | "assets
{KINDS.find((k) => k.value === item.kind)?.label} {item.title} - {item.redacted ? ( - 内容已隐藏 · 查看详情后显示 - ) : null} +
+ + +
+
+                          {prettyContent(item.content)}
+                        
+
+ 来源证据 +

{item.evidence}

+
@@ -321,7 +303,7 @@ export function IntelligencePanel({ scope, scopeId }: { scope: "tasks" | "assets {selected?.title ?? "情报详情"} - 保留实际来源;凭据与敏感数据仅在点击显示后加载。 + 完整展示记录内容与实际来源证据。 {selected ? (
@@ -353,52 +335,31 @@ export function IntelligencePanel({ scope, scopeId }: { scope: "tasks" | "assets {STATUS[selected.kind][selected.status]} · 请到来源任务修改状态

)} - {selected.kind !== "lead" ? ( +
+ 内容 - ) : null} - {detail ? ( - <> -
- 内容 - -
-
-                    {prettyContent(detail.content)}
-                  
-

来源证据

-
-                    {detail.evidence}
-                  
- - ) : ( -

敏感内容和来源证据已隐藏

- )} +
+
+                {prettyContent(selected.content)}
+              
+

来源证据

+
+                {selected.evidence}
+              
关联资产:{selected.asset_ids.map((id) => `#${id}`).join("、") || "未关联"} 发现于 {time(selected.created_at)} diff --git a/web/src/lib/types.ts b/web/src/lib/types.ts index b8a7dd2..2d4ae6c 100644 --- a/web/src/lib/types.ts +++ b/web/src/lib/types.ts @@ -1759,7 +1759,6 @@ export interface TaskIntelligence { created_at: string; updated_at: string; inherited: boolean; - redacted: boolean; } export interface IntelligencePage { items: TaskIntelligence[];