Spaces:
Sleeping
Sleeping
File size: 8,382 Bytes
e4bf523 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 | #!/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()
|