Files
ARTEX/server/skill_upload_test.go
T
BaaRai e233c02314 Translate all Chinese text to English
Comments, doc comments, UI labels, log and error messages, LLM prompt
strings, markdown docs, test data and assertions. Identifiers, enum
keys, routes, placeholders and code structure are unchanged.

Notes:
- docs/漏洞流量证据.md renamed to docs/vulnerability-traffic-evidence.md
- tests that exercised CJK specific handling (GBK zip names, Chinese
  full text search) now use ASCII equivalents; go build passes and the
  test suite matches the pre translation baseline (remaining failures
  need a PostgreSQL DSN and fail identically on the original)
- frontend mention syntax now uses English kinds (finding, asset, ...)
  matching the backend, chat mentions tests pass
2026-10-08 23:14:34 +02:00

244 lines
8.0 KiB
Go
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package server
import (
"archive/zip"
"bytes"
"encoding/json"
"io"
"mime/multipart"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strings"
"testing"
"github.com/klauspost/compress/zstd"
"golang.org/x/text/encoding/simplifiedchinese"
)
func TestValidSkillName(t *testing.T) {
ok := []string{"web-recon", "a", "nuclei2", "chinese-skill", "port-scan-x", "japanese-skill"}
bad := []string{
"", "Web-Recon", "-lead", "trail-", "dou--ble", "has space", "chinese skill",
"dot.name", "a/b", `a\b`, "..", ".", "chinese/skill", "sk\x00ill", "ba‮d",
strings.Repeat("a", 65), "1abc", "chinese-skill!",
}
for _, n := range ok {
if !validSkillName(n) {
t.Errorf("validSkillName(%q) = false, want true", n)
}
}
for _, n := range bad {
if validSkillName(n) {
t.Errorf("validSkillName(%q) = true, want false", n)
}
}
}
func TestSkillRelPath(t *testing.T) {
ok := map[string]string{
"SKILL.md": "SKILL.md",
"scripts/run.py": "scripts/run.py",
"references/notes.md": "references/notes.md",
"references/a b.txt": "references/a b.txt",
"./SKILL.md": "SKILL.md",
"assets/image-1_v2.png": "assets/image-1_v2.png",
}
for in, want := range ok {
got, msg := skillRelPath(in)
if msg != "" || got != want {
t.Errorf("skillRelPath(%q) = (%q, %q), want (%q, \"\")", in, got, msg, want)
}
}
bad := []string{
"", "../etc/passwd", "a/../../b", "/abs/path", "a//b", `..\..\x`,
"%2e%2e/x", "a\x00b", "chinese name.md", "ba‮d.md", "a#b.md", "a?b.md",
"a:b.md", string([]byte{0xd6, 0xd0}) + ".md", // raw GBK bytes: invalid UTF-8
strings.Repeat("a", maxSkillPathLen+1),
}
for _, in := range bad {
if got, msg := skillRelPath(in); msg == "" {
t.Errorf("skillRelPath(%q) = (%q, \"\"), want rejection", in, got)
}
}
}
type nopWriteCloser struct{ io.Writer }
func (nopWriteCloser) Close() error { return nil }
// zipFile describes one entry for buildZip.
type zipFile struct {
name string
body string
method uint16
nonUTF8 bool // write the name bytes as-is (GBK zip)
}
func buildZip(t *testing.T, files ...zipFile) []byte {
t.Helper()
var buf bytes.Buffer
zw := zip.NewWriter(&buf)
zw.RegisterCompressor(zipMethodZstd, zstd.ZipCompressor())
// Deflate64 has no pure-Go encoder; the body is written as-is just to tag the entry
// with method 9 — the assertion checks the message given for an unsupported method,
// nothing is actually decompressed.
zw.RegisterCompressor(zipMethodDeflate64, func(w io.Writer) (io.WriteCloser, error) {
return nopWriteCloser{w}, nil
})
for _, f := range files {
h := &zip.FileHeader{Name: f.name, Method: f.method, NonUTF8: f.nonUTF8}
w, err := zw.CreateHeader(h)
if err != nil {
t.Fatalf("CreateHeader(%q): %v", f.name, err)
}
if _, err := io.WriteString(w, f.body); err != nil {
t.Fatalf("write %q: %v", f.name, err)
}
}
if err := zw.Close(); err != nil {
t.Fatalf("zip close: %v", err)
}
return buf.Bytes()
}
// uploadZip posts raw zip bytes to fsUploadSkill and returns the response.
func uploadZip(t *testing.T, skillDir string, filename string, data []byte) (*httptest.ResponseRecorder, map[string]any) {
t.Helper()
var body bytes.Buffer
mw := multipart.NewWriter(&body)
fw, err := mw.CreateFormFile("file", filename)
if err != nil {
t.Fatal(err)
}
if _, err := fw.Write(data); err != nil {
t.Fatal(err)
}
mw.Close()
req := httptest.NewRequest(http.MethodPost, "/api/skills/upload", &body)
req.Header.Set("Content-Type", mw.FormDataContentType())
rr := httptest.NewRecorder()
(&Server{skillDir: skillDir}).fsUploadSkill(rr, req)
var out map[string]any
_ = json.Unmarshal(rr.Body.Bytes(), &out)
return rr, out
}
const zhSkillMD = "---\nname: chinese-skill\ndescription: test\n---\nbody\n"
// A zstd-compressed archive (an optional WinZip compression method) used to blow up with
// "zip: unsupported compression"; it now installs like any Deflate archive.
func TestUploadSkillZstdAndChineseNames(t *testing.T) {
dir := t.TempDir()
data := buildZip(t,
zipFile{name: "chinese-skill/SKILL.md", body: zhSkillMD, method: zipMethodZstd},
zipFile{name: "chinese-skill/references/notes doc.md", body: "reference", method: zipMethodZstd},
zipFile{name: "chinese-skill/scripts/run.py", body: "print(1)", method: zip.Deflate},
)
rr, out := uploadZip(t, dir, "chinese-skill.zip", data)
if rr.Code != 201 {
t.Fatalf("status = %d, body = %s", rr.Code, rr.Body)
}
if out["name"] != "chinese-skill" {
t.Fatalf("name = %v, want chinese-skill", out["name"])
}
for _, rel := range []string{"SKILL.md", "references/notes doc.md", "scripts/run.py"} {
if _, err := os.Stat(filepath.Join(dir, "chinese-skill", rel)); err != nil {
t.Errorf("missing extracted file %q: %v", rel, err)
}
}
}
// GBK-named entries (7-Zip / Explorer on Chinese Windows) must be decoded rather
// than rejected as invalid UTF-8 paths.
func TestUploadSkillGBKNames(t *testing.T) {
gbk := func(s string) string {
b, err := simplifiedchinese.GBK.NewEncoder().String(s)
if err != nil {
t.Fatalf("gbk encode %q: %v", s, err)
}
return b
}
dir := t.TempDir()
data := buildZip(t,
zipFile{name: gbk("chinese-skill/SKILL.md"), body: zhSkillMD, method: zip.Deflate, nonUTF8: true},
zipFile{name: gbk("chinese-skill/reference-materials.md"), body: "content", method: zip.Deflate, nonUTF8: true},
)
rr, out := uploadZip(t, dir, "skill.zip", data)
if rr.Code != 201 {
t.Fatalf("status = %d, body = %s", rr.Code, rr.Body)
}
if out["name"] != "chinese-skill" {
t.Fatalf("name = %v, want chinese-skill", out["name"])
}
if _, err := os.Stat(filepath.Join(dir, "chinese-skill", "reference-materials.md")); err != nil {
t.Errorf("GBK-named entry not extracted: %v", err)
}
}
// An archive we genuinely cannot decode should name the method in the error instead of
// surfacing "zip: unsupported compression algorithm".
func TestUploadSkillUnsupportedMethod(t *testing.T) {
data := buildZip(t,
zipFile{name: "demo/SKILL.md", body: "---\nname: demo\n---\n", method: zipMethodDeflate64},
)
rr, out := uploadZip(t, t.TempDir(), "demo.zip", data)
if rr.Code != 400 {
t.Fatalf("status = %d, want 400 (body %s)", rr.Code, rr.Body)
}
msg, _ := out["error"].(string)
if !strings.Contains(msg, "Deflate64") || !strings.Contains(msg, "unsupported compression method") {
t.Fatalf("error = %q, want a message naming Deflate64", msg)
}
}
func TestUploadSkillEncrypted(t *testing.T) {
data := buildZip(t, zipFile{name: "demo/SKILL.md", body: "---\nname: demo\n---\n", method: zip.Deflate})
// flip the "encrypted" general-purpose flag bit in the local file header
// (offset 6) and in the central directory copy (offset 8).
local := bytes.Index(data, []byte("PK\x03\x04"))
central := bytes.Index(data, []byte("PK\x01\x02"))
if local < 0 || central < 0 {
t.Fatal("could not locate zip headers")
}
data[local+6] |= 1
data[central+8] |= 1
rr, out := uploadZip(t, t.TempDir(), "demo.zip", data)
if rr.Code != 400 {
t.Fatalf("status = %d, want 400 (body %s)", rr.Code, rr.Body)
}
if msg, _ := out["error"].(string); !strings.Contains(msg, "archive is encrypted") {
t.Fatalf("error = %q, want an encryption hint", msg)
}
}
// Zip-slip must still be refused now that the path check accepts Unicode.
func TestUploadSkillRejectsTraversal(t *testing.T) {
data := buildZip(t,
zipFile{name: "demo/SKILL.md", body: "---\nname: demo\n---\n", method: zip.Deflate},
zipFile{name: "demo/../../evil.sh", body: "rm -rf /", method: zip.Deflate},
)
dir := t.TempDir()
rr, out := uploadZip(t, dir, "demo.zip", data)
if rr.Code != 400 {
t.Fatalf("status = %d, want 400 (body %s)", rr.Code, rr.Body)
}
if msg, _ := out["error"].(string); !strings.Contains(msg, "illegal path") {
t.Fatalf("error = %q, want an illegal-path error", msg)
}
if entries, _ := os.ReadDir(dir); len(entries) != 0 {
t.Fatalf("upload left files behind: %v", entries)
}
}
func TestSkillNameFromFrontmatterQuoted(t *testing.T) {
got := skillNameFromFrontmatter([]byte("---\nname: \"chinese-skill\"\ndescription: x\n---\n"))
if got != "chinese-skill" {
t.Fatalf("name = %q, want chinese-skill", got)
}
}