obcon-scada / scripts /anonymize_sample.py
chanmin0723's picture
Initial obcon SCADA deploy
e4bf523
Raw
History Blame Contribute Delete
8.38 kB
#!/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()