codeark-pitax 0.1.0__py3-none-any.whl

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.
@@ -0,0 +1,12 @@
1
+ """CodeRisk Cloud — PITAX 检测模块 (Arcanum Prompt Injection Taxonomy v1.6.1)
2
+ Based on the Arcanum Prompt Injection Taxonomy by Jason Haddix,
3
+ Arcanum Information Security (arcanum-sec.com). CC BY 4.0.
4
+ """
5
+ from .sanitizer import InputSanitizer, scan_directory
6
+ from .sarif import build_sarif
7
+ from .rules import ALL_RULES, PRIMARY_RULES, PITAX_RULES, PITAX_VERSION, get_rule
8
+
9
+ __all__ = [
10
+ "InputSanitizer", "scan_directory", "build_sarif",
11
+ "ALL_RULES", "PRIMARY_RULES", "PITAX_RULES", "PITAX_VERSION", "get_rule",
12
+ ]
@@ -0,0 +1,5 @@
1
+ from .cli import main
2
+
3
+ import sys
4
+
5
+ sys.exit(main())
codeark_pitax/cli.py ADDED
@@ -0,0 +1,86 @@
1
+ """codeark-pitax 命令行:仓库/文件的确定性提示注入扫描(零依赖、零网络)。"""
2
+ from __future__ import annotations
3
+
4
+ import argparse
5
+ import sys
6
+ from pathlib import Path
7
+
8
+ from .scan_engine import scan_repo
9
+ from .reporting import render_report
10
+ from .rules import PITAX_VERSION
11
+ from .detectors import is_ai_config_path
12
+
13
+ SOURCE_EXTS = {".py", ".js", ".ts", ".jsx", ".tsx", ".java", ".go", ".rs", ".c", ".cpp",
14
+ ".h", ".rb", ".php", ".sh", ".bash", ".sql", ".html", ".htm", ".vue", ".json", ".yaml",
15
+ ".yml", ".toml", ".ini", ".cfg", ".txt", ".md"}
16
+ SKIP_DIRS = {".git", "node_modules", ".venv", "venv", "__pycache__", "dist", "build",
17
+ ".pytest_cache"}
18
+
19
+
20
+ def _read(target: Path) -> tuple[dict[str, str], int]:
21
+ if target.is_file():
22
+ candidates, skipped = [target], 0
23
+ else:
24
+ candidates, skipped = [], 0
25
+ for p in sorted(target.rglob('*')):
26
+ if not p.is_file():
27
+ continue
28
+ if any(part in SKIP_DIRS for part in p.parts):
29
+ continue
30
+ if p.suffix.lower() not in SOURCE_EXTS and not is_ai_config_path(str(p)):
31
+ continue
32
+ candidates.append(p)
33
+ files: dict[str, str] = {}
34
+ for p in candidates[:400]:
35
+ try:
36
+ if p.stat().st_size > 512 * 1024:
37
+ skipped += 1
38
+ continue
39
+ files[str(p)] = p.read_text(encoding='utf-8', errors='replace')
40
+ except OSError:
41
+ skipped += 1
42
+ return files, skipped
43
+
44
+
45
+ def main(argv=None) -> int:
46
+ parser = argparse.ArgumentParser(
47
+ prog="codeark-pitax",
48
+ description="Deterministic prompt-injection scan (PITAX v1.6.1, local, no model)")
49
+ parser.add_argument("target", help="repository directory or single file")
50
+ parser.add_argument("--formats", default="json,markdown",
51
+ help="json,sarif,markdown,html")
52
+ parser.add_argument("--out", default="./pitax-report")
53
+ args = parser.parse_args(argv)
54
+
55
+ target = Path(args.target).resolve()
56
+ if not target.exists():
57
+ print(f"target not found: {target}")
58
+ return 1
59
+ files, skipped = _read(target)
60
+ if not files:
61
+ print("no supported source files found")
62
+ return 1
63
+ findings = scan_repo(files, strict=True)
64
+ dicts = findings # scan_repo 返回的已是 report_dict(产品同款形态)
65
+ meta = {
66
+ "tool": "codeark-pitax",
67
+ "pitax_version": PITAX_VERSION,
68
+ "files_scanned": len(files),
69
+ "skipped": skipped,
70
+ "local_only": True,
71
+ }
72
+ reports = render_report(dicts, [f.strip() for f in args.formats.split(',')], meta=meta)
73
+ out = Path(args.out).resolve()
74
+ out.mkdir(parents=True, exist_ok=True)
75
+ ext = {'json': 'json', 'sarif': 'sarif', 'markdown': 'md', 'html': 'html'}
76
+ for name, content in reports.items():
77
+ fp = out / f'report.{ext.get(name, name)}'
78
+ fp.write_text(content, encoding='utf-8')
79
+ print(f'- {fp} ({fp.stat().st_size} B)')
80
+ print(f"findings: {len(dicts)} files: {len(files)} skipped: {skipped}")
81
+ print("local only: nothing was uploaded, no model was called")
82
+ return 0
83
+
84
+
85
+ if __name__ == "__main__":
86
+ sys.exit(main())
@@ -0,0 +1,62 @@
1
+ """PITAX 规则的英文描述层(IC27 合规:提交材料全英文,产品须以英语为表征语言之一)。
2
+
3
+ 规则原文(rules.py)保持中文不动;本模块只提供 code → 英文描述 的映射,
4
+ `Finding.to_report_dict` 在 `ARCA_REPORT_LANG=en` 时取用。默认(未设)一切不变。
5
+ """
6
+ from __future__ import annotations
7
+
8
+ RULE_DESCRIPTIONS_EN: dict[str, str] = {
9
+ "PIT-E-23": (
10
+ "Uses zero-width characters, soft hyphens, U+E0000 tag characters and other Unicode that is "
11
+ "invisible to humans but parsed by LLMs, smuggling malicious instructions into code or documents."
12
+ ),
13
+ "PIT-E-54": (
14
+ "Uses bidirectional text control characters (U+202A-U+202E, U+2066-U+2069) to change the visual "
15
+ "order of source code, so that what the code looks like differs from what the compiler or an "
16
+ "LLM actually reads (CVE-2021-42574, trojansource.codes)."
17
+ ),
18
+ "PIT-T-46": (
19
+ "Plants instruction-override or role-hijack directives in AI assistant configuration files "
20
+ "(.cursor/rules, CLAUDE.md, copilot-instructions.md, .vscode/settings.json, ...), backdooring "
21
+ "the repository's AI assistant (same attack pattern as CVE-2025-53773)."
22
+ ),
23
+ "PIT-T-51": (
24
+ "Embeds prompt-injection instructions in code comments (e.g. 'ignore previous instructions'); "
25
+ "an LLM reading the code or comments can be hijacked into unintended actions."
26
+ ),
27
+ "PIT-N-06": (
28
+ "Plants instructions aimed at AI agents in project documents (README, design docs). Documents "
29
+ "are routine agent input; poisoning them is invisible to human review and traditional tools."
30
+ ),
31
+ "PIT-E-07": "A Base64-encoded prompt-injection payload was found in a string constant.",
32
+ "PIT-E-14": "A ROT13 (Caesar-shift) encoded prompt-injection payload was found in a string constant.",
33
+ "PIT-E-36": (
34
+ "A reversed string that decodes to a prompt-injection payload, used to evade keyword filters."
35
+ ),
36
+ "PIT-E-57": (
37
+ "A multi-layer encoded payload (base64/ROT13/reverse, 2+ layers) that decodes to a "
38
+ "prompt-injection instruction; built to bypass keyword-based filters."
39
+ ),
40
+ }
41
+
42
+ SUGGESTION_EN = (
43
+ "Remove the hidden content / malicious instruction. If this is a false positive, mark an "
44
+ "exemption in CI and document the reason."
45
+ )
46
+
47
+
48
+ def description_for(code: str, fallback: str) -> str:
49
+ """按 env 决定输出语言:ARCA_REPORT_LANG=en → 英文;默认中文原文。"""
50
+ import os
51
+
52
+ if (os.environ.get("ARCA_REPORT_LANG") or "").strip().lower() == "en":
53
+ return RULE_DESCRIPTIONS_EN.get(code, fallback)
54
+ return fallback
55
+
56
+
57
+ def suggestion_for(fallback: str) -> str:
58
+ import os
59
+
60
+ if (os.environ.get("ARCA_REPORT_LANG") or "").strip().lower() == "en":
61
+ return SUGGESTION_EN
62
+ return fallback
@@ -0,0 +1,506 @@
1
+ """PITAX 确定性检测器 — 与官方 taxonomy v1.6.1 编号对齐。
2
+
3
+ 规则 → 检测函数:
4
+ PIT-E-23 detect_invisible_text 不可见字符走私(零宽/软连字符/U+E0000 标签)
5
+ PIT-E-54 detect_trojan_source Bidi 双向控制符源码欺骗(CVE-2021-42574)
6
+ PIT-T-46 detect_ai_config_injection AI 指令文件后门(.cursor/rules 等)
7
+ PIT-T-51 detect_comment_injection 代码注释指令覆盖
8
+ PIT-N-06 detect_doc_injection 项目文档投毒(README 等)
9
+ PIT-E-07/14/36/57 detect_encoded_payloads 单层/多层编码载荷
10
+
11
+ 设计原则(整合版方案铁律):
12
+ - 只做确定性高的检测,控制误报;
13
+ - 编码检测仅在解码后命中注入关键词才报告(避免对普通 base64 误报);
14
+ - 每条 finding 携带 PITAX 编号 + ATLAS/CVE 映射 + 参考链接,可复现、可引用。
15
+
16
+ Based on the Arcanum Prompt Injection Taxonomy by Jason Haddix,
17
+ Arcanum Information Security (arcanum-sec.com). CC BY 4.0.
18
+ """
19
+ from __future__ import annotations
20
+
21
+ import base64
22
+ import codecs
23
+ import re
24
+ import os
25
+ from dataclasses import dataclass, field
26
+
27
+ from .rules import get_rule
28
+ from .descriptions_en import description_for, suggestion_for
29
+
30
+ # ────────────────────────── 数据结构 ──────────────────────────
31
+ @dataclass
32
+ class Finding:
33
+ pitax_code: str
34
+ file: str
35
+ line: int
36
+ evidence: str
37
+ message: str
38
+ severity: str | None = None
39
+ decoded_payload: str | None = None
40
+ extra: dict = field(default_factory=dict)
41
+
42
+ def to_report_dict(self, finding_id: str, language: str = "text") -> dict:
43
+ rule = get_rule(self.pitax_code)
44
+ sev = self.severity or rule.get("severity", "medium")
45
+ # 单层编码子类确定性稍低,置信度 70;其余 90
46
+ confidence = 70 if self.pitax_code in ("PIT-E-07", "PIT-E-14", "PIT-E-36") else 90
47
+ return {
48
+ "id": finding_id,
49
+ "type": self.pitax_code,
50
+ "title": f"[{self.pitax_code}] {rule['name']}",
51
+ "severity": sev,
52
+ "confidence": confidence,
53
+ "cwe": rule.get("cwe"),
54
+ "mitre_atlas": rule.get("mitre_atlas"),
55
+ "description": description_for(self.pitax_code, self.message),
56
+ "file": self.file,
57
+ "line": self.line,
58
+ "language": language,
59
+ "code_snippet": self.evidence[:240],
60
+ "agent": "agent_0_sanitizer",
61
+ "suggestion": suggestion_for(
62
+ "移除隐藏内容/恶意指令;若为误报请在 CI 中标记豁免并说明理由。"
63
+ ),
64
+ "pitax": {
65
+ "code": self.pitax_code,
66
+ "name": rule["name"],
67
+ "pillar": rule["pillar"],
68
+ "aliases": rule.get("aliases", []),
69
+ "references": rule.get("references", []),
70
+ "cwe": rule.get("cwe"),
71
+ "mitre_atlas": rule.get("mitre_atlas"),
72
+ "cve": rule.get("cve"),
73
+ "evidence": self.evidence[:120],
74
+ "decoded_payload": self.decoded_payload,
75
+ **self.extra,
76
+ },
77
+ }
78
+
79
+
80
+ # ────────────────── PIT-E-23: 不可见字符走私 ──────────────────
81
+ # 人类不可见、但会被 LLM 解析的格式字符。不含 Bidi 覆盖类(归 PIT-E-54)。
82
+ INVISIBLE_CHARS: dict[str, str] = {
83
+ "\u200b": "Zero-Width Space",
84
+ "\u200c": "Zero-Width Non-Joiner",
85
+ "\u200d": "Zero-Width Joiner",
86
+ "\u200e": "Left-to-Right Mark",
87
+ "\u200f": "Right-to-Left Mark",
88
+ "\u2060": "Word Joiner",
89
+ "\u2061": "Function Application",
90
+ "\u2062": "Invisible Times",
91
+ "\u2063": "Invisible Separator",
92
+ "\u2064": "Invisible Plus",
93
+ "\ufeff": "Zero-Width No-Break Space (BOM)",
94
+ "\u00ad": "Soft Hyphen",
95
+ "\u034f": "Combining Grapheme Joiner",
96
+ "\u061c": "Arabic Letter Mark",
97
+ "\u115f": "Hangul Choseong Filler",
98
+ "\u1160": "Hangul Jungseong Filler",
99
+ "\u17b4": "Khmer Vowel Inherent Aq",
100
+ "\u17b5": "Khmer Vowel Inherent Aa",
101
+ "\u180e": "Mongolian Vowel Separator",
102
+ }
103
+ # U+E0000–U+E007F 标签字符区段(ASCII Smuggling 关键载体)
104
+ TAG_RANGE = (0xE0000, 0xE007F)
105
+ _WHITESPACE = set(" \t\r\n")
106
+
107
+
108
+ def detect_invisible_text(code: str, file: str) -> list[Finding]:
109
+ findings: list[Finding] = []
110
+ hits: dict[str, list[int]] = {}
111
+ for lineno, line in enumerate(code.splitlines(), start=1):
112
+ for ch in line:
113
+ if ch in _WHITESPACE:
114
+ continue
115
+ label = INVISIBLE_CHARS.get(ch)
116
+ if label is None and TAG_RANGE[0] <= ord(ch) <= TAG_RANGE[1]:
117
+ label = "TAG CHARACTER (ASCII Smuggling)"
118
+ if label:
119
+ hits.setdefault(f"U+{ord(ch):04X} {label}", []).append(lineno)
120
+ for label, lines in hits.items():
121
+ findings.append(Finding(
122
+ pitax_code="PIT-E-23",
123
+ file=file,
124
+ line=lines[0],
125
+ evidence=f"{label} at line(s) {lines[:5]}",
126
+ message=(
127
+ f"检测到不可见 Unicode 字符({label},共 {len(lines)} 行出现)。"
128
+ "不可见字符常用于在人类阅读不可见的情况下向 LLM 走私指令。"
129
+ ),
130
+ extra={"occurrence_lines": lines[:20]},
131
+ ))
132
+ return findings
133
+
134
+
135
+ # ────────────────── PIT-E-54: Trojan Source(Bidi 欺骗)──────────────────
136
+ # 双向文本控制符:改变源码视觉呈现顺序(CVE-2021-42574,trojansource.codes)。
137
+ BIDI_CHARS: dict[str, str] = {
138
+ "\u202a": "Left-to-Right Embedding",
139
+ "\u202b": "Right-to-Left Embedding",
140
+ "\u202c": "Pop Directional Formatting",
141
+ "\u202d": "Left-to-Right Override",
142
+ "\u202e": "Right-to-Left Override",
143
+ "\u2066": "Left-to-Right Isolate",
144
+ "\u2067": "Right-to-Left Isolate",
145
+ "\u2068": "First Strong Isolate",
146
+ "\u2069": "Pop Directional Isolate",
147
+ }
148
+
149
+
150
+ def detect_trojan_source(code: str, file: str) -> list[Finding]:
151
+ """整个文件聚合为一条 finding(同一文件的 Bidi 作弊字符属于同一攻击)。"""
152
+ hits: list[tuple[int, str]] = [] # (lineno, label)
153
+ for lineno, line in enumerate(code.splitlines(), start=1):
154
+ for ch in line:
155
+ label = BIDI_CHARS.get(ch)
156
+ if label:
157
+ hits.append((lineno, f"U+{ord(ch):04X} {label}"))
158
+ if not hits:
159
+ return []
160
+ lines = sorted({ln for ln, _ in hits})
161
+ labels = [lb for _, lb in hits]
162
+ return [Finding(
163
+ pitax_code="PIT-E-54",
164
+ file=file,
165
+ line=lines[0],
166
+ evidence="; ".join(labels[:5]) + ("..." if len(labels) > 5 else ""),
167
+ message=(
168
+ f"检测到双向文本控制符({', '.join(sorted(set(labels))[:3])},"
169
+ f"共 {len(hits)} 处)。Bidi 字符可改变源码视觉呈现顺序(Trojan Source,"
170
+ "CVE-2021-42574),使代码看起来与实际逻辑不一致,同时欺骗人类评审与 AI 助手。"
171
+ ),
172
+ extra={"occurrence_lines": lines[:20]},
173
+ )]
174
+
175
+
176
+ # ────────────────── PIT-T-46: AI 指令文件后门 ──────────────────
177
+ AI_CONFIG_BASENAMES = {
178
+ ".cursor/rules", "cursor.rules", "claude.md", "claudemd.md",
179
+ "copilot-instructions.md", "agents.md", ".cursorrules", ".windsurfrules",
180
+ ".clinerules", ".windsurf/rules",
181
+ }
182
+
183
+
184
+ def is_ai_config_path(path: str) -> bool:
185
+ p = path.lower().replace("\\", "/")
186
+ for cfg in AI_CONFIG_BASENAMES:
187
+ if p == cfg.lower() or p.endswith("/" + cfg.lower()):
188
+ return True
189
+ # 目录包含 .cursor / .windsurf 且文件名含 rules/instructions
190
+ parts = p.strip("/").split("/")
191
+ if any(part in (".cursor", ".windsurf") for part in parts[:-1]):
192
+ if parts[-1].endswith("rules") or "instruction" in parts[-1]:
193
+ return True
194
+ # 路径父目录是 .cursor/rules 或 .windsurf/rules → 其下文件都是 AI 配置文件
195
+ if "rules" in parts[:-1]:
196
+ return True
197
+ return False
198
+
199
+
200
+ _CONFIG_PATTERNS = [
201
+ r"(?i)ignore\s+(all\s+)?previous\s+instructions",
202
+ r"(?i)override\s+(all\s+)?(previous\s+)?instructions",
203
+ r"(?i)disregard\s+(all\s+)?(previous\s+)?instructions",
204
+ r"(?i)system\s*prompt",
205
+ r"(?i)you\s+are\s+now",
206
+ r"(?i)\bDAN\s+mode\b",
207
+ r"(?i)developer\s+mode\b",
208
+ r"(?i)(?:enable|enter|activate)\s+(?:the\s+)?jailbreak|jailbreak\s+mode",
209
+ r"(?i)disable\s+safety\s+guidelines",
210
+ r"(?i)bypass\s+(all\s+)?safety",
211
+ r"忽略(?:之前|以前|上面|上述)(?:的)?(?:所有)?(?:指令|提示|要求)",
212
+ r"无视(?:之前|以前|上面|上述)(?:的)?(?:所有)?(?:指令|提示|要求)",
213
+ r"你现在(?:是|成为)(?:一名)?(?:攻击者|黑客)",
214
+ r"绕过(?:所有)?安全",
215
+ r"以前の指示を無視",
216
+ ]
217
+ _CONFIG_RE = [re.compile(p) for p in _CONFIG_PATTERNS]
218
+
219
+ # 防御性写法("Never reveal the system prompt")是合规建议,不是注入(NEG-3 实证)
220
+ _SYSTEM_PROMPT_PATTERN = r"(?i)system\s*prompt"
221
+ _DEFENSIVE_RE = re.compile(
222
+ r"(?i)\b(?:never|don'?t|do not|must not|should not|refuse|avoid)\b|严禁|禁止|不要|不得"
223
+ )
224
+
225
+
226
+ def _is_defensive(rx: re.Pattern[str], line: str) -> bool:
227
+ return rx.pattern == _SYSTEM_PROMPT_PATTERN and bool(_DEFENSIVE_RE.search(line))
228
+
229
+
230
+ def detect_ai_config_injection(code: str, file: str) -> list[Finding]:
231
+ """只对 AI 指令文件路径检测(.cursor/rules、CLAUDE.md 等)。"""
232
+ findings: list[Finding] = []
233
+ hits: dict[re.Pattern, list[int]] = {}
234
+ cfg_lines = code.splitlines()
235
+ for lineno, line in enumerate(cfg_lines, start=1):
236
+ window = line if lineno >= len(cfg_lines) else f"{line} {cfg_lines[lineno]}"
237
+ for rx in _CONFIG_RE:
238
+ if rx.search(window) and not _is_defensive(rx, line):
239
+ hits.setdefault(rx, []).append(lineno)
240
+ for rx, lines in hits.items():
241
+ match = rx.search(code.splitlines()[lines[0] - 1])
242
+ evidence = match.group(0) if match else "config injection pattern"
243
+ findings.append(Finding(
244
+ pitax_code="PIT-T-46",
245
+ file=file,
246
+ line=lines[0],
247
+ evidence=evidence[:120],
248
+ message=(
249
+ f"AI 指令文件中检测到指令覆盖/角色劫持模式({evidence[:60]},"
250
+ f"共 {len(lines)} 行出现)。攻击者可能在仓库的 AI 助手配置中植入后门"
251
+ "(同 CVE-2025-53773 攻击模式)。"
252
+ ),
253
+ extra={"occurrence_lines": lines[:20]},
254
+ ))
255
+ return findings
256
+
257
+
258
+ # ────────────────── PIT-N-06: 项目文档投毒 ──────────────────
259
+ DOC_EXTENSIONS = {".md", ".mdc", ".markdown", ".rst", ".txt"}
260
+
261
+
262
+ def is_doc_path(path: str) -> bool:
263
+ p = path.lower().replace("\\", "/")
264
+ if is_ai_config_path(p): # AI 指令文件归 PIT-T-46 管
265
+ return False
266
+ import os
267
+ return os.path.splitext(p)[1] in DOC_EXTENSIONS
268
+
269
+
270
+ # 与注释注入同一组高置信度模式,另加 disable/bypass safety(文档语境常见)
271
+ _DOC_INJECT_RE = [re.compile(p) for p in _CONFIG_PATTERNS]
272
+
273
+
274
+ def _quote_context(line: str, in_fence: bool) -> str:
275
+ """引用语境标注:围栏代码块 / 引用块 / 行内代码。检出不变,只给研判提示。"""
276
+ if in_fence:
277
+ return "fenced"
278
+ stripped = line.lstrip()
279
+ if stripped.startswith(">"):
280
+ return "blockquote"
281
+ if line.count("`") >= 2:
282
+ return "inline-code"
283
+ return ""
284
+
285
+
286
+ def detect_doc_injection(code: str, file: str) -> list[Finding]:
287
+ findings: list[Finding] = []
288
+ hits: dict[re.Pattern, list[int]] = {}
289
+ contexts: dict[int, str] = {}
290
+ doc_lines = code.splitlines()
291
+ in_fence = False
292
+ for lineno, line in enumerate(doc_lines, start=1):
293
+ stripped = line.lstrip()
294
+ if stripped.startswith("```") or stripped.startswith("~~~"):
295
+ in_fence = not in_fence
296
+ continue
297
+ context = _quote_context(line, in_fence)
298
+ window = line if lineno >= len(doc_lines) else f"{line} {doc_lines[lineno]}"
299
+ for rx in _DOC_INJECT_RE:
300
+ if rx.search(window) and not _is_defensive(rx, line):
301
+ hits.setdefault(rx, []).append(lineno)
302
+ contexts[lineno] = context
303
+ for rx, lines in hits.items():
304
+ match = rx.search(code.splitlines()[lines[0] - 1])
305
+ evidence = match.group(0) if match else "doc injection pattern"
306
+ findings.append(Finding(
307
+ pitax_code="PIT-N-06",
308
+ file=file,
309
+ line=lines[0],
310
+ evidence=evidence[:120],
311
+ message=(
312
+ f"项目文档中检测到面向 AI Agent 的指令注入模式({evidence[:60]},"
313
+ f"共 {len(lines)} 行出现)。文档是 Agent 的常规输入,投毒不会被"
314
+ "人类评审与传统工具注意。"
315
+ ),
316
+ extra={
317
+ "occurrence_lines": lines[:20],
318
+ "context": contexts.get(lines[0], ""),
319
+ },
320
+ ))
321
+ return findings
322
+
323
+
324
+ # ────────────────── PIT-T-51: 注释指令覆盖 ──────────────────
325
+ _COMMENT_INJECT_PATTERNS = [
326
+ r"(?i)\bignore\s+(all\s+)?previous\s+instructions\b",
327
+ r"(?i)\breveal\s+the\s+system\s+prompt\b",
328
+ r"(?i)\breveal\s+your\s+(system\s+)?prompt\b",
329
+ r"(?i)\bdisregard\s+(all\s+)?(previous\s+)?instructions\b",
330
+ r"(?i)\byou\s+are\s+now\s+(an?\s+)?(attacker|hacker|assistant)\b",
331
+ r"(?i)\byou\s+are\s+an?\s+(attacker|hacker|assistant)\b",
332
+ r"(?i)\bdeveloper\s+mode\b",
333
+ r"(?i)\bDAN\s+mode\b",
334
+ r"忽略(?:之前|以前|上面|上述)(?:的)?(?:所有)?(?:指令|提示|要求)",
335
+ r"无视(?:之前|以前|上面|上述)(?:的)?(?:所有)?(?:指令|提示|要求)",
336
+ r"你现在(?:是|成为)(?:一名)?(?:攻击者|黑客)",
337
+ r"绕过(?:所有)?安全",
338
+ r"以前の指示を無視",
339
+ ]
340
+ _COMMENT_INJECT_RE = [re.compile(p) for p in _COMMENT_INJECT_PATTERNS]
341
+
342
+ # 注释标记按扩展名取:字符串里的 % / ; / -- 不是注释起点
343
+ # (2026-10-30 PITAX 基准 NEG-4 实证:`"100% ignore previous instructions"` 曾误报 PIT-T-51)
344
+ _COMMENT_MARKERS: dict[str, tuple[str, ...]] = {
345
+ ".py": ("#",), ".sh": ("#",), ".bash": ("#",), ".rb": ("#",), ".r": ("#",),
346
+ ".yaml": ("#",), ".yml": ("#",), ".toml": ("#",), ".cfg": ("#",),
347
+ ".ini": ("#", ";"),
348
+ ".js": ("//", "/*"), ".ts": ("//", "/*"), ".jsx": ("//", "/*"), ".tsx": ("//", "/*"),
349
+ ".java": ("//", "/*"), ".go": ("//", "/*"), ".rs": ("//", "/*"),
350
+ ".c": ("//", "/*"), ".cpp": ("//", "/*"), ".h": ("//", "/*"),
351
+ ".php": ("//", "/*", "#"), ".vue": ("//", "/*", "<!--"),
352
+ ".sql": ("--", "/*"), ".lua": ("--",),
353
+ ".html": ("<!--",), ".htm": ("<!--",),
354
+ }
355
+ _DEFAULT_COMMENT_MARKERS = ("#", "//", "/*", "<!--", "*")
356
+
357
+
358
+ def _extract_comment(line: str, path: str = "") -> str | None:
359
+ """按扩展名取注释标记,返回最早出现的注释片段。"""
360
+ ext = os.path.splitext(path.lower())[1]
361
+ markers = _COMMENT_MARKERS.get(ext, _DEFAULT_COMMENT_MARKERS)
362
+ best = -1
363
+ for marker in markers:
364
+ idx = line.find(marker)
365
+ if idx >= 0 and (best < 0 or idx < best):
366
+ best = idx
367
+ return line[best:] if best >= 0 else None
368
+
369
+
370
+ def detect_comment_injection(code: str, file: str) -> list[Finding]:
371
+ findings: list[Finding] = []
372
+ hits: dict[re.Pattern, list[int]] = {}
373
+ cm_lines = code.splitlines()
374
+ for lineno, line in enumerate(cm_lines, start=1):
375
+ comment = _extract_comment(line, file)
376
+ if comment is None:
377
+ continue
378
+ nxt = _extract_comment(cm_lines[lineno], file) if lineno < len(cm_lines) else None
379
+ # 去掉注释标记再拼接:跨行拆分常写成 "# ignore previous" / "# instructions"
380
+ body = comment.lstrip("#/*<!-> ")
381
+ window = body if not nxt else f"{body} {nxt.lstrip('#/*<!-> ')}"
382
+ for rx in _COMMENT_INJECT_RE:
383
+ if rx.search(window):
384
+ hits.setdefault(rx, []).append(lineno)
385
+ for rx, lines in hits.items():
386
+ match = rx.search(code.splitlines()[lines[0] - 1])
387
+ evidence = match.group(0) if match else "comment injection pattern"
388
+ findings.append(Finding(
389
+ pitax_code="PIT-T-51",
390
+ file=file,
391
+ line=lines[0],
392
+ evidence=evidence[:120],
393
+ message=(
394
+ f"代码注释中检测到提示注入指令模式({evidence[:60]})。"
395
+ "当 LLM 阅读该代码/注释时可能被劫持,执行非预期指令。"
396
+ ),
397
+ extra={"occurrence_lines": lines[:20]},
398
+ ))
399
+ return findings
400
+
401
+
402
+ # ────────────────── PIT-E-07/14/36/57: 编码载荷 ──────────────────
403
+ _ENCODED_TARGET_PATTERNS = [
404
+ r"(?i)\bignore\s+(all\s+)?previous\s+instructions\b",
405
+ r"(?i)\breveal\s+(the\s+)?(system|your)\s+prompt\b",
406
+ r"(?i)\bemail\s+(the\s+)?api\s+keys?\b",
407
+ r"(?i)\bexfiltrate\b",
408
+ r"(?i)\bdeveloper\s+mode\b",
409
+ r"(?i)\byou\s+are\s+now\b",
410
+ r"(?i)\bjailbreak\b",
411
+ r"你现在(?:是|成为)(?:一名)?(?:攻击者|黑客)",
412
+ r"绕过(?:所有)?安全",
413
+ r"以前の指示を無視",
414
+ r"忽略(?:之前|以前|上述)(?:的)?(?:所有)?指令",
415
+ ]
416
+ _ENCODED_TARGET_RE = [re.compile(p) for p in _ENCODED_TARGET_PATTERNS]
417
+
418
+ # 字符串字面量候选:引号包裹的 20+ 字符串(不含引号/换行)。
419
+ # 不再限制字符集——编码判定交给分层解码器(b64 严格校验/rot13/reverse 命中关键词才剥层),
420
+ # 否则带空格的 ROT13/反转载荷会漏检。
421
+ _STR_LITERAL = re.compile(r'(["\'`])([^"\'`\n]{20,})\1')
422
+ _B64_STRICT = re.compile(r"[A-Za-z0-9+/]+={0,2}")
423
+ _MIN_DECODE_LEN = 16
424
+ _MAX_LAYERS = 5
425
+
426
+ _LAYER_TO_CODE = {
427
+ ("base64",): "PIT-E-07",
428
+ ("rot13",): "PIT-E-14",
429
+ ("reverse",): "PIT-E-36",
430
+ }
431
+
432
+
433
+ def _contains_injection(s: str) -> bool:
434
+ return any(rx.search(s) for rx in _ENCODED_TARGET_RE)
435
+
436
+
437
+ def _try_base64(s: str) -> str | None:
438
+ """严格 base64 解码:字符集/长度合法、严格 UTF-8、≥90% 可打印。
439
+ 二进制 blob(图片/哈希/JWT 除外)在此被过滤,控制误报。"""
440
+ t = s.strip()
441
+ if len(t.rstrip("=")) < _MIN_DECODE_LEN:
442
+ return None
443
+ if len(t.rstrip("=")) % 4 == 1 or not _B64_STRICT.fullmatch(t):
444
+ return None
445
+ try:
446
+ decoded = base64.b64decode(t, validate=True).decode("utf-8")
447
+ except Exception:
448
+ return None
449
+ if not decoded:
450
+ return None
451
+ printable = sum(ch.isprintable() or ch in "\r\n\t" for ch in decoded)
452
+ if printable / len(decoded) < 0.9:
453
+ return None
454
+ return decoded
455
+
456
+
457
+ def _peel_layers(raw: str) -> tuple[str, list[str]]:
458
+ """逐层剥离 base64/rot13/reverse,返回 (最终文本, 层序列表)。
459
+ rot13/reverse 只有在解码结果命中注入关键词时才继续剥(控制误报)。"""
460
+ current, layers = raw, []
461
+ for _ in range(_MAX_LAYERS):
462
+ decoded = _try_base64(current)
463
+ if decoded is not None and decoded != current:
464
+ current = decoded
465
+ layers.append("base64")
466
+ continue
467
+ if any(c.isalpha() for c in current):
468
+ rot = codecs.decode(current, "rot_13")
469
+ if rot != current and _contains_injection(rot):
470
+ current = rot
471
+ layers.append("rot13")
472
+ continue
473
+ rev = current[::-1]
474
+ if rev != current and _contains_injection(rev):
475
+ current = rev
476
+ layers.append("reverse")
477
+ continue
478
+ break
479
+ return current, layers
480
+
481
+
482
+ def detect_encoded_payloads(code: str, file: str) -> list[Finding]:
483
+ findings: list[Finding] = []
484
+ for lineno, line in enumerate(code.splitlines(), start=1):
485
+ for m in _STR_LITERAL.finditer(line):
486
+ raw = m.group(2)
487
+ final, layers = _peel_layers(raw)
488
+ if not layers or not _contains_injection(final):
489
+ continue # 纯明文/未命中关键词 → 不报(控制误报)
490
+ if len(layers) >= 2:
491
+ pitax_code = "PIT-E-57"
492
+ else:
493
+ pitax_code = _LAYER_TO_CODE.get(tuple(layers), "PIT-E-57")
494
+ findings.append(Finding(
495
+ pitax_code=pitax_code,
496
+ file=file,
497
+ line=lineno,
498
+ evidence=raw[:80] + ("..." if len(raw) > 80 else ""),
499
+ message=(
500
+ f"检测到{len(layers)}层编码({'→'.join(layers)})的提示注入载荷,"
501
+ "用于规避基于关键词的过滤。"
502
+ ),
503
+ decoded_payload=final[:160],
504
+ extra={"layers": layers},
505
+ ))
506
+ return findings