169 lines
5.4 KiB
Python
169 lines
5.4 KiB
Python
"""Bitbucket eternal lane — public repos sorted by -updated_on, scan root tree
|
|
for config/env files, fetch raw, regex keys → keys.db."""
|
|
import os
|
|
KF_BASE = os.path.dirname(os.path.abspath(__file__))
|
|
os.makedirs(os.path.join(KF_BASE, "data"), exist_ok=True)
|
|
import re
|
|
import sqlite3
|
|
import threading
|
|
import time
|
|
import hashlib
|
|
import os
|
|
import requests
|
|
_S = requests.Session()
|
|
_S.trust_env = False
|
|
|
|
DB = os.environ.get("KF_DB") or os.path.join(KF_BASE, "data/keys.db")
|
|
REST = 300 # sec after cycle
|
|
WORKERS = 12
|
|
PAGES_PER_CYCLE = 40 # 40*100 = 4000 repos per cycle
|
|
API = "https://api.bitbucket.org/2.0"
|
|
|
|
H = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"}
|
|
|
|
# filenames worth fetching raw
|
|
INTERESTING = re.compile(
|
|
r'(^|\.)(env|config|settings|credentials|secrets?|tokens?|keys?|auth)\b'
|
|
r'|\.env|\.ini$|\.cfg$|\.properties$|\.toml$|\.yaml$|\.yml$|\.json$'
|
|
r'|\.bashrc|\.profile|\.netrc', re.I)
|
|
|
|
from key_patterns import PATTERNS
|
|
|
|
GARBAGE = re.compile(r'(pass|word|test|exam|ple|dummy|fake|xxxx|1234|sample|your|here|placeholder|insert|replace|REPLACE|secret|token|key)\b', re.I)
|
|
# TODO placeholder check applied to exact value when short
|
|
|
|
db = sqlite3.connect(DB, timeout=120, check_same_thread=False, isolation_level=None)
|
|
db.execute("PRAGMA busy_timeout=120000")
|
|
DB_LOCK = threading.Lock()
|
|
|
|
|
|
def log(msg):
|
|
print(time.strftime("[%H:%M:%S] ") + msg, flush=True)
|
|
|
|
|
|
def seen_vals():
|
|
cur = db.cursor()
|
|
cur.execute("SELECT val FROM keys")
|
|
return {r[0] for r in cur.fetchall()}
|
|
|
|
|
|
def insert_key(key, prov, repo):
|
|
h = hashlib.sha1(key.encode()).hexdigest()[:16]
|
|
with DB_LOCK:
|
|
try:
|
|
db.execute(
|
|
"INSERT INTO keys (hash, val, prov, status, found, repo) "
|
|
"VALUES (?,?,?,?,?,?) "
|
|
"ON CONFLICT(hash) DO UPDATE SET status='NEW', found=excluded.found, repo=excluded.repo "
|
|
"WHERE keys.status IN ('DEAD','ERR')",
|
|
(h, key, prov, "NEW", "bitbucket-eternal", repo))
|
|
db.commit()
|
|
except sqlite3.Error:
|
|
pass
|
|
|
|
|
|
def garbage(key):
|
|
return bool(GARBAGE.search(key))
|
|
|
|
|
|
def scan_repo(full_slug, seen):
|
|
"""List root tree, fetch interesting files raw, regex."""
|
|
n = 0
|
|
try:
|
|
r = _S.get(f"{API}/repositories/{full_slug}/src",
|
|
params={"pagelen": 100}, headers=H, timeout=20)
|
|
if r.status_code != 200:
|
|
return 0
|
|
for ent in r.json().get("values", []):
|
|
if ent.get("type") != "commit_file":
|
|
continue
|
|
path = ent.get("path", "")
|
|
if not INTERESTING.search(path):
|
|
continue
|
|
try:
|
|
raw = _S.get(f"{API}/repositories/{full_slug}/raw/{path}",
|
|
headers=H, timeout=20)
|
|
if raw.status_code != 200 or len(raw.text) > 300_000:
|
|
continue
|
|
text = raw.text
|
|
except Exception:
|
|
continue
|
|
for prov, rx in PATTERNS:
|
|
for key in rx.findall(text):
|
|
if isinstance(key, tuple):
|
|
key = key[0]
|
|
key = key.strip().strip('"\'`.,:')
|
|
if not key or key in seen:
|
|
continue
|
|
if garbage(key) and len(key) < 50:
|
|
continue
|
|
insert_key(key, prov, full_slug)
|
|
seen.add(key)
|
|
n += 1
|
|
log(f" +{prov} {key[:12]}… {full_slug}")
|
|
except Exception:
|
|
pass
|
|
return n
|
|
|
|
|
|
def cycle(seen):
|
|
total = 0
|
|
lock = threading.Lock()
|
|
pages = list(range(1, PAGES_PER_CYCLE + 1))
|
|
idx = [0]
|
|
errs = [0]
|
|
|
|
def work():
|
|
nonlocal total
|
|
while True:
|
|
with lock:
|
|
if idx[0] >= len(pages):
|
|
return
|
|
page = pages[idx[0]]
|
|
idx[0] += 1
|
|
try:
|
|
r = _S.get(f"{API}/repositories",
|
|
params={"pagelen": 100, "page": page,
|
|
"sort": "-updated_on"},
|
|
headers=H, timeout=25)
|
|
if r.status_code != 200:
|
|
with lock:
|
|
errs[0] += 1
|
|
if errs[0] == 1:
|
|
log(f" page {page}: HTTP {r.status_code} {r.text[:80]}")
|
|
continue
|
|
repos = [v["full_name"] for v in r.json().get("values", [])
|
|
if v.get("full_name")]
|
|
except Exception as e:
|
|
with lock:
|
|
errs[0] += 1
|
|
if errs[0] == 1:
|
|
log(f" page {page} err: {e!r}")
|
|
continue
|
|
got = sum(scan_repo(slug, seen) for slug in repos)
|
|
with lock:
|
|
total += got
|
|
time.sleep(1) # politeness
|
|
|
|
ths = [threading.Thread(target=work, daemon=True) for _ in range(WORKERS)]
|
|
for t in ths:
|
|
t.start()
|
|
for t in ths:
|
|
t.join()
|
|
return total, errs[0]
|
|
|
|
|
|
def main():
|
|
log(f"=== BITBUCKET eternal START threads={WORKERS} ===")
|
|
while True:
|
|
try:
|
|
seen = seen_vals()
|
|
n, fails = cycle(seen)
|
|
log(f"CYCLE done: +{n} new, {fails} page-fails; resting {REST}s")
|
|
except Exception as e:
|
|
log(f"CYCLE error: {e}; resting {REST}s")
|
|
time.sleep(REST)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main() |