# Copyright (C) 2025 James Brotosky, Brandon Wickline # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published # by the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . import json import logging import os import platform import re import subprocess import tempfile import tkinter as tk from tkinter import filedialog, messagebox, simpledialog from typing import Callable, Optional, TypeVar import pandas as pd logger = logging.getLogger(__name__) T = TypeVar("T") def load_env_json(key: str, default: str): raw = os.getenv(key, default) try: return json.loads(raw) except json.JSONDecodeError: try: escaped = raw.encode('unicode_escape').decode('utf-8') return json.loads(escaped) except Exception as e: logging.error(f"Failed to parse {key}: {e}") return json.loads(default) def load_env(key: str, cast_type: Callable[[str], T] = str, default: Optional[T] = None) -> Optional[T]: """ Safely retrieves an environment variable and casts it to the desired type. Parameters: key (str): The name of the environment variable. cast_type (Callable[[str], T], optional): Function to cast the value. Defaults to str. default (Optional[T], optional): Default value if the variable is not set or invalid. Returns: Optional[T]: The casted value or the default. """ value = os.getenv(key) if value is None: logger.warning(f"Environment variable '{key}' not set.") return default try: value = value.strip("'\"") # Strip surrounding quotes return cast_type(value) except (ValueError, TypeError): logger.warning(f"Invalid value for env var '{key}': {value}. Expected type {cast_type.__name__}.") return default def import_to_dataframe(file_path: str) -> pd.DataFrame: df = pd.DataFrame() try: if not os.path.exists(file_path): print(colorText(f"Error: File '{file_path}' does not exist.", "red")) return df ext = os.path.splitext(file_path)[1].lower() if ext == ".csv": df = pd.read_csv(file_path) elif ext == ".parquet": df = pd.read_parquet(file_path) else: print(colorText(f"Error: Unsupported file extension '{ext}'.", "red")) return df if df.empty: print(colorText("Error: File has headers but no data rows.", "red")) else: print(colorText(f"Data loaded successfully from {file_path}", "green")) return df except pd.errors.EmptyDataError: print( colorText( "Notice: CSV file is completely empty, falling back to empty frame", "white", ) ) return pd.DataFrame() except Exception as e: print(colorText(f"Error reading file: {e}", "red")) return pd.DataFrame() def choose_directory(): root = tk.Tk() root.withdraw() # Hide the main window directory = filedialog.askdirectory(title="Select a Directory") print("Selected directory:", directory) return directory def choose_file(initial_directory=None, required_substring=None): """Open a file dialog and ensure the selected file contains a required substring.""" while True: root = tk.Tk() root.withdraw() # Hide the main window file_path = filedialog.askopenfilename(initialdir=initial_directory) if not file_path: print("No file selected.") return None if required_substring and required_substring not in file_path: print( f"The selected file must contain '{required_substring}' in its path or name. Please try again." ) else: return file_path def choose_save_location(): root = tk.Tk() root.withdraw() save_path = filedialog.asksaveasfilename(defaultextension=".txt") return save_path def ask_user_input(message): root = tk.Tk() root.withdraw() user_input = simpledialog.askstring("Input", "{message}]:") return user_input def show_info_message_cli(title, message): root = tk.Tk() root.withdraw() root.after(100, lambda: messagebox.showinfo(title, message)) root.mainloop() def show_confirm_question(): root = tk.Tk() root.withdraw() response = messagebox.askquestion("Confirm", "Do you want to continue?") print("User response:", response) def regulator(paths, case_insensitive=True): """ Build a regex pattern that matches any of the given Windows path fragments. """ escaped = [re.escape(p) for p in paths] pattern = "(?:" + "|".join(escaped) + ")" if case_insensitive: pattern = "(?i)" + pattern # Add inline case-insensitive flag print(f"Regulator is providing: {pattern}") return pattern def displayIntro(): print( colorText( r""" ███ ████ ░████████ █████████████ ███████████████ █████████████████████ █████████████████████ ███████████████████ ██████████████████████▓ ███████████████████ ██████████████████████ █████████████████████ ███████████████████████ ████████████████████████████████████████████████████████ █████████ ██ ██ █████████ █████████ ██ ███ █ █████████ █████████ ██ ████ █████ █████████████ █████████ ██ ██████ █████████████ ████████ ██ ███████ ████████████░ ███████ ██ ██▓ ██████ ████████████ ██████ ██ ████ █████ ███████████ █████████████████████████████████████████████████ ▒████████████████████ ██████████████████ ███████████████████ ███████████████▒ ███████████████ █████████████ ██████████ ███████████ ████████ ████ """, "yellow", ) ) print( colorText( r""" _____ .__ .__ __ ___________ .__ / _ \ |__|______| | ____ ____ | | __ \__ ___/___ ____ | | ______ / /_\ \| \_ __ \ | / _ \_/ ___\| |/ / | | / _ \ / _ \| | / ___/ / | \ || | \/ |_( <_> ) \___| < | |( <_> | <_> ) |__\___ \ \____|__ /__||__| |____/\____/ \___ >__|_ \ |____| \____/ \____/|____/____ > \/ \/ \/ \/ """, "cyan", ) ) print( colorText( "=================================================================================", "cyan", ) ) print( colorText( "======================== Welcome to the Airlock API Tool ========================", "cyan", ) ) print( colorText( "=================================================================================", "cyan", ) ) def printEnforceChecklist(selected_policies, destination_policy, destination_allowlist): working_dir = load_env("WORKING_DIR") print( colorText( "\n --------------------------------------------------------------------", "cyan", ) ) print( colorText( " ------------- 🛠️ 🔒 Prepare to Enforce Policy 🛠️ 🔒 ------------------", "cyan", ) ) print( colorText( " --------------------------------------------------------------------", "cyan", ) ) print( colorText( "\nSequentually follow these steps to prepare a policy for enforcement:", "white", ) ) print( colorText( "\n1. Choose which originating policy or policies to move to enforcement", "cyan", ) ) if not selected_policies: print(colorText(" [✗] No policies have been chosen", "red")) else: print(colorText("The following policies have been choosen:", "green")) for policy in selected_policies: print(colorText(f" [✓] {policy.name}", "green")) print(colorText("2. Choose the destination policy and allowlist", "cyan")) if not destination_policy: print(colorText(" [✗] No destination policy has been chosen", "red")) elif destination_policy: print(colorText(f" [✓] {destination_policy[0].name} has been selected as the destination policy", "green")) if not destination_allowlist: print(colorText(" [✗] No allowlist has been chosen", "red")) elif destination_allowlist: print( colorText( f" [✓] {destination_allowlist[0].name} has been selected as allowlist", "green", ) ) print( colorText( "3. Pull and stage event history, combine the histories, add hash info, then categorize the hashes", "cyan", ) ) if not selected_policies: print(colorText(" [✗] No policies have been chosen", "red")) else: if os.path.exists(f"{working_dir}\\Needs_Review\\Review_First\\approved_executions.csv"): print(colorText(" [✓] Data has been fetched", "green")) else: print(colorText(" [✗] Data has not been fetched", "red")) print(colorText("4. Manually review the files:", "cyan")) print( colorText( " 'prepare_policy\\needs_approved\\good_hashes.csv' and 'prepare_policy\\needs_approved\\unknown_hashes.csv'\n", "cyan", ) ) print( colorText( " Remove the rows containing hashes you do not approve of, and those you would not approve of without metarules.", "cyan", ) ) print( colorText( " If metarules need to be created, please make note of them, and remove the row from the csv.", "cyan", ) ) print( colorText( " When complete, save both csv files to the directory 'approved' and choose this option.", "cyan", ) ) print( colorText( " This will combine these approved hashes with the automatically approved hashes and generate a list of paths to be reviewed", "cyan", ) ) if os.path.exists(f"{working_dir}\\Approved\\approved_executions.csv"): print(colorText(" [✓] Reviewed hashes have been loaded", "green")) else: print(colorText(" [✗] Reviewed hashes have not been loaded", "red")) if os.path.exists( f"{working_dir}\\Needs_Review\\Review_Second\\primary_Paths.csv", ): print(colorText(" [✓] Path review list created", "green")) else: print(colorText(" [✗] Path review list has not been created", "red")) print( colorText( "5. Manually review the files \n 'prepare_policy\\needs_approved\\primary_Paths.csv'\n 'prepare_policy\\needs_approved\\secondary_Paths.csv'", "cyan", ) ) print( colorText( " Remove the rows containing path exclusions you do not approve of. The secondary list can be not added at all if nothing is useful", "cyan", ) ) print(colorText(" When complete, save the csv file to the directory 'approved'", "cyan")) print( colorText( " Do the same process with the list of publishers forthe same directories", "cyan", ) ) print(colorText(" Preflight Lists will be generated", "cyan")) if os.path.exists( f"{working_dir}\\Approved\\primary_Paths.csv", ): print(colorText(" [✓] Reviewed path list detected", "green")) else: print(colorText(" [✗] Path review list has not been detected", "red")) if os.path.exists(f"{working_dir}\\Preflight\\approved_paths.csv") and os.path.exists( f"{working_dir}\\Preflight\\approved_hashes.csv" ): print(colorText(" [✓] Preflight Path Exclusion List has been generated", "green")) else: print(colorText(" [✗] Preflight Path Exclusion List has not been generated", "red")) print(colorText("6. Test ------------------------------------------------------", "cyan")) print(colorText(" Print rather than apply selected data.", "cyan")) print(colorText("7. Liftoff ------------------------------------------------------", "cyan")) print( colorText( " Apply path exclusions according to allowed and approved paths", "cyan", ) ) print(colorText(" Apply signed or attested hashes to Parent Allow List", "cyan")) print(colorText(" Apply approved, but unsigned hashes to the Child Allow List", "cyan")) print( colorText( "R. Remove/Reset Generated data - will prompt to allow keeping execution history", "cyan", ) ) print(colorText("F. 📂 - Open Working Directory", "cyan")) print(colorText("Q. 🔚 - Quit", "cyan")) def areYouSure(): print( colorText( "🛑****************************************************************************************************************************************🛑", "red", ) ) print( colorText( "⚠️=========================================================================================================================================⚠️", "yellow", ) ) print( colorText( "🛑========================================================================================================================================🛑", "red", ) ) print( colorText( "⚠️-------------This program will now begin to make changes to the Airlock Console. Do you understand and agree to proceed? ----------------⚠️", "yellow", ) ) print( colorText( "🛑========================================================================================================================================🛑", "red", ) ) print( colorText( "⚠️=========================================================================================================================================⚠️", "yellow", ) ) print( colorText( "🛑****************************************************************************************************************************************🛑", "red", ) ) def locked(): print( colorText( r""" ████████████████████████████████████████████████████████████████ ███ ██ ██ ██████ ███ ██ ████████████ ███ ██ ████ ███ ███ ██ ███ ███ ███ ██ ███ ███ ███ ██ ▒████████████████████ ███ ██ ██████████████████████ ███ ██ ██████████████████████ ███ ██ ██████████████████████ ███ ██ ██████████████████████ ███ ██ ██████████████████████ ███ ██ ███ ███ ███ ████████████████████████████████████████████████████████████████████ ▒██████████████████████████████████████████████████████████████████▒ ▒████ ▒████ ▓██████████████████████████████████████████ █████████████████████████████████████████████░ """, "yellow", ) ) def printDeviceEnforceChecklist(): print( colorText( "\n --------------------------------------------------------------------", "cyan", ) ) print( colorText( " ------------- 🛠️ 🔒 Prepare to Enforce Policy 🛠️ 🔒 ------------------", "cyan", ) ) print( colorText( " --------------------------------------------------------------------", "cyan", ) ) print( colorText( "\nSequentually follow these steps to prepare a policy for enforcement:", "white", ) ) print( colorText( "\n1. Choose which originating policy or policies to move to enforcement", "cyan", ) ) print( colorText( "2. Pull and stage event history, combine the histories, add hash info, then categorize the hashes", "cyan", ) ) print(colorText("3. Manually review the files:", "cyan")) print( colorText( " 'needs_approved\\good_{first_policy}_{second_policy}.csv' and 'needs_approved\\unknown_{first_policy}_{second_policy}.csv'", "cyan", ) ) print( colorText( " Remove the rows containing hashes you do not approve of, and those you would not approve of without metarules.", "cyan", ) ) print( colorText( " If metarules need to be created, please make note of them, and remove the row from the csv.", "cyan", ) ) print( colorText( " When complete, save both csv files to the directory 'approved' and choose this option.", "cyan", ) ) print( colorText( " This will combine these approved hashes with the automatically approved hashes and generate a list of paths to be reviewed", "cyan", ) ) print( colorText( "4. Manually review the file 'needs_approved\\paths_needing_review.csv'", "cyan", ) ) print( colorText( " Remove the rows containing path exclusions you do not approve of", "cyan", ) ) print(colorText(" When complete, save the csv file to the directory 'approved'", "cyan")) print( colorText( " Do the same process with the list of publishers forthe same directories", "cyan", ) ) print(colorText(" Preflight Lists will be generated", "cyan")) print(colorText("5. Choose the destination policy and parent and child allow list", "cyan")) print(colorText("6. Test ------------------------------------------------------", "cyan")) print(colorText(" Print rather than apply selected data.", "cyan")) print(colorText("7. Liftoff ------------------------------------------------------", "cyan")) print( colorText( " Apply path exclusions according to allowed and approved paths", "cyan", ) ) print(colorText(" Apply signed or attested hashes to Parent Allow List", "cyan")) print(colorText(" Apply approved, but unsigned hashes to the Child Allow List", "cyan")) print( colorText( "R. Remove/Reset Generated data - will prompt to allow keeping execution history", "cyan", ) ) print(colorText("Q. Quit", "cyan")) def colorText(text, color): colors = { "red": "\033[91m", "green": "\033[92m", "yellow": "\033[93m", "blue": "\033[94m", "magenta": "\033[95m", "cyan": "\033[96m", "white": "\033[97m", "reset": "\033[0m", } return f"{colors.get(color, colors['reset'])}{text}{colors['reset']}" def formatHTML(df, output_html_path=None, overwrite=True): from datetime import datetime # Get current date and filename for subtitle today = datetime.now().strftime("%d %B %Y") # Changed to "Day Month Year" filename = output_html_path.replace(".html", "") if output_html_path else "Report" dark_css = """ """ header = f"""

Airlock Tools

{filename} - {today}

""" html_table = df.to_html(index=False, escape=False) styled_html = ( f"\n" f"Airlock Tools Report\n" f"\n" f"{dark_css}\n" f"{header}\n" f"
\n" f" {html_table}\n" f"
\n" f"\n" f"" ) if output_html_path: with open(output_html_path, "w", encoding="utf-8") as f: f.write(styled_html) print(f"✅ Styled table saved to '{output_html_path}'") elif overwrite: with tempfile.NamedTemporaryFile( suffix=".html", delete=False, mode="w", encoding="utf-8" ) as f: f.write(styled_html) temp_path = f.name print(f"✅ Styled table saved to temporary file: {temp_path}") else: return styled_html def open_directory(path): system = platform.system() if system == "Windows": os.startfile(path) elif system == "Linux": subprocess.run(["xdg-open", path]) else: raise OSError(f"Unsupported operating system: {system}")