mission17-ai / scripts /migrate_anticheat_storage.py
GitHub Action
sync: fix security audit findings and document readiness
9f41e0a
Raw History Blame Contribute Delete
5.55 kB
"""Validate legacy hashes and backfill MongoDB anti-cheat records safely.
The command is dry-run only unless ``--apply`` is supplied. It never removes
the SQLite or JSON sources.
"""
import argparse
import json
import sqlite3
import sys
from collections import Counter
from datetime import datetime, timezone
from pathlib import Path
from pymongo import MongoClient
from pymongo.errors import PyMongoError
AI_ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(AI_ROOT))
from utils.anticheat import AntiCheatEngine, AntiCheatUnavailable # noqa: E402
def read_json_hashes(path):
if not path.exists():
return []
with path.open('r', encoding='utf-8') as source:
payload = json.load(source)
values = payload.get('hashes', []) if isinstance(payload, dict) else []
return values if isinstance(values, list) else []
def read_sqlite_hashes(path):
if not path.exists():
return []
connection = None
try:
connection = sqlite3.connect(f'file:{path.as_posix()}?mode=ro', uri=True)
return [row[0] for row in connection.execute('SELECT hash_str FROM hashes')]
except sqlite3.Error as error:
raise RuntimeError(f'Could not read legacy SQLite database: {error}') from error
finally:
if connection is not None:
connection.close()
def validated_legacy_hashes(sqlite_path, json_path):
raw_by_source = {
'sqlite': read_sqlite_hashes(sqlite_path),
'json': read_json_hashes(json_path),
}
valid = set()
counts = Counter()
for source_name, values in raw_by_source.items():
counts[f'{source_name}_read'] = len(values)
for value in values:
normalized = AntiCheatEngine.normalize_hash(value)
if normalized:
valid.add(normalized)
counts[f'{source_name}_valid'] += 1
else:
counts[f'{source_name}_invalid'] += 1
counts['unique_valid'] = len(valid)
return sorted(valid), counts
def backfill_buckets(collection, apply_changes):
scanned = updated = invalid = 0
projection = {'hashValue': 1, 'buckets': 1}
for record in collection.find({}, projection):
scanned += 1
normalized = AntiCheatEngine.normalize_hash(record.get('hashValue'))
if not normalized:
invalid += 1
continue
expected = AntiCheatEngine._buckets(normalized)
if record.get('hashValue') != normalized or record.get('buckets') != expected:
updated += 1
if apply_changes:
collection.update_one(
{'_id': record['_id']},
{'$set': {'hashValue': normalized, 'buckets': expected}},
)
return {'scanned': scanned, 'updates_needed': updated, 'invalid': invalid}
def migrate_legacy(collection, hashes, apply_changes):
inserted = existing = 0
now = datetime.now(timezone.utc)
for hash_value in hashes:
selector = {
'hashType': AntiCheatEngine.LEGACY_HASH_TYPE,
'algorithmVersion': AntiCheatEngine.LEGACY_ALGORITHM_VERSION,
'hashValue': hash_value,
}
if collection.find_one(selector, {'_id': 1}):
existing += 1
continue
inserted += 1
if apply_changes:
collection.update_one(
selector,
{'$setOnInsert': {
**selector,
'buckets': AntiCheatEngine._buckets(hash_value),
'createdAt': now,
'migrationSource': 'legacy-local-stores',
}},
upsert=True,
)
return {'inserts_needed': inserted, 'already_present': existing}
def parse_args():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--apply', action='store_true', help='Write validated changes to MongoDB.')
parser.add_argument('--sqlite', type=Path, default=AI_ROOT / 'anticheat.db')
parser.add_argument('--json', type=Path, default=AI_ROOT / 'anticheat_hashes.json')
return parser.parse_args()
def get_collection(engine, apply_changes):
if apply_changes:
return engine._get_collection()
if not engine.mongo_uri:
raise AntiCheatUnavailable('ANTICHEAT_MONGO_URI is not configured.')
try:
client = MongoClient(
engine.mongo_uri,
serverSelectionTimeoutMS=5_000,
connectTimeoutMS=5_000,
socketTimeoutMS=5_000,
)
client.admin.command('ping')
return client[engine.db_name][engine.collection_name]
except PyMongoError as error:
raise AntiCheatUnavailable('Durable anti-cheat storage is unavailable.') from error
def main():
args = parse_args()
hashes, source_counts = validated_legacy_hashes(args.sqlite, args.json)
engine = AntiCheatEngine()
try:
collection = get_collection(engine, args.apply)
except AntiCheatUnavailable as error:
print(json.dumps({'status': 'error', 'message': str(error)}, indent=2))
return 2
report = {
'mode': 'apply' if args.apply else 'dry-run',
'legacy_sources': dict(source_counts),
'legacy_import': migrate_legacy(collection, hashes, args.apply),
'bucket_backfill': backfill_buckets(collection, args.apply),
'source_files_removed': False,
}
print(json.dumps(report, indent=2, sort_keys=True))
return 0
if __name__ == '__main__':
raise SystemExit(main())