Initialer Import des AssetManagers
This commit is contained in:
Executable
+178
@@ -0,0 +1,178 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Fail when a source tree contains files unsuitable for public release."""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import ast
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
TEXT_SUFFIXES = {
|
||||
".css", ".env", ".html", ".ini", ".js", ".json", ".md", ".py",
|
||||
".sh", ".toml", ".txt", ".yaml", ".yml", ".ps1", ".cmd", ".bat",
|
||||
}
|
||||
SKIP_DIRS = {".git", ".venv", "venv", "__pycache__", "dist", "THIRD_PARTY_LICENSES"}
|
||||
ALLOWED_RUNTIME_MARKER = ".gitkeep"
|
||||
FORBIDDEN_FILES = {
|
||||
".env",
|
||||
"data/config/config.json",
|
||||
"data/config/APPINFO.json",
|
||||
"app/main_old.py",
|
||||
"PRUEFEN-0.5.5.5.sh",
|
||||
}
|
||||
FORBIDDEN_RUNTIME_DIRS = {
|
||||
"data/postgres", "data/uploads", "data/logs", "data/backups",
|
||||
"data/backup", "data/scripts", "app/static/uploads",
|
||||
}
|
||||
# Comparison language is blocked independently of a specific competitor name.
|
||||
# Real integrations should be described factually instead of "similar to ...".
|
||||
COMPARISON_PATTERNS = {
|
||||
"similar-to comparison": re.compile(r"\b(?:ähnlich(?:e|er|es|en|em)?|similar to)\b", re.IGNORECASE),
|
||||
"modeled-after comparison": re.compile(r"\b(?:nach (?:dem )?Vorbild|modelled after|modeled after)\b", re.IGNORECASE),
|
||||
"inspired-by comparison": re.compile(r"\b(?:inspiriert von|inspired by)\b", re.IGNORECASE),
|
||||
"like-in comparison": re.compile(r"\b(?:wie bei|as in)\b", re.IGNORECASE),
|
||||
"based-on comparison": re.compile(r"\b(?:angelehnt an|based on the UI of)\b", re.IGNORECASE),
|
||||
}
|
||||
PRIVATE_DOMAIN_PATTERN = re.compile(r"(?:https?://|wss?://|ldaps?://|@)[^\s/\"'<>]+\.(?:local|lan|internal)\b", re.IGNORECASE)
|
||||
IPV4_PATTERN = re.compile(r"(?<!\d)(?:\d{1,3}\.){3}\d{1,3}(?!\d)")
|
||||
PRIVATE_KEY_PATTERN = re.compile(r"-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----")
|
||||
IGNORE_PRODUCT_SCAN = {
|
||||
"tools/check_public_release.py",
|
||||
"THIRD_PARTY_NOTICES.md",
|
||||
"LICENSE.txt",
|
||||
}
|
||||
|
||||
|
||||
def rel(path: Path, root: Path) -> str:
|
||||
return path.relative_to(root).as_posix()
|
||||
|
||||
|
||||
def public_files(root: Path):
|
||||
for path in root.rglob("*"):
|
||||
if not path.is_file():
|
||||
continue
|
||||
relative = path.relative_to(root)
|
||||
if any(part in SKIP_DIRS for part in relative.parts):
|
||||
continue
|
||||
yield path
|
||||
|
||||
|
||||
def read_text(path: Path) -> str | None:
|
||||
if path.suffix.lower() not in TEXT_SUFFIXES and path.name not in {"VERSION", ".gitignore", ".dockerignore", ".gitattributes"}:
|
||||
return None
|
||||
try:
|
||||
return path.read_text(encoding="utf-8")
|
||||
except UnicodeDecodeError:
|
||||
return None
|
||||
|
||||
|
||||
def version_from_python(path: Path) -> str:
|
||||
tree = ast.parse(path.read_text(encoding="utf-8"), filename=str(path))
|
||||
for node in tree.body:
|
||||
if isinstance(node, ast.Assign):
|
||||
for target in node.targets:
|
||||
if isinstance(target, ast.Name) and target.id == "APP_VERSION" and isinstance(node.value, ast.Constant) and isinstance(node.value.value, str):
|
||||
return node.value.value
|
||||
return ""
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--root", type=Path, default=Path(__file__).resolve().parents[1])
|
||||
parser.add_argument("--denylist", type=Path, help="Optional local file with one private literal per line")
|
||||
args = parser.parse_args()
|
||||
root = args.root.resolve()
|
||||
errors: list[str] = []
|
||||
local_literals: list[str] = []
|
||||
if args.denylist and args.denylist.is_file():
|
||||
local_literals = [
|
||||
line.strip().casefold()
|
||||
for line in args.denylist.read_text(encoding="utf-8").splitlines()
|
||||
if line.strip() and not line.lstrip().startswith("#")
|
||||
]
|
||||
|
||||
for name in FORBIDDEN_FILES:
|
||||
if (root / name).is_file():
|
||||
errors.append(f"forbidden public file: {name}")
|
||||
|
||||
for directory in FORBIDDEN_RUNTIME_DIRS:
|
||||
path = root / directory
|
||||
if not path.exists():
|
||||
continue
|
||||
for item in path.rglob("*"):
|
||||
if item.is_file() and item.name != ALLOWED_RUNTIME_MARKER:
|
||||
errors.append(f"runtime/user data must not be public: {rel(item, root)}")
|
||||
|
||||
for path in public_files(root):
|
||||
relative = rel(path, root)
|
||||
lower = path.name.lower()
|
||||
if lower.endswith((".db", ".sqlite", ".sqlite3")):
|
||||
errors.append(f"database file must not be public: {relative}")
|
||||
text = read_text(path)
|
||||
if text is None:
|
||||
continue
|
||||
if PRIVATE_KEY_PATTERN.search(text):
|
||||
errors.append(f"private key material found: {relative}")
|
||||
if PRIVATE_DOMAIN_PATTERN.search(text):
|
||||
errors.append(f"private DNS suffix found in {relative}")
|
||||
for candidate in IPV4_PATTERN.findall(text):
|
||||
try:
|
||||
octets = [int(part) for part in candidate.split(".")]
|
||||
except ValueError:
|
||||
continue
|
||||
if any(part > 255 for part in octets):
|
||||
continue
|
||||
is_rfc1918 = (
|
||||
octets[0] == 10
|
||||
or (octets[0] == 172 and 16 <= octets[1] <= 31)
|
||||
or (octets[0] == 192 and octets[1] == 168)
|
||||
)
|
||||
if is_rfc1918:
|
||||
errors.append(f"private IPv4 address found in {relative}")
|
||||
lowered = text.casefold()
|
||||
for literal in local_literals:
|
||||
if literal in lowered:
|
||||
errors.append(f"local denylist value found in {relative}")
|
||||
if relative not in IGNORE_PRODUCT_SCAN:
|
||||
for label, pattern in COMPARISON_PATTERNS.items():
|
||||
if pattern.search(text):
|
||||
errors.append(f"{label} found in {relative}")
|
||||
|
||||
license_path = root / "LICENSE.txt"
|
||||
if not license_path.is_file() or "Apache License" not in license_path.read_text(encoding="utf-8", errors="replace") or "Version 2.0, January 2004" not in license_path.read_text(encoding="utf-8", errors="replace"):
|
||||
errors.append("LICENSE.txt is not the complete Apache License 2.0 text")
|
||||
|
||||
for appinfo_name in ("APPINFO.json", "APPINFO.example.json"):
|
||||
path = root / appinfo_name
|
||||
try:
|
||||
info = json.loads(path.read_text(encoding="utf-8"))
|
||||
except (OSError, json.JSONDecodeError) as exc:
|
||||
errors.append(f"cannot read {appinfo_name}: {exc}")
|
||||
continue
|
||||
if info.get("license") != "Apache-2.0":
|
||||
errors.append(f"{appinfo_name} must use SPDX identifier Apache-2.0")
|
||||
|
||||
try:
|
||||
version = (root / "VERSION").read_text(encoding="utf-8").strip()
|
||||
py_version = version_from_python(root / "app/version.py")
|
||||
readme = (root / "README.md").read_text(encoding="utf-8")
|
||||
if not version or version != py_version:
|
||||
errors.append(f"version mismatch: VERSION={version!r}, app/version.py={py_version!r}")
|
||||
if version and version not in readme:
|
||||
errors.append(f"README.md does not contain current version {version}")
|
||||
except OSError as exc:
|
||||
errors.append(f"version check failed: {exc}")
|
||||
|
||||
if errors:
|
||||
print("Public release check failed:")
|
||||
for item in sorted(set(errors)):
|
||||
print(f" - {item}")
|
||||
return 1
|
||||
print(f"Public release check passed for {root}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user