Files
2026-10-19 22:44:51 +08:00

150 lines
6.2 KiB
Python

#!/usr/bin/env python3
"""release_check.py -- checksum, verify and scan a release directory.
python release_check.py sums <release_dir> # (re)write 00_SHA256SUMS
python release_check.py verify <release_dir> # check every listed file
python release_check.py scan <release_dir> [--env ~/skills_env.md] [--mac AA:BB:..]...
[--src-dirs 07_ps_app_src,08_imx6_app_src] [--allow KEY]...
scan reports (never prints a secret value, only the file and the key name):
* values of *PASS* / *PASSWORD* / *TOKEN* / *SECRET* / *KEY* keys in --env found
in any text file, and "user:password@" URLs; --allow KEY moves that key's hits
to "acknowledged" (e.g. a vendor factory-default account that ships in the
firmware and is also the brand name) -- still listed, no failure;
* tool state files (*.upstate.json) inside --src-dirs;
* private keys (-----BEGIN ... PRIVATE KEY-----);
* the given board MACs (--mac, e.g. a test board's) and any other MAC literal;
* development-machine absolute paths (X:\\..., /home/<user>, /Users/<user>);
* ELF / object / archive files inside --src-dirs (build output in a source tree);
* Windows reserved names (CON, PRN, AUX, NUL, COM1-9, LPT1-9) that break copies.
Exit status 0 = scan finished (read the report), 2 = a secret was found.
"""
import hashlib, os, re, sys
SUMS = "00_SHA256SUMS"
RESERVED = re.compile(r"^(con|prn|aux|nul|com[0-9]|lpt[0-9])(\..*)?$", re.I)
MAC = re.compile(r"\b[0-9A-Fa-f]{2}(?::[0-9A-Fa-f]{2}){5}\b")
WINPATH = re.compile(r"\b[A-Za-z]:[\\/](?:[^\s\"'<>|]+)")
NIXHOME = re.compile(r"/(?:home|Users)/[A-Za-z0-9_.-]+")
URLCRED = re.compile(r"[a-z][a-z0-9+.-]*://[^/\s:@]+:[^/\s@]+@", re.I)
PRIVKEY = re.compile(r"-----BEGIN [A-Z ]*PRIVATE KEY-----")
def files(root):
for d, _, fs in os.walk(root):
for f in fs:
yield os.path.join(d, f)
def relp(root, p):
return "./" + os.path.relpath(p, root).replace(os.sep, "/")
def sha(p):
h = hashlib.sha256()
with open(p, "rb") as f:
for b in iter(lambda: f.read(1 << 20), b""):
h.update(b)
return h.hexdigest()
def is_text(b):
return b"\0" not in b[:8192]
def cmd_sums(root):
lines = [f"{sha(p)} {relp(root, p)}" for p in sorted(files(root))
if os.path.basename(p) != SUMS]
open(os.path.join(root, SUMS), "w", newline="\n").write("\n".join(lines) + "\n")
print(f"{len(lines)} files -> {SUMS}")
def cmd_verify(root):
bad = n = 0
listed = set()
for line in open(os.path.join(root, SUMS)):
h, p = line.rstrip("\n").split(" ", 1)
listed.add(p); n += 1
fp = os.path.join(root, p[2:])
if not os.path.exists(fp) or sha(fp) != h:
bad += 1; print("BAD", p)
extra = [relp(root, p) for p in files(root)
if os.path.basename(p) != SUMS and relp(root, p) not in listed]
for p in extra:
print("NOT LISTED", p)
print(f"{n} listed, {bad} bad, {len(extra)} not listed")
sys.exit(1 if bad or extra else 0)
def env_secrets(path):
out = {}
if not path or not os.path.exists(path):
return out
for line in open(path, encoding="utf-8", errors="replace"):
m = re.match(r"\s*([A-Z0-9_]*(?:PASS|PASSWORD|TOKEN|SECRET|KEY)[A-Z0-9_]*)\s*=\s*(\S+)", line)
if m and len(m.group(2)) >= 4:
out[m.group(1)] = m.group(2)
return out
def cmd_scan(root, args):
env = os.path.expanduser(args[args.index("--env") + 1]) if "--env" in args else None
macs = [args[i + 1].lower() for i, a in enumerate(args) if a == "--mac"]
srcd = args[args.index("--src-dirs") + 1].split(",") if "--src-dirs" in args else []
allow = {args[i + 1] for i, a in enumerate(args) if a == "--allow"}
secrets = env_secrets(env)
found_secret = False
rep = {k: [] for k in ("secret", "acknowledged", "url_cred", "privkey", "board_mac", "mac", "dev_path",
"src_binary", "reserved")}
for p in files(root):
r = relp(root, p)
if RESERVED.match(os.path.basename(p)):
rep["reserved"].append(r)
b = open(p, "rb").read()
if any(r.startswith("./" + s.strip("/") + "/") for s in srcd):
if p.endswith(".upstate.json"):
rep["src_binary"].append(r + " (tool state file, remove)")
elif b[:4] == b"\x7fELF" or p.endswith((".o", ".a", ".so", ".ko")):
rep["src_binary"].append(r)
if not is_text(b):
continue
t = b.decode("utf-8", "replace")
for k, v in secrets.items():
if v in t:
if k in allow:
rep["acknowledged"].append(f"{r} (value of {k})")
else:
rep["secret"].append(f"{r} (value of {k})"); found_secret = True
if URLCRED.search(t):
rep["url_cred"].append(r); found_secret = True
if PRIVKEY.search(t):
rep["privkey"].append(r)
ms = {m.lower() for m in MAC.findall(t)}
for m in ms:
(rep["board_mac"] if m in macs else rep["mac"]).append(f"{r} {m}")
dp = sorted(set(WINPATH.findall(t)) | set(NIXHOME.findall(t)))
if dp:
rep["dev_path"].append(f"{r} ({len(dp)}: {dp[0][:60]}{' ...' if len(dp) > 1 else ''})")
titles = {"secret": "SECRET VALUES (must be removed)",
"acknowledged": "acknowledged values (--allow; say so in the README)",
"url_cred": "user:password@ URLs",
"privkey": "private keys (check: product key or leaked?)",
"board_mac": "given board MACs (must not ship)", "mac": "other MAC literals (review)",
"dev_path": "development-machine paths (review / document)",
"src_binary": "binaries inside source dirs", "reserved": "Windows reserved names"}
for k, title in titles.items():
print(f"== {title}: {len(rep[k])}")
for x in rep[k][:40]:
print(" ", x)
if len(rep[k]) > 40:
print(f" ... {len(rep[k]) - 40} more")
sys.exit(2 if found_secret else 0)
if __name__ == "__main__":
if len(sys.argv) < 3:
sys.exit(__doc__)
c, root = sys.argv[1], sys.argv[2]
{"sums": lambda: cmd_sums(root), "verify": lambda: cmd_verify(root),
"scan": lambda: cmd_scan(root, sys.argv[3:])}[c]()