Files
artex/server/customtool_test.go
dela 0335d572de
ci / go (push) Waiting to run
ci / go-db (agent) (push) Waiting to run
ci / go-db (config) (push) Waiting to run
ci / go-db (db) (push) Waiting to run
ci / go-db (evidence) (push) Waiting to run
ci / go-db (llmrec) (push) Waiting to run
ci / go-db (server) (push) Waiting to run
web / web (push) Waiting to run
docs / links (push) Canceled after 0s
detections / detections (push) Canceled after 0s
First Commit
2026-10-09 08:38:16 +08:00

188 lines
5.8 KiB
Go

package server
import (
"context"
"encoding/json"
"io"
"net/http"
"net/http/httptest"
"os/exec"
"strings"
"testing"
"time"
)
func TestRenderTemplateCommand(t *testing.T) {
// command: string params shell-quoted; array → JSON (also quoted).
got := renderTemplate("nmap -p {ports} {target}", map[string]any{
"target": "10.0.0.1; rm -rf /", // injection attempt → must be single-quoted
"ports": []any{80.0, 443.0},
}, shellQuote)
if !strings.Contains(got, `'10.0.0.1; rm -rf /'`) {
t.Fatalf("target not shell-quoted: %q", got)
}
if strings.Contains(got, "rm -rf /'") && !strings.Contains(got, `'10.0.0.1; rm -rf /'`) {
t.Fatalf("possible injection leak: %q", got)
}
if strings.Contains(got, "{target}") || strings.Contains(got, "{ports}") {
t.Fatalf("placeholders not replaced: %q", got)
}
}
func TestRenderTemplateHTTP(t *testing.T) {
// http: identity (no shell quoting) — raw substitution into url/body.
got := renderTemplate("https://x/submit?flag={flag}", map[string]any{"flag": "CTF{abc}"}, identity)
if got != "https://x/submit?flag=CTF{abc}" {
t.Fatalf("http render: %q", got)
}
}
func TestScalarStr(t *testing.T) {
cases := []struct {
v any
want string
ok bool
}{
{"hi", "hi", true},
{true, "true", true},
{float64(80), "80", true}, // integer-valued float → no decimal
{float64(1.5), "1.5", true}, // real float
{[]any{1, 2}, "", false}, // array → not scalar
{map[string]any{}, "", false},
}
for _, c := range cases {
got, ok := scalarStr(c.v)
if ok != c.ok || (ok && got != c.want) {
t.Fatalf("scalarStr(%v) = (%q,%v), want (%q,%v)", c.v, got, ok, c.want, c.ok)
}
}
}
func TestEnsureSchema(t *testing.T) {
// empty → thin {args:string}
m := ensureSchema(nil)
props, _ := m["properties"].(map[string]any)
if _, ok := props["args"]; !ok {
t.Fatalf("empty schema should default to args: %v", m)
}
// non-empty → passthrough
raw := json.RawMessage(`{"type":"object","properties":{"target":{"type":"string"}}}`)
m2 := ensureSchema(raw)
p2, _ := m2["properties"].(map[string]any)
if _, ok := p2["target"]; !ok {
t.Fatalf("non-empty schema should pass through: %v", m2)
}
}
func TestShellQuote(t *testing.T) {
if got := shellQuote("a'b"); got != `'a'\''b'` {
t.Fatalf("shellQuote(a'b) = %q", got)
}
}
// TestExecPython runs a real Python script end-to-end: it must read params from
// stdin JSON and the mirrored env var, then print — verifying the whole script
// param-passing path. Skips if no python3.
func TestExecPython(t *testing.T) {
interp, err := exec.LookPath("python3")
if err != nil {
t.Skip("python3 unavailable")
}
code := `import json,sys,os
a = json.load(sys.stdin)
print("stdin_target=" + a["target"])
print("env_target=" + os.environ.get("TOOL_TARGET",""))
print("port=" + str(a["port"]))
`
out, err := execPython(context.Background(), interp, "test", code,
map[string]any{"target": "example.com", "port": float64(8080)},
t.TempDir(), nil, 10*time.Second)
if err != nil {
t.Fatalf("execPython error: %v", err)
}
for _, want := range []string{"stdin_target=example.com", "env_target=example.com", "port=8080"} {
if !strings.Contains(out, want) {
t.Fatalf("output missing %q:\n%s", want, out)
}
}
}
// TestRunHTTPTool functionally exercises the http executor against a local server:
// method/url/header/body templates are rendered from params, the request is sent,
// and the response status+body are returned. No recording proxy → s.m untouched.
func TestRunHTTPTool(t *testing.T) {
var gotMethod, gotAuth, gotBody string
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
gotMethod = r.Method
gotAuth = r.Header.Get("Authorization")
b, _ := io.ReadAll(r.Body)
gotBody = string(b)
w.WriteHeader(201)
_, _ = w.Write([]byte(`{"ok":true}`))
}))
defer srv.Close()
execRaw, _ := json.Marshal(map[string]any{
"method": "POST",
"url": srv.URL + "/submit?flag={flag}",
"headers": map[string]string{"Authorization": "Bearer {token}"},
"body": `{"flag":"{flag}"}`,
})
res, err := (&Server{}).runHTTPTool(context.Background(), execRaw,
map[string]any{"flag": "CTF{x}", "token": "sekret"}, nil)
if err != nil {
t.Fatalf("runHTTPTool: %v", err)
}
if res.IsError || len(res.Content) == 0 {
t.Fatalf("unexpected result: %+v", res)
}
var out struct {
Status int `json:"status"`
Body string `json:"body"`
}
_ = json.Unmarshal([]byte(res.Content[0].Text), &out)
if gotMethod != "POST" {
t.Fatalf("method: %q", gotMethod)
}
if gotAuth != "Bearer sekret" {
t.Fatalf("header template not rendered: %q", gotAuth)
}
if gotBody != `{"flag":"CTF{x}"}` {
t.Fatalf("body template not rendered: %q", gotBody)
}
if out.Status != 201 || !strings.Contains(out.Body, `"ok":true`) {
t.Fatalf("response not captured: %+v", out)
}
}
func TestDetectPython(t *testing.T) {
if _, err := exec.LookPath("python3"); err != nil {
t.Skip("python3 unavailable")
}
if p := detectPython(); p == "" {
t.Fatal("detectPython returned empty despite python3 on PATH")
}
}
// The http tool must truncate an oversized response through norma's Capture, the
// same valve every other tool uses — a session with no OutputDir falls back to a
// head+tail cut, so a body far past the default cap comes back shortened.
func TestHTTPToolTruncatesLargeBody(t *testing.T) {
big := strings.Repeat("x", 40000) // past Capture's 30000 default
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write([]byte(big))
}))
defer srv.Close()
execRaw, _ := json.Marshal(map[string]any{"method": "GET", "url": srv.URL})
res, err := (&Server{}).runHTTPTool(context.Background(), execRaw, map[string]any{}, nil)
if err != nil {
t.Fatalf("runHTTPTool: %v", err)
}
out := res.Flatten()
if len(out) >= 40000 {
t.Fatalf("oversized http body was not truncated: len=%d", len(out))
}
}