dn-scribe commited on
Commit
383db3a
·
unverified ·
1 Parent(s): 57b9942

first step in migratng

Browse files
.vscode/launch.json CHANGED
@@ -61,7 +61,7 @@
61
  "SCRIBE_TOKEN": "${env:SCRIBE_TOKEN}",
62
  }
63
  },
64
- {
65
  "name": "get attestation",
66
  "type": "debugpy",
67
  "request": "launch",
@@ -76,7 +76,23 @@
76
  "env": {
77
  "SCRIBE_TOKEN": "${env:SCRIBE_TOKEN}",
78
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
79
  }
80
 
 
81
  ]
82
  }
 
61
  "SCRIBE_TOKEN": "${env:SCRIBE_TOKEN}",
62
  }
63
  },
64
+ {
65
  "name": "get attestation",
66
  "type": "debugpy",
67
  "request": "launch",
 
76
  "env": {
77
  "SCRIBE_TOKEN": "${env:SCRIBE_TOKEN}",
78
  }
79
+ },
80
+ {
81
+ "name": "get metadata",
82
+ "type": "debugpy",
83
+ "request": "launch",
84
+ "module": "scribe_python_client.cli",
85
+ "console": "internalConsole",
86
+ "args": [
87
+ "--api-call",
88
+ "get-dataset-tables-dict",
89
+ "--env", "prod",
90
+ ],
91
+ "env": {
92
+ "SCRIBE_TOKEN": "${env:SCRIBE_TOKEN}",
93
+ }
94
  }
95
 
96
+
97
  ]
98
  }
scribe_python_client/cli.py CHANGED
@@ -2,6 +2,7 @@ import argparse
2
  import os
3
  import logging
4
  from .client import ScribeClient
 
5
 
6
  def main():
7
  logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
@@ -21,6 +22,10 @@ def main():
21
  "query-lineage",
22
  "query-risk",
23
  "query-findings",
 
 
 
 
24
  ], help="Which API call to execute")
25
  parser.add_argument("--api-token", required=False, default=os.getenv('SCRIBE_TOKEN'), help="Your API token from ScribeHub integrations page (defaults to SCRIBE_TOKEN environment variable)")
26
  parser.add_argument("--env", default="prod", choices=["prod", "dev", "test", "ci"], help="Which environment to use")
@@ -28,6 +33,9 @@ def main():
28
  parser.add_argument("--attestation-id", required=False, help="The ID of the attestation (required for get-attestation API call)")
29
  parser.add_argument("--query", required=False, help="The query to run on the dataset (required for query commands)")
30
  parser.add_argument("--lineage-graph-file", required=False, help="The file to save the lineage graph to (optional)")
 
 
 
31
 
32
  args = parser.parse_args()
33
 
@@ -50,8 +58,13 @@ def main():
50
  base_url = "https://api.scribesecurity.com"
51
  if args.env != "prod":
52
  base_url = f"https://api.{args.env}.scribesecurity.com"
 
 
 
 
 
53
 
54
- client = ScribeClient(api_token=args.api_token, base_url=base_url, env = args.env)
55
 
56
  logging.info(f"Executing API call: {args.api_call}")
57
 
@@ -86,5 +99,64 @@ def main():
86
  elif args.api_call == "query-findings":
87
  print(client.query_findings(querystr=args.query))
88
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
89
  if __name__ == "__main__":
90
  main()
 
2
  import os
3
  import logging
4
  from .client import ScribeClient
5
+ import json
6
 
7
  def main():
8
  logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
 
22
  "query-lineage",
23
  "query-risk",
24
  "query-findings",
25
+ "get-dataset-links",
26
+ "get-dataset-data",
27
+ "get-dataset-tables-dict",
28
+ "get-dataset-tables-md",
29
  ], help="Which API call to execute")
30
  parser.add_argument("--api-token", required=False, default=os.getenv('SCRIBE_TOKEN'), help="Your API token from ScribeHub integrations page (defaults to SCRIBE_TOKEN environment variable)")
31
  parser.add_argument("--env", default="prod", choices=["prod", "dev", "test", "ci"], help="Which environment to use")
 
33
  parser.add_argument("--attestation-id", required=False, help="The ID of the attestation (required for get-attestation API call)")
34
  parser.add_argument("--query", required=False, help="The query to run on the dataset (required for query commands)")
