#!/usr/bin/env python3 """release_check.py -- checksum, verify and scan a release directory. python release_check.py sums # (re)write 00_SHA256SUMS python release_check.py verify # check every listed file python release_check.py scan [--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/, /Users/); * 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]()