118 lines
5.3 KiB
Go
118 lines
5.3 KiB
Go
package restrict
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// TestSensitiveCall 是 design/11 §4 敏感清单的判定表:执行命令/修改文件/
|
|
// 网络写方法敏感,读文件/检索/纯计算自主。
|
|
func TestSensitiveCall(t *testing.T) {
|
|
t.Parallel()
|
|
cases := []struct {
|
|
name string
|
|
args map[string]any
|
|
wantOp SensitiveOp
|
|
wantIn string // 摘要应包含的片段(wantSensitive 时)
|
|
sensitive bool
|
|
}{
|
|
{name: "terminal", args: map[string]any{"command": "rm -rf /tmp/x"}, wantOp: SensitiveExecCommand, wantIn: "rm -rf /tmp/x", sensitive: true},
|
|
{name: "terminal", args: nil, wantOp: SensitiveExecCommand, wantIn: "terminal", sensitive: true},
|
|
{name: "file", args: map[string]any{"action": "write", "path": "/data/a.md"}, wantOp: SensitiveWriteFile, wantIn: "/data/a.md", sensitive: true},
|
|
{name: "file", args: map[string]any{"action": "write", "path": "/data/models.json"}, wantOp: SensitiveConfigChange, wantIn: "models.json", sensitive: true},
|
|
{name: "file", args: map[string]any{"action": "write", "path": "/data/cyrene.db"}, wantOp: SensitiveWriteData, wantIn: "cyrene.db", sensitive: true},
|
|
{name: "file", args: map[string]any{"action": "read", "path": "/data/.env"}, wantOp: SensitiveSecretAccess, wantIn: "/data/.env", sensitive: true},
|
|
{name: "file", args: map[string]any{"action": "read", "path": "/data/a.md"}},
|
|
{name: "file", args: map[string]any{"action": "list"}},
|
|
{name: "file", args: map[string]any{"action": "search", "query": "x"}},
|
|
{name: "http", args: map[string]any{"method": "POST", "url": "https://example.com/hook"}, wantOp: SensitiveNetwork, wantIn: "example.com/hook", sensitive: true},
|
|
{name: "http", args: map[string]any{"method": "post", "url": "https://example.com/hook"}, wantOp: SensitiveNetwork, sensitive: true},
|
|
{name: "http", args: map[string]any{"url": "https://example.com"}}, // method 缺省 = GET
|
|
{name: "http", args: map[string]any{"method": "GET", "url": "https://example.com"}},
|
|
{name: "http", args: map[string]any{"method": "HEAD"}},
|
|
{name: "knowledge", args: map[string]any{"query": "设计文档"}},
|
|
{name: "web-search", args: map[string]any{"query": "x"}},
|
|
{name: "web-fetch", args: map[string]any{"url": "https://example.com"}},
|
|
{name: "session_read", args: map[string]any{"sessionId": "web"}},
|
|
{name: "calculator", args: map[string]any{"expression": "1+1"}},
|
|
// 纯计算工具的参数里也出现写动作词(text 的 operation=replace),
|
|
// 判据必须只看**工具名**,绝不能被参数里的动词带跑。
|
|
{name: "text", args: map[string]any{"operation": "replace", "old": "a", "new": "b"}},
|
|
{name: "mcp:some-server/tool", args: map[string]any{"anything": true}},
|
|
{name: "unknown-tool", args: nil},
|
|
}
|
|
for _, tc := range cases {
|
|
op, detail, sensitive := SensitiveCall(tc.name, tc.args)
|
|
if sensitive != tc.sensitive {
|
|
t.Fatalf("SensitiveCall(%s, %v) sensitive = %v, want %v", tc.name, tc.args, sensitive, tc.sensitive)
|
|
}
|
|
if !tc.sensitive {
|
|
if op != "" || detail != "" {
|
|
t.Fatalf("SensitiveCall(%s) 非敏感时不应给出类别/摘要,得到 (%q, %q)", tc.name, op, detail)
|
|
}
|
|
continue
|
|
}
|
|
if op != tc.wantOp {
|
|
t.Fatalf("SensitiveCall(%s) op = %q, want %q", tc.name, op, tc.wantOp)
|
|
}
|
|
if tc.wantIn != "" && !strings.Contains(detail, tc.wantIn) {
|
|
t.Fatalf("SensitiveCall(%s) detail = %q, want it to contain %q", tc.name, detail, tc.wantIn)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestSensitiveCall_SummaryIsClipped 锁定动作摘要的截断:长参数不许整段
|
|
// 进入小字/审计(同时不切开多字节字符)。
|
|
func TestSensitiveCall_SummaryIsClipped(t *testing.T) {
|
|
t.Parallel()
|
|
long := strings.Repeat("昔", 200)
|
|
_, detail, sensitive := SensitiveCall("terminal", map[string]any{"command": long})
|
|
if !sensitive {
|
|
t.Fatal("terminal 应判定为敏感")
|
|
}
|
|
if got := len([]rune(detail)); got > sensitiveSummaryRunes+len("terminal ")+1 {
|
|
t.Fatalf("摘要 rune 数 = %d,未按 %d 截断: %q", got, sensitiveSummaryRunes, detail)
|
|
}
|
|
if !strings.HasSuffix(detail, "…") {
|
|
t.Fatalf("被截断的摘要应以省略号结尾: %q", detail)
|
|
}
|
|
}
|
|
|
|
// TestSensitiveCall_SummaryIsRedacted 锁定「参数摘要进审计与前端小字之前必须
|
|
// 打码」:terminal 的 command、http 的 url 里常常就带着凭据,截断不是脱敏。
|
|
func TestSensitiveCall_SummaryIsRedacted(t *testing.T) {
|
|
t.Parallel()
|
|
const secret = "sk-live-9f8e7d6c5b4a3210"
|
|
cases := []struct {
|
|
name string
|
|
args map[string]any
|
|
start string // 摘要应保留的开头(打码不能把摘要打没)
|
|
}{
|
|
{
|
|
name: "terminal",
|
|
args: map[string]any{"command": "curl -H 'api_key=" + secret + "' https://x"},
|
|
start: "terminal ",
|
|
},
|
|
{
|
|
name: "http",
|
|
args: map[string]any{"method": "POST", "url": "https://x/hook?token=" + secret},
|
|
start: "http POST ",
|
|
},
|
|
}
|
|
for _, tc := range cases {
|
|
_, detail, sensitive := SensitiveCall(tc.name, tc.args)
|
|
if !sensitive {
|
|
t.Fatalf("%s 应判定为敏感", tc.name)
|
|
}
|
|
if strings.Contains(detail, secret) {
|
|
t.Fatalf("%s 摘要泄露了凭据原文: %q", tc.name, detail)
|
|
}
|
|
if !strings.Contains(detail, "[redacted]") {
|
|
t.Fatalf("%s 摘要应带打码标记: %q", tc.name, detail)
|
|
}
|
|
if !strings.HasPrefix(detail, tc.start) {
|
|
t.Fatalf("%s 摘要应以 %q 开头(动作不能被一起打掉): %q", tc.name, tc.start, detail)
|
|
}
|
|
}
|
|
}
|