mirror of
https://github.com/Anon-Planet/thgtoa.git
synced 2026-05-06 11:34:18 +02:00
Build pipeline WIP
Signed-off-by: nopeitsnothing <no@anonymousplanet.org>
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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())
|
||||
@@ -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())
|
||||
Reference in New Issue
Block a user