35
  parser.add_argument("--lineage-graph-file", required=False, help="The file to save the lineage graph to (optional)")
36
+ parser.add_argument("--username", required=False, default = os.getenv('SUPERSET_DEV_USERNAME'), help="Superset username (required for Superset API calls)")
37
+ parser.add_argument("--password", required=False, default = os.getenv('SUPERSET_DEV_PASSWORD'), help="Superset password (required for Superset API calls)")
38
+
39
 
40
  args = parser.parse_args()
41
 
 
58
  base_url = "https://api.scribesecurity.com"
59
  if args.env != "prod":
60
  base_url = f"https://api.{args.env}.scribesecurity.com"
61
+
62
+ if args.api_call in ["get-dataset-links", "get-dataset-data", "get-dataset-tables-dict", "get-dataset-tables-md"]:
63
+ if not args.username or not args.password:
64
+ logging.error("--username and --password are required for Superset table export commands.")
65
+ exit(1)
66
 
67
+ client = ScribeClient(api_token=args.api_token, base_url=base_url, env = args.env, username= args.username, password=args.password)
68
 
69
  logging.info(f"Executing API call: {args.api_call}")
70
 
 
99
  elif args.api_call == "query-findings":
100
  print(client.query_findings(querystr=args.query))
101
 
102
+ # Get dataset metadata section:
103
+ elif args.api_call == "get-dataset-links":
104
+ links = client.get_dataset_links()
105
+ if links:
106
+ print("Dataset Links:")
107
+ for link in links:
108
+ print(link)
109
+ else:
110
+ logging.error("No dataset links found.")
111
+ result = None
112
+ elif args.api_call == "get-dataset-data":
113
+ dataset_links = client.get_dataset_links()
114
+ if dataset_links:
115
+ client.get_dataset_data(dataset_links, output_file="dataset_metadata.csv")
116
+ logging.info("Metadata export completed successfully.")
117
+ else:
118
+ logging.error("No dataset links found.")
119
+ result = None
120
+ elif args.api_call == "get-dataset-tables-dict":
121
+ dataset_links = client.get_dataset_links()
122
+ if dataset_links:
123
+ client.get_dataset_data(dataset_links, output_file="dataset_metadata.csv")
124
+ logging.info("Metadata export completed successfully.")
125
+ else:
126
+ logging.error("No dataset links found.")
127
+ result = None
128
+
129
+ tables_dict = client.get_dataset_tables_dict()
130
+ #Output path is set to docs/dataset_tables.json
131
+ output_path = os.path.join("docs", "dataset_tables.json")
132
+
133
+ with open(output_path, "w", encoding="utf-8") as f:
134
+ json.dump(tables_dict, f, indent=2)
135
+
136
+ logging.info(f"Saved table metadata dictionary to {output_path}")
137
+
138
+ if result is not None:
139
+ print(result)
140
+ elif args.api_call == "get-dataset-tables-md":
141
+ if not client.jwt_token:
142
+ username = input("Enter your Superset username: ")
143
+ password = input("Enter your Superset password: ")
144
+ client.username = username
145
+ client.password = password
146
+ if not client.authenticate():
147
+ logging.error("Superset login failed.")
148
+ exit(1)
149
+ dataset_links = client.get_dataset_links()
150
+ if dataset_links:
151
+ client.get_dataset_data(dataset_links, output_file="dataset_metadata.csv")
152
+ md_output = client.get_dataset_tables_md()
153
+ #Output path is set to docs/dataset_tables.md
154
+ output_path = os.path.join("docs", "dataset_tables.md")
155
+ with open(output_path, "w", encoding="utf-8") as f:
156
+ f.write(md_output)
157
+ logging.info(f"Saved table metadata Markdown to {output_path}")
158
+ else:
159
+ logging.error("No dataset links found.")
160
+
161
  if __name__ == "__main__":
162
  main()
scribe_python_client/client.py CHANGED
@@ -47,7 +47,8 @@ class ScribeClient:
47
  return True
48
  return (datetime.datetime.now() - last_update).days > 1
49
 
50
- def __init__(self, api_token=None, ignore_env = False, base_url="https://api.scribesecurity.com", refresh_interval=600, cached_data={}, env = "prod"):
 
51
  def update_from_cached_data(cached_data):
52
  if "datasets" in cached_data:
53
  self.datasets = cached_data["datasets"]
@@ -75,8 +76,16 @@ class ScribeClient:
75
  if not self.api_token:
