Publish sanitized current homelab architecture
Some checks failed
public-safety / privacy-and-secret-scan (push) Has been cancelled

This commit is contained in:
fizzlepoof
2026-09-08 21:47:41 +00:00
commit cc35523888
13 changed files with 901 additions and 0 deletions

162
scripts/check-public-safety.py Executable file
View File

@@ -0,0 +1,162 @@
#!/usr/bin/env python3
"""Fail when tracked content is unsafe for a public showcase."""
from __future__ import annotations
import math
import re
import subprocess
import sys
from collections import Counter
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
SELF = "scripts/check-public-safety.py"
PLACEHOLDER_WORDS = (
"change_me",
"redacted",
"placeholder",
"example",
"your_token",
"your_key",
"***",
)
ANY_IPV4 = re.compile(r"(?<![\d.])(?:\d{1,3}\.){3}\d{1,3}(?![\d.])")
ANY_IPV6 = re.compile(r"(?i)(?<![0-9a-f:])(?:[0-9a-f]{0,4}:){2,8}[0-9a-f]{0,4}(?![0-9a-f:])")
MAC = re.compile(r"(?i)\b(?:[0-9a-f]{2}[:-]){5}[0-9a-f]{2}\b")
UUID = re.compile(r"(?i)\b[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}\b")
INTERNAL_DOMAIN = re.compile(r"(?i)\b[a-z0-9.-]+\.(?:local|lan|internal|home|private)\b")
PRIVATE_HOME = re.compile(r"(?i)/home/(?!user\b|example\b|operator\b)[a-z0-9._-]+\b")
PRIVATE_MOUNT = re.compile(r"(?i)/mnt/(?!storage\b|media\b|backups\b|example\b)[a-z0-9._-]+\b")
PRIVATE_OPT = re.compile(r"(?i)/opt/(?!example\b|application\b)[a-z0-9._-]+\b")
PRIVATE_KEY = re.compile(r"-----BEGIN [A-Z0-9 ]*PRIVATE KEY-----", re.I)
EMAIL = re.compile(r"(?i)\b[A-Z0-9._%+-]+@([A-Z0-9.-]+\.[A-Z]{2,})\b")
SECRET_ASSIGNMENT = re.compile(
r"(?i)\b(?:api[_-]?(?:key|token)|access[_-]?token|auth[_-]?token|token|password|passwd|"
r"client[_-]?secret|cookie|private[_-]?key)\b\s*[:=]\s*[\"']?([^\s\"',}]+)"
)
AUTHORIZATION = re.compile(
r"(?i)authorization\s*[:=]\s*(?:bearer|token|basic)\s+([^\s\"']+)"
)
OPAQUE = re.compile(r"\b[A-Za-z0-9_+/=-]{32,}\b")
RISKY_SUFFIXES = {
".pem", ".key", ".p12", ".pfx", ".kdbx", ".ovpn", ".mobileconfig"
}
RISKY_NAMES = {
".env", "id_rsa", "id_ed25519", "id_ecdsa", "id_dsa", "wg0.conf"
}
def tracked_files() -> list[Path]:
result = subprocess.run(
["git", "-C", str(ROOT), "ls-files", "-z"],
check=True,
capture_output=True,
)
return [ROOT / value.decode() for value in result.stdout.split(b"\0") if value]
def is_placeholder(value: str) -> bool:
lowered = value.lower()
return value.startswith("${") or any(word in lowered for word in PLACEHOLDER_WORDS)
def entropy(value: str) -> float:
counts = Counter(value)
total = len(value)
return -sum((count / total) * math.log2(count / total) for count in counts.values())
def main() -> int:
findings: set[str] = set()
files = tracked_files()
for path in files:
relative = path.relative_to(ROOT).as_posix()
if relative == SELF:
continue
name = path.name.lower()
if name.startswith(".env") and name != ".env.example":
findings.add(f"{relative}: prohibited environment filename")
if name in RISKY_NAMES or re.fullmatch(r"id_[a-z0-9_-]+", name):
findings.add(f"{relative}: prohibited credential filename")
if path.suffix.lower() in RISKY_SUFFIXES:
findings.add(f"{relative}: prohibited key/config suffix")
if re.fullmatch(r"wg\d+\.conf", name):
findings.add(f"{relative}: prohibited VPN configuration filename")
try:
data = path.read_bytes()
except OSError as exc:
findings.add(f"{relative}: unreadable: {exc}")
continue
if b"\0" in data[:4096]:
continue
for line_number, line in enumerate(data.decode("utf-8", errors="ignore").splitlines(), 1):
prefix = f"{relative}:{line_number}"
if ANY_IPV4.search(line):
findings.add(f"{prefix}: literal IPv4 address")
if ANY_IPV6.search(line):
findings.add(f"{prefix}: literal IPv6 address")
if MAC.search(line):
findings.add(f"{prefix}: MAC address")
if UUID.search(line):
findings.add(f"{prefix}: UUID/device identifier")
if INTERNAL_DOMAIN.search(line):
findings.add(f"{prefix}: internal domain")
if PRIVATE_HOME.search(line):
findings.add(f"{prefix}: private home path")
if PRIVATE_MOUNT.search(line):
findings.add(f"{prefix}: private mount path")
if PRIVATE_OPT.search(line):
findings.add(f"{prefix}: private application path")
if PRIVATE_KEY.search(line):
findings.add(f"{prefix}: private-key block")
for match in EMAIL.finditer(line):
domain = match.group(1).lower()
if not domain.endswith(("example.com", "example.net", "example.org")):
findings.add(f"{prefix}: non-example email address")
for match in SECRET_ASSIGNMENT.finditer(line):
value = match.group(1)
if not is_placeholder(value):
findings.add(f"{prefix}: non-placeholder secret assignment")
for match in AUTHORIZATION.finditer(line):
value = match.group(1)
if not is_placeholder(value):
findings.add(f"{prefix}: authorization material")
for candidate in OPAQUE.findall(line):
if is_placeholder(candidate):
continue
# Commit hashes and content digests are still identifiers; require a label.
labelled_hash = re.search(
r"(?i)\b(?:sha(?:1|256|512)|digest|commit|checksum|example[_-]?hash)\b",
line,
)
if labelled_hash and re.fullmatch(r"[0-9a-fA-F]{32,128}", candidate):
continue
if entropy(candidate) >= 3.5:
findings.add(f"{prefix}: opaque high-entropy value")
break
if findings:
print("Public-safety scan failed:")
for finding in sorted(findings):
print(f"- {finding}")
return 1
print(f"Public-safety scan passed: {len(files)} tracked files checked")
return 0
if __name__ == "__main__":
sys.exit(main())