From 3ee762a0a145f5385179d48ef36ed4d6fd7bacb8 Mon Sep 17 00:00:00 2001 From: Zarithas Date: Fri, 14 Nov 2025 17:07:49 -0500 Subject: [PATCH] Restructured TUI, expanded quietagent workflow --- Loxide.py | 2 +- Loxide_Icon.ico | Bin 0 -> 114411 bytes {widgets => TUI}/OTP_generate.py | 0 utils/tui.py => TUI/TUI.py | 42 +- {widgets => TUI}/agentmoveoperations.py | 26 +- TUI/allowlistselectionscreen.py | 616 +++++++++++++ {screens => TUI}/moveagentworkflowscreen.py | 6 +- {widgets => TUI}/multiagentselector.py | 0 {screens => TUI}/otpactivityscreen.py | 308 ++++++- {screens => TUI}/otpworkflowscreen.py | 2 +- {widgets => TUI}/policyselector.py | 5 +- {screens => TUI}/policyselectorscreen.py | 2 +- {widgets => TUI}/policytreewidget.py | 0 TUI/quietagentworkflowscreen.py | 848 ++++++++++++++++++ {widgets => TUI}/resultsdisplay.py | 0 .../theme_amber_terminal.py | 2 +- .../theme_retro_terminal.py | 0 {widgets => TUI}/themeselector.py | 0 flows/otp.py | 82 +- services/API.py | 13 + 20 files changed, 1770 insertions(+), 184 deletions(-) create mode 100644 Loxide_Icon.ico rename {widgets => TUI}/OTP_generate.py (100%) rename utils/tui.py => TUI/TUI.py (92%) rename {widgets => TUI}/agentmoveoperations.py (96%) create mode 100644 TUI/allowlistselectionscreen.py rename {screens => TUI}/moveagentworkflowscreen.py (92%) rename {widgets => TUI}/multiagentselector.py (100%) rename {screens => TUI}/otpactivityscreen.py (69%) rename {screens => TUI}/otpworkflowscreen.py (94%) rename {widgets => TUI}/policyselector.py (99%) rename {screens => TUI}/policyselectorscreen.py (98%) rename {widgets => TUI}/policytreewidget.py (100%) create mode 100644 TUI/quietagentworkflowscreen.py rename {widgets => TUI}/resultsdisplay.py (100%) rename themes/amber_terminal_theme.py => TUI/theme_amber_terminal.py (93%) rename themes/retro_terminal_theme.py => TUI/theme_retro_terminal.py (100%) rename {widgets => TUI}/themeselector.py (100%) diff --git a/Loxide.py b/Loxide.py index 47c9562..7fa448c 100644 --- a/Loxide.py +++ b/Loxide.py @@ -30,8 +30,8 @@ import urllib3 from services.API import AirlockAPIWrapper from services.security import getAPI +from TUI.TUI import run_Loxide from utils.setup import get_base_directory, setup -from utils.TUI import run_Loxide from utils.utils import irtang urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) diff --git a/Loxide_Icon.ico b/Loxide_Icon.ico new file mode 100644 index 0000000000000000000000000000000000000000..dce491feea5ab384c0552600e5a5dcc404401361 GIT binary patch literal 114411 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 literal 0 HcmV?d00001 diff --git a/widgets/OTP_generate.py b/TUI/OTP_generate.py similarity index 100% rename from widgets/OTP_generate.py rename to TUI/OTP_generate.py diff --git a/utils/tui.py b/TUI/TUI.py similarity index 92% rename from utils/tui.py rename to TUI/TUI.py index 9f32133..990cde3 100644 --- a/utils/tui.py +++ b/TUI/TUI.py @@ -22,25 +22,25 @@ from textual.widgets import ( from flows.otp import otp_revoke from flows.prepPolicy import menu_policy_enforce -from flows.quietAgent import findQuietAgents from models.agent import Agent from models.policy import Policy -from screens.moveagentworkflowscreen import MoveAgentWorkflowScreen -from screens.otpactivityscreen import OTPActivitiesScreen -from screens.otpworkflowscreen import OTPWorkflowScreen from services.API import AirlockAPIWrapper from services.policyhandler import confirmUpdateAfromE -from themes.amber_terminal_theme import get_amber_terminal_theme -from themes.retro_terminal_theme import get_retro_terminal_theme +from TUI.agentmoveoperations import AgentMoveOperations +from TUI.moveagentworkflowscreen import MoveAgentWorkflowScreen +from TUI.multiagentselector import MultiAgentSelector +from TUI.OTP_generate import OTPGenerator +from TUI.otpactivityscreen import OTPActivitiesScreen +from TUI.otpworkflowscreen import OTPWorkflowScreen +from TUI.policytreewidget import PolicyTreeWidget +from TUI.quietagentworkflowscreen import QuietAgentWorkflowScreen +from TUI.resultsdisplay import ResultsDisplay +from TUI.theme_amber_terminal import get_amber_terminal_theme +from TUI.theme_retro_terminal import get_retro_terminal_theme +from TUI.themeselector import ThemeSelector from utils.configmanager import load_env from utils.setup import get_base_directory, load_user_config from utils.utils import open_directory -from widgets.agentmoveoperations import AgentMoveOperations -from widgets.multiagentselector import MultiAgentSelector -from widgets.OTP_generate import OTPGenerator -from widgets.policytreewidget import PolicyTreeWidget -from widgets.resultsdisplay import ResultsDisplay -from widgets.themeselector import ThemeSelector dotenv.load_dotenv() @@ -114,8 +114,8 @@ class MainMenuScreen(Screen): "šŸ–„ļø - Find, Move, or Generate OTP for Agents", "move_agent_workflow_button", ), - ("šŸ“Š - OTP Activities By Agent", "otp_activities_button"), - ("šŸ”‡ - Find Quiet Hosts", "find_quiet_button"), + ("šŸ“Š - Review and appove OTP Activities", "otp_activities_button"), + ("šŸ”‡ - Find and Move Quiet Hosts to Enforcement", "find_quiet_button"), ], "policy": [ ("šŸ”’ - Prepare Policy For Enforcement", "policy_prep_button"), @@ -242,7 +242,6 @@ class MainMenuScreen(Screen): """Handle OTP generation request from the workflow.""" global _PENDING_JOB - # Log what we received logger.info( "OTP Generation requested: %d devices, requestor=%s, reason=%s, duration=%d", len(message.devices), @@ -251,7 +250,6 @@ class MainMenuScreen(Screen): message.duration, ) - # Set up the job to run the OTP generation _PENDING_JOB = ( "otp_workflow", message.devices, @@ -321,23 +319,21 @@ class MainMenuScreen(Screen): match button_id: case "move_agent_workflow_button": - # Push Move Agent workflow screen self.app.push_screen(MoveAgentWorkflowScreen(self.app.devices)) event.stop() - return # Don't exit the app case "otp_generate_button": - # NEW: Push OTP workflow screen instead of legacy function self.app.push_screen(OTPWorkflowScreen(self.app.devices)) event.stop() - return # Don't exit the app case "find_quiet_button": - _PENDING_JOB = ("legacy", findQuietAgents, (self.app.api,), {}) + self.app.push_screen( + QuietAgentWorkflowScreen(self.app.api, self.app.policies) + ) + event.stop() + return case "otp_activities_button": - # === FIXED: push the Textual OTPActivitiesScreen and return immediately === - # This must return so we don't fall through to the code that exits the app. self.app.push_screen(OTPActivitiesScreen()) event.stop() return diff --git a/widgets/agentmoveoperations.py b/TUI/agentmoveoperations.py similarity index 96% rename from widgets/agentmoveoperations.py rename to TUI/agentmoveoperations.py index 03e5da4..d3615f0 100644 --- a/widgets/agentmoveoperations.py +++ b/TUI/agentmoveoperations.py @@ -1,23 +1,3 @@ -""" -Agent Move Operations Widget Module - -This module provides a Textual-based UI widget for performing bulk operations on -agent devices in the Airlock system. It allows users to: -- View selected agents and their current policy assignments -- Move agents to local approval mode with OTP enforcement -- Toggle agents between audit and enforcement policy modes -- Select and move agents to alternate policies (future implementation) - -The widget tracks operation state, manages button availability, and displays -results with success/failure summaries that can be copied to clipboard. - -Dependencies: - - textual: TUI framework for building the widget and UI components - - models.agent: Agent model class - - services.agenthandler: Core agent operation functions - - flows.localApproval: Local approval workflow handling -""" - from dataclasses import asdict from datetime import datetime import logging @@ -33,9 +13,9 @@ from textual.widget import Widget from textual.widgets import Button, DataTable, Header, Static, TextArea from models.agent import Agent -from screens.otpworkflowscreen import OTPWorkflowScreen -from screens.policyselectorscreen import PolicySelectorScreen -from widgets.OTP_generate import OTPGenerator +from TUI.OTP_generate import OTPGenerator +from TUI.otpworkflowscreen import OTPWorkflowScreen +from TUI.policyselectorscreen import PolicySelectorScreen logger = logging.getLogger(__name__) diff --git a/TUI/allowlistselectionscreen.py b/TUI/allowlistselectionscreen.py new file mode 100644 index 0000000..fd84f20 --- /dev/null +++ b/TUI/allowlistselectionscreen.py @@ -0,0 +1,616 @@ +from __future__ import annotations + +import logging +from typing import Optional + +import pandas as pd +from textual.app import ComposeResult +from textual.binding import Binding +from textual.containers import Horizontal, Vertical +from textual.screen import Screen +from textual.widgets import ( + Button, + DataTable, + Footer, + Header, + Static, + TextArea, +) + +logger = logging.getLogger(__name__) + + +class AllowlistSelectionWidget(Static): + """ + Widget for selecting an allowlist and adding hashes to it. + Can be reused in different workflows. + """ + + DEFAULT_CSS = """ + AllowlistSelectionWidget { + height: 1fr; + layout: vertical; + } + #allowlist_main { + height: 100%; + width: 100%; + } + #left_panel { + width: 50%; + padding: 1; + border: solid $primary; + } + #right_panel { + width: 50%; + padding: 1; + border: solid $primary; + } + #allowlist_table { + height: 70%; + margin: 1 0; + } + #allowlist_table > .datatable--header { + text-style: bold; + background: $boost; + } + #allowlist_table Row { + height: 1; + } + #preview_area { + height: 60%; + margin: 1 0; + } + #action_buttons { + height: 10%; + padding: 1; + content-align: center middle; + } + .panel-title { + text-style: bold; + margin: 0 0 1 0; + } + .info-text { + margin: 1 0; + } + """ + + def __init__( + self, + selected_data: pd.DataFrame, + api=None, + hostname: Optional[str] = None, + otpid: Optional[str] = None, + hash_column: str = "sha256", # Default hash column name + ): + """ + Initialize the allowlist selection widget. + + Args: + selected_data: DataFrame containing the selected activities + api: API instance for making allowlist calls + hostname: Optional hostname for context + otpid: Optional OTP ID for context + hash_column: Name of the column containing hashes (default: "sha256") + """ + super().__init__() + self.selected_data = selected_data + self.api = api + self.hostname = hostname + self.otpid = otpid + self.hash_column = hash_column + self.allowlists = [] + self.selected_allowlist = None + self.hashes_to_add = [] + + def compose(self) -> ComposeResult: + with Horizontal(id="allowlist_main"): + # Left panel - Allowlist selection + with Vertical(id="left_panel"): + yield Static("Select Allowlist", classes="panel-title") + yield Static( + f"Choose an allowlist to add {len(self.selected_data)} selected items", + classes="info-text", + ) + + # Allowlist table + self.allowlist_table = DataTable(id="allowlist_table") + self.allowlist_table.cursor_type = "row" + yield self.allowlist_table + + # Refresh button + self.refresh_btn = Button( + "šŸ”„ Refresh Allowlists", id="refresh_allowlists_btn" + ) + yield self.refresh_btn + + # Right panel - Preview and actions + with Vertical(id="right_panel"): + yield Static("Preview", classes="panel-title") + + # Context information + context_text = [] + if self.hostname: + context_text.append(f"Host: {self.hostname}") + if self.otpid: + context_text.append(f"OTP: {self.otpid}") + context_text.append(f"Selected Activities: {len(self.selected_data)}") + + yield Static(" | ".join(context_text), classes="info-text") + + # Preview text area + self.preview_area = TextArea( + id="preview_area", read_only=True, language="markdown" + ) + yield self.preview_area + + # Hash statistics + self.stats_label = Static("", id="stats_label", classes="info-text") + yield self.stats_label + + # Action buttons at bottom + with Horizontal(id="action_buttons"): + self.back_btn = Button("← Back", id="back_btn") + self.add_btn = Button("āž• Add to Allowlist", id="add_to_allowlist_btn") + + self.back_btn.styles.width = "50%" + self.add_btn.styles.width = "50%" + self.add_btn.disabled = True # Disabled until allowlist selected + + yield self.back_btn + yield self.add_btn + + async def on_mount(self) -> None: + """Load allowlists when widget mounts.""" + await self.load_allowlists() + await self.extract_and_preview_hashes() + + async def load_allowlists(self) -> None: + """Load available allowlists from API, grouped by policy association.""" + if not self.api: + logger.error("No API available") + self.allowlist_table.add_column("Error") + self.allowlist_table.add_row("No API available") + return + + try: + # First, try to get the host's policy if hostname is provided + host_policy_allowlists = [] + host_policy_ids = set() + policy_name = None + + if self.hostname: + try: + # Get agent info to find its policy + agents_df = self.api.agent_find_by_hostname(self.hostname) + if not agents_df.empty: + # Get the policy group ID for this host + group_id = agents_df.iloc[0].get("groupid") + policy_name = agents_df.iloc[0].get( + "groupname", "Unknown Policy" + ) + + if group_id: + # Get allowlists for this policy + policy_allowlists_df = self.api.policy_list_allowlists( + group_id + ) + if not policy_allowlists_df.empty: + host_policy_allowlists = policy_allowlists_df.to_dict( + orient="records" + ) + host_policy_ids = { + al.get("applicationid") + for al in host_policy_allowlists + } + logger.info( + f"Found {len(host_policy_allowlists)} allowlists for host's policy" + ) + except Exception as e: + logger.warning(f"Could not get host's policy allowlists: {e}") + + # Get all allowlists + all_allowlists_df = self.api.allowlist_find_all() + + if all_allowlists_df.empty: + self.allowlist_table.add_column("No Allowlists") + self.allowlist_table.add_row("No allowlists found") + return + + all_allowlists = all_allowlists_df.to_dict(orient="records") + + # Separate into two groups: policy-associated and others + other_allowlists = [ + al + for al in all_allowlists + if al.get("applicationid") not in host_policy_ids + ] + + # Sort each group alphabetically by name + host_policy_allowlists.sort(key=lambda x: x.get("name", "").lower()) + other_allowlists.sort(key=lambda x: x.get("name", "").lower()) + + # Combine lists with policy-associated first + self.allowlists = host_policy_allowlists + other_allowlists + + # Setup table columns + self.allowlist_table.clear() + self.allowlist_table.add_columns("Name", "Application ID", "Type") + + # Track which rows are headers vs actual allowlists + self._row_to_allowlist_map = {} + current_row = 0 + + # Add policy-associated allowlists if any + if host_policy_allowlists: + # Add section header + header_text = f"━━━ Policy: {policy_name or 'Host Policy'} ━━━" + self.allowlist_table.add_row(header_text, "", "", key="header_policy") + current_row += 1 + + # Add policy allowlists + for idx, allowlist in enumerate(host_policy_allowlists): + name = allowlist.get("name", "Unknown") + app_id = allowlist.get("applicationid", "Unknown") + + self.allowlist_table.add_row( + f" {name}", # Indent to show grouping + app_id, + "Policy", + key=f"policy_{idx}", + ) + self._row_to_allowlist_map[current_row] = idx + current_row += 1 + + # Add other allowlists + if other_allowlists: + # Add section header + if host_policy_allowlists: + # Add spacer if we have policy allowlists above + self.allowlist_table.add_row("", "", "", key="spacer") + current_row += 1 + + self.allowlist_table.add_row( + "━━━ Other Available Allowlists ━━━", "", "", key="header_other" + ) + current_row += 1 + + # Add other allowlists + for idx, allowlist in enumerate(other_allowlists): + name = allowlist.get("name", "Unknown") + app_id = allowlist.get("applicationid", "Unknown") + + self.allowlist_table.add_row( + f" {name}", # Indent to show grouping + app_id, + "General", + key=f"other_{idx}", + ) + # Map to the correct index in the combined list + actual_idx = len(host_policy_allowlists) + idx + self._row_to_allowlist_map[current_row] = actual_idx + current_row += 1 + + # Log summary + logger.info( + f"Loaded {len(self.allowlists)} total allowlists: " + f"{len(host_policy_allowlists)} policy-associated, " + f"{len(other_allowlists)} others" + ) + + # Update stats label if no allowlists in policy + if self.hostname and not host_policy_allowlists: + self.stats_label.update( + f"Note: No allowlists found for {self.hostname}'s policy | " + + self.stats_label.content.plain + ) + + except Exception as exc: + logger.exception(f"Failed to load allowlists: {exc}") + self.allowlist_table.add_column("Error") + self.allowlist_table.add_row(f"Failed to load: {str(exc)}") + + async def extract_and_preview_hashes(self) -> None: + """Extract hashes from selected data and show preview.""" + preview_lines = ["## Hash Extraction Summary\n"] + + # Check for hash column + if self.hash_column not in self.selected_data.columns: + # Try to find a hash column + possible_hash_cols = [ + "sha256", + "SHA256", + "hash", + "Hash", + "sha1", + "SHA1", + "md5", + "MD5", + "filehash", + "file_hash", + ] + found_col = None + for col in possible_hash_cols: + if col in self.selected_data.columns: + found_col = col + break + + if found_col: + self.hash_column = found_col + preview_lines.append(f"āœ“ Found hash column: **{found_col}**\n") + else: + preview_lines.append("āš ļø **No hash column found**\n") + preview_lines.append("Available columns:\n") + for col in self.selected_data.columns: + if col != "_row_id": + preview_lines.append(f" - {col}\n") + + self.preview_area.text = "".join(preview_lines) + self.stats_label.update("No hashes to add") + return + + # Extract unique hashes + hashes = self.selected_data[self.hash_column].dropna().unique() + self.hashes_to_add = [h for h in hashes if h and str(h).strip()] + + # Build preview + preview_lines.append(f"### Found {len(self.hashes_to_add)} unique hashes\n\n") + + # Show sample of hashes (first 10) + preview_lines.append("**Sample hashes to be added:**\n```\n") + for i, hash_val in enumerate(self.hashes_to_add[:10]): + preview_lines.append(f"{i+1}. {hash_val}\n") + if len(self.hashes_to_add) > 10: + preview_lines.append(f"... and {len(self.hashes_to_add) - 10} more\n") + preview_lines.append("```\n\n") + + # Show sample of source data + preview_lines.append("**Sample source activities:**\n") + sample_cols = [ + col + for col in self.selected_data.columns + if col not in ["_row_id"] and col in ["filename", "path", "action", "user"] + ] + if not sample_cols: + sample_cols = [ + col for col in self.selected_data.columns if col != "_row_id" + ][:3] + + if sample_cols: + preview_lines.append("```\n") + for i, row in self.selected_data[sample_cols].head(5).iterrows(): + row_text = " | ".join([f"{col}: {row[col]}" for col in sample_cols]) + preview_lines.append(f"{row_text}\n") + preview_lines.append("```\n") + + self.preview_area.text = "".join(preview_lines) + + # Update statistics + self.stats_label.update( + f"Ready to add {len(self.hashes_to_add)} unique hashes | " + f"From {len(self.selected_data)} selected activities" + ) + + async def on_data_table_row_selected(self, event) -> None: + """Handle allowlist selection.""" + try: + # Extract row index from event - handle different event structures + row_index = None + + # Try to get row index from coordinate + if hasattr(event, "coordinate") and hasattr(event.coordinate, "row"): + row_index = event.coordinate.row + # Try cursor_row as fallback + elif hasattr(event, "cursor_row"): + row_index = event.cursor_row + # Try getting from the table itself + else: + table = self.allowlist_table + if hasattr(table, "cursor_row"): + row_index = table.cursor_row + + # Validate row index + if row_index is not None and isinstance(row_index, int): + # Account for group headers in the row count + actual_allowlist_index = self._get_allowlist_index_from_row(row_index) + + if ( + actual_allowlist_index is not None + and 0 <= actual_allowlist_index < len(self.allowlists) + ): + self.selected_allowlist = self.allowlists[actual_allowlist_index] + self.add_btn.disabled = False + self.add_btn.label = ( + f"āž• Add to '{self.selected_allowlist.get('name', 'Unknown')}'" + ) + + # Update preview with selection + await self._update_preview_with_selection() + + logger.info( + f"Selected allowlist: {self.selected_allowlist.get('name')}" + ) + else: + logger.debug(f"Row {row_index} is a header or invalid") + else: + logger.warning(f"Could not extract valid row index from event: {event}") + + except Exception as exc: + logger.exception(f"Failed to select allowlist: {exc}") + + def _get_allowlist_index_from_row(self, row_index: int) -> Optional[int]: + """Convert table row index to allowlist list index, accounting for group headers.""" + # This will be updated when we have group headers + if hasattr(self, "_row_to_allowlist_map"): + return self._row_to_allowlist_map.get(row_index) + return row_index + + async def _update_preview_with_selection(self) -> None: + """Update preview when an allowlist is selected.""" + if not self.selected_allowlist: + return + + current_text = self.preview_area.text + # Remove any existing selection header + if "### Selected Allowlist:" in current_text: + lines = current_text.split("\n") + # Find and remove the selection lines + new_lines = [] + skip_next = False + for line in lines: + if line.startswith("### Selected Allowlist:"): + skip_next = True + continue + if skip_next and line.startswith("Application ID:"): + skip_next = False + continue + if not skip_next: + new_lines.append(line) + current_text = "\n".join(new_lines) + + # Add new selection at the top + selection_text = ( + f"### Selected Allowlist: **{self.selected_allowlist.get('name')}**\n" + f"Application ID: {self.selected_allowlist.get('applicationid')}\n\n" + ) + self.preview_area.text = selection_text + current_text + + async def on_button_pressed(self, event) -> None: + """Handle button presses.""" + btn = getattr(event, "button", None) or getattr(event, "sender", None) + btn_id = getattr(btn, "id", None) or getattr(event, "button_id", None) + + if btn is self.back_btn or btn_id == "back_btn": + await self.app.pop_screen() + event.stop() + return + + if btn is self.refresh_btn or btn_id == "refresh_allowlists_btn": + await self.load_allowlists() + event.stop() + return + + if btn is self.add_btn or btn_id == "add_to_allowlist_btn": + await self.add_hashes_to_allowlist() + event.stop() + return + + async def add_hashes_to_allowlist(self) -> None: + """Add the extracted hashes to the selected allowlist.""" + if not self.selected_allowlist or not self.hashes_to_add: + self.app.notify( + "No allowlist selected or no hashes to add", severity="warning" + ) + return + + if not self.api: + self.app.notify("API not available", severity="error") + return + + try: + # Disable button during operation + self.add_btn.disabled = True + self.add_btn.label = "ā³ Adding hashes..." + + # Call API to add hashes + app_id = self.selected_allowlist.get("applicationid") + allowlist_name = self.selected_allowlist.get("name", "Unknown") + + logger.info( + f"Adding {len(self.hashes_to_add)} hashes to allowlist {allowlist_name} (ID: {app_id})" + ) + + result = self.api.hash_add_to_allowlist(app_id, self.hashes_to_add) + + # Success notification + self.app.notify( + f"āœ… Successfully added {len(self.hashes_to_add)} hashes to '{allowlist_name}'", + title="Success", + severity="information", + timeout=5, + ) + + # Update preview to show success + self.preview_area.text = ( + f"## āœ… SUCCESS\n\n" + f"Added **{len(self.hashes_to_add)} hashes** to allowlist:\n" + f"**{allowlist_name}** (ID: {app_id})\n\n" + f"### Operation Details:\n" + f"- Source: {self.hostname or 'Multiple hosts'}\n" + f"- OTP ID: {self.otpid or 'N/A'}\n" + f"- Activities processed: {len(self.selected_data)}\n" + f"- Unique hashes added: {len(self.hashes_to_add)}\n" + ) + + # Change button to "Done" + self.add_btn.label = "āœ… Done - Close" + self.add_btn.disabled = False + + # When clicked again, close the screen + self.add_btn_success = True + + except Exception as exc: + logger.exception(f"Failed to add hashes to allowlist: {exc}") + self.app.notify( + f"āŒ Failed to add hashes: {str(exc)}", + title="Error", + severity="error", + timeout=10, + ) + + # Re-enable button + self.add_btn.disabled = False + self.add_btn.label = "āž• Retry Add to Allowlist" + + +class AllowlistSelectionScreen(Screen): + """ + Screen wrapper for the AllowlistSelectionWidget. + """ + + BINDINGS = [ + Binding("b", "back", "Back"), + Binding("r", "refresh", "Refresh Allowlists"), + Binding("enter", "confirm", "Add to Allowlist"), + ] + + def __init__( + self, + selected_data: pd.DataFrame, + api=None, + hostname: Optional[str] = None, + otpid: Optional[str] = None, + hash_column: str = "sha256", + ): + super().__init__() + self.selected_data = selected_data + self.api = api + self.hostname = hostname + self.otpid = otpid + self.hash_column = hash_column + + def compose(self) -> ComposeResult: + yield Header(show_clock=True) + self.widget = AllowlistSelectionWidget( + self.selected_data, + api=self.api, + hostname=self.hostname, + otpid=self.otpid, + hash_column=self.hash_column, + ) + yield self.widget + yield Footer() + + async def action_back(self) -> None: + """Go back to previous screen.""" + await self.app.pop_screen() + + async def action_refresh(self) -> None: + """Refresh the allowlists.""" + if hasattr(self, "widget") and self.widget: + await self.widget.load_allowlists() + + async def action_confirm(self) -> None: + """Confirm and add to allowlist.""" + if hasattr(self, "widget") and self.widget: + if self.widget.selected_allowlist and self.widget.hashes_to_add: + await self.widget.add_hashes_to_allowlist() diff --git a/screens/moveagentworkflowscreen.py b/TUI/moveagentworkflowscreen.py similarity index 92% rename from screens/moveagentworkflowscreen.py rename to TUI/moveagentworkflowscreen.py index da96331..8398ddf 100644 --- a/screens/moveagentworkflowscreen.py +++ b/TUI/moveagentworkflowscreen.py @@ -4,9 +4,9 @@ from textual.app import ComposeResult from textual.screen import Screen from models.agent import Agent -from widgets.agentmoveoperations import AgentMoveOperations -from widgets.multiagentselector import MultiAgentSelector -from widgets.resultsdisplay import ResultsDisplay +from TUI.agentmoveoperations import AgentMoveOperations +from TUI.multiagentselector import MultiAgentSelector +from TUI.resultsdisplay import ResultsDisplay class MoveAgentWorkflowScreen(Screen): diff --git a/widgets/multiagentselector.py b/TUI/multiagentselector.py similarity index 100% rename from widgets/multiagentselector.py rename to TUI/multiagentselector.py diff --git a/screens/otpactivityscreen.py b/TUI/otpactivityscreen.py similarity index 69% rename from screens/otpactivityscreen.py rename to TUI/otpactivityscreen.py index cc98655..84847e0 100644 --- a/screens/otpactivityscreen.py +++ b/TUI/otpactivityscreen.py @@ -11,6 +11,7 @@ from textual.containers import Horizontal, Vertical from textual.screen import Screen from textual.widgets import Button, DataTable, Footer, Header, Static +from TUI.allowlistselectionscreen import AllowlistSelectionScreen from utils.configmanager import load_env logger = logging.getLogger(__name__) @@ -134,14 +135,12 @@ class OTPActivitiesWidget(Static): or getattr(event, "button_id", None) or getattr(event, "id", None) ) - # ---- Back ---- if btn is self.back_btn or btn_id == getattr(self.back_btn, "id", None): while len(self.app.screen_stack) > 2: self.app.pop_screen() event.stop() return - # ---- Continue ---- if btn is self.continue_btn or btn_id == getattr(self.continue_btn, "id", None): if self._activities_df is None or self._activities_df.empty: @@ -172,7 +171,6 @@ class OTPActivitiesWidget(Static): except Exception as exc: logger.exception("Failed to push ActivityDetailScreen: %s", exc) return - # Unknown button on widget logger.debug( "Unhandled OTPActivitiesWidget button pressed (resolved btn=%r, id=%r)", @@ -212,7 +210,6 @@ class OTPActivitiesWidget(Static): row_key = getattr(event, attr, None) if row_key is not None: break - # If coordinate: try to extract .row or tuple[0] if row_key is None: coord = getattr(event, "coordinate", None) or getattr( @@ -223,7 +220,6 @@ class OTPActivitiesWidget(Static): row_key = coord.row elif isinstance(coord, (tuple, list)) and len(coord) >= 1: row_key = coord[0] - # If still nothing, maybe the event provides the row's cell values directly row_values = None for attr in ("values", "cells", "row", "row_values", "selected_row_values"): @@ -232,7 +228,6 @@ class OTPActivitiesWidget(Static): # Prefer actual sequence of cell values row_values = val break - # If we have row_values, try to map them back to the sessions DataFrame if row_values is not None: # Normalize into list of strings for comparison @@ -323,7 +318,6 @@ class OTPActivitiesWidget(Static): # Helpful debug hint for you to paste back if still failing: logger.debug("Event repr for debugging: %r", event) return - # At this point we should have an integer idx try: idx = int(idx) @@ -332,7 +326,6 @@ class OTPActivitiesWidget(Static): "Final normalization of selected row index failed: %r", idx ) return - # Validate sessions df if self._sessions_df is None or self._sessions_df.empty: logger.warning("Sessions DataFrame empty; nothing to select.") @@ -479,6 +472,7 @@ class ActivityDetailWidget(Static): """ Interactive widget for Activity Detail screen. Shows the provided DataFrame in a DataTable and offers Export + Back buttons. + Now includes Select All/None and Add to Allowlist functionality. """ DEFAULT_CSS = """ @@ -487,9 +481,14 @@ class ActivityDetailWidget(Static): layout: vertical; } #detail_table_container { - height: 85%; + height: 75%; padding: 1 1; } + #selection_buttons { + height: 10%; + padding: 1 1; + content-align: center middle; + } #detail_buttons { height: 15%; padding: 1 1; @@ -504,8 +503,16 @@ class ActivityDetailWidget(Static): if isinstance(activities_df, pd.DataFrame) else pd.DataFrame(activities_df) ) + # Add a unique identifier column if not present + if "_row_id" not in self.activities_df.columns: + self.activities_df["_row_id"] = range(len(self.activities_df)) + self.otpid = otpid self.hostname = hostname + self.selected_row_ids = set() # Track selected rows by unique ID + self.row_key_to_id = {} # Map DataTable row keys to unique row IDs + self.table_row_to_id = {} # Map table row indices to unique row IDs + self._last_sort = None # Track last sort column and order def compose(self) -> ComposeResult: yield Static( @@ -516,73 +523,267 @@ class ActivityDetailWidget(Static): with Vertical(id="detail_table_container"): self.detail_table = DataTable(id="detail_table") yield self.detail_table - # Buttons at bottom + + # Original buttons at bottom with Horizontal(id="detail_buttons"): self.detail_back_btn = Button("Back", id="detail_back_btn") - self.detail_export_btn = Button("Export (CSV)", id="detail_export_btn") - # Make them stretch equally - self.detail_back_btn.styles.width = "50%" - self.detail_export_btn.styles.width = "50%" + self.add_allowlist_btn = Button( + "šŸ“‹ Add Selected to Allowlist", id="add_allowlist_btn" + ) + yield self.add_allowlist_btn yield self.detail_back_btn - yield self.detail_export_btn async def on_mount(self) -> None: - # Populate table from activities_df - self.detail_table.clear() - if self.activities_df is None or self.activities_df.empty: - logger.info("ActivityDetailWidget mounted with empty dataframe.") - return - # Add columns - for col in self.activities_df.columns: - self.detail_table.add_column(col) + await self._build_table(rebuild=True) + self._update_button_states() + + def _update_button_states(self) -> None: + """Update button states based on selection.""" + has_selection = len(self.selected_row_ids) > 0 + self.add_allowlist_btn.disabled = not has_selection + + # Update button labels with count + count = len(self.selected_row_ids) + total = len(self.activities_df) + + if has_selection: + self.add_allowlist_btn.label = f"šŸ“‹ Add {count} Selected to Allowlist" + else: + self.add_allowlist_btn.label = "šŸ“‹ Add Selected to Allowlist" + + async def _build_table(self, rebuild: bool = True) -> None: + """Rebuild the DataTable. If rebuild=False, only refresh rows.""" + if rebuild: + # Full rebuild: clear columns and rows + self.detail_table.clear() + self.detail_table.columns.clear() + self.row_key_to_id.clear() + self.table_row_to_id.clear() + + if self.activities_df is None or self.activities_df.empty: + logger.info("ActivityDetailWidget mounted with empty dataframe.") + return + + # Add columns (checkbox + data columns, excluding internal _row_id) + self.detail_table.add_column("Select", key="select") + for col in self.activities_df.columns: + if col != "_row_id": # Don't display the internal ID column + self.detail_table.add_column(col) + else: + # Partial rebuild: clear rows only + self.detail_table.clear() + self.row_key_to_id.clear() + self.table_row_to_id.clear() + # Add rows - for _, row in self.activities_df.iterrows(): - vals = ["" if pd.isna(v) else v for v in row.to_list()] - self.detail_table.add_row(*[str(v) for v in vals]) - # Allow sorting / cursor - self.detail_table.cursor_type = "row" + for table_idx, (df_idx, row) in enumerate(self.activities_df.iterrows()): + # Get the unique row ID + row_id = row["_row_id"] + + # Build values list (excluding _row_id column) + vals = [] + for col in self.activities_df.columns: + if col != "_row_id": + v = row[col] + vals.append("" if pd.isna(v) else str(v)) + + # Check if this row is selected + checkbox = "ā˜‘" if row_id in self.selected_row_ids else "☐" + + # Add row to table + row_key = self.detail_table.add_row(checkbox, *vals) + + # Map the row key and table index to the unique row ID + self.row_key_to_id[row_key] = row_id + self.table_row_to_id[table_idx] = row_id + + async def on_data_table_cell_selected(self, event: DataTable.CellSelected) -> None: + # Toggle selection when the "Select" column is clicked + if event.cell_key.column_key.value == "select": + table_row_index = event.coordinate.row + + # Get the unique row ID for this table row + row_id = self.table_row_to_id.get(table_row_index) + if row_id is not None: + # Get the row key for updating the cell + row_key = event.cell_key.row_key + + if row_id in self.selected_row_ids: + self.selected_row_ids.remove(row_id) + self.detail_table.update_cell(row_key, "select", "☐") # Unchecked + else: + self.selected_row_ids.add(row_id) + self.detail_table.update_cell(row_key, "select", "ā˜‘") # Checked + + self._update_button_states() + + async def on_data_table_header_selected( + self, event: DataTable.HeaderSelected + ) -> None: + column_key = event.column_key.value if event.column_key else None + if not column_key: + col_index = event.column_index + if col_index == 0: # First column is "Select" + return + # Adjust for hidden _row_id column + visible_cols = [ + col for col in self.activities_df.columns if col != "_row_id" + ] + if col_index - 1 < len(visible_cols): + column_key = visible_cols[col_index - 1] + else: + return + if column_key == "select" or column_key == "_row_id": + return + + ascending = True + if self._last_sort == (column_key, True): + ascending = False + self._last_sort = (column_key, ascending) + + try: + self.activities_df.sort_values( + by=column_key, ascending=ascending, inplace=True + ) + except Exception as exc: + logger.exception("Failed to sort by column %s: %s", column_key, exc) + return + + # āœ… Only refresh rows, not columns + await self._build_table(rebuild=False) async def on_button_pressed(self, event) -> None: btn = getattr(event, "button", None) or getattr(event, "sender", None) btn_id = getattr(btn, "id", None) or getattr(event, "button_id", None) - # Back button in ActivityDetailWidget if btn is self.detail_back_btn or btn_id == "detail_back_btn": - # Pop screens until only the main menu remains while len(self.app.screen_stack) > 2: self.app.pop_screen() event.stop() return - # Export button - if btn is self.detail_export_btn or btn_id == "detail_export_btn": - await self._export_detail_activities() + + if btn is self.add_allowlist_btn or btn_id == "add_allowlist_btn": + await self._open_allowlist_screen() return - async def _export_detail_activities(self) -> None: + async def _select_all(self) -> None: + """Select all rows in the table.""" + # Add all row IDs to selected set + self.selected_row_ids = set(self.activities_df["_row_id"].tolist()) + # Update all checkboxes in the table + for row_key, row_id in self.row_key_to_id.items(): + self.detail_table.update_cell(row_key, "select", "ā˜‘") + + self._update_button_states() + logger.info(f"Selected all {len(self.selected_row_ids)} rows") + + async def _select_none(self) -> None: + """Deselect all rows in the table.""" + # Clear selected set + self.selected_row_ids.clear() + + # Update all checkboxes in the table + for row_key, row_id in self.row_key_to_id.items(): + self.detail_table.update_cell(row_key, "select", "☐") + + self._update_button_states() + logger.info("Cleared all selections") + + async def _open_allowlist_screen(self) -> None: + """Open the allowlist selection screen with selected activities.""" + if not self.selected_row_ids: + self.app.notify("No rows selected", severity="warning") + return + + # Get selected data + selected_df = self.get_selected_data() + + # Get API from app + api = getattr(self.app, "api", None) + if api is None: + logger.error("No API available on self.app.api") + self.app.notify("API not available", severity="error") + return + + # Create and push AllowlistSelectionScreen + try: + allowlist_screen = AllowlistSelectionScreen( + selected_df, api=api, hostname=self.hostname, otpid=self.otpid + ) + await self.app.push_screen(allowlist_screen) + logger.info( + f"Opened allowlist screen with {len(selected_df)} selected activities" + ) + except ImportError as e: + logger.error(f"Failed to import AllowlistSelectionScreen: {e}") + self.app.notify("Allowlist screen module not found", severity="error") + except Exception as e: + logger.exception(f"Failed to open allowlist screen: {e}") + self.app.notify( + f"Error opening allowlist screen: {str(e)}", severity="error" + ) + + async def _export_detail_activities(self) -> None: if self.activities_df is None or self.activities_df.empty: logger.info("No activities to export.") - notification = Static("āŒ No activities to export.", classes="notification") - self.mount(notification) + await self.mount( + Static("āŒ No activities to export.", classes="notification") + ) + return + if not self.selected_row_ids: + logger.info("No rows selected for export.") + await self.mount( + Static("āŒ No rows selected for export.", classes="notification") + ) return try: working_dir = load_env("WORKING_DIR") or os.getcwd() timestamp = datetime.now().strftime("%Y-%m-%d_%H-%M-%S") filename = f"otp_activities_detail_{timestamp}.csv" file_path = os.path.join(working_dir, filename) - self.activities_df.to_csv(file_path, index=False) - logger.info("Exported detail activities to %s", file_path) - # Show success notification - notification = Static( - f"āœ… Exported activities to: {filename}", classes="notification" + selected_df = self.get_selected_data() + selected_df.to_csv(file_path, index=False) + logger.info("Exported selected activities to %s", file_path) + await self.mount( + Static( + f"āœ… Exported selected activities to: {filename}", + classes="notification", + ) ) - self.mount(notification) except Exception as exc: logger.exception("Failed to export detail activities: %s", exc) - notification = Static( - "āŒ Failed to export activities; check logs.", classes="notification" + await self.mount( + Static( + "āŒ Failed to export activities; check logs.", + classes="notification", + ) ) - self.mount(notification) + + # āœ… Helper methods + def get_selected_data(self) -> pd.DataFrame: + """Return a DataFrame of the selected rows.""" + if not self.selected_row_ids: + return pd.DataFrame() + # Filter by selected row IDs and drop the internal _row_id column + selected_df = self.activities_df[ + self.activities_df["_row_id"].isin(self.selected_row_ids) + ].copy() + if "_row_id" in selected_df.columns: + selected_df = selected_df.drop(columns=["_row_id"]) + return selected_df + + def get_selected_records(self) -> list[dict]: + """Return selected rows as a list of dicts.""" + if not self.selected_row_ids: + return [] + # Filter by selected row IDs and drop the internal _row_id column + selected_df = self.activities_df[ + self.activities_df["_row_id"].isin(self.selected_row_ids) + ].copy() + if "_row_id" in selected_df.columns: + selected_df = selected_df.drop(columns=["_row_id"]) + return selected_df.to_dict(orient="records") class ActivityDetailScreen(Screen): @@ -590,7 +791,12 @@ class ActivityDetailScreen(Screen): Screen that wraps ActivityDetailWidget. Expects a DataFrame passed on init. """ - BINDINGS = [Binding("b", "back", "Back"), Binding("e", "export", "Export")] + BINDINGS = [ + Binding("b", "back", "Back"), + Binding("e", "export", "Export"), + Binding("a", "select_all", "Select All"), + Binding("n", "select_none", "Select None"), + ] def __init__(self, activities_df: pd.DataFrame, otpid=None, hostname=None) -> None: super().__init__() @@ -621,6 +827,16 @@ class ActivityDetailScreen(Screen): if hasattr(self, "widget") and self.widget is not None: await self.widget._export_detail_activities() + async def action_select_all(self) -> None: + """Handle 'a' key for select all.""" + if hasattr(self, "widget") and self.widget is not None: + await self.widget._select_all() + + async def action_select_none(self) -> None: + """Handle 'n' key for select none.""" + if hasattr(self, "widget") and self.widget is not None: + await self.widget._select_none() + class OTPActivitiesScreen(Screen): """ diff --git a/screens/otpworkflowscreen.py b/TUI/otpworkflowscreen.py similarity index 94% rename from screens/otpworkflowscreen.py rename to TUI/otpworkflowscreen.py index 519ff6b..d7d7322 100644 --- a/screens/otpworkflowscreen.py +++ b/TUI/otpworkflowscreen.py @@ -6,7 +6,7 @@ from textual.app import ComposeResult from textual.screen import Screen from models.agent import Agent -from widgets.OTP_generate import OTPGenerator +from TUI.OTP_generate import OTPGenerator class OTPWorkflowScreen(Screen): diff --git a/widgets/policyselector.py b/TUI/policyselector.py similarity index 99% rename from widgets/policyselector.py rename to TUI/policyselector.py index fb34218..d6e4bdf 100644 --- a/widgets/policyselector.py +++ b/TUI/policyselector.py @@ -14,7 +14,7 @@ import pandas as pd from textual.containers import Horizontal, Vertical from textual.message import Message from textual.widget import Widget -from textual.widgets import Button, DataTable, Footer, Header, Static, TextArea +from textual.widgets import Button, DataTable, Static, TextArea from models.policy import Policy @@ -93,7 +93,6 @@ class PolicySelector(Widget): - Policy table displaying available policies - Back buttons for navigation """ - yield Header(show_clock=True, icon="āš™") title_text = Static( "šŸŽÆ Select Target Policy", id="policy_selector_title", @@ -164,8 +163,6 @@ class PolicySelector(Widget): policy_table.styles.margin = (1, 0, 1, 0) yield policy_table - yield Footer() - def on_mount(self) -> None: """ Initialize the policy table when the widget is mounted. diff --git a/screens/policyselectorscreen.py b/TUI/policyselectorscreen.py similarity index 98% rename from screens/policyselectorscreen.py rename to TUI/policyselectorscreen.py index 5937860..07ec2ee 100644 --- a/screens/policyselectorscreen.py +++ b/TUI/policyselectorscreen.py @@ -25,7 +25,7 @@ import logging from textual.app import ComposeResult from textual.screen import Screen -from widgets.policyselector import PolicySelector +from TUI.policyselector import PolicySelector logger = logging.getLogger(__name__) diff --git a/widgets/policytreewidget.py b/TUI/policytreewidget.py similarity index 100% rename from widgets/policytreewidget.py rename to TUI/policytreewidget.py diff --git a/TUI/quietagentworkflowscreen.py b/TUI/quietagentworkflowscreen.py new file mode 100644 index 0000000..552f214 --- /dev/null +++ b/TUI/quietagentworkflowscreen.py @@ -0,0 +1,848 @@ +# 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 . + +""" +Quiet Agent Workflow Screen Module + +Provides a TUI workflow for identifying quiet agents and moving them to target policies. +This screen replaces the legacy quietAgent.py with a comprehensive TUI interface that: +1. Allows selection of an initial policy to analyze +2. Categorizes devices into "Enforce Ready" and "Non-Enforce Ready" based on activity +3. Allows users to select target policies for each category +4. Uses the API to move devices to their target policies +""" + +import datetime +import logging +import os +from typing import List, Optional + +import pandas as pd +from textual.app import ComposeResult +from textual.containers import Horizontal, Vertical +from textual.reactive import reactive +from textual.screen import Screen +from textual.widgets import Button, DataTable, Footer, Header, Static + +from models.policy import Policy +from services.API import AirlockAPIWrapper +from services.policyhandler import getPolicyInfo +from TUI.policyselector import PolicySelector +from utils.configmanager import load_env + +logger = logging.getLogger(__name__) + + +class QuietAgentWorkflowScreen(Screen): + """ + A Textual screen for the Quiet Agent analysis and migration workflow. + + This screen provides a multi-step workflow: + 1. Select initial policy to analyze + 2. View categorized agents (enforce ready vs. non-enforce ready) + 3. Select target policies for each category + 4. Execute agent migrations + + Attributes: + api (AirlockAPIWrapper): API wrapper for Airlock operations + policies (List[Policy]): List of all available policies + selected_policy (Optional[Policy]): The initially selected policy to analyze + history_days (int): Number of days of history to pull (default: 150) + quiet_days (int): Number of days without execution to be considered quiet (default: 45) + agents_df (Optional[pd.DataFrame]): DataFrame of all agents with analysis results + enforce_ready_df (Optional[pd.DataFrame]): DataFrame of agents ready for enforcement + non_enforce_ready_df (Optional[pd.DataFrame]): DataFrame of agents not ready for enforcement + workflow_stage (str): Current stage of the workflow + """ + + BINDINGS = [ + ("escape", "go_back", "Back"), + ] + + workflow_stage = reactive("select_policy") # Tracks current workflow stage + + def __init__(self, api: AirlockAPIWrapper, policies: List[Policy]): + """ + Initialize the QuietAgentWorkflowScreen. + + Args: + api (AirlockAPIWrapper): API wrapper for Airlock operations + policies (List[Policy]): List of all available policies + """ + super().__init__() + self.api = api + self.policies = policies + self.selected_policy: Optional[Policy] = None + self.history_days = 150 # Fixed as per requirements + self.quiet_days = 45 # Default value + self.agents_df: Optional[pd.DataFrame] = None + self.enforce_ready_df: Optional[pd.DataFrame] = None + self.non_enforce_ready_df: Optional[pd.DataFrame] = None + self.enforce_ready_target_policy: Optional[Policy] = None + self.non_enforce_ready_target_policy: Optional[Policy] = None + + def compose(self) -> ComposeResult: + """Build the UI layout for the workflow screen.""" + # Include Header and Footer like other standalone screens + yield Header(show_clock=True, icon="āš™") + + # Title area + title = Static("šŸ”’ Quiet Agent Workflow", id="workflow_title") + title.styles.margin = (0, 0, 0, 1) + yield title + + # Status area + status = Static("Step 1: Select Policy to Analyze", id="workflow_status") + status.styles.margin = (0, 0, 1, 1) + yield status + + # Content area - dynamically populated based on workflow stage + yield Vertical(id="content_area") + + yield Footer() + + def on_mount(self) -> None: + """Initialize the screen when mounted.""" + # Show initial policy selection + self._show_policy_selection() + + def watch_workflow_stage(self, old_value: str, new_value: str) -> None: + """React to workflow stage changes.""" + logger.debug(f"Workflow stage changed from {old_value} to {new_value}") + self._update_status_message() + + def _update_status_message(self) -> None: + """Update the status message based on current workflow stage.""" + status_widget = self.query_one("#workflow_status", Static) + + stage_messages = { + "select_policy": "Step 1: Select Policy to Analyze", + "select_quiet_days": "Step 2: Select Quiet Time Period", + "analyzing": "šŸ“Š Analyzing agent activity...", + "view_results": "Step 3: Review Categorized Agents", + "select_enforce_target": "Step 4: Select Target Policy for Enforce Ready Agents", + "select_non_enforce_target": "Step 5: Select Target Policy for Non-Enforce Ready Agents", + "confirm_migration": "Step 6: Confirm and Execute Migration", + "executing": "ā³ Executing agent migrations...", + "complete": "āœ… Migration Complete", + } + + status_widget.update(stage_messages.get(self.workflow_stage, "Unknown Stage")) + + def _show_policy_selection(self) -> None: + """Show the initial policy selection screen.""" + self.workflow_stage = "select_policy" + content = self.query_one("#content_area", Vertical) + content.remove_children() + + # Create policy selector widget + policy_selector = PolicySelector(self.policies) + content.mount(policy_selector) + + def on_policy_selector_policy_selected( + self, message: PolicySelector.PolicySelected + ) -> None: + """Handle policy selection from PolicySelector widget.""" + # Handle based on current workflow stage + if self.workflow_stage == "select_policy": + # Initial policy selection for analysis + self.selected_policy = message.policy + logger.info(f"Selected policy for analysis: {self.selected_policy.name}") + self._show_quiet_days_selection() + elif self.workflow_stage == "select_enforce_target": + # Target policy selection for enforce ready agents + self.enforce_ready_target_policy = message.policy + logger.info( + f"Selected target policy for enforce ready: {message.policy.name}" + ) + self._show_non_enforce_target_selection() + elif self.workflow_stage == "select_non_enforce_target": + # Target policy selection for non-enforce ready agents + self.non_enforce_ready_target_policy = message.policy + logger.info( + f"Selected target policy for non-enforce ready: {message.policy.name}" + ) + self._show_migration_confirmation() + + def _show_quiet_days_selection(self) -> None: + """Show the quiet days selection screen.""" + self.workflow_stage = "select_quiet_days" + content = self.query_one("#content_area", Vertical) + content.remove_children() + + # Create info text + info_widget = Static( + f"Policy Selected: {self.selected_policy.name}\n\n" + f"History Period: {self.history_days} days\n\n" + "Select quiet time period (days without untrusted execution):", + id="quiet_days_info", + ) + info_widget.styles.margin = (0, 0, 2, 0) + content.mount(info_widget) + + # Create button container and mount it first + button_container = Vertical(id="quiet_days_buttons") + button_container.styles.height = "auto" + content.mount(button_container) + + # Now add buttons to the mounted container + for days in [15, 30, 45, 60]: + btn = Button( + f"{days} days {'(Default)' if days == 45 else ''}", + id=f"quiet_days_{days}", + classes="quiet_day_btn", + ) + btn.styles.width = "100%" + btn.styles.margin = (0, 0, 1, 0) + button_container.mount(btn) + + back_btn = Button("← Back", id="back_to_policy_selection") + back_btn.styles.width = "100%" + back_btn.styles.margin = (2, 0, 0, 0) + button_container.mount(back_btn) + + def on_button_pressed(self, event: Button.Pressed) -> None: + """Handle button press events.""" + button_id = event.button.id + + # Quiet days selection buttons + if button_id and button_id.startswith("quiet_days_"): + days = int(button_id.split("_")[-1]) + self.quiet_days = days + logger.info(f"Selected quiet days: {days}") + self._start_analysis() + return + + # Navigation buttons + if button_id == "back_to_policy_selection": + self._show_policy_selection() + return + + if button_id == "back_to_results": + self._show_results() + return + + if button_id == "select_enforce_target_btn": + self._show_enforce_target_selection() + return + + if button_id == "select_non_enforce_target_btn": + self._show_non_enforce_target_selection() + return + + if button_id == "skip_enforce_target_btn": + # Skip enforce ready target selection + self.enforce_ready_target_policy = None + self._show_non_enforce_target_selection() + return + + if button_id == "skip_non_enforce_target_btn": + # Skip non-enforce ready target selection + self.non_enforce_ready_target_policy = None + self._show_migration_confirmation() + return + + if button_id == "confirm_migration_btn": + self._execute_migration() + return + + if button_id == "cancel_migration_btn": + self._show_results() + return + + if button_id == "export_results_btn": + self._export_results() + return + + if button_id == "start_over_btn": + self._show_policy_selection() + return + + def _start_analysis(self) -> None: + """Start the agent activity analysis.""" + self.workflow_stage = "analyzing" + content = self.query_one("#content_area", Vertical) + content.remove_children() + + # Show analyzing message with detailed steps + analyzing_msg = Static( + f"šŸ“Š Analyzing Agent Activity\n" + f"{'=' * 50}\n\n" + f"Policy: {self.selected_policy.name}\n" + f"History Period: {self.history_days} days\n" + f"Quiet Threshold: {self.quiet_days} days\n\n" + f"Progress:\n" + f"ā³ Step 1/4: Fetching agents from policy...\n" + f"ā±ļø Step 2/4: Pulling execution history (this may take a moment)...\n" + f"ā±ļø Step 3/4: Analyzing activity patterns...\n" + f"ā±ļø Step 4/4: Categorizing agents...\n\n" + f"Please wait - this operation cannot be cancelled.", + id="analyzing_message", + ) + analyzing_msg.styles.margin = (2, 1) + content.mount(analyzing_msg) + + # Show notification + self.app.notify( + "Starting analysis - this may take several minutes for large policies", + severity="information", + timeout=5, + ) + + # Perform the analysis asynchronously + self.call_later(self._perform_analysis) + + def _perform_analysis(self) -> None: + """Perform the actual agent activity analysis.""" + try: + # Update status: Fetching agents + self._update_analysis_status("Step 1/4: Fetching agents from policy...") + + # Get agents in the selected policy + agents = self.api.agents_find_by_group(self.selected_policy.groupid) + + if agents.empty: + self.app.notify( + f"No agents found in policy: {self.selected_policy.name}", + severity="warning", + timeout=5, + ) + self._show_policy_selection() + return + + agent_count = len(agents) + self.app.notify( + f"Found {agent_count} agents - fetching execution history...", + severity="information", + timeout=3, + ) + + # Update status: Pulling execution history + self._update_analysis_status( + f"Step 2/4: Pulling execution history for {agent_count} agents...\n" + f"(This may take several minutes - progress shown in terminal)" + ) + + # Get execution history (this shows progress bars in terminal via airlock_libs) + policy_exec_history = getPolicyInfo( + self.api, self.selected_policy, [1, 2, 6, 7], self.history_days + ) + + # Update status: Analyzing patterns + self._update_analysis_status("Step 3/4: Analyzing activity patterns...") + self.app.notify( + "History retrieved - analyzing patterns...", + severity="information", + timeout=2, + ) + + if policy_exec_history.empty: + logger.info( + "No execution history found for the selected policy and time range." + ) + # All agents are quiet (no executions) + agents["execution_count"] = 0 + agents["days_since"] = None + agents["required_quiet"] = self.quiet_days + agents["enforce_ready"] = True + else: + # Convert datetime column + policy_exec_history["datetime"] = pd.to_datetime( + policy_exec_history["datetime"], + format="%Y-%m-%dT%H:%M:%SZ", + utc=True, + ) + + # Calculate days ago + now = datetime.datetime.now(datetime.timezone.utc) + policy_exec_history["days_ago"] = policy_exec_history["datetime"].apply( + lambda dt: (now - dt).days + ) + + # Count total executions per hostname + hostname_counts = policy_exec_history["hostname"].value_counts() + agents["execution_count"] = ( + agents["hostname"].map(hostname_counts).fillna(0).astype(int) + ) + + # Find most recent execution per hostname + most_recent_exec = policy_exec_history.sort_values( + by="days_ago" + ).drop_duplicates(subset="hostname", keep="first") + + # Map most recent execution age to agents + agents["days_since"] = agents["hostname"].map( + most_recent_exec.set_index("hostname")["days_ago"] + ) + + # Check for enforcement readiness + agents["required_quiet"] = self.quiet_days + agents["enforce_ready"] = agents["days_since"].apply( + lambda x: True if pd.isna(x) or x > self.quiet_days else False + ) + + # Update status: Categorizing + self._update_analysis_status("Step 4/4: Categorizing agents...") + + # Sort agents + agents = agents.sort_values( + by=["execution_count", "hostname"], ascending=[True, True] + ) + + # Store the results + self.agents_df = agents + + # Categorize agents into DataFrames + self.enforce_ready_df = agents[agents["enforce_ready"] == True].copy() + self.non_enforce_ready_df = agents[agents["enforce_ready"] == False].copy() + + logger.info( + f"Analysis complete: {len(self.enforce_ready_df)} enforce ready, " + f"{len(self.non_enforce_ready_df)} non-enforce ready" + ) + + self.app.notify( + f"Analysis complete! Found {len(self.enforce_ready_df)} enforce ready, " + f"{len(self.non_enforce_ready_df)} not ready", + severity="success", + timeout=5, + ) + + # Show results + self._show_results() + + except Exception as e: + logger.error(f"Error during analysis: {e}", exc_info=True) + self.app.notify(f"Analysis failed: {str(e)}", severity="error", timeout=5) + self._show_policy_selection() + + def _update_analysis_status(self, status_text: str) -> None: + """Update the analysis status message.""" + try: + analyzing_msg = self.query_one("#analyzing_message", Static) + + # Build updated message + updated_text = ( + f"šŸ“Š Analyzing Agent Activity\n" + f"{'=' * 50}\n\n" + f"Policy: {self.selected_policy.name}\n" + f"History Period: {self.history_days} days\n" + f"Quiet Threshold: {self.quiet_days} days\n\n" + f"Progress:\n" + f"āœ… {status_text}\n\n" + f"Please wait - this operation cannot be cancelled." + ) + + analyzing_msg.update(updated_text) + except Exception as e: + logger.debug(f"Could not update analysis status: {e}") + + def _show_results(self) -> None: + """Show the categorized results.""" + self.workflow_stage = "view_results" + content = self.query_one("#content_area", Vertical) + content.remove_children() + + # Create results display container and mount it first + results_container = Vertical(id="results_container") + results_container.styles.height = "auto" + results_container.styles.margin = (1, 1) + content.mount(results_container) + + # Summary statistics + total_agents = len(self.enforce_ready_df) + len(self.non_enforce_ready_df) + ready_count = len(self.enforce_ready_df) + not_ready_count = len(self.non_enforce_ready_df) + ready_percentage = (ready_count / total_agents * 100) if total_agents > 0 else 0 + + summary = Static( + f"Analysis Results for: {self.selected_policy.name}\n\n" + f"šŸ“Š Total Agents: {total_agents}\n" + f"āœ… Enforce Ready: {ready_count} ({ready_percentage:.1f}%)\n" + f"āŒ Not Ready: {not_ready_count} ({100 - ready_percentage:.1f}%)\n\n" + f"Quiet Threshold: {self.quiet_days} days\n" + f"History Period: {self.history_days} days", + id="results_summary", + ) + summary.styles.margin = (0, 0, 2, 0) + results_container.mount(summary) + + # Action buttons + button_container = Horizontal(id="results_buttons") + button_container.styles.height = "auto" + results_container.mount(button_container) + + if ready_count > 0: + enforce_btn = Button( + f"Select Target for Enforce Ready ({ready_count})", + id="select_enforce_target_btn", + ) + enforce_btn.styles.margin = (0, 1, 1, 0) + button_container.mount(enforce_btn) + + if not_ready_count > 0: + non_enforce_btn = Button( + f"Select Target for Non-Enforce Ready ({not_ready_count})", + id="select_non_enforce_target_btn", + ) + non_enforce_btn.styles.margin = (0, 1, 1, 0) + button_container.mount(non_enforce_btn) + + export_btn = Button("šŸ’¾ Export Results", id="export_results_btn") + export_btn.styles.margin = (0, 1, 1, 0) + button_container.mount(export_btn) + + start_over_btn = Button("šŸ”„ Start Over", id="start_over_btn") + start_over_btn.styles.margin = (0, 0, 1, 0) + button_container.mount(start_over_btn) + + # Tables showing agents + tables_container = Horizontal() + tables_container.styles.height = "1fr" + results_container.mount(tables_container) + + # Enforce Ready table + if ready_count > 0: + enforce_col = Vertical() + enforce_col.styles.width = "1fr" + enforce_col.styles.margin = (1, 1, 0, 0) + tables_container.mount(enforce_col) + + enforce_label = Static("āœ… Enforce Ready Agents") + enforce_label.styles.margin = (0, 0, 1, 0) + enforce_col.mount(enforce_label) + + enforce_table = DataTable(id="enforce_ready_table") + enforce_table.styles.height = "1fr" + enforce_table.add_columns("Hostname", "Last Exec (days)") + + # Display first 50 agents + for idx, row in self.enforce_ready_df.head(50).iterrows(): + days_since = row["days_since"] + days_str = f"{int(days_since)}" if not pd.isna(days_since) else "Never" + enforce_table.add_row(row["hostname"], days_str) + + if len(self.enforce_ready_df) > 50: + enforce_table.add_row( + f"... and {len(self.enforce_ready_df) - 50} more", "" + ) + + enforce_col.mount(enforce_table) + + # Non-Enforce Ready table + if not_ready_count > 0: + non_enforce_col = Vertical() + non_enforce_col.styles.width = "1fr" + non_enforce_col.styles.margin = (1, 0, 0, 1) + tables_container.mount(non_enforce_col) + + non_enforce_label = Static("āŒ Non-Enforce Ready Agents") + non_enforce_label.styles.margin = (0, 0, 1, 0) + non_enforce_col.mount(non_enforce_label) + + non_enforce_table = DataTable(id="non_enforce_ready_table") + non_enforce_table.styles.height = "1fr" + non_enforce_table.add_columns("Hostname", "Last Exec (days)") + + # Display first 50 agents + for idx, row in self.non_enforce_ready_df.head(50).iterrows(): + days_since = row["days_since"] + days_str = f"{int(days_since)}" if not pd.isna(days_since) else "N/A" + non_enforce_table.add_row(row["hostname"], days_str) + + if len(self.non_enforce_ready_df) > 50: + non_enforce_table.add_row( + f"... and {len(self.non_enforce_ready_df) - 50} more", "" + ) + + non_enforce_col.mount(non_enforce_table) + + def _show_enforce_target_selection(self) -> None: + """Show policy selection for enforce ready agents.""" + self.workflow_stage = "select_enforce_target" + content = self.query_one("#content_area", Vertical) + content.remove_children() + + # Info message + info = Static( + f"Select target policy for {len(self.enforce_ready_df)} Enforce Ready agents\n" + f"Source Policy: {self.selected_policy.name}", + id="enforce_target_info", + ) + info.styles.margin = (0, 0, 2, 0) + content.mount(info) + + # Policy selector + policy_selector = PolicySelector(self.policies) + content.mount(policy_selector) + + # Skip button + skip_btn = Button("ā­•ļø Skip - No Migration", id="skip_enforce_target_btn") + skip_btn.styles.width = "50%" + skip_btn.styles.margin = (2, 0, 0, 0) + content.mount(skip_btn) + + def _show_non_enforce_target_selection(self) -> None: + """Show policy selection for non-enforce ready agents.""" + self.workflow_stage = "select_non_enforce_target" + content = self.query_one("#content_area", Vertical) + content.remove_children() + + # Info message + info = Static( + f"Select target policy for {len(self.non_enforce_ready_df)} Non-Enforce Ready agents\n" + f"Source Policy: {self.selected_policy.name}", + id="non_enforce_target_info", + ) + info.styles.margin = (0, 0, 2, 0) + content.mount(info) + + # Policy selector + policy_selector = PolicySelector(self.policies) + content.mount(policy_selector) + + # Skip button + skip_btn = Button("ā­•ļø Skip - No Migration", id="skip_non_enforce_target_btn") + skip_btn.styles.width = "50%" + skip_btn.styles.margin = (2, 0, 0, 0) + content.mount(skip_btn) + + def _show_migration_confirmation(self) -> None: + """Show migration confirmation screen.""" + self.workflow_stage = "confirm_migration" + content = self.query_one("#content_area", Vertical) + content.remove_children() + + # Build confirmation message + confirmation_lines = [ + "šŸ” Migration Summary\n", + f"Source Policy: {self.selected_policy.name}\n", + ] + + if self.enforce_ready_target_policy: + confirmation_lines.append( + f"\nāœ… Enforce Ready Migration:\n" + f" • Agents: {len(self.enforce_ready_df)}\n" + f" • Target: {self.enforce_ready_target_policy.name}\n" + ) + + if self.non_enforce_ready_target_policy: + confirmation_lines.append( + f"\nāŒ Non-Enforce Ready Migration:\n" + f" • Agents: {len(self.non_enforce_ready_df)}\n" + f" • Target: {self.non_enforce_ready_target_policy.name}\n" + ) + + if ( + not self.enforce_ready_target_policy + and not self.non_enforce_ready_target_policy + ): + confirmation_lines.append("\nāš ļø No migrations will be performed.") + + confirmation = Static("".join(confirmation_lines), id="migration_confirmation") + confirmation.styles.margin = (1, 1, 2, 1) + content.mount(confirmation) + + # Action buttons - mount container first, then add buttons + button_container = Horizontal(id="confirmation_buttons") + button_container.styles.height = "auto" + button_container.styles.margin = (1, 1) + content.mount(button_container) + + if self.enforce_ready_target_policy or self.non_enforce_ready_target_policy: + confirm_btn = Button("āœ… Confirm Migration", id="confirm_migration_btn") + confirm_btn.styles.margin = (0, 1, 0, 0) + button_container.mount(confirm_btn) + + cancel_btn = Button("āŒ Cancel", id="cancel_migration_btn") + button_container.mount(cancel_btn) + + def _execute_migration(self) -> None: + """Execute the agent migrations.""" + self.workflow_stage = "executing" + content = self.query_one("#content_area", Vertical) + content.remove_children() + + # Show executing message + executing_msg = Static( + "ā³ Executing agent migrations...\nPlease wait...", + id="executing_message", + ) + executing_msg.styles.margin = (2, 1) + content.mount(executing_msg) + + # Perform migrations asynchronously + self.call_later(self._perform_migrations) + + def _perform_migrations(self) -> None: + """Perform the actual agent migrations.""" + successful_migrations = [] + failed_migrations = [] + + try: + # Migrate enforce ready agents + if self.enforce_ready_target_policy: + for idx, row in self.enforce_ready_df.iterrows(): + try: + result = self.api.agent_move( + row["agentid"], self.enforce_ready_target_policy.groupid + ) + successful_migrations.append( + (row["hostname"], self.enforce_ready_target_policy.name) + ) + logger.debug( + f"Moved {row['hostname']} to {self.enforce_ready_target_policy.name}" + ) + except Exception as e: + failed_migrations.append((row["hostname"], str(e))) + logger.error(f"Failed to move {row['hostname']}: {e}") + + # Migrate non-enforce ready agents + if self.non_enforce_ready_target_policy: + for idx, row in self.non_enforce_ready_df.iterrows(): + try: + result = self.api.agent_move( + row["agentid"], self.non_enforce_ready_target_policy.groupid + ) + successful_migrations.append( + (row["hostname"], self.non_enforce_ready_target_policy.name) + ) + logger.debug( + f"Moved {row['hostname']} to {self.non_enforce_ready_target_policy.name}" + ) + except Exception as e: + failed_migrations.append((row["hostname"], str(e))) + logger.error(f"Failed to move {row['hostname']}: {e}") + + # Show completion results + self._show_completion_results(successful_migrations, failed_migrations) + + except Exception as e: + logger.error(f"Error during migration execution: {e}", exc_info=True) + self.app.notify(f"Migration failed: {str(e)}", severity="error", timeout=5) + self._show_results() + + def _show_completion_results( + self, successful: List[tuple], failed: List[tuple] + ) -> None: + """Show migration completion results.""" + self.workflow_stage = "complete" + content = self.query_one("#content_area", Vertical) + content.remove_children() + + # Results summary + total_attempted = len(successful) + len(failed) + success_rate = ( + (len(successful) / total_attempted * 100) if total_attempted > 0 else 0 + ) + + results = Static( + f"āœ… Migration Complete\n\n" + f"Total Agents Migrated: {len(successful)}\n" + f"Failed Migrations: {len(failed)}\n" + f"Success Rate: {success_rate:.1f}%", + id="completion_summary", + ) + results.styles.margin = (1, 1, 2, 1) + content.mount(results) + + # Details tables + if successful: + success_container = Vertical() + success_container.styles.margin = (0, 1) + content.mount(success_container) + + success_label = Static("āœ… Successful Migrations") + success_label.styles.margin = (0, 0, 1, 0) + success_container.mount(success_label) + + success_table = DataTable(id="success_table") + success_table.styles.height = "auto" + success_table.add_columns("Hostname", "Target Policy") + + for hostname, target_policy in successful[:25]: # Show first 25 + success_table.add_row(hostname, target_policy) + + if len(successful) > 25: + success_table.add_row(f"... and {len(successful) - 25} more", "") + + success_container.mount(success_table) + + if failed: + failed_container = Vertical() + failed_container.styles.margin = (2, 1, 0, 1) + content.mount(failed_container) + + failed_label = Static("āŒ Failed Migrations") + failed_label.styles.margin = (0, 0, 1, 0) + failed_container.mount(failed_label) + + failed_table = DataTable(id="failed_table") + failed_table.styles.height = "auto" + failed_table.add_columns("Hostname", "Error") + + for hostname, error in failed[:25]: # Show first 25 + failed_table.add_row(hostname, error[:50]) # Truncate error + + if len(failed) > 25: + failed_table.add_row(f"... and {len(failed) - 25} more", "") + + failed_container.mount(failed_table) + + # Action button + done_btn = Button("āœ” Done", id="start_over_btn") + done_btn.styles.width = "50%" + done_btn.styles.margin = (2, 0, 0, 0) + content.mount(done_btn) + + def _export_results(self) -> None: + """Export analysis results to CSV.""" + try: + working_dir = load_env("WORKING_DIR") or os.getcwd() + filename = os.path.join( + working_dir, + f"{self.selected_policy.name}_quiet_analysis_{datetime.datetime.now().strftime('%Y%m%d_%H%M%S')}.csv", + ) + + self.agents_df.to_csv(filename, index=False) + logger.info(f"Exported results to {filename}") + self.app.notify( + f"Results exported to:\n{filename}", + severity="information", + timeout=5, + ) + + except Exception as e: + logger.error(f"Failed to export results: {e}") + self.app.notify(f"Export failed: {str(e)}", severity="error", timeout=5) + + def action_go_back(self) -> None: + """Handle back/escape action.""" + # Depending on stage, go back to previous stage or exit + if self.workflow_stage in ["select_policy", "view_results", "complete"]: + self.app.pop_screen() + elif self.workflow_stage == "select_quiet_days": + self._show_policy_selection() + elif self.workflow_stage == "select_enforce_target": + self._show_results() + elif self.workflow_stage == "select_non_enforce_target": + if self.enforce_ready_target_policy: + self._show_enforce_target_selection() + else: + self._show_results() + elif self.workflow_stage == "confirm_migration": + self._show_non_enforce_target_selection() + else: + self.app.pop_screen() diff --git a/widgets/resultsdisplay.py b/TUI/resultsdisplay.py similarity index 100% rename from widgets/resultsdisplay.py rename to TUI/resultsdisplay.py diff --git a/themes/amber_terminal_theme.py b/TUI/theme_amber_terminal.py similarity index 93% rename from themes/amber_terminal_theme.py rename to TUI/theme_amber_terminal.py index 169c325..3662892 100644 --- a/themes/amber_terminal_theme.py +++ b/TUI/theme_amber_terminal.py @@ -12,7 +12,7 @@ def get_amber_terminal_theme(): success=Color.parse("#ffb733"), warning=Color.parse("#ffff66"), error=Color.parse("#ff3300"), - surface=Color.parse("#3a1f00"), # brighter brown for blending + surface=Color.parse("#49331a"), # brighter brown for blending ) diff --git a/themes/retro_terminal_theme.py b/TUI/theme_retro_terminal.py similarity index 100% rename from themes/retro_terminal_theme.py rename to TUI/theme_retro_terminal.py diff --git a/widgets/themeselector.py b/TUI/themeselector.py similarity index 100% rename from widgets/themeselector.py rename to TUI/themeselector.py diff --git a/flows/otp.py b/flows/otp.py index 720aab3..ee07c98 100644 --- a/flows/otp.py +++ b/flows/otp.py @@ -14,98 +14,18 @@ # along with this program. If not, see . -from datetime import datetime import logging -import os import pandas as pd from services.agenthandler import selectAgents from services.API import AirlockAPIWrapper -from utils.configmanager import load_env from utils.selector import Selector -from utils.utils import colorText, get_sanitized_input +from utils.utils import get_sanitized_input logger = logging.getLogger(__name__) -def otp_activities_by_agent(api: AirlockAPIWrapper): - activeagents = api.otp_find_active() - awaitingagents = api.otp_find_awaiting() - enforcedagents = api.otp_find_enforced() - revokedagents = api.otp_find_revoked() - - # Add a 'status' column to each DataFrame - activeagents["status"] = "active" - awaitingagents["status"] = "awaiting" - enforcedagents["status"] = "enforced" - revokedagents["status"] = "revoked" - - # Combine all into one DataFrame - combined_agents = pd.concat( - [activeagents, awaitingagents, enforcedagents, revokedagents], ignore_index=True - ) - combined_agents = combined_agents.sort_values(by="otpid", ascending=False) - - # Optionally, select specific hosts - user_input = ( - get_sanitized_input("\nWould you like to search for a specific device? (y/n): ") - .strip() - .lower() - ) - if user_input == "y": - agentnames = [] - agents = selectAgents(api) - for agent in agents: - agentnames.append(agent.hostname) - - combined_agents = combined_agents[combined_agents["hostname"].isin(agentnames)] - - # Present and select rows - selected_rows = Selector.select_dataframe_with_mode( - combined_agents, - columns=["otpid", "hostname", "status", "purpose", "granted"], - header="OTP Sessions", - ) - combined_df = pd.DataFrame() - - for row in selected_rows: - otpid = row["otpid"] - hostname = row["hostname"] - result = api.otp_get_activities(otpid) - result["hostname"] = hostname - if not result.empty: - logger.info(f"Activities for {hostname} (otpid: {otpid}):\n{result}") - combined_df = pd.concat([combined_df, result], ignore_index=True) - else: - logger.info(f"No activities found for {hostname} (otpid: {otpid})") - - user_input = ( - get_sanitized_input( - "\nWould you like to export the results to a CSV file? (y/n): " - ) - .strip() - .lower() - ) - if user_input == "y": - working_dir = load_env("WORKING_DIR") - timestamp = datetime.now().strftime("%Y-%m-%d_%H-%M-%S") - filename = f"otp_activities_{timestamp}.csv" - file_path = os.path.join(str(working_dir), filename) - - combined_df.to_csv(file_path, index=False) - logging.info(f"Exported Data to {file_path}") - - print( - colorText( - f"\nāœ… OTP Activity exported to: {working_dir}\\{filename}", - "green", - ) - ) - else: - logging.debug("User declined to export the DataFrame.") - - def otp_revoke(api: AirlockAPIWrapper): activeagents = api.otp_find_active() diff --git a/services/API.py b/services/API.py index 34ce788..7fab0ac 100644 --- a/services/API.py +++ b/services/API.py @@ -253,6 +253,19 @@ class AirlockAPIWrapper: } return self._post("/v1/group/settings/script_custom", payload) + def policy_set_upgradetarget( + self, + groupid: str, + windows: str, + macos: str, + ) -> dict: + payload = { + "groupid": groupid, + "windows": windows, + "macos": macos, + } + return self._post("/v1/group/settings/selfupgrade/target", payload) + # Execution History def history_logging( self, type: List[str], checkpoint: str, policy: List[str]