88 lines
3.9 KiB
Python
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()
|