Files
niro-gen5w/tools/check_publication.py

88 lines
3.9 KiB
Python

#!/usr/bin/env python3
"""Small publication gate, not a comprehensive security or licensing audit."""
import os
from pathlib import Path
import re
import subprocess
ROOT = Path(__file__).resolve().parents[1]
ALLOWED = {
".gitattributes", ".gitignore", "README.md", "RIGHTS.md", "LICENSE",
"SECURITY.md", "docs/ARCHITECTURE_KO.md", "docs/FEATURES_KO.md",
"docs/RESEARCH_KO.md", "docs/PUBLICATION_KO.md",
"telemetry/java/com/chaegeon/niro/HscanRpmDecoder.java",
"weather/java/com/chaegeon/niro/WeatherRefreshGate.java",
"weather/java/com/chaegeon/niro/WeatherRefreshCounters.java",
"backend/php/NiroMqttKeepalive.php", "backend/php/NiroRawSnappy.php",
"navigation/archived/v038/patch_nav_vehicle_speed_source.py",
"tests/java/HscanRpmDecoderTest.java", "tests/java/WeatherRefreshGateTest.java",
"tests/java/WeatherRefreshCountersTest.java", "tests/php/NiroMqttKeepaliveTest.php",
"tests/php/NiroRawSnappyTest.php", "tests/python/test_archived_speed_patch.py",
"tools/run_tests.py", "tools/check_publication.py",
}
PATTERNS = {
"private-key material": re.compile(rb"-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----"),
"GitHub credential candidate": re.compile(rb"\b(?:ghp_[A-Za-z0-9]{30,}|github_pat_[A-Za-z0-9_]{50,})\b"),
"literal bearer candidate": re.compile(rb"Bearer [A-Za-z0-9_./+~-]{24,}"),
"phone-number candidate": re.compile(rb"(?<![A-Za-z0-9])01[016789](?:[ -]?[0-9]){7,8}(?![A-Za-z0-9])"),
"private operational hostname": re.compile(rb"niro-gen5w\.tail[0-9a-z]+\.ts\.net|car\.chaegeon\.com"),
}
def git(*arguments):
# Use the executable, not a shell function or a machine-specific wrapper.
return subprocess.run(
["git", *arguments], cwd=ROOT, check=True, stdout=subprocess.PIPE,
stderr=subprocess.PIPE, encoding="utf-8",
).stdout
def main():
errors = []
files = []
for directory, directories, names in os.walk(ROOT, followlinks=False):
base = Path(directory)
for name in list(directories):
target = base / name
relative = target.relative_to(ROOT).as_posix()
if relative == ".git":
directories.remove(name)
elif target.is_symlink():
errors.append("symlink directory: " + relative)
directories.remove(name)
for name in names:
path = base / name
relative = path.relative_to(ROOT).as_posix()
files.append(relative)
if path.is_symlink() or relative not in ALLOWED:
errors.append("file is not approved: " + relative)
continue
content = path.read_bytes()
if len(content) > 128 * 1024 or b"\0" in content:
errors.append("binary or unexpectedly large file: " + relative)
continue
try:
content.decode("utf-8")
except UnicodeDecodeError:
errors.append("non-UTF-8 file: " + relative)
for description, pattern in PATTERNS.items():
if pattern.search(content):
# Do not reveal a matched secret or personal value in output.
errors.append(description + ": " + relative)
if (ROOT / ".git").exists():
for relative in git("ls-files").splitlines():
if relative not in ALLOWED:
errors.append("unapproved tracked file: " + relative)
for email in set(git("log", "--all", "--format=%ae%n%ce").splitlines()):
if email and not email.endswith("@users.noreply.git.chaegeon.com"):
errors.append("commit author/committer email needs privacy review")
if errors:
raise SystemExit("Publication checks failed:\n" + "\n".join(sorted(set(errors))))
print("PASS: publication allowlist and basic privacy checks (" + str(len(files)) + " files)")
if __name__ == "__main__":
main()