#!/usr/bin/env python3 """Check publishable files without printing possible credential values.""" from pathlib import Path import argparse import hashlib import json import re import subprocess import sys root = Path(__file__).resolve().parents[1] p = argparse.ArgumentParser(description=__doc__) p.add_argument('--release', action='store_true', help='Also require licensing and rendered repository metadata') a = p.parse_args() errors = [] ignored_dirs = {'.git', '.gradle', 'build', '__pycache__', '.idea'} for path in root.rglob('*'): rel = path.relative_to(root) if any(part in ignored_dirs for part in rel.parts): continue if path.is_symlink(): errors.append(f'{rel}: symbolic link is not allowed') continue if not path.is_file(): continue if path.suffix.lower() in {'.jks', '.keystore', '.p8', '.p12', '.pfx', '.apk', '.aab', '.pem'} or path.name.startswith('.env'): errors.append(f'{rel}: private key, environment file or compiled package') if path.name == 'local.properties': errors.append(f'{rel}: local build configuration must not be exported') data = path.read_bytes() if path.suffix.lower() in {'.png', '.jpg', '.jpeg'}: continue if b'\0' in data: errors.append(f'{rel}: unexpected binary file') continue text = data.decode('utf-8', errors='replace') patterns = [ r'-----BEGIN (?:RSA |EC |OPENSSH |DSA )?PRIVATE' + r' KEY-----', r'AI' + r'za[0-9A-Za-z_-]{35}', r'\b(?:AK' + r'IA|ASIA)[A-Z0-9]{16}\b', r'\b(?:gh' + r'p_|github_pat_)[A-Za-z0-9_]{20,}', r'\b(?:postgres(?:ql)?|mysql|redis|https?)://[^\s/:]+:[^\s/@]+@', ] if any(re.search(pattern, text) for pattern in patterns): errors.append(f'{rel}: possible secret; inspect privately') if path.suffix == '.gradle' and re.search(r'storePassword|keyPassword|signingConfig\b|release\.jks', text): errors.append(f'{rel}: release signing credentials/configuration must be absent') for entry in json.loads((root / 'packaging/exported-sources.json').read_text()): path = root / entry['path'] if not path.is_file() or hashlib.sha256(path.read_bytes()).hexdigest() != entry['sha256']: errors.append(f"{entry['path']}: source changed since export; review and refresh inventory") main = (root / 'app/src/main/java/com/glosira/app/MainActivity.kt').read_text() if any(x in main for x in ['refreshConfig(', 'app-config.json', 'glosira.apk', 'showUpdateRequired']): errors.append('MainActivity: website updater still present') if "applicationId 'com.glosira.app.fdroid'" not in (root / 'app/build.gradle').read_text(): errors.append('Unexpected application ID') for name, limit in [('short_description.txt', 80), ('title.txt', 50)]: if len((root / 'fastlane/metadata/android/en-US' / name).read_text().strip()) > limit: errors.append(f'{name}: metadata too long') # A .gitignore cannot protect files already tracked. Check the index separately. r = subprocess.run(['git', '-C', str(root), 'ls-files', '-z'], capture_output=True) if r.returncode == 0: for name in r.stdout.decode().split('\0'): if name and (any(part in ignored_dirs - {'.git'} for part in Path(name).parts) or Path(name).suffix in {'.apk', '.aab', '.jks', '.keystore', '.p8', '.p12', '.pfx'}): errors.append(f'{name}: forbidden tracked file') if a.release: if not (root / 'LICENSE').is_file() or 'PENDING' in (root / 'LICENSING.txt').read_text(): errors.append('Owner-approved source/artwork licenses are required before release') metadata = root / 'packaging/com.glosira.app.fdroid.yml' if not metadata.exists() or re.search(r'@[A-Z_]+@', metadata.read_text()): errors.append('Public repository URLs and finalized F-Droid metadata are required') if errors: print('\n'.join(errors)) sys.exit(1) print('Public source tree checks passed' + (' (release prerequisites included)' if a.release else ' (publication prerequisites checked separately with --release)'))