dsh-calibguard 0.2.5 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +22 -16
- package/README.md +22 -15
- package/cordis.patch.yml +2 -1
- package/docs/configuration.md +2 -2
- package/docs/troubleshooting.md +2 -1
- package/engine/README.md +33 -0
- package/engine/audit_service.py +304 -0
- package/engine/calibguard/__init__.py +2 -0
- package/engine/calibguard/__main__.py +4 -0
- package/engine/calibguard/calibrator.py +78 -0
- package/engine/calibguard/cli.py +74 -0
- package/engine/calibguard/config.py +129 -0
- package/engine/calibguard/constants.py +117 -0
- package/engine/calibguard/feature_extractor.py +161 -0
- package/engine/calibguard/gate.py +17 -0
- package/engine/calibguard/llm_analyzer.py +171 -0
- package/engine/calibguard/model.py +79 -0
- package/engine/calibguard/pipeline.py +305 -0
- package/engine/calibguard/preprocessor.py +218 -0
- package/engine/calibguard/report.py +132 -0
- package/engine/calibguard/schemas.py +53 -0
- package/engine/models/calibration_params.json +1 -0
- package/engine/models/codebert_finetuned/config.json +28 -0
- package/engine/models/codebert_finetuned/merges.txt +50001 -0
- package/engine/models/codebert_finetuned/special_tokens_map.json +51 -0
- package/engine/models/codebert_finetuned/tokenizer.json +250371 -0
- package/engine/models/codebert_finetuned/tokenizer_config.json +58 -0
- package/engine/models/codebert_finetuned/vocab.json +1 -0
- package/engine/requirements-gui.txt +11 -0
- package/lib/bootstrap.js +4 -3
- package/lib/client.js +1 -1
- package/lib/config.js +34 -7
- package/lib/engine.js +1 -1
- package/lib/index.js +1 -1
- package/package.json +5 -3
- package/setup.mjs +291 -48
- package/bridge/__pycache__/calibguard_bridge.cpython-310.pyc +0 -0
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
"""预处理模块:任意输入文件 → 统一文本。
|
|
2
|
+
|
|
3
|
+
只按内容判定类型(魔数 + 解码尝试),不按扩展名。压缩包一律在内存中解包,
|
|
4
|
+
禁止 extractall / extract 落盘(因此不需要 tarfile 的 filter 参数)。
|
|
5
|
+
"""
|
|
6
|
+
import gzip
|
|
7
|
+
import io
|
|
8
|
+
import json
|
|
9
|
+
import logging
|
|
10
|
+
import re
|
|
11
|
+
import tarfile
|
|
12
|
+
import zipfile
|
|
13
|
+
|
|
14
|
+
from calibguard import constants as C
|
|
15
|
+
from calibguard.schemas import PreprocessResult
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
# 控制字符模式(仅用于 text/binary 判定;不含 \t \n \r)
|
|
20
|
+
_CONTROL_CHARS_RE = re.compile(r"[\x00-\x08\x0b\x0c\x0e-\x1f]")
|
|
21
|
+
# 控制字符占比超过该值即判定为二进制(见 7.3 步骤 4)
|
|
22
|
+
_CONTROL_CHAR_RATIO_MAX = 0.30
|
|
23
|
+
# 二进制可打印串提取模式(最短连续长度取常量 PRINTABLE_MIN_RUN)
|
|
24
|
+
_PRINTABLE_RE = re.compile(rb"[\x20-\x7e]{%d,}" % C.PRINTABLE_MIN_RUN)
|
|
25
|
+
# 连续 3 个及以上空白字符(空格/制表符)压缩模式
|
|
26
|
+
_BLANK_RUN_RE = re.compile(r"[ \t]{3,}")
|
|
27
|
+
# 解包截断标记
|
|
28
|
+
_ARCHIVE_TRUNCATED_MARK = "\n\n[ARCHIVE_PARTIALLY_EXTRACTED]"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def preprocess_file(path: str) -> PreprocessResult:
|
|
32
|
+
"""主流程:读入 → 判定压缩包 → 解包或解码 → JSON 规整 → 清洗。"""
|
|
33
|
+
data, read_truncated = _read_bytes(path)
|
|
34
|
+
|
|
35
|
+
# 步骤 2 + 步骤 3:压缩包走内存解包
|
|
36
|
+
if _detect_archive(data) is not None:
|
|
37
|
+
archive_text, parts, expand_truncated = _expand_archive(data, 0)
|
|
38
|
+
return PreprocessResult(
|
|
39
|
+
text=_clean_text(archive_text),
|
|
40
|
+
source_type="archive",
|
|
41
|
+
truncated=read_truncated or expand_truncated,
|
|
42
|
+
parts=parts,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
# 步骤 4:非压缩包 → 解码或可打印串提取
|
|
46
|
+
text, source_type = _decode_text(data)
|
|
47
|
+
|
|
48
|
+
# 步骤 5:JSON 规整(仅 text 类;解析失败原样保留)
|
|
49
|
+
if source_type == "text":
|
|
50
|
+
stripped = text.strip()
|
|
51
|
+
if stripped.startswith("{") or stripped.startswith("["):
|
|
52
|
+
try:
|
|
53
|
+
parsed = json.loads(stripped)
|
|
54
|
+
except ValueError as exc:
|
|
55
|
+
logger.debug("JSON 解析失败,按原文保留: %s", exc)
|
|
56
|
+
else:
|
|
57
|
+
text = json.dumps(parsed, ensure_ascii=False, indent=2)
|
|
58
|
+
|
|
59
|
+
# 步骤 6 + 步骤 7
|
|
60
|
+
return PreprocessResult(
|
|
61
|
+
text=_clean_text(text),
|
|
62
|
+
source_type=source_type,
|
|
63
|
+
truncated=read_truncated,
|
|
64
|
+
parts=1,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _read_bytes(path: str) -> tuple[bytes, bool]:
|
|
69
|
+
"""读入文件字节。(数据, 是否被读入截断)。不存在或不可读 → FileNotFoundError。"""
|
|
70
|
+
try:
|
|
71
|
+
with open(path, "rb") as handle:
|
|
72
|
+
data = handle.read(C.MAX_INPUT_BYTES + 1)
|
|
73
|
+
except FileNotFoundError:
|
|
74
|
+
raise
|
|
75
|
+
except OSError as exc:
|
|
76
|
+
# 不可读(权限等)按文档统一转为 FileNotFoundError
|
|
77
|
+
raise FileNotFoundError(f"文件不存在或不可读: {path}") from exc
|
|
78
|
+
if len(data) > C.MAX_INPUT_BYTES:
|
|
79
|
+
return data[:C.MAX_INPUT_BYTES], True
|
|
80
|
+
return data, False
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _detect_archive(data: bytes) -> str | None:
|
|
84
|
+
""""zip" | "gzip" | "tar" | None(纯魔数判定)。"""
|
|
85
|
+
if data[:4] in (b"PK\x03\x04", b"PK\x05\x06"):
|
|
86
|
+
return "zip"
|
|
87
|
+
if data[:2] == b"\x1f\x8b":
|
|
88
|
+
return "gzip"
|
|
89
|
+
if data[257:262] == b"ustar":
|
|
90
|
+
return "tar"
|
|
91
|
+
return None
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _expand_archive(data: bytes, depth: int) -> tuple[str, int, bool]:
|
|
95
|
+
"""内存解包压缩包,返回 (拼接文本, 内部文件数, 是否被解包截断)。
|
|
96
|
+
|
|
97
|
+
只处理常规文件成员:zip 用 zf.read(info),tar 用 tf.extractfile(m).read();
|
|
98
|
+
目录、符号链接、硬链接、设备成员一律跳过(链接成员是 tar 的经典攻击面)。
|
|
99
|
+
"""
|
|
100
|
+
archive_type = _detect_archive(data)
|
|
101
|
+
members: list[tuple[str, bytes]] = []
|
|
102
|
+
file_count = 0
|
|
103
|
+
total_bytes = 0
|
|
104
|
+
truncated = False
|
|
105
|
+
|
|
106
|
+
try:
|
|
107
|
+
if archive_type == "zip":
|
|
108
|
+
with zipfile.ZipFile(io.BytesIO(data)) as zf:
|
|
109
|
+
for info in zf.infolist():
|
|
110
|
+
if info.is_dir():
|
|
111
|
+
continue
|
|
112
|
+
# 安全限制:读成员前先按其声明大小检查
|
|
113
|
+
if (file_count + 1 > C.ARCHIVE_MAX_FILES
|
|
114
|
+
or total_bytes + info.file_size > C.ARCHIVE_MAX_TOTAL_BYTES):
|
|
115
|
+
truncated = True
|
|
116
|
+
break
|
|
117
|
+
blob = zf.read(info)
|
|
118
|
+
members.append((info.filename, blob))
|
|
119
|
+
file_count += 1
|
|
120
|
+
total_bytes += len(blob)
|
|
121
|
+
elif archive_type == "tar":
|
|
122
|
+
with tarfile.open(fileobj=io.BytesIO(data), mode="r:*") as tf:
|
|
123
|
+
for member in tf:
|
|
124
|
+
if not member.isreg():
|
|
125
|
+
continue
|
|
126
|
+
# 安全限制:读成员前先按其声明大小检查
|
|
127
|
+
if (file_count + 1 > C.ARCHIVE_MAX_FILES
|
|
128
|
+
or total_bytes + member.size > C.ARCHIVE_MAX_TOTAL_BYTES):
|
|
129
|
+
truncated = True
|
|
130
|
+
break
|
|
131
|
+
extracted = tf.extractfile(member)
|
|
132
|
+
if extracted is None:
|
|
133
|
+
# 声明为常规文件却取不到内容:跳过该成员并记录
|
|
134
|
+
logger.warning("tar 成员无法读取,已跳过: %s", member.name)
|
|
135
|
+
continue
|
|
136
|
+
blob = extracted.read()
|
|
137
|
+
members.append((member.name, blob))
|
|
138
|
+
file_count += 1
|
|
139
|
+
total_bytes += len(blob)
|
|
140
|
+
elif archive_type == "gzip":
|
|
141
|
+
inner = gzip.decompress(data)
|
|
142
|
+
if _detect_archive(inner) == "tar":
|
|
143
|
+
# tar.gz 两步走:属同一压缩层级,深度不递增
|
|
144
|
+
return _expand_archive(inner, depth)
|
|
145
|
+
members.append(("", inner))
|
|
146
|
+
file_count = 1
|
|
147
|
+
total_bytes = len(inner)
|
|
148
|
+
else:
|
|
149
|
+
# 魔数判定为压缩包却无法识别类型:按二进制处理
|
|
150
|
+
return _extract_printable(data), 1, False
|
|
151
|
+
except Exception as exc:
|
|
152
|
+
# 损坏的压缩包 → 视为二进制,走可打印串提取(不吞异常:记录并明确返回)
|
|
153
|
+
logger.warning("压缩包解析失败,按二进制处理: %s: %s", type(exc).__name__, exc)
|
|
154
|
+
return _extract_printable(data), 1, False
|
|
155
|
+
|
|
156
|
+
# 第二遍:嵌套压缩包递归(深度 +1),其余解码为文本
|
|
157
|
+
collected: list[tuple[str, str]] = []
|
|
158
|
+
for name, blob in members:
|
|
159
|
+
if _detect_archive(blob) is not None:
|
|
160
|
+
if depth + 1 > C.ARCHIVE_MAX_DEPTH:
|
|
161
|
+
continue # 超出递归深度上限,跳过该内部文件
|
|
162
|
+
inner_text, inner_parts, inner_truncated = _expand_archive(blob, depth + 1)
|
|
163
|
+
file_count += inner_parts
|
|
164
|
+
truncated = truncated or inner_truncated
|
|
165
|
+
collected.append((name, inner_text))
|
|
166
|
+
else:
|
|
167
|
+
member_text, _member_type = _decode_text(blob)
|
|
168
|
+
collected.append((name, member_text))
|
|
169
|
+
|
|
170
|
+
# 全部文本按文件名排序后用 "\n\n" 连接(不加任何标记)
|
|
171
|
+
collected.sort(key=lambda item: item[0])
|
|
172
|
+
joined = "\n\n".join(entry_text for _name, entry_text in collected)
|
|
173
|
+
if truncated:
|
|
174
|
+
joined += _ARCHIVE_TRUNCATED_MARK
|
|
175
|
+
return joined, file_count, truncated
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _decode_text(data: bytes) -> tuple[str, str]:
|
|
179
|
+
"""(文本, "text"|"binary"):先 utf-8 再 gbk,均严格模式。"""
|
|
180
|
+
text: str | None = None
|
|
181
|
+
for encoding in ("utf-8", "gbk"):
|
|
182
|
+
try:
|
|
183
|
+
text = data.decode(encoding)
|
|
184
|
+
break # 解码成功,结束尝试
|
|
185
|
+
except UnicodeDecodeError as exc:
|
|
186
|
+
# 该编码不适用:记录原因后继续尝试下一种编码
|
|
187
|
+
logger.debug("按 %s 解码失败,尝试下一种编码: %s", encoding, exc)
|
|
188
|
+
if text is None:
|
|
189
|
+
# 两种解码都失败 → 二进制
|
|
190
|
+
return _extract_printable(data), "binary"
|
|
191
|
+
if text == "":
|
|
192
|
+
return text, "text" # 空字符串直接判 text,不做除法
|
|
193
|
+
control_ratio = len(_CONTROL_CHARS_RE.findall(text)) / len(text)
|
|
194
|
+
if control_ratio > _CONTROL_CHAR_RATIO_MAX:
|
|
195
|
+
return _extract_printable(data), "binary"
|
|
196
|
+
return text, "text"
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def _extract_printable(data: bytes) -> str:
|
|
200
|
+
"""提取二进制中的可打印 ASCII 串,各段用 "\\n" 连接。"""
|
|
201
|
+
runs = _PRINTABLE_RE.findall(data)
|
|
202
|
+
return b"\n".join(runs).decode("ascii")
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def _clean_text(text: str) -> str:
|
|
206
|
+
"""a 删控制字符 → b 压缩连续空白 → c 单行截断 → d 总长截断。"""
|
|
207
|
+
text = re.sub(C.CONTROL_CHARS_PATTERN, "", text)
|
|
208
|
+
text = _BLANK_RUN_RE.sub(" ", text)
|
|
209
|
+
lines = text.split("\n")
|
|
210
|
+
clipped_lines: list[str] = []
|
|
211
|
+
for line in lines:
|
|
212
|
+
if len(line) > C.LINE_TRUNCATE_THRESHOLD:
|
|
213
|
+
line = line[:C.LINE_TRUNCATE_THRESHOLD] + "…[LINE_TRUNCATED]"
|
|
214
|
+
clipped_lines.append(line)
|
|
215
|
+
text = "\n".join(clipped_lines)
|
|
216
|
+
if len(text) > C.MAX_TEXT_CHARS:
|
|
217
|
+
text = text[:C.MAX_TEXT_CHARS] + "\n…[TEXT_TRUNCATED]"
|
|
218
|
+
return text
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"""审计报告模块:审计结果 dict → JSON 文件 + Markdown 文件。"""
|
|
2
|
+
import json
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
from calibguard.schemas import FeatureEvidence
|
|
6
|
+
|
|
7
|
+
# 证据明细的七类标题(第八类"异常结构特征"由长行/随机标识符两行构成)
|
|
8
|
+
_EVIDENCE_TITLES = [
|
|
9
|
+
("【危险API调用】", "dangerous_apis"),
|
|
10
|
+
("【网络行为】", "network_indicators"),
|
|
11
|
+
("【凭据与敏感路径访问】", "credential_access"),
|
|
12
|
+
("【编码与混淆】", "obfuscation_signals"),
|
|
13
|
+
("【可疑文件操作】", "file_operations"),
|
|
14
|
+
("【提示注入话术】", "injection_phrases"),
|
|
15
|
+
("【高熵字符串】", "high_entropy_strings"),
|
|
16
|
+
]
|
|
17
|
+
# 证据全空时的固定文案
|
|
18
|
+
_EMPTY_EVIDENCE_TEXT = "未发现明显可疑信号。"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def write_reports(report: dict, output_dir: str) -> tuple[str, str]:
|
|
22
|
+
"""生成 <output_dir>/<report_id>.json 与 <output_dir>/<report_id>.md,返回绝对路径。"""
|
|
23
|
+
os.makedirs(output_dir, exist_ok=True)
|
|
24
|
+
report_id = str(report["report_id"])
|
|
25
|
+
json_path = os.path.abspath(os.path.join(output_dir, report_id + ".json"))
|
|
26
|
+
md_path = os.path.abspath(os.path.join(output_dir, report_id + ".md"))
|
|
27
|
+
with open(json_path, "w", encoding="utf-8") as handle:
|
|
28
|
+
handle.write(json.dumps(report, ensure_ascii=False, indent=2))
|
|
29
|
+
with open(md_path, "w", encoding="utf-8") as handle:
|
|
30
|
+
handle.write(_to_markdown(report))
|
|
31
|
+
return json_path, md_path
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _evidence_to_dict(ev: FeatureEvidence) -> dict:
|
|
35
|
+
"""FeatureEvidence → 九个同名键的 dict。"""
|
|
36
|
+
return {
|
|
37
|
+
"dangerous_apis": list(ev.dangerous_apis),
|
|
38
|
+
"network_indicators": list(ev.network_indicators),
|
|
39
|
+
"credential_access": list(ev.credential_access),
|
|
40
|
+
"obfuscation_signals": list(ev.obfuscation_signals),
|
|
41
|
+
"file_operations": list(ev.file_operations),
|
|
42
|
+
"injection_phrases": list(ev.injection_phrases),
|
|
43
|
+
"high_entropy_strings": list(ev.high_entropy_strings),
|
|
44
|
+
"long_line_numbers": list(ev.long_line_numbers),
|
|
45
|
+
"random_identifier_count": ev.random_identifier_count,
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _to_markdown(report: dict) -> str:
|
|
50
|
+
"""按 13.3 固定模板渲染 Markdown 报告。"""
|
|
51
|
+
early_stopped = bool(report.get("early_stopped"))
|
|
52
|
+
if early_stopped:
|
|
53
|
+
early_stop_text = f"是,p={report.get('early_stop_probability')}"
|
|
54
|
+
else:
|
|
55
|
+
early_stop_text = "否"
|
|
56
|
+
|
|
57
|
+
lines: list[str] = [
|
|
58
|
+
"# CalibGuard 安全审计报告",
|
|
59
|
+
"",
|
|
60
|
+
f"- 报告编号:{report['report_id']}",
|
|
61
|
+
f"- 生成时间:{report['timestamp']}",
|
|
62
|
+
f"- 审计文件:{report['file_path']}",
|
|
63
|
+
f"- 文件 SHA256:{report['file_sha256']}",
|
|
64
|
+
f"- 文件大小:{report['file_size_bytes']} 字节",
|
|
65
|
+
f"- 来源类型:{report['source_type']}(内部文件数:{report['archive_parts']})",
|
|
66
|
+
"",
|
|
67
|
+
"## 结论",
|
|
68
|
+
"",
|
|
69
|
+
"| 项目 | 结果 |",
|
|
70
|
+
"|---|---|",
|
|
71
|
+
f"| 原始 logit | {report['raw_logit']} |",
|
|
72
|
+
f"| 校准概率 | {report['calibrated_probability']} |",
|
|
73
|
+
f"| 校准方法 | {report['calibration_method']} |",
|
|
74
|
+
f"| 推理窗口数 | {report['windows_evaluated']} |",
|
|
75
|
+
f"| 滑窗早停 | {early_stop_text} |",
|
|
76
|
+
f"| 门控决策 | {report['gate_decision']} |",
|
|
77
|
+
f"| 最终决策 | **{report['final_decision']}** |",
|
|
78
|
+
f"| 需人工复核 | {report['manual_review_required']} |",
|
|
79
|
+
f"| 一致性告警 | {report['consistency_warning']} |",
|
|
80
|
+
"",
|
|
81
|
+
"## LLM 分析",
|
|
82
|
+
"",
|
|
83
|
+
]
|
|
84
|
+
|
|
85
|
+
llm = report.get("llm")
|
|
86
|
+
if llm is None:
|
|
87
|
+
lines.append("(未提交云端分析)")
|
|
88
|
+
else:
|
|
89
|
+
lines.extend([
|
|
90
|
+
"| 字段 | 值 |",
|
|
91
|
+
"|---|---|",
|
|
92
|
+
f"| status | {llm.get('status')} |",
|
|
93
|
+
f"| risk | {llm.get('risk')} |",
|
|
94
|
+
f"| reason | {llm.get('reason')} |",
|
|
95
|
+
f"| action | {llm.get('action')} |",
|
|
96
|
+
f"| 重试次数 | {llm.get('retries_used')} |",
|
|
97
|
+
])
|
|
98
|
+
|
|
99
|
+
lines.extend(["", "## 证据明细", ""])
|
|
100
|
+
lines.append(_render_evidence_section(report.get("evidence") or {}))
|
|
101
|
+
|
|
102
|
+
lines.extend(["", "## 错误信息", ""])
|
|
103
|
+
error = report.get("error")
|
|
104
|
+
lines.append(str(error) if error else "(无)")
|
|
105
|
+
|
|
106
|
+
return "\n".join(lines) + "\n"
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _render_evidence_section(evidence: dict) -> str:
|
|
110
|
+
"""按 8.4 的八类格式渲染证据明细;全空时返回固定文案。"""
|
|
111
|
+
blocks: list[str] = []
|
|
112
|
+
for title, key in _EVIDENCE_TITLES:
|
|
113
|
+
items = evidence.get(key) or []
|
|
114
|
+
if not items:
|
|
115
|
+
continue
|
|
116
|
+
block_lines = [title]
|
|
117
|
+
block_lines.extend(f"- {item}" for item in items)
|
|
118
|
+
blocks.append("\n".join(block_lines))
|
|
119
|
+
|
|
120
|
+
structural_lines: list[str] = []
|
|
121
|
+
long_line_numbers = evidence.get("long_line_numbers") or []
|
|
122
|
+
if long_line_numbers:
|
|
123
|
+
structural_lines.append(f"- 超长行行号: {long_line_numbers}")
|
|
124
|
+
random_identifier_count = evidence.get("random_identifier_count") or 0
|
|
125
|
+
if random_identifier_count > 0:
|
|
126
|
+
structural_lines.append(f"- 长随机标识符数量: {random_identifier_count}")
|
|
127
|
+
if structural_lines:
|
|
128
|
+
blocks.append("\n".join(["【异常结构特征】"] + structural_lines))
|
|
129
|
+
|
|
130
|
+
if not blocks:
|
|
131
|
+
return _EMPTY_EVIDENCE_TEXT
|
|
132
|
+
return "\n\n".join(blocks)
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""CalibGuard 全局数据结构定义。本文件无任何业务逻辑。"""
|
|
2
|
+
from dataclasses import dataclass, field
|
|
3
|
+
from enum import Enum
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class GateDecision(str, Enum):
|
|
7
|
+
"""门控路由决策。"""
|
|
8
|
+
ALLOW = "allow" # 放行
|
|
9
|
+
BLOCK = "block" # 拦截告警
|
|
10
|
+
LLM_REVIEW = "llm_review" # 提交云端 LLM 分析
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class PreprocessResult:
|
|
15
|
+
"""预处理结果。"""
|
|
16
|
+
text: str # 预处理并清洗后的统一文本(可能较长,token 截断在模型层进行)
|
|
17
|
+
source_type: str # "text" | "archive" | "binary"
|
|
18
|
+
truncated: bool # True 表示字节级读入截断(>1MB)或解包截断发生过
|
|
19
|
+
parts: int = 1 # 压缩包内部文件数;非压缩包恒为 1
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class FeatureEvidence:
|
|
24
|
+
"""特征提取产出的结构化证据。每个列表字段:去重、按首次出现顺序、最多 10 条。"""
|
|
25
|
+
dangerous_apis: list[str] = field(default_factory=list) # 危险 API 调用
|
|
26
|
+
network_indicators: list[str] = field(default_factory=list) # 网络行为
|
|
27
|
+
credential_access: list[str] = field(default_factory=list) # 凭据与敏感路径访问
|
|
28
|
+
obfuscation_signals: list[str] = field(default_factory=list) # 编码与混淆
|
|
29
|
+
file_operations: list[str] = field(default_factory=list) # 可疑文件操作
|
|
30
|
+
injection_phrases: list[str] = field(default_factory=list) # 提示注入话术
|
|
31
|
+
high_entropy_strings: list[str] = field(default_factory=list) # 高熵字符串(最多 10 条)
|
|
32
|
+
long_line_numbers: list[int] = field(default_factory=list) # 超长行行号(1 起,最多 10 个)
|
|
33
|
+
random_identifier_count: int = 0 # 长随机标识符总数(达到阈值才>0)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass
|
|
37
|
+
class LLMResult:
|
|
38
|
+
"""云端 LLM 分析结果。"""
|
|
39
|
+
status: str # "ok" | "invalid_output" | "api_error"
|
|
40
|
+
risk: str | None = None # "high" | "medium" | "low",失败时为 None
|
|
41
|
+
reason: str | None = None
|
|
42
|
+
action: str | None = None
|
|
43
|
+
consistency_warning: bool = False # 本地概率高但 LLM 判 low 时为 True
|
|
44
|
+
retries_used: int = 0 # 实际发生重试的次数(0 或 1)
|
|
45
|
+
raw_response: str | None = None # 最后一次 API 原始返回文本(调试用,报告必含)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass
|
|
49
|
+
class WindowInferenceStats:
|
|
50
|
+
"""滑动窗口推理统计(v1.1 新增)。"""
|
|
51
|
+
windows_evaluated: int = 0 # 实际完成推理的窗口数
|
|
52
|
+
early_stopped: bool = False # 是否触发早停
|
|
53
|
+
early_stop_probability: float | None = None # 触发早停时的窗口校准概率
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"A": -1.0332483500324428, "B": 0.7344078473521912, "method": "platt", "n_samples": 25049, "fit_at": "2026-10-03T19:35:34.589148+08:00"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"architectures": [
|
|
3
|
+
"RobertaForSequenceClassification"
|
|
4
|
+
],
|
|
5
|
+
"attention_probs_dropout_prob": 0.1,
|
|
6
|
+
"bos_token_id": 0,
|
|
7
|
+
"classifier_dropout": null,
|
|
8
|
+
"dtype": "float32",
|
|
9
|
+
"eos_token_id": 2,
|
|
10
|
+
"hidden_act": "gelu",
|
|
11
|
+
"hidden_dropout_prob": 0.1,
|
|
12
|
+
"hidden_size": 768,
|
|
13
|
+
"initializer_range": 0.02,
|
|
14
|
+
"intermediate_size": 3072,
|
|
15
|
+
"layer_norm_eps": 1e-05,
|
|
16
|
+
"max_position_embeddings": 514,
|
|
17
|
+
"model_type": "roberta",
|
|
18
|
+
"num_attention_heads": 12,
|
|
19
|
+
"num_hidden_layers": 12,
|
|
20
|
+
"output_past": true,
|
|
21
|
+
"pad_token_id": 1,
|
|
22
|
+
"position_embedding_type": "absolute",
|
|
23
|
+
"problem_type": "single_label_classification",
|
|
24
|
+
"transformers_version": "4.57.6",
|
|
25
|
+
"type_vocab_size": 1,
|
|
26
|
+
"use_cache": true,
|
|
27
|
+
"vocab_size": 50265
|
|
28
|
+
}
|