76
  logging.error("No API token provided. Please set the SCRIBE_TOKEN environment variable or pass it as an argument.")
77
  return
 
 
78
  self.env = env
79
  self.base_url = base_url
 
 
 
 
 
 
80
  self.refresh_interval = refresh_interval
81
  self.last_refresh = datetime.datetime.now() - datetime.timedelta(days=5) # Initialize to a time in the past
82
 
@@ -1091,4 +1100,271 @@ class ScribeClient:
1091
  r = self.query_superset(self.scribe_findings_dataset, query, time_tokens=["timestamp"])
1092
  if r:
1093
  return f"{title}\n{self.format_query_result_table(r)}"
1094
- return "Error running query"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
47
  return True
48
  return (datetime.datetime.now() - last_update).days > 1
49
 
50
+ def __init__(self, api_token=None, ignore_env = False, base_url="https://api.scribesecurity.com", refresh_interval=600, cached_data={}, env = "prod",
51
+ username=None, password=None):
52
  def update_from_cached_data(cached_data):
53
  if "datasets" in cached_data:
54
  self.datasets = cached_data["datasets"]
 
76
  if not self.api_token:
77
  logging.error("No API token provided. Please set the SCRIBE_TOKEN environment variable or pass it as an argument.")
78
  return
79
+ self.username = username
80
+ self.password = password
81
  self.env = env
82
  self.base_url = base_url
83
+
84
+ if env == "prod":
85
+ self.superset_base_url = "https://superset.scribesecurity.com"
86
+ else:
87
+ self.superset_base_url = f"https://internal-superset.{env}.scribesecurity.com/login/"
88
+
89
  self.refresh_interval = refresh_interval
90
  self.last_refresh = datetime.datetime.now() - datetime.timedelta(days=5) # Initialize to a time in the past
91
 
 
1100
  r = self.query_superset(self.scribe_findings_dataset, query, time_tokens=["timestamp"])
1101
  if r:
1102
  return f"{title}\n{self.format_query_result_table(r)}"
1103
+ return "Error running query"
1104
+
1105
+ def superset_authenticate(self):
1106
+ if not self.username or not self.password:
1107
+ logging.error("Username and password must be provided for Superset login.")
1108
+ return False
1109
+
1110
+ login_url = f"{self.superset_base_url}/api/v1/security/login"
1111
+ body = {
1112
+ "username": self.username,
1113
+ "password": self.password,
1114
+ "provider": "db"
1115
+ }
1116
+
1117
+ response = self.send_post_request(login_url, body)
1118
+ if not response:
1119
+ logging.error("Login failed. No response received.")
1120
+ return False
1121
+
1122
+ try:
1123
+ result = json.loads(response)
1124
+ except json.JSONDecodeError:
1125
+ logging.error("Failed to parse login response.")
1126
+ return False
1127
+
1128
+ access_token = result.get("access_token")
1129
+ if access_token:
1130
+ self.jwt_token = access_token
1131
+ self.superset_token = access_token
1132
+ logging.info("Login successful. Tokens obtained.")
1133
+ return True
1134
+ else:
1135
+ logging.error("Login failed. No access token found.")
1136
+ return False
1137
+
1138
+ def get_dataset_links(self):
1139
+
1140
+ self.superset_authenticate()
1141
+
1142
+ if not self.jwt_token:
1143
+ logging.error("No JWT token found. Please authenticate first.")
1144
+ return []
1145
+
1146
+ dataset_links = []
1147
+
1148
+ #Use slug to directly fetch the API dashboard's datasets
1149
+ dashboard_slug = "api-dashboard"
1150
+ datasets_url = f"{self.superset_base_url}/api/v1/dashboard/{dashboard_slug}/datasets"
1151
+ logging.info(f"Fetching datasets from {datasets_url}")
1152
+
1153
+ datasets_response = self.send_get_request(datasets_url, token=self.jwt_token)
1154
+ if not datasets_response:
1155
+ logging.error("Failed to fetch datasets for API dashboard.")
1156
+ return []
1157
+
1158
+ try:
1159
+ datasets_data = json.loads(datasets_response)
1160
+ datasets = datasets_data.get("result", [])
1161
+
1162
+ for dataset in datasets:
1163
+ dataset_id = dataset.get("id")
1164
+ if dataset_id:
1165
+ link = f"{self.base_url}/explore/?datasource_type=table&datasource_id={dataset_id}"
1166
+ dataset_links.append(link)
1167
+
1168
+ except json.JSONDecodeError as e:
1169
+ logging.error(f"Error parsing dataset response: {e}")
1170
+
1171
+ logging.info(f"Total dataset links collected: {len(dataset_links)}")
1172
+ return dataset_links
1173
+
1174
+
1175
+ def get_dataset_data(self, dataset_links, output_file="dataset_metadata.csv"):
1176
+ records = []
1177
+
1178
+ for link in dataset_links:
1179
+ #Properly parse dataset ID from the URL
1180
+ parsed_url = urlparse(link)
1181
+ query_params = parse_qs(parsed_url.query)
1182
+ dataset_id_list = query_params.get("datasource_id")
1183
+ if not dataset_id_list:
1184
+ logging.error(f"Could not find dataset_id in link: {link}")
1185
+ continue
1186
+ dataset_id = dataset_id_list[0]
1187
+
1188
+ dataset_url = f"{self.base_url}/api/v1/dataset/{dataset_id}"
1189
+ logging.info(f"Fetching metadata from {dataset_url}")
1190
+
1191
+ response = self.send_get_request(dataset_url)
1192
+ if not response:
1193
+ logging.error(f"Failed to fetch metadata for dataset ID {dataset_id}")
1194
+ continue
1195
+ try:
1196
+ metadata_json = json.loads(response)
1197
+ dataset_metadata = metadata_json.get("result", {})
1198
+ except json.JSONDecodeError as e:
1199
+ logging.error(f"Failed to parse metadata JSON for dataset ID {dataset_id}: {e}")
1200
+ continue
1201
+
1202
+ verbose_name = dataset_metadata.get("verbose_name")
1203
+ if not verbose_name or verbose_name == dataset_metadata.get("table_name"):
1204
+ verbose_name = dataset_metadata.get("table_name", "").replace("_", " ").title()
1205
+
1206
+ #Get dataset metadata
1207
+ dataset_info = {
1208
+ "dataset_id": dataset_metadata.get("id"),
1209
+ "catalog": dataset_metadata.get("catalog"),
1210
+ "changed_on": dataset_metadata.get("changed_on"),
1211
+ "table_name": dataset_metadata.get("table_name"),
1212
+ "verbose_name": verbose_name,
1213
+ "schema": dataset_metadata.get("schema"),
1214
+ "datasource_type": dataset_metadata.get("datasource_type"),
1215
+ "description": dataset_metadata.get("description"),
1216
+ }
1217
+
1218
+ #Get column metadata
1219
+ columns = dataset_metadata.get("columns", [])
1220
+ if columns:
1221
+ for col in columns:
1222
+ record = dataset_info.copy()
1223
+ record.update({
1224
+ "column_id": col.get("id"),
1225
+ "column_name": col.get("column_name"),
1226
+ "column_verbose_name": col.get("verbose_name") or col.get("column_name", "").replace("_", " ").title(),
1227
+ "column_type": col.get("type"),
1228
+ "groupby": col.get("groupby"),
1229
+ "filterable": col.get("filterable"),
1230
+ "is_dttm": col.get("is_dttm"),
1231
+ "column_description": col.get("description"),
1232
+ })
1233
+ records.append(record)
1234
+ else:
1235
+ records.append(dataset_info)
1236
+
1237
+ #Write to CSV
1238
+ if records:
1239
+ fieldnames = [
1240
+ "dataset_id", "table_name", "verbose_name", "schema", "catalog",
1241
+ "datasource_type", "changed_on", "description",
1242
+ "column_id", "column_name", "column_verbose_name", "column_type",
1243
+ "groupby", "filterable", "is_dttm", "column_description"
1244
+ ]
1245
+ fieldnames = [field for field in fieldnames if any(field in rec for rec in records)]
1246
+
1247
+ with open(output_file, mode='w', newline='', encoding='utf-8') as csvfile:
1248
+ writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
1249
+ writer.writeheader()
1250
+ writer.writerows(records)
1251
+
1252
+ logging.info(f"Dataset metadata exported to {output_file}")
1253
+ else:
1254
+ logging.warning("No dataset metadata exported.")
1255
+
1256
+ def get_dataset_tables_dict(self, tables=[]) -> dict:
1257
+ if not self.jwt_token:
1258
+ self.superset_authenticate()
1259
+
1260
+ dataset_links = self.get_dataset_links()
1261
+ if not dataset_links:
1262
+ logging.error("No dataset links found.")
1263
+ return {}
1264
+
1265
+ self.get_dataset_data(dataset_links)
1266
+
1267
+ #Load data from the CSV file (generated by get_dataset_data)
1268
+ records = []
1269
+ try:
1270
+ with open("dataset_metadata.csv", mode="r", encoding="utf-8") as f:
1271
+ reader = csv.DictReader(f)
1272
+ for row in reader:
1273
+ records.append(row)
1274
+ except Exception as e:
1275
+ logging.error(f"Failed to load dataset_metadata.csv: {e}")
1276
+ return {}
1277
+
1278
+ #Group column data by table name
1279
+ table_dict = {}
1280
+ for row in records:
1281
+ table_name = row.get("table_name")
1282
+ if tables and table_name not in tables:
1283
+ continue
1284
+
1285
+ if table_name not in table_dict:
1286
+ table_dict[table_name] = []
1287
+
1288
+ table_dict[table_name].append({
1289
+ "column_name": row.get("column_name", ""),
1290
+ "type": row.get("column_type", ""),
1291
+ "label": row.get("column_verbose_name") or row.get("column_name", "").replace("_", " ").title(),
1292
+ "description": row.get("column_description", "")
1293
+ })
1294
+
1295
+ return table_dict
1296
+
1297
+
1298
+ def get_dataset_tables_md(self, tables=[]) -> str:
1299
+ if not self.jwt_token:
1300
+ self.superset_authenticate()
1301
+
1302
+ dataset_links = self.get_dataset_links()
1303
+ if not dataset_links:
1304
+ logging.error("No dataset links found.")
1305
+ return ""
1306
+
1307
+ self.get_dataset_data(dataset_links)
1308
+
1309
+ # Load data from the CSV file
1310
+ records = []
1311
+ try:
1312
+ with open("dataset_metadata.csv", mode="r", encoding="utf-8") as f:
1313
+ reader = csv.DictReader(f)
1314
+ for row in reader:
1315
+ records.append(row)
1316
+ except Exception as e:
1317
+ logging.error(f"Failed to load dataset_metadata.csv: {e}")
1318
+ return ""
1319
+
1320
+ # Group columns by table_name
1321
+ table_columns = {}
1322
+ for row in records:
1323
+ table_name = row.get("table_name")
1324
+ if tables and table_name not in tables:
1325
+ continue
1326
+
1327
+ if table_name not in table_columns:
1328
+ table_columns[table_name] = []
1329
+
1330
+ column_name = row.get("column_name", "")
1331
+ col_type = row.get("column_type", "")
1332
+ col_type = row.get("column_type", "").upper()
1333
+ expr = row.get("expression", "") or ""
1334
+
1335
+ if col_type == "STRING" and (" in (" in expr.lower() or " in(" in expr.lower()):
1336
+ like_compliant = False
1337
+ else:
1338
+ like_compliant = True
1339
+
1340
+
1341
+ table_columns[table_name].append({
1342
+ "column_name": column_name,
1343
+ "label": row.get("column_verbose_name") or column_name.replace("_", " ").title(),
1344
+ "type": col_type,
1345
+ "description": row.get("column_description", ""),
1346
+ "like_compliant": str(like_compliant)
1347
+ })
1348
+
1349
+ # Build markdown
1350
+ md_parts = []
1351
+ for table_name, cols in table_columns.items():
1352
+ md_parts.append(f"## `{table_name}` Columns\n\n")
1353
+ md_parts.append("| Column Name | Label | Type | Description | Like-compliant |\n")
1354
+ md_parts.append("|-------------|-------|------|-------------|----------------|\n")
1355
+
1356
+ for col in cols:
1357
+ md_parts.append(
1358
+ f"| `{col['column_name']}` | {col['label']} | {col['type']} | "
1359
+ f"{col['description']} | {col['like_compliant']} |\n"
1360
+ )
1361
+ md_parts.append("\n")
1362
+
1363
+ markdown_output = "".join(md_parts)
1364
+
1365
+ # Write to file
1366
+ with open("dataset_tables.md", "w", encoding="utf-8") as f:
1367
+ f.write(markdown_output)
1368
+
1369
+ logging.info("Saved table metadata Markdown to dataset_tables.md")
1370
+ return markdown_output