Spaces:
Running
Running
| #!/usr/bin/env python3 | |
| """obcon-hf — sample DB → anonymized seed SQL. | |
| Reads from a live MariaDB (defaults to local 127.0.0.1:3306/obcondb) and writes | |
| INSERT statements with PHI/PII scrubbed: | |
| · sites.name → "Demo Site N" | |
| · users.authenticateId / emailWork / emailHome → "userN@demo.local" | |
| · users.name → "User N" | |
| · users.hash → MD5('Admin@2026') (모든 사용자 동일 비밀번호로 통일) | |
| · devices.name / location → "Device N" / "loc-N" | |
| · GPS 좌표 → 한국 중심 근처에 ±0.5° 랜덤 지터 | |
| · devicedata* → 최근 7일치 중 사이트당 최대 N행으로 샘플링 | |
| · 운영 로그 / 세션 / loginHistories / audit 등은 통째로 제외 | |
| 요구사항: pip install pymysql (이 스크립트만 추가 의존성). | |
| """ | |
| import argparse | |
| import hashlib | |
| import random | |
| from datetime import datetime, timedelta | |
| try: | |
| import pymysql | |
| except ImportError: | |
| raise SystemExit("pymysql required: pip install pymysql") | |
| # ---- PHI/PII 가 들어 있을 수 있어 통째로 제외하는 테이블 ---------------------- | |
| EXCLUDED_TABLES = { | |
| "loginHistories", "sessions", "auditLogs", "audits", | |
| "notifications", "tasks", "events", "messages", | |
| "smsHistories", "emailHistories", | |
| "thingplugs", | |
| # devicedata* 는 따로 샘플링 | |
| } | |
| # ---- 사이트당 보존할 devicedata 행 개수 ----------------------------------- | |
| DEVICEDATA_PER_SITE_LIMIT = 200 | |
| # ---- 비결정성 막기 위해 시드 고정 ----------------------------------------- | |
| random.seed(20260101) | |
| KR_CENTER_LAT = 36.5 | |
| KR_CENTER_LNG = 127.8 | |
| JITTER = 0.5 # 약 50km | |
| def jittered_coord(): | |
| return ( | |
| round(KR_CENTER_LAT + random.uniform(-JITTER, JITTER), 6), | |
| round(KR_CENTER_LNG + random.uniform(-JITTER, JITTER), 6), | |
| ) | |
| def sql_literal(v): | |
| if v is None: | |
| return "NULL" | |
| if isinstance(v, bool): | |
| return "1" if v else "0" | |
| if isinstance(v, (int, float)): | |
| return str(v) | |
| if isinstance(v, (datetime,)): | |
| return "'" + v.strftime("%Y-%m-%d %H:%M:%S") + "'" | |
| if isinstance(v, bytes): | |
| return "0x" + v.hex() | |
| s = str(v).replace("\\", "\\\\").replace("'", "''") | |
| return "'" + s + "'" | |
| def fetch_columns(cur, table): | |
| cur.execute(f"DESCRIBE `{table}`") | |
| return [row[0] for row in cur.fetchall()] | |
| def list_tables(cur, dbname): | |
| cur.execute( | |
| "SELECT table_name FROM information_schema.tables " | |
| "WHERE table_schema=%s AND table_type='BASE TABLE' ORDER BY table_name", | |
| (dbname,), | |
| ) | |
| return [r[0] for r in cur.fetchall()] | |
| def anon_users(cur, out): | |
| cols = fetch_columns(cur, "users") | |
| admin_hash = hashlib.md5(b"Admin@2026").hexdigest() | |
| cur.execute( | |
| f"SELECT {','.join(f'`{c}`' for c in cols)} FROM users " | |
| "WHERE deleted=0 AND status='Active' " | |
| "ORDER BY isGlobalAdmin DESC, isAdmin DESC, id LIMIT 30" | |
| ) | |
| rows = cur.fetchall() | |
| out.write(f"\n-- users (anonymized, {len(rows)} rows; password = Admin@2026)\n") | |
| for i, row in enumerate(rows, 1): | |
| d = dict(zip(cols, row)) | |
| d["authenticateId"] = f"user{i}@demo.local" if i > 1 else "admin@demo" | |
| d["name"] = f"Demo User {i}" if i > 1 else "Demo Admin" | |
| d["hash"] = admin_hash | |
| for f in ("emailWork", "emailHome", "phoneMobile", "phoneWork", | |
| "phoneFax", "address", "addressDetail", "postalCode", | |
| "department", "title", "description"): | |
| if f in d and d[f] is not None: | |
| d[f] = "" if isinstance(d[f], str) else None | |
| values = ", ".join(sql_literal(d[c]) for c in cols) | |
| out.write( | |
| f"INSERT IGNORE INTO `users` (`{'`,`'.join(cols)}`) VALUES ({values});\n" | |
| ) | |
| def anon_sites(cur, out): | |
| cols = fetch_columns(cur, "sites") | |
| cur.execute( | |
| f"SELECT {','.join(f'`{c}`' for c in cols)} FROM sites " | |
| "WHERE deleted=0 AND status='Active' ORDER BY id LIMIT 20" | |
| ) | |
| rows = cur.fetchall() | |
| out.write(f"\n-- sites (anonymized, {len(rows)} rows)\n") | |
| for i, row in enumerate(rows, 1): | |
| d = dict(zip(cols, row)) | |
| d["name"] = f"Demo Site {i}" | |
| d["siteKey"] = f"{i:02d}" | |
| for f in ("address", "addressDetail", "description"): | |
| if f in d and d[f] is not None: | |
| d[f] = "" if isinstance(d[f], str) else None | |
| values = ", ".join(sql_literal(d[c]) for c in cols) | |
| out.write( | |
| f"INSERT IGNORE INTO `sites` (`{'`,`'.join(cols)}`) VALUES ({values});\n" | |
| ) | |
| def anon_devices(cur, out, keep_site_ids): | |
| cols = fetch_columns(cur, "devices") | |
| cur.execute( | |
| f"SELECT {','.join(f'`{c}`' for c in cols)} FROM devices " | |
| f"WHERE deleted=0 AND statusUse='Active' AND site IN ({','.join(str(s) for s in keep_site_ids)}) " | |
| "ORDER BY site, type, deviceKey" | |
| ) | |
| rows = cur.fetchall() | |
| out.write(f"\n-- devices (anonymized GPS + names, {len(rows)} rows)\n") | |
| for i, row in enumerate(rows, 1): | |
| d = dict(zip(cols, row)) | |
| d["name"] = f"Device {i}" | |
| if "location" in d: | |
| d["location"] = f"loc-{i}" | |
| if "description" in d: | |
| d["description"] = "" | |
| if "latitude" in d and "longitude" in d: | |
| lat, lng = jittered_coord() | |
| d["latitude"] = lat | |
| d["longitude"] = lng | |
| if "latitudeDetail" in d: | |
| d["latitudeDetail"] = lat | |
| if "longitudeDetail" in d: | |
| d["longitudeDetail"] = lng | |
| values = ", ".join(sql_literal(d[c]) for c in cols) | |
| out.write( | |
| f"INSERT IGNORE INTO `devices` (`{'`,`'.join(cols)}`) VALUES ({values});\n" | |
| ) | |
| def anon_devicedata(cur, out, table, keep_site_keys): | |
| cols = fetch_columns(cur, table) | |
| if "statusDatetime" not in cols or "siteKey" not in cols: | |
| return 0 | |
| cutoff = (datetime.now() - timedelta(days=7)).strftime("%Y%m%d%H%M%S") | |
| placeholders = ",".join(["%s"] * len(keep_site_keys)) | |
| cur.execute( | |
| f"SELECT {','.join(f'`{c}`' for c in cols)} FROM `{table}` " | |
| f"WHERE statusDatetime > %s AND siteKey IN ({placeholders}) " | |
| f"ORDER BY statusDatetime DESC LIMIT {DEVICEDATA_PER_SITE_LIMIT * len(keep_site_keys)}", | |
| [cutoff] + list(keep_site_keys), | |
| ) | |
| rows = cur.fetchall() | |
| if not rows: | |
| return 0 | |
| out.write(f"\n-- {table} (recent sample, {len(rows)} rows)\n") | |
| for row in rows: | |
| d = dict(zip(cols, row)) | |
| values = ", ".join(sql_literal(d[c]) for c in cols) | |
| out.write( | |
| f"INSERT IGNORE INTO `{table}` (`{'`,`'.join(cols)}`) VALUES ({values});\n" | |
| ) | |
| return len(rows) | |
| def main(): | |
| ap = argparse.ArgumentParser() | |
| ap.add_argument("--host", default="127.0.0.1") | |
| ap.add_argument("--port", type=int, default=3306) | |
| ap.add_argument("--user", default="scada") | |
| ap.add_argument("--password", default="scada") | |
| ap.add_argument("--db", default="obcondb") | |
| ap.add_argument("--out", required=True) | |
| args = ap.parse_args() | |
| conn = pymysql.connect( | |
| host=args.host, port=args.port, user=args.user, | |
| password=args.password, database=args.db, charset="utf8mb4" | |
| ) | |
| cur = conn.cursor() | |
| with open(args.out, "w", encoding="utf-8") as out: | |
| out.write("-- obcon-hf anonymized sample data\n") | |
| out.write(f"-- generated {datetime.now().isoformat(timespec='seconds')}\n") | |
| out.write("SET FOREIGN_KEY_CHECKS=0;\n") | |
| anon_sites(cur, out) | |
| # 위 sites INSERT 한 사이트들만 device + devicedata 에 포함 | |
| cur.execute( | |
| "SELECT id, siteKey FROM sites WHERE deleted=0 AND status='Active' " | |
| "ORDER BY id LIMIT 20" | |
| ) | |
| site_pairs = list(cur.fetchall()) | |
| keep_site_ids = [s[0] for s in site_pairs] | |
| keep_site_keys = [s[1] for s in site_pairs if s[1]] | |
| anon_users(cur, out) | |
| anon_devices(cur, out, keep_site_ids) | |
| total = 0 | |
| for table in list_tables(cur, args.db): | |
| if table.startswith("devicedata") and table.endswith("s"): | |
| total += anon_devicedata(cur, out, table, keep_site_keys) | |
| out.write(f"\n-- devicedata total inserts: {total}\n") | |
| out.write("SET FOREIGN_KEY_CHECKS=1;\n") | |
| print(f"[anon] wrote {args.out}") | |
| if __name__ == "__main__": | |
| main() | |