初始化奇妙小屏幕控制器项目
This commit is contained in:
@@ -0,0 +1,284 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import ipaddress
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path, PurePosixPath
|
||||
|
||||
|
||||
SCRIPT_PATH = Path(__file__).resolve()
|
||||
WORKSPACE_ROOT = SCRIPT_PATH.parents[2]
|
||||
PRIVATE_CREDENTIAL = PurePosixPath("测试相关资料/核桃派的用户名和密码和ip/用户名密码ip.txt")
|
||||
EXAMPLE_CREDENTIAL = PurePosixPath(
|
||||
"测试相关资料/核桃派的用户名和密码和ip/用户名密码ip.example.txt"
|
||||
)
|
||||
BINARY_EXTENSIONS = {
|
||||
".deb",
|
||||
".dll",
|
||||
".docx",
|
||||
".exe",
|
||||
".gif",
|
||||
".gz",
|
||||
".img",
|
||||
".jpeg",
|
||||
".jpg",
|
||||
".otf",
|
||||
".ota",
|
||||
".pdf",
|
||||
".png",
|
||||
".rar",
|
||||
".so",
|
||||
".ttf",
|
||||
".whl",
|
||||
".woff",
|
||||
".woff2",
|
||||
".zip",
|
||||
}
|
||||
PRIVATE_IP_ALLOWED_PREFIXES = (
|
||||
"整体开发需求/",
|
||||
"测试相关资料/如何测试/",
|
||||
"核桃派软件源代码/",
|
||||
"发布更新相关/其他依赖/",
|
||||
)
|
||||
SAFE_SECRET_VALUES = {
|
||||
"changeme",
|
||||
"example",
|
||||
"example123",
|
||||
"fake",
|
||||
"fake-secret",
|
||||
"password",
|
||||
"secret123",
|
||||
"test",
|
||||
"test-password",
|
||||
}
|
||||
# The password is a high-confidence private marker. Addresses, usernames and
|
||||
# hostnames are checked structurally because common fixture values also occur in
|
||||
# provisioning source and tests.
|
||||
CURRENT_DEVICE_KEYS = ("密码",)
|
||||
PUBLIC_IMAGE_KEYS = (
|
||||
"镜像默认用户名",
|
||||
"镜像默认账户密码",
|
||||
"镜像默认WiFi SSID",
|
||||
"镜像默认WiFi密码",
|
||||
)
|
||||
|
||||
|
||||
class HygieneError(RuntimeError):
|
||||
"""Repository state cannot be audited safely."""
|
||||
|
||||
|
||||
def _git(*arguments: str, check: bool = True) -> subprocess.CompletedProcess[bytes]:
|
||||
return subprocess.run(
|
||||
("git", *arguments),
|
||||
cwd=WORKSPACE_ROOT,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
check=check,
|
||||
)
|
||||
|
||||
|
||||
def _decode_paths(payload: bytes) -> list[PurePosixPath]:
|
||||
return [
|
||||
PurePosixPath(item.decode("utf-8", errors="surrogateescape"))
|
||||
for item in payload.split(b"\0")
|
||||
if item
|
||||
]
|
||||
|
||||
|
||||
def candidate_paths(staged: bool) -> list[PurePosixPath]:
|
||||
if staged:
|
||||
result = _git("diff", "--cached", "--name-only", "--diff-filter=ACMR", "-z")
|
||||
return _decode_paths(result.stdout)
|
||||
tracked = _decode_paths(_git("ls-files", "-z").stdout)
|
||||
untracked = _decode_paths(_git("ls-files", "--others", "--exclude-standard", "-z").stdout)
|
||||
return sorted(set((*tracked, *untracked)), key=str)
|
||||
|
||||
|
||||
def _is_binary(path: Path) -> bool:
|
||||
if path.suffix.casefold() in BINARY_EXTENSIONS:
|
||||
return True
|
||||
try:
|
||||
return b"\0" in path.read_bytes()[:8192]
|
||||
except OSError as error:
|
||||
raise HygieneError(f"无法读取候选文件:{path.relative_to(WORKSPACE_ROOT)}") from error
|
||||
|
||||
|
||||
def _host_text_patterns() -> list[re.Pattern[str]]:
|
||||
windows_prefix = r"[A-Za-z]:[\\/]" + r"(?:Users|Documents and Settings)[\\/]"
|
||||
mac_prefix = r"/" + r"Users/[A-Za-z0-9._-]+/"
|
||||
linux_home = r"/" + r"home/[A-Za-z0-9._-]+/"
|
||||
patterns = [re.compile(windows_prefix, re.IGNORECASE), re.compile(mac_prefix), re.compile(linux_home)]
|
||||
for value in (Path.home().name, os.environ.get("COMPUTERNAME", "")):
|
||||
if value and len(value) >= 4:
|
||||
patterns.append(re.compile(re.escape(value), re.IGNORECASE))
|
||||
return patterns
|
||||
|
||||
|
||||
def _private_value_markers() -> tuple[str, ...]:
|
||||
path = WORKSPACE_ROOT.joinpath(*PRIVATE_CREDENTIAL.parts)
|
||||
if not path.is_file():
|
||||
return ()
|
||||
fields: dict[str, str] = {}
|
||||
for raw_line in path.read_text(encoding="utf-8").splitlines():
|
||||
line = raw_line.strip()
|
||||
if not line or line.startswith("#"):
|
||||
continue
|
||||
separator = ":" if ":" in line else ":" if ":" in line else None
|
||||
if separator is None:
|
||||
continue
|
||||
key, value = (part.strip() for part in line.split(separator, 1))
|
||||
fields[key] = value
|
||||
public_values = {fields.get(key, "") for key in PUBLIC_IMAGE_KEYS}
|
||||
return tuple(
|
||||
value
|
||||
for key in CURRENT_DEVICE_KEYS
|
||||
if len(value := fields.get(key, "")) >= 4 and value not in public_values
|
||||
)
|
||||
|
||||
|
||||
def _binary_markers(private_values: tuple[str, ...] = ()) -> list[bytes]:
|
||||
markers: list[bytes] = []
|
||||
host_values = {
|
||||
str(Path.home()),
|
||||
str(WORKSPACE_ROOT),
|
||||
Path.home().name,
|
||||
os.environ.get("COMPUTERNAME", ""),
|
||||
}
|
||||
for value in (*host_values, *private_values):
|
||||
if value and len(value) >= 4:
|
||||
variants = {value, value.replace("\\", "/")}
|
||||
for variant in variants:
|
||||
markers.extend((variant.encode("utf-8"), variant.encode("utf-16le")))
|
||||
return markers
|
||||
|
||||
|
||||
def _binary_contains_forbidden_marker(path: Path, private_values: tuple[str, ...]) -> bool:
|
||||
markers = _binary_markers(private_values)
|
||||
overlap = max(map(len, markers)) - 1
|
||||
tail = b""
|
||||
with path.open("rb") as stream:
|
||||
while chunk := stream.read(8 * 1024 * 1024):
|
||||
sample = tail + chunk
|
||||
lowered_sample = sample.lower()
|
||||
if any(marker.lower() in lowered_sample for marker in markers):
|
||||
return True
|
||||
tail = sample[-overlap:] if overlap else b""
|
||||
return False
|
||||
|
||||
|
||||
def _text_issues(
|
||||
relative: PurePosixPath,
|
||||
text: str,
|
||||
private_values: tuple[str, ...] = (),
|
||||
) -> list[str]:
|
||||
issues: list[str] = []
|
||||
if any(pattern.search(text) for pattern in _host_text_patterns()):
|
||||
issues.append("包含开发电脑专属路径、用户名或主机名")
|
||||
if any(value in text for value in private_values):
|
||||
issues.append("包含当前设备私有凭据值")
|
||||
|
||||
key_header = re.compile("-----BEGIN " + r"(?:OPENSSH |RSA |EC |DSA )?PRIVATE KEY-----")
|
||||
if key_header.search(text):
|
||||
issues.append("包含私钥正文")
|
||||
|
||||
relative_string = relative.as_posix()
|
||||
if not (
|
||||
relative_string.startswith("核桃派软件源代码/tests/")
|
||||
or PurePosixPath(relative_string).name.startswith("test_")
|
||||
or relative == EXAMPLE_CREDENTIAL
|
||||
):
|
||||
assignment = re.compile(
|
||||
r"(?i)(?:password|passwd|token|secret|api[_-]?key|密码)\s*[:=]\s*[\"']([^\"']{4,})[\"']"
|
||||
)
|
||||
for match in assignment.finditer(text):
|
||||
if match.group(1).casefold() not in SAFE_SECRET_VALUES:
|
||||
issues.append("包含疑似硬编码秘密")
|
||||
break
|
||||
|
||||
if not (
|
||||
relative_string.startswith(PRIVATE_IP_ALLOWED_PREFIXES)
|
||||
or "/tests/" in f"/{relative_string}"
|
||||
):
|
||||
for token in re.findall(r"(?<![0-9])(?:[0-9]{1,3}\.){3}[0-9]{1,3}(?![0-9])", text):
|
||||
try:
|
||||
address = ipaddress.ip_address(token)
|
||||
except ValueError:
|
||||
continue
|
||||
if address.is_private and not address.is_loopback and not address.is_unspecified:
|
||||
issues.append("归档或普通文档包含私有 IPv4 地址")
|
||||
break
|
||||
return issues
|
||||
|
||||
|
||||
def audit_paths(paths: list[PurePosixPath], *, scan_binary: bool) -> list[tuple[str, str]]:
|
||||
findings: list[tuple[str, str]] = []
|
||||
private_values = _private_value_markers()
|
||||
for relative in paths:
|
||||
if relative == PRIVATE_CREDENTIAL:
|
||||
findings.append((relative.as_posix(), "真实凭据进入 Git 候选集合"))
|
||||
continue
|
||||
path = WORKSPACE_ROOT.joinpath(*relative.parts)
|
||||
if not path.is_file():
|
||||
continue
|
||||
if _is_binary(path):
|
||||
if scan_binary and _binary_contains_forbidden_marker(path, private_values):
|
||||
findings.append((relative.as_posix(), "二进制包含开发电脑标识、主目录路径或当前设备秘密"))
|
||||
continue
|
||||
try:
|
||||
text = path.read_text(encoding="utf-8")
|
||||
except UnicodeDecodeError:
|
||||
findings.append((relative.as_posix(), "文本候选不是有效 UTF-8"))
|
||||
continue
|
||||
findings.extend(
|
||||
(relative.as_posix(), issue) for issue in _text_issues(relative, text, private_values)
|
||||
)
|
||||
return findings
|
||||
|
||||
|
||||
def _check_repository_contract(paths: list[PurePosixPath], staged: bool) -> list[tuple[str, str]]:
|
||||
findings: list[tuple[str, str]] = []
|
||||
ignored = _git("check-ignore", "--quiet", "--", PRIVATE_CREDENTIAL.as_posix(), check=False)
|
||||
if ignored.returncode != 0:
|
||||
findings.append((PRIVATE_CREDENTIAL.as_posix(), "真实凭据未被 .gitignore 精确排除"))
|
||||
|
||||
if staged:
|
||||
example_in_index = _git("cat-file", "-e", f":{EXAMPLE_CREDENTIAL.as_posix()}", check=False)
|
||||
if example_in_index.returncode != 0:
|
||||
findings.append((EXAMPLE_CREDENTIAL.as_posix(), "示例凭据没有进入本次提交"))
|
||||
elif EXAMPLE_CREDENTIAL not in paths:
|
||||
findings.append((EXAMPLE_CREDENTIAL.as_posix(), "示例凭据不在 Git 候选集合"))
|
||||
|
||||
return findings
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description="检查 Git 候选文件中的凭据、主机路径和秘密")
|
||||
parser.add_argument("--staged", action="store_true", help="只检查已暂存的新建或修改文件")
|
||||
parser.add_argument(
|
||||
"--skip-binary-scan",
|
||||
action="store_true",
|
||||
help="跳过大体积二进制字节扫描,仅供快速诊断",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
try:
|
||||
paths = candidate_paths(args.staged)
|
||||
findings = _check_repository_contract(paths, args.staged)
|
||||
findings.extend(audit_paths(paths, scan_binary=not args.skip_binary_scan))
|
||||
except (HygieneError, OSError, subprocess.CalledProcessError) as error:
|
||||
print(f"仓库卫生检查无法完成:{error}", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
if findings:
|
||||
print("仓库卫生检查失败;以下输出只包含文件路径和问题类型:", file=sys.stderr)
|
||||
for path, issue in findings:
|
||||
print(f"- {path}: {issue}", file=sys.stderr)
|
||||
return 1
|
||||
print(f"仓库卫生检查通过:已检查 {len(paths)} 个 Git 候选文件,未输出任何秘密值。")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user