Build pipeline WIP

Signed-off-by: nopeitsnothing <no@anonymousplanet.org>
This commit is contained in:
nopeitsnothing
2026-04-16 06:32:57 -04:00
parent 41ac52de0a
commit 5636291c8a
12 changed files with 771 additions and 40 deletions
+6 -6
View File
@@ -1,16 +1,16 @@
#!/usr/bin/env python3
"""Extended version of build_guide_pdf.py with dark mode support.
"""Experimental dark mode support.
This script builds both light and dark mode MkDocs site, then renders docs/guide/ to single PDFs via Chromium.
Usage:
python scripts/build_guide_pdf_extended.py # Generate light mode PDF only
python scripts/build_guide_pdf_extended.py --dark-mode # Generate dark mode PDF only
python scripts/build_guide_pdf_extended.py --both # Generate both light and dark mode PDFs
python scripts/build_guide_pdf.py # Generate light mode PDF only
python scripts/build_guide_pdf.py --dark-mode # Generate dark mode PDF only
python scripts/build_guide_pdf.py --both # Generate both light and dark mode PDFs
Examples:
python scripts/build_guide_pdf_extended.py --site-dir build/html --pdf-light export/thgtoa.pdf
python scripts/build_guide_pdf_extended.py --dark-mode --pdf-dark export/thgtoa-dark.pdf
python scripts/build_guide_pdf.py --site-dir build/html --pdf-light export/thgtoa.pdf
python scripts/build_guide_pdf.py --dark-mode --pdf-dark export/thgtoa-dark.pdf
"""
from __future__ import annotations
+322
View File
@@ -0,0 +1,322 @@
#!/usr/bin/env python3
"""Setup helper for PDF workflow configuration.
This script helps you configure the necessary GitHub Secrets for the automated
PDF build, signing, and VirusTotal scanning workflows.
Usage:
python scripts/setup_workflow.py
Requirements:
- Python 3.8+
- GPG installed (for key export)
- Access to GitHub repository settings
What it does:
1. Validates your GPG key setup
2. Exports the public key for verification
3. Provides instructions for adding secrets to GitHub
"""
from __future__ import annotations
import subprocess
import sys
from pathlib import Path
def repo_root() -> Path:
return Path(__file__).resolve().parent.parent
def check_gpg_installed() -> bool:
"""Check if GPG is installed and accessible."""
try:
result = subprocess.run(
["gpg", "--version"],
capture_output=True,
text=True,
timeout=10,
)
return result.returncode == 0
except (FileNotFoundError, subprocess.TimeoutExpired):
return False
def list_gpg_keys() -> list[dict]:
"""List all GPG keys in the keyring."""
try:
result = subprocess.run(
["gpg", "--list-keys", "--with-colons"],
capture_output=True,
text=True,
check=True,
)
keys = []
current_key = {}
for line in result.stdout.split('\n'):
if line.startswith('pub:'):
if current_key:
keys.append(current_key)
parts = line.split(':')
current_key = {
'type': parts[1],
'key_id': parts[4],
'fingerprint': parts[9] if len(parts) > 9 else None,
'created': parts[5],
'expires': parts[6],
'uid': None,
}
elif line.startswith('uid:'):
parts = line.split(':')
current_key['uid'] = parts[9] if len(parts) > 9 else None
if current_key:
keys.append(current_key)
return keys
except subprocess.CalledProcessError as e:
print(f"Error listing GPG keys: {e}")
return []
def export_public_key(key_id: str, output_file: Path | None = None) -> str | None:
"""Export a public key in ASCII armor format."""
try:
result = subprocess.run(
["gpg", "--armor", "--export", key_id],
capture_output=True,
text=True,
check=True,
)
if output_file:
output_file.write_text(result.stdout)
print(f"✓ Public key exported to {output_file}")
return result.stdout
except subprocess.CalledProcessError as e:
print(f"Error exporting public key: {e}")
return None
def export_private_key(key_id: str, output_file: Path | None = None) -> str | None:
"""Export a private key in ASCII armor format (requires passphrase)."""
try:
# This will prompt for passphrase interactively
result = subprocess.run(
["gpg", "--armor", "--export-secret-keys", key_id],
capture_output=True,
text=True,
check=True,
)
if output_file:
output_file.write_text(result.stdout)
print(f"✓ Private key exported to {output_file}")
return result.stdout
except subprocess.CalledProcessError as e:
print(f"Error exporting private key: {e}")
return None
def validate_gpg_key(key_id: str) -> bool:
"""Validate that a GPG key has signing capability."""
try:
result = subprocess.run(
["gpg", "--list-keys", "--with-colons", key_id],
capture_output=True,
text=True,
check=True,
)
# Check for 's' (signing) in the pub line
for line in result.stdout.split('\n'):
if line.startswith('pub:'):
flags = line.split(':')[1]
return 's' in flags
return False
except subprocess.CalledProcessError:
return False
def print_setup_instructions():
"""Print instructions for configuring GitHub Secrets."""
print("\n" + "="*70)
print("GITHUB SECRETS SETUP INSTRUCTIONS")
print("="*70)
print("""
To enable the automated PDF workflow, you need to add three secrets to your
GitHub repository:
1. GPG_PRIVATE_KEY
- Your GPG private key in ASCII armor format
- Used to sign PDFs and hash files
- IMPORTANT: Keep this secret! Never commit it publicly
2. GPG_PASSPHRASE
- The passphrase for your GPG private key
- Required to unlock the private key for signing
3. VT_API_KEY (optional but recommended)
- VirusTotal API key for malware scanning
- Get a free key at: https://www.virustotal.com/gui/join-us
HOW TO ADD SECRETS:
1. Go to your repository on GitHub
2. Click 'Settings''Secrets and variables''Actions'
3. Click 'New repository secret' for each secret below:
Secret Name | Value Format
---------------------|--------------------------------------------------
GPG_PRIVATE_KEY | Paste the entire ASCII armored key (BEGIN PGP...)
GPG_PASSPHRASE | Your key's passphrase (no special characters issues)
VT_API_KEY | Your VirusTotal API key
VERIFYING YOUR SETUP:
After adding secrets, you can test by:
1. Going to 'Actions' tab
2. Selecting 'Build guide PDF' workflow
3. Clicking 'Run workflow'
4. Checking if the workflow completes successfully
TROUBLESHOOTING:
- If GPG signing fails: Check that your key has signing capability ('s' flag)
- If passphrase is wrong: Verify you're using the correct passphrase
- If VT scan fails: Ensure API key is valid and within rate limits
SECURITY NOTES:
⚠ NEVER share your private key or passphrase publicly
⚠ Always use repository secrets, never hardcode in scripts
⚠ Rotate keys periodically if compromised
⚠ Use strong passphrases (12+ characters recommended)
""")
def main() -> int:
print("\n" + "="*70)
print("PDF WORKFLOW SETUP HELPER")
print("="*70)
# Check GPG installation
if not check_gpg_installed():
print("⚠ WARNING: GPG is not installed or not in PATH")
print("Please install GPG before continuing:")
print(" - Linux: sudo apt install gnupg")
print(" - macOS: brew install gnupg")
print(" - Windows: https://www.gpg4win.org/")
print("\nContinuing anyway...")
# List available keys
print("\n🔑 Available GPG Keys:")
print("-" * 70)
keys = list_gpg_keys()
if not keys:
print("No GPG keys found in your keyring.")
print("Generate a key with: gpg --full-generate-key")
return 1
for i, key in enumerate(keys, 1):
status = "" if validate_gpg_key(key['key_id']) else ""
print(f"\n{i}. {status} Key ID: {key['key_id']}")
print(f" Fingerprint: {key.get('fingerprint', 'N/A')}")
print(f" UID: {key.get('uid', 'Unknown')}")
print(f" Created: {key.get('created', 'Unknown')}")
if key.get('expires'):
print(f" Expires: {key['expires']}")
# Ask user to select key
print("\n" + "-" * 70)
try:
choice = input("\nEnter the number of the key you want to use (1-{}): ".format(len(keys)))
selected_index = int(choice) - 1
if not (0 <= selected_index < len(keys)):
print("Invalid selection!")
return 1
except ValueError:
print("Invalid input! Please enter a number.")
return 1
selected_key = keys[selected_index]
# Validate key has signing capability
if not validate_gpg_key(selected_key['key_id']):
print(f"\n⚠ WARNING: Selected key does not have signing capability!")
print("You need a key with 's' (signing) flag for PDF signatures.")
confirm = input("Continue anyway? (y/N): ")
if confirm.lower() != 'y':
return 1
# Export public key
print(f"\n📤 Exporting public key for {selected_key['uid']}...")
public_key_file = repo_root() / "pgp" / "workflow-public.asc"
public_key = export_public_key(selected_key['key_id'], public_key_file)
if not public_key:
print("Failed to export public key!")
return 1
# Show public key info
print("\n✓ Public Key Information:")
print("-" * 70)
for line in public_key.split('\n')[:5]:
print(line)
print("...")
# Instructions for private key export
print("\n🔐 Private Key Export:")
print("-" * 70)
print("""
To get your private key for the GPG_PRIVATE_KEY secret:
1. Run this command (you'll be prompted for passphrase):
gpg --armor --export-secret-keys {} > workflow-private.asc
2. Copy the ENTIRE output including BEGIN and END lines
3. Add it to GitHub Secrets as 'GPG_PRIVATE_KEY'
⚠ IMPORTANT: Keep your private key secure! Never commit it publicly.
""".format(selected_key['key_id']))
# Print setup instructions
print_setup_instructions()
print("\n" + "="*70)
print("SETUP COMPLETE!")
print("="*70)
print(f"\nPublic key saved to: {public_key_file}")
print("Next steps:")
print("1. Export your private key (see instructions above)")
print("2. Add all three secrets to GitHub repository settings")
print("3. Test the workflow by triggering a manual build")
print("\nFor more information, see: docs/guide/pdf-workflow.md\n")
return 0
if __name__ == "__main__":
raise SystemExit(main())
+222
View File
@@ -0,0 +1,222 @@
#!/usr/bin/env python3
"""Verification script for PDF files.
This script verifies:
1. SHA256 hash integrity of PDF files
2. GPG signature authenticity
3. VirusTotal scan status (optional)
Usage:
python scripts/verify_pdf.py --all # Verify everything
python scripts/verify_pdf.py --hashes # Only verify hashes
python scripts/verify_pdf.py --signatures # Only verify signatures
python scripts/verify_pdf.py --vt # Check VT status (requires API key)
Examples:
python scripts/verify_pdf.py --all
python scripts/verify_pdf.py --hashes --file export/thgtoa.pdf
"""
from __future__ import annotations
import argparse
import hashlib
import os
import subprocess
import sys
from pathlib import Path
def repo_root() -> Path:
return Path(__file__).resolve().parent.parent
def calculate_sha256(file_path: Path) -> str:
"""Calculate SHA256 hash of a file."""
sha256_hash = hashlib.sha256()
with open(file_path, "rb") as f:
for byte_block in iter(lambda: f.read(4096), b""):
sha256_hash.update(byte_block)
return sha256_hash.hexdigest()
def verify_hash(file_path: Path, expected_hash: str) -> bool:
"""Verify file hash against expected value."""
actual_hash = calculate_sha256(file_path)
is_valid = actual_hash == expected_hash
status = "✓ PASS" if is_valid else "✗ FAIL"
print(f"{status}: {file_path.name}")
print(f" Expected: {expected_hash}")
print(f" Actual: {actual_hash}")
return is_valid
def verify_signature(file_path: Path, sig_file: Path) -> bool:
"""Verify GPG signature of a file."""
if not sig_file.exists():
print(f"✗ FAIL: Signature file not found: {sig_file}")
return False
try:
result = subprocess.run(
["gpg", "--verify", str(sig_file), str(file_path)],
capture_output=True,
text=True,
check=False,
)
if result.returncode == 0:
print(f"✓ PASS: {file_path.name} signature verified")
# Extract key info from GPG output
for line in result.stdout.split('\n'):
if 'Good signature' in line or 'key ID' in line.lower():
print(f" {line.strip()}")
return True
else:
print(f"✗ FAIL: {file_path.name} signature verification failed")
print(f" Error: {result.stderr}")
return False
except FileNotFoundError:
print("⚠ WARNING: GPG not installed. Skipping signature verification.")
return None
def verify_from_hash_file(file_path: Path, hash_file: Path) -> bool:
"""Verify file hash from a hash file."""
if not hash_file.exists():
print(f"✗ FAIL: Hash file not found: {hash_file}")
return False
expected_hash = None
with open(hash_file, 'r') as f:
for line in f:
parts = line.strip().split()
if len(parts) >= 2 and parts[1] == str(file_path):
expected_hash = parts[0]
break
if not expected_hash:
print(f"✗ FAIL: Hash not found in {hash_file.name} for {file_path.name}")
return False
return verify_hash(file_path, expected_hash)
def check_virustotal(file_hash: str, api_key: str | None = None) -> dict | None:
"""Check VirusTotal scan status for a file hash."""
if not api_key:
print("⚠ WARNING: VT_API_KEY not set. Skipping VirusTotal check.")
return None
try:
import urllib.request
import json
url = f"https://www.virustotal.com/api/v3/files/{file_hash}"
request = urllib.request.Request(url, headers={"x-apikey": api_key})
with urllib.request.urlopen(request, timeout=30) as response:
data = json.loads(response.read().decode())
stats = data.get('data', {}).get('attributes', {}).get('last_analysis_stats', {})
total = sum(stats.values()) if stats else 0
print(f"\n🦠 VirusTotal Results for {file_hash[:16]}...")
print(f" Total scans: {total}")
if stats:
print(f" Malicious: {stats.get('malicious', 0)}")
print(f" Suspicious: {stats.get('suspicious', 0)}")
print(f" Undetected: {stats.get('undetected', 0)}")
print(f" Clean: {stats.get('harmless', 0)}")
return data
except Exception as e:
print(f"⚠ ERROR checking VirusTotal: {e}")
return None
def main() -> int:
root = repo_root()
ap = argparse.ArgumentParser(description="Verify PDF files (hashes, signatures, VT).")
# File paths
ap.add_argument(
"--light-pdf",
type=Path,
default=root / "export" / "thgtoa.pdf",
help="Light mode PDF file",
)
ap.add_argument(
"--dark-pdf",
type=Path,
default=root / "export" / "thgtoa-dark.pdf",
help="Dark mode PDF file",
)
ap.add_argument(
"--hash-file",
type=Path,
default=root / "sha256sum-light.txt",
help="Hash file to verify against",
)
# Verification modes
group = ap.add_mutually_exclusive_group()
group.add_argument("--all", action="store_true", help="Verify everything")
group.add_argument("--hashes", action="store_true", help="Only verify hashes")
group.add_argument("--signatures", action="store_true", help="Only verify signatures")
ap.add_argument("--vt", action="store_true", help="Check VirusTotal status")
args = ap.parse_args()
# Determine what to verify
if not any([args.all, args.hashes, args.signatures, args.vt]):
args.all = True
all_passed = True
pdf_files = [
("Light", args.light_pdf),
("Dark", args.dark_pdf),
]
for mode_name, pdf_file in pdf_files:
if not pdf_file.exists():
print(f"⚠ WARNING: {pdf_file.name} not found. Skipping.")
continue
print(f"\n{'='*60}")
print(f"Verifying {mode_name} PDF: {pdf_file.name}")
print('='*60)
# Verify hash if requested
if args.all or args.hashes:
if not verify_from_hash_file(pdf_file, args.hash_file):
all_passed = False
# Verify signature if requested
if args.all or args.signatures:
sig_file = pdf_file.with_suffix(pdf_file.suffix + ".sig")
result = verify_signature(pdf_file, sig_file)
if result is False: # None means skipped (GPG not installed)
all_passed = False
# Check VirusTotal if requested
if args.all or args.vt:
file_hash = calculate_sha256(pdf_file)
api_key = os.environ.get("VT_API_KEY")
check_virustotal(file_hash, api_key)
print(f"\n{'='*60}")
if all_passed:
print("✓ All verifications PASSED")
return 0
else:
print("✗ Some verifications FAILED")
return 1
if __name__ == "__main__":
raise SystemExit(main())