From b3bced32b3111d73ecdb7ee65b41406d8d442dff Mon Sep 17 00:00:00 2001 From: = <=> Date: Tue, 26 Aug 2025 20:49:03 -0400 Subject: [PATCH] This is working better so far --- AirlockTools.py | 219 ++++++++++++++++++++++++------------------------ 1 file changed, 109 insertions(+), 110 deletions(-) diff --git a/AirlockTools.py b/AirlockTools.py index b5c8ee5..3012cd1 100644 --- a/AirlockTools.py +++ b/AirlockTools.py @@ -22,6 +22,7 @@ import utils.pathfunctions import utils.pretty as ct import urllib3 import pandas as pd +import json import ast urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) @@ -52,6 +53,20 @@ def apivalidation(): case _: menu_main() + + +def tryToReadCSV(csv): + try: + df =pd.read_csv(csv) + if df.empty: + print(ct.colorText("Error: CSV file has headers but no data rows.", "red")) + else: + print(ct.colorText(f"Data loaded successfully from {csv}", "green")) + except pd.errors.EmptyDataError: + print(ct.colorText("Notice : CSV file is completely empty (no headers, no data), falling back to empty frame", "white")) + df = pd.DataFrame() # Create an empty DataFrame as fallback + return df + def menu_main(): while True: print(ct.colorText("\n-----------------------------------", "magenta")) @@ -141,53 +156,47 @@ def menu_prepare_to_enforce(): print(ct.colorText(f" [✓] {first_policy} has been selected as Policy 1","green")) print(ct.colorText(f" [✓] {second_policy} has been selected as Policy 2","green")) - print(ct.colorText("2. Pull and stage event history", "cyan")) + print(ct.colorText("2. Pulls and stages event history, combines the histories, adds hash info, then categorizes the hashes", "cyan")) - if os.path.exists(f"dataframe_csv\\df_aggregated_{first_policy}.csv") == True: - print(ct.colorText(f" [✓] This has been completed for {first_policy}","green")) - elif os.path.exists(f"dataframe_csv\\df_aggregated_{first_policy}.csv") == False: - print(ct.colorText(f" [✗] This step has not been completed","red")) - elif second_policy is not first_policy and os.path.exists(f"dataframe_csv\\df_aggregated_{second_policy}.csv") == True: - print(ct.colorText(f" [✓] This has been completed for {second_policy}","green")) - elif second_policy is not first_policy and os.path.exists(f"dataframe_csv\\df_aggregated_{second_policy}.csv") == False: - print(ct.colorText(f" [✓] This has not been completed for {second_policy}","red")) + if os.path.exists(f"dataframe_csv\\executionhist_{first_policy}.csv"): + print(ct.colorText(f" [✓] Execution history has been compiled for {first_policy}","green")) + elif not os.path.exists(f"dataframe_csv\\executionhist_{first_policy}.csv"): + print(ct.colorText(f" [✗] Execution history has not been compiled for {first_policy}","red")) + elif second_policy is not first_policy and os.path.exists(f"dataframe_csv\\executionhist_{second_policy}.csv"): + print(ct.colorText(f" [✓] Execution history has been compiled for {second_policy}","green")) + elif second_policy is not first_policy and not os.path.exists(f"dataframe_csv\\executionhist_{second_policy}.csv"): + print(ct.colorText(f" [✗] Execution history has not been compiled for {second_policy}","red")) - print(ct.colorText("3. Combine Staged policies", "cyan")) - - if os.path.exists(f"dataframe_csv\\df_aggregated_combo_{first_policy}_{second_policy}.csv") == True: - print(ct.colorText(" [✓] This step has been completed","green")) + if os.path.exists(f"dataframe_csv\\execuctionhist_combined_{first_policy}_{second_policy}.csv"): + print(ct.colorText(f" [✓] Execution history has been_combined_for {first_policy} and_{second_policy}", "green")) + else: + print(ct.colorText(f" [✗] Execution history has not been_combined_for {first_policy} and_{second_policy}", "red")) + + if os.path.exists(f"dataframe_csv\\augmented_combo_{first_policy}_{second_policy}.csv"): + print(ct.colorText(f" [✓] Hash Info has been added to the combined execution history", "green")) + else: + print(ct.colorText(f" [✗] Hash Info has not been added to the combined execution history", "red")) + + if os.path.exists(f"dataframe_csv\\hashes_needing_approval_{first_policy}_{second_policy}.csv") and os.path.exists(f"dataframe_csv\\automatically_approved_hashes_{first_policy}_{second_policy}.csv") and os.path.exists(f"dataframe_csv\\unapproved_hashes__{first_policy}_{second_policy}.csv"): + print(ct.colorText(f" [✓] Hashes have been cateogrized", "green")) else: - print(ct.colorText(" [✗] This step has not been completed","red")) + print(ct.colorText(f" [✗] Hashes have not been cateogrized", "red")) - print(ct.colorText("4. Add hash threat information to list of executions", "cyan")) - - if os.path.exists(f"dataframe_csv\\df_augmented_combo_{first_policy}_{second_policy}.csv") == True: - print(ct.colorText(" [✓] This step has been completed","green")) - else: - print(ct.colorText(" [✗] This step has not been completed","red")) - - print(ct.colorText("5. Categorize your hashes ", "cyan")) - - if os.path.isfile(f"dataframe_csv\\df_hashes_needing_approval_{first_policy}_{second_policy}.csv") and os.path.isfile(f"dataframe_csv\\df_automatically_approved_hashes_{first_policy}_{second_policy}.csv") and os.path.isfile(f"dataframe_csv\\df_unapproved_hashes__{first_policy}_{second_policy}.csv"): - print(ct.colorText(" [✓] This step has been completed","green")) - else: - print(ct.colorText(" [✗] This step has not been completed","red")) - - print(ct.colorText(f"6. Manually review the files \\dataframe_csv\\df_hashes_needing_approval_{first_policy}_{second_policy}.csv and dataframe_csv\\df_automatically_approved_hashes_{first_policy}_{second_policy}.csv", "cyan")) + print(ct.colorText(f"3. Manually review the files '\\dataframe_csv\\hashes_needing_approval_{first_policy}_{second_policy}.csv' and 'dataframe_csv\\automatically_approved_hashes_{first_policy}_{second_policy}.csv'", "cyan")) print(ct.colorText(" Remove the rows containing hashes you do not approve of, and those you would not approve of without metarules.", "cyan")) print(ct.colorText(" If metarules need to be created, please make note of them, and remove the row from the csv.", "cyan")) print(ct.colorText(" When complete, save both csv files to the directory 'manuallyapproved' and choose this option to combine these approved hashes with the automatically approved hashes and generate a list of paths to be reviewed", "cyan")) - if os.path.isfile(f"dataframe_csv\\df_paths_needing_review_{first_policy}_{second_policy}.csv"): + if os.path.isfile(f"dataframe_csv\\paths_needing_review_{first_policy}_{second_policy}.csv"): print(ct.colorText(" [✓] This step has been completed","green")) else: print(ct.colorText(" [✗] This step has not been completed","red")) - print(ct.colorText(f"7. Manually review the file df_paths_needing_review_{first_policy}_{second_policy}.csv", "cyan")) + print(ct.colorText(f"4. Manually review the file 'paths_needing_review_{first_policy}_{second_policy}.csv'", "cyan")) print(ct.colorText(" Remove the rows containing path exclusions you do not approve of" , "cyan")) print(ct.colorText(" When complete, save the csv file to the directory 'manuallyapproved' and choose this option to generate the preflight lists", "cyan")) - if os.path.isfile(f"manuallyapproved\\df_paths_needing_review_{first_policy}_{second_policy}.csv") and os.path.isfile("dataframe_csv\\df_hashdestination_{first_policy}_{second_policy}.csv") and os.path.isfile(f"dataframe_csv\\df_addtochildpolicy_{first_policy}_{second_policy}.csv") and os.path.isfile(f"dataframe_csv\\f_addtobaseline_{first_policy}_{second_policy}.csv"): + if os.path.isfile(f"manuallyapproved\\paths_needing_review_{first_policy}_{second_policy}.csv") and os.path.isfile("dataframe_csv\\hashdestination_{first_policy}_{second_policy}.csv") and os.path.isfile(f"dataframe_csv\\addtochildpolicy_{first_policy}_{second_policy}.csv") and os.path.isfile(f"dataframe_csv\\addtobaseline_{first_policy}_{second_policy}.csv"): print(ct.colorText(" [✓] This step has been completed","green")) else: print(ct.colorText(" [✗] This step has not been completed","red")) @@ -213,83 +222,85 @@ def menu_prepare_to_enforce(): print(ct.colorText("Please answer with 'yes' or 'no'.", "red")) elif choice == "2": - if not os.path.exists("dataframe_csv\\df_aggregated_{first_policy}.csv"): - executionhist_policy1 = utils.allowlist.pullPolicyExechistories(url,first_policy_tuple[0], first_policy_tuple[1],True) - df_aggregated_policy1 = utils.hashfunctions.aggregateHashes(executionhist_policy1) - df_aggregated_policy1.to_csv(f"dataframe_csv\\df_aggregated_{first_policy}.csv", index=False) - ct.style_dataframe_dark(df_aggregated_policy1, f"dataframe_html\\df_aggregated_{first_policy}.html") - print(ct.colorText(f"Staging of Exection history for policy: {first_policy} is complete","green")) + if not os.path.exists(f"dataframe_csv\\executionhist_{first_policy}.csv"): + exe1 = utils.allowlist.pullPolicyExechistories(url,first_policy_tuple[0], first_policy_tuple[1],True) + data = json.loads(exe1) + executionhist_policy1 = pd.DataFrame(data["response"]["exechistories"]) + executionhist_policy1.to_csv(f"dataframe_csv\\executionhist_{first_policy}.csv", index=False) + ct.style_dataframe_dark(executionhist_policy1, f"dataframe_html\\executionhist_{first_policy}.html") + print(ct.colorText(f"Staging of Execution history for policy: {first_policy} is complete","green")) - if not os.path.exists("dataframe_csv\\df_aggregated_{second_policy}.csv"): - executionhist_policy2 = utils.allowlist.pullPolicyExechistories(url,second_policy_tuple[0], second_policy_tuple[1],True) - df_aggregated_policy2 = utils.hashfunctions.aggregateHashes(executionhist_policy2) - df_aggregated_policy2.to_csv(f"dataframe_csv\\df_aggregated_{second_policy}.csv", index=False) - ct.style_dataframe_dark(df_aggregated_policy2, f"dataframe_html\\df_aggregated_{second_policy}.html") - print(ct.colorText(f"Staging of Exection history for policy: {second_policy} is complete","green")) + if not os.path.exists(f"dataframe_csv\\executionhist_{second_policy}.csv"): + exe2 = utils.allowlist.pullPolicyExechistories(url,first_policy_tuple[0], first_policy_tuple[1],True) + data2 = json.loads(exe2) + executionhist_policy2 = pd.DataFrame(data2["response"]["exechistories"]) + executionhist_policy2.to_csv(f"dataframe_csv\\executionhist_{second_policy}.csv", index=False) + ct.style_dataframe_dark(executionhist_policy2, f"dataframe_html\\executionhist_{second_policy}.html") + print(ct.colorText(f"Staging of Exection history for policy: {first_policy} is complete","green")) - elif choice == "3": - if second_policy is first_policy and os.path.exists(f"dataframe_csv\\df_aggregated_{first_policy}.csv"): - df1 = tryToReadCSV(f"dataframe_csv\\df_aggregated_{first_policy}.csv") - df_aggregated_combo = df1 - df_aggregated_combo.to_csv(f"dataframe_csv\\df_aggregated_combo_{first_policy}_{second_policy}.csv", index=False) - ct.style_dataframe_dark(df_aggregated_combo, f"dataframe_html\\df_aggregated_combo_{first_policy}_{second_policy}.html") - print(ct.colorText(f"Dataframes have been aggregated (combined)","green")) + #Combine the two policies execution histories + + if second_policy is first_policy: + execuctionhist_combined = executionhist_policy1 + execuctionhist_combined.to_csv(f"dataframe_csv\\execuctionhist_combined_{first_policy}_{second_policy}.csv", index=False) + ct.style_dataframe_dark(df_aggregated_combo, f"dataframe_html\\execuctionhist_combined_{first_policy}_{second_policy}.html") + print(ct.colorText(f"Dataframes have been combined","green")) - elif os.path.exists(f"dataframe_csv\\df_aggregated_{first_policy}.csv") and os.path.exists(f"dataframe_csv\\df_aggregated_{second_policy}.csv"): - df1 = tryToReadCSV(f"dataframe_csv\\df_aggregated_{first_policy}.csv") - df2 = tryToReadCSV(f"dataframe_csv\\df_aggregated_{second_policy}.csv") - df_aggregated_combo = pd.concat([df1 , df2], ignore_index=True) - df_aggregated_combo.to_csv(f"dataframe_csv\\df_aggregated_combo_{first_policy}_{second_policy}.csv", index=False) - ct.style_dataframe_dark(df_aggregated_combo, f"dataframe_html\\df_aggregated_combo_{first_policy}_{second_policy}.html") - print(ct.colorText(f"Dataframes have been aggregated (combined)","green")) + elif os.path.exists(f"dataframe_csv\\executionhist_{first_policy}.csv") and os.path.exists(f"dataframe_csv\\executionhist_{second_policy}.csv"): + execuctionhist_combined = pd.concat([tryToReadCSV(f"dataframe_csv\\executionhist_{first_policy}.csv") , tryToReadCSV(f"dataframe_csv\\executionhist_{second_policy}.csv")], ignore_index=True) + execuctionhist_combined.to_csv(f"dataframe_csv\\execuctionhist_combined_{first_policy}_{second_policy}.csv", index=False) + ct.style_dataframe_dark(df_aggregated_combo, f"dataframe_html\\execuctionhist_combined_{first_policy}_{second_policy}.html") + print(ct.colorText(f"Dataframes have been combined","green")) - else: - print(ct.colorText(f"Please stage your data before attempting this step","red")) + #Keep only unique combinations of hash, filename, and hostname + if f"dataframe_csv\\execuctionhist_combined_{first_policy}_{second_policy}.csv": + unique_executions = tryToReadCSV(f"dataframe_csv\\execuctionhist_combined_{first_policy}_{second_policy}.csv").drop_duplicates(subset=['sha256', 'filename', 'hostname']) + unique_executions.to_csv(f"dataframe_csv\\unique_executions{first_policy}_{second_policy}.csv") + ct.style_dataframe_dark(unique_executions, f"dataframe_html\\unique_execuctions.html") - elif choice == "4": - if os.path.exists(f"dataframe_csv\\df_aggregated_combo_{first_policy}_{second_policy}.csv"): - df_augmented = utils.hashfunctions.augmentAggregatedHashes(url,tryToReadCSV(f"dataframe_csv\\df_aggregated_combo_{first_policy}_{second_policy}.csv")) - df_augmented.to_csv(f"dataframe_csv\\df_augmented_combo_{first_policy}_{second_policy}.csv", index=False) - ct.style_dataframe_dark(df_augmented, f"dataframe_html\\df_augmented_combo_{first_policy}_{second_policy}.html") + + #Add Hash info to the combined execution history + if not os.path.exists(f"dataframe_html\\augmented_combo_{first_policy}_{second_policy}.html"): + print(ct.colorText(f"Preparing to pull hash info","green")) + augmented_combo= utils.hashfunctions.augmentAggregatedHashes(url,tryToReadCSV(f"dataframe_csv\\unique_executions{first_policy}_{second_policy}.csv")) + augmented_combo.to_csv(f"dataframe_csv\\augmented_combo_{first_policy}_{second_policy}.csv", index=False) + ct.style_dataframe_dark(augmented_combo, f"dataframe_html\\augmented_combo_{first_policy}_{second_policy}.html") print(ct.colorText(f"Hash reputation info added to dataframe","green")) + #Categorize the hashes + if os.path.exists(f"dataframe_csv\\hashes_needing_approval_{first_policy}_{second_policy}.csv") and os.path.exists(f"dataframe_csv\\automatically_approved_hashes_{first_policy}_{second_policy}.csv") and os.path.exists(f"dataframe_csv\\unapproved_hashes__{first_policy}_{second_policy}.csv"): + break else: - print(ct.colorText(f"Please combine your data with step 3 prior to attempting this step","red")) + categorized = utils.hashfunctions.categorizeHashes(pd.read_csv(f"dataframe_csv\\augmented_combo_{first_policy}_{second_policy}.csv"), threat_tolerance_constant, badpublisherlist) - elif choice == "5": - if os.path.exists(f"dataframe_csv\\df_augmented_combo_{first_policy}_{second_policy}.csv"): - categorized = utils.hashfunctions.categorizeHashes(pd.read_csv(f"dataframe_csv\\df_augmented_combo_{first_policy}_{second_policy}.csv"), threat_tolerance_constant, badpublisherlist) + categorized[0].to_csv(f"dataframe_csv\\hashes_needing_approval_{first_policy}_{second_policy}.csv", index=False) + ct.style_dataframe_dark(categorized[0], f"dataframe_html\\dashes_needing_approval_{first_policy}_{second_policy}.html") - categorized[0].to_csv(f"dataframe_csv\\df_hashes_needing_approval_{first_policy}_{second_policy}.csv", index=False) - ct.style_dataframe_dark(categorized[0], f"dataframe_html\\df_hashes_needing_approval_{first_policy}_{second_policy}.html") - - categorized[1].to_csv(f"dataframe_csv\\df_automatically_approved_hashes_{first_policy}_{second_policy}.csv", index=False) - ct.style_dataframe_dark(categorized[1], f"dataframe_html\\df_automatically_approved_hashes_{first_policy}_{second_policy}.html") - - categorized[2].to_csv(f"dataframe_csv\\df_unapproved_hashes__{first_policy}_{second_policy}.csv", index=False) - ct.style_dataframe_dark(categorized[2], f"dataframe_html\\df_unapproved_hashes_{first_policy}_{second_policy}.html") + categorized[1].to_csv(f"dataframe_csv\\automatically_approved_hashes_{first_policy}_{second_policy}.csv", index=False) + ct.style_dataframe_dark(categorized[1], f"dataframe_html\\automatically_approved_hashes_{first_policy}_{second_policy}.html") + categorized[2].to_csv(f"dataframe_csv\\unapproved_hashes__{first_policy}_{second_policy}.csv", index=False) + ct.style_dataframe_dark(categorized[2], f"dataframe_html\\unapproved_hashes_{first_policy}_{second_policy}.html") + print(ct.colorText(f"Hashes have been categorized","green")) - else: - print(ct.colorText(f"Please Augment your data with hash threat info using step 4 prior to attempting this step","red")) - - elif choice == "6": - if os.path.exists(f"manuallyapproved\\df_hashes_needing_approval_{first_policy}_{second_policy}.csv") and os.path.exists(f"manuallyapproved\\df_automatically_approved_hashes_{first_policy}_{second_policy}.csv"): - df1 = tryToReadCSV(f"manuallyapproved\\df_hashes_needing_approval_{first_policy}_{second_policy}.csv") - df2 = tryToReadCSV(f"manuallyapproved\\df_automatically_approved_hashes_{first_policy}_{second_policy}.csv") + + elif choice == "3": + if os.path.exists(f"manuallyapproved\\hashes_needing_approval_{first_policy}_{second_policy}.csv") and os.path.exists(f"manuallyapproved\\automatically_approved_hashes_{first_policy}_{second_policy}.csv"): + df1 = tryToReadCSV(f"manuallyapproved\\hashes_needing_approval_{first_policy}_{second_policy}.csv") + df2 = tryToReadCSV(f"manuallyapproved\\automatically_approved_hashes_{first_policy}_{second_policy}.csv") df_all_approved_hashes = pd.concat([df1 , df2], ignore_index=True) - df_all_approved_hashes.to_csv(f"dataframe_csv\\df_all_approved_hashes_{first_policy}_{second_policy}.csv", index=False) - ct.style_dataframe_dark(df_all_approved_hashes, f"dataframe_html\\df_all_approved_hashes_{first_policy}_{second_policy}.html") + df_all_approved_hashes.to_csv(f"dataframe_csv\\all_approved_hashes_{first_policy}_{second_policy}.csv", index=False) + ct.style_dataframe_dark(df_all_approved_hashes, f"dataframe_html\\all_approved_hashes_{first_policy}_{second_policy}.html") - df_paths_needing_review, df_path_ineligible = utils.pathfunctions.filepathInitialGroup(pd.read_csv(f"dataframe_csv\\df_all_approved_hashes_{first_policy}_{second_policy}.csv")) + df_paths_needing_review, df_path_ineligible = utils.pathfunctions.filepathInitialGroup(pd.read_csv(f"dataframe_csv\\all_approved_hashes_{first_policy}_{second_policy}.csv")) - df_paths_needing_review.to_csv(f"dataframe_csv\\df_paths_needing_review_{first_policy}_{second_policy}.csv", index=False) - ct.style_dataframe_dark(df_paths_needing_review, f"dataframe_html\\df_paths_needing_review_{first_policy}_{second_policy}.html") + df_paths_needing_review.to_csv(f"dataframe_csv\\paths_needing_review_{first_policy}_{second_policy}.csv", index=False) + ct.style_dataframe_dark(df_paths_needing_review, f"dataframe_html\\paths_needing_review_{first_policy}_{second_policy}.html") - df_path_ineligible.to_csv(f"dataframe_csv\\df_path_ineligible_{first_policy}_{second_policy}.csv", index=False) - ct.style_dataframe_dark(df_path_ineligible, f"dataframe_html\\df_path_ineligible_{first_policy}_{second_policy}.html") + df_path_ineligible.to_csv(f"dataframe_csv\\path_ineligible_{first_policy}_{second_policy}.csv", index=False) + ct.style_dataframe_dark(df_path_ineligible, f"dataframe_html\\path_ineligible_{first_policy}_{second_policy}.html") print(ct.colorText(f"Eligible paths determined","green")) @@ -297,11 +308,11 @@ def menu_prepare_to_enforce(): print(ct.colorText(f"Please manually approve hashes prior to this step","red")) - elif choice == "7": - if os.path.exists(f"manuallyapproved\\df_paths_needing_review_{first_policy}_{second_policy}.csv"): - df_approved = tryToReadCSV(f"manuallyapproved\\df_paths_needing_review_{first_policy}_{second_policy}.csv") - df_eligible = tryToReadCSV(f"dataframe_csv\\df_paths_needing_review_{first_policy}_{second_policy}.csv") - df_ineligible = tryToReadCSV(f"dataframe_csv\\df_path_ineligible_{first_policy}_{second_policy}.csv") + elif choice == "4": + if os.path.exists(f"manuallyapproved\\paths_needing_review_{first_policy}_{second_policy}.csv"): + df_approved = tryToReadCSV(f"manuallyapproved\\paths_needing_review_{first_policy}_{second_policy}.csv") + df_eligible = tryToReadCSV(f"dataframe_csv\\paths_needing_review_{first_policy}_{second_policy}.csv") + df_ineligible = tryToReadCSV(f"dataframe_csv\\path_ineligible_{first_policy}_{second_policy}.csv") approved_set = set([tuple(map(tuple, row)) for row in df_approved.values]) @@ -315,11 +326,11 @@ def menu_prepare_to_enforce(): ct.style_dataframe_dark(df_approved, f"preflight\\Approved_Path_Exclusions_{first_policy}_{second_policy}.html") #Seperate out what we arent excluding by path into those that will go into the baseline, and those that will b added to the child. - df_addtobaseline = df_ineligible[df_ineligible['reputation status'] == 'KNOWN'] + df_addtobaseline = df_ineligible[df_ineligible['reputation_status'] == 'KNOWN'] df_addtobaseline.to_csv(f"preflight\\Add_to_Baseline_{first_policy}_{second_policy}.csv", index=False) ct.style_dataframe_dark(df_addtobaseline, f"preflight\\Add_to_Baseline_{first_policy}_{second_policy}.html") - df_addtochildpolicy = df_ineligible[df_ineligible['reputation status'] == 'UNKNOWN'] + df_addtochildpolicy = df_ineligible[df_ineligible['reputation_status'] == 'UNKNOWN'] df_addtochildpolicy.to_csv(f"preflight\\Add_to_Child_Policy_{first_policy}_{second_policy}.csv", index=False) ct.style_dataframe_dark(df_addtochildpolicy, f"preflight\\Add_to_Child_Policy_{first_policy}_{second_policy}.html") @@ -338,18 +349,6 @@ def menu_prepare_to_enforce(): -def tryToReadCSV(csv): - try: - df =pd.read_csv(csv) - if df.empty: - print(ct.colorText("Error: CSV file has headers but no data rows.", "red")) - else: - print(ct.colorText("Data loaded successfully.", "green")) - except pd.errors.EmptyDataError: - print(ct.colorText("Notice : CSV file is completely empty (no headers, no data), falling back to empty frame", "white")) - df = pd.DataFrame() # Create an empty DataFrame as fallback - return df - if __name__ == "__main__":