Compare commits

..

11 Commits

Author SHA1 Message Date
brotoskyj 0dbc744471 Merge remote-tracking branch 'origin/RustImplementation' into RustImplementation 2025-12-15 14:14:16 -05:00
brotoskyj 7a912bddab Bug Fixes
Security Vulnerabilities Patched
RUSTSEC-2025-0009 - Some AES functions may panic when overflow checking is enabled
RUSTSEC-2024-0336 - rustls::Connection::Common::complete_io could fall into an infinite loop based on network input
closes #47
2025-12-15 14:13:57 -05:00
brotoskyj 24211c318b Merge remote-tracking branch 'origin/RustImplementation' into RustImplementation
Build Library / Build Library (push) Successful in 5m2s
2025-12-15 14:12:30 -05:00
brotoskyj 630e0a3cdf Bug Fixes
Security Vulnerabilities Patched
RUSTSEC-2025-0009 - Some AES functions may panic when overflow checking is enabled
RUSTSEC-2024-0336 - rustls::Connection::Common::complete_io could fall into an infinite loop based on network input
2025-12-15 14:12:03 -05:00
Zarithas 797d0f4462 fix(policy-prep): implement table editors and workflow improvements
- Add table editors for Policy Prep workflow
- 'Add to policy' remains a placeholder
- Apply planned tweaks:
  - Replace ballot checkbox with ✓ for selection
  - Relocate loading screen text to bottom:
    'Building Path exclusions and publisher lists...
     This may take a moment for large datasets.'
  - Ensure interaction with all tables before allowing review steps
  - Move excessive logging to debug level
  - Add Step 0 to explain process before user begins

Notes:
Further discussion needed on enforcing table interaction before review.
2025-12-11 16:55:12 -05:00
brotoskyj 59bb97ec4e Refactored LoxideLibs
Build Library / Build Library (push) Successful in 5m30s
1. Added compatibility check, LoxideLibs will now abort the entire program if OS is not linux or windows.
2. Changed the python data extraction compatibility layer, LoxideLibs was calling the extract data function twice, causing very slight overhead. I have now changed this so that the function returns a Struct that is now easily extractable via dot method notation.
2025-12-11 11:47:50 -05:00
brotoskyj 0ac3b54d89 Refactored Progress Bar
Build Library / Build Library (push) Successful in 5m54s
Refactored Progress Bar to remove multiprogress bar and only draw one instance. #36 is still open and not fixed with this push, but I believe this is the way to fix the issue.
Also implemented an Arc Mutex on the progress bar so it can be controlled via different threads.
2025-12-05 17:19:40 -05:00
Zarithas 154a7efcc8 Bug fix for Revoke OTP resolved, no longer crashes when select all is chosen when there are no active sessions, Swapped Revoke OTP and Quiet Hosts locations on menus 2025-12-05 15:30:24 -05:00
Zarithas ab5f00d8e7 Merge branch 'RustImplementation' of https://git.racooncity.org/brotoskyj/Airlocktools into RustImplementation 2025-12-05 15:06:06 -05:00
Zarithas 3ab803c12e Quiet Agent UI improvements 2025-12-05 15:05:46 -05:00
Zarithas 98cb23e5ea Bugfix for Issue 39.
Fixes:
brotoskyj/AirlockTools#39
2025-12-05 14:48:23 -05:00
13 changed files with 1984 additions and 1434 deletions
+30 -31
View File
@@ -58,9 +58,14 @@ class OTPRevokeWidget(Static):
} }
#button_container { #button_container {
height: auto; height: auto;
width: 100%;
padding: 1; padding: 1;
align: center middle; align: center middle;
} }
#button_container Button {
min-width: 16;
margin: 0 1;
}
#result_container { #result_container {
height: auto; height: auto;
max-height: 10; max-height: 10;
@@ -87,29 +92,10 @@ class OTPRevokeWidget(Static):
# Action buttons # Action buttons
with Horizontal(id="button_container"): with Horizontal(id="button_container"):
self.refresh_button = Button("🔄 Refresh", id="refresh_btn") yield Button("Refresh", id="refresh_btn")
self.refresh_button.styles.width = "15%" yield Button("Select All", id="select_all_btn")
self.refresh_button.styles.margin = (1, 1, 1, 1) yield Button("Clear Selection", id="select_none_btn")
yield self.refresh_button yield Button("Revoke Selected", id="revoke_btn", variant="error")
self.select_all_button = Button("☑️ Select All", id="select_all_btn")
self.select_all_button.styles.width = "15%"
self.select_all_button.styles.margin = (1, 1, 1, 1)
yield self.select_all_button
self.select_none_button = Button(
"❌ Clear Selection", id="select_none_btn"
)
self.select_none_button.styles.width = "20%"
self.select_none_button.styles.margin = (1, 1, 1, 1)
yield self.select_none_button
self.revoke_button = Button(
"🛑 Revoke Selected", id="revoke_btn", variant="error"
)
self.revoke_button.styles.width = "20%"
self.revoke_button.styles.margin = (1, 1, 1, 1)
yield self.revoke_button
# Results display # Results display
with Vertical(id="result_container"): with Vertical(id="result_container"):
@@ -122,7 +108,7 @@ class OTPRevokeWidget(Static):
# Configure sessions table # Configure sessions table
self.sessions_table.clear() self.sessions_table.clear()
self.sessions_table.add_columns( self.sessions_table.add_columns(
"", "OTP ID", "Hostname", "Status", "Purpose", "Granted" "", "OTP ID", "Hostname", "Status", "Purpose", "Granted"
) )
# Enable row selection with checkbox column # Enable row selection with checkbox column
@@ -213,7 +199,11 @@ class OTPRevokeWidget(Static):
elif btn.id == "select_all_btn": elif btn.id == "select_all_btn":
# Select all visible rows # Select all visible rows
if self._filtered_df is not None: if (
self._filtered_df is not None
and not self._filtered_df.empty
and "otpid" in self._filtered_df.columns
):
self._selected_otpids = set(str(x) for x in self._filtered_df["otpid"]) self._selected_otpids = set(str(x) for x in self._filtered_df["otpid"])
await self._refresh_table() await self._refresh_table()
@@ -235,7 +225,12 @@ class OTPRevokeWidget(Static):
# Get the row index from the cursor row # Get the row index from the cursor row
row_index = self.sessions_table.cursor_row row_index = self.sessions_table.cursor_row
if self._filtered_df is not None and row_index < len(self._filtered_df): if (
self._filtered_df is not None
and not self._filtered_df.empty
and "otpid" in self._filtered_df.columns
and row_index < len(self._filtered_df)
):
# Get the OTP ID for this row # Get the OTP ID for this row
otpid = str(self._filtered_df.iloc[row_index]["otpid"]) otpid = str(self._filtered_df.iloc[row_index]["otpid"])
@@ -257,12 +252,12 @@ class OTPRevokeWidget(Static):
async def _revoke_selected(self) -> None: async def _revoke_selected(self) -> None:
"""Revoke the selected OTP sessions.""" """Revoke the selected OTP sessions."""
if not self._selected_otpids: if not self._selected_otpids:
self.results_display.update("No sessions selected for revocation") self.results_display.update("No sessions selected for revocation")
return return
api = getattr(self.app, "api", None) api = getattr(self.app, "api", None)
if not api: if not api:
self.results_display.update("API not available") self.results_display.update("API not available")
return return
# Collect results # Collect results
@@ -303,13 +298,13 @@ class OTPRevokeWidget(Static):
else "No response" else "No response"
) )
results.append( results.append(
f"Failed to revoke OTP {otpid} for {hostname}: {error_msg}" f"Failed to revoke OTP {otpid} for {hostname}: {error_msg}"
) )
logger.error(f"Failed to revoke OTP {otpid}: {error_msg}") logger.error(f"Failed to revoke OTP {otpid}: {error_msg}")
except Exception as e: except Exception as e:
failure_count += 1 failure_count += 1
results.append(f"Error revoking OTP {otpid}: {str(e)}") results.append(f"Error revoking OTP {otpid}: {str(e)}")
logger.exception(f"Exception revoking OTP {otpid}: {e}") logger.exception(f"Exception revoking OTP {otpid}: {e}")
# Update results display # Update results display
@@ -368,7 +363,11 @@ class OTPRevokeScreen(Screen):
async def action_select_all(self) -> None: async def action_select_all(self) -> None:
"""Select all visible sessions.""" """Select all visible sessions."""
if self.widget._filtered_df is not None: if (
self.widget._filtered_df is not None
and not self.widget._filtered_df.empty
and "otpid" in self.widget._filtered_df.columns
):
self.widget._selected_otpids = set( self.widget._selected_otpids = set(
str(x) for x in self.widget._filtered_df["otpid"] str(x) for x in self.widget._filtered_df["otpid"]
) )
File diff suppressed because it is too large Load Diff
+183 -110
View File
@@ -34,7 +34,7 @@ from textual.app import ComposeResult
from textual.containers import Horizontal, Vertical from textual.containers import Horizontal, Vertical
from textual.reactive import reactive from textual.reactive import reactive
from textual.screen import Screen from textual.screen import Screen
from textual.widgets import Button, DataTable, Footer, Header, Static from textual.widgets import Button, DataTable, Footer, Header, Input, Static
from models.policy import Policy from models.policy import Policy
from services.API import AirlockAPIWrapper from services.API import AirlockAPIWrapper
@@ -51,16 +51,17 @@ class QuietAgentWorkflowScreen(Screen):
This screen provides a multi-step workflow: This screen provides a multi-step workflow:
1. Select initial policy to analyze 1. Select initial policy to analyze
2. View categorized agents (enforce ready vs. non-enforce ready) 2. Configure analysis parameters (history period and quiet time period)
3. Select target policies for each category 3. View categorized agents (enforce ready vs. non-enforce ready)
4. Execute agent migrations 4. Select target policies for each category
5. Execute agent migrations
Attributes: Attributes:
api (AirlockAPIWrapper): API wrapper for Airlock operations api (AirlockAPIWrapper): API wrapper for Airlock operations
policies (List[Policy]): List of all available policies policies (List[Policy]): List of all available policies
selected_policy (Optional[Policy]): The initially selected policy to analyze selected_policy (Optional[Policy]): The initially selected policy to analyze
history_days (int): Number of days of history to pull (default: 150) history_days (int): Number of days of history to pull (default: 150, range: 1-365)
quiet_days (int): Number of days without execution to be considered quiet (default: 45) quiet_days (int): Number of days without execution to be considered quiet (default: 45, range: 1-365)
agents_df (Optional[pd.DataFrame]): DataFrame of all agents with analysis results agents_df (Optional[pd.DataFrame]): DataFrame of all agents with analysis results
enforce_ready_df (Optional[pd.DataFrame]): DataFrame of agents ready for enforcement 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 non_enforce_ready_df (Optional[pd.DataFrame]): DataFrame of agents not ready for enforcement
@@ -86,7 +87,7 @@ class QuietAgentWorkflowScreen(Screen):
self.api = api self.api = api
self.policies = policies self.policies = policies
self.selected_policy: Optional[Policy] = None self.selected_policy: Optional[Policy] = None
self.history_days = 150 # Fixed as per requirements self.history_days = 150 # Default value, user-selectable
self.quiet_days = 45 # Default value self.quiet_days = 45 # Default value
self.agents_df: Optional[pd.DataFrame] = None self.agents_df: Optional[pd.DataFrame] = None
self.enforce_ready_df: Optional[pd.DataFrame] = None self.enforce_ready_df: Optional[pd.DataFrame] = None
@@ -130,7 +131,7 @@ class QuietAgentWorkflowScreen(Screen):
stage_messages = { stage_messages = {
"select_policy": "Step 1: Select Policy to Analyze", "select_policy": "Step 1: Select Policy to Analyze",
"select_quiet_days": "Step 2: Select Quiet Time Period", "select_history_days": "Step 2: Configure Analysis Parameters",
"analyzing": "Analyzing agent activity...", "analyzing": "Analyzing agent activity...",
"view_results": "Step 3: Review Categorized Agents", "view_results": "Step 3: Review Categorized Agents",
"select_enforce_target": "Step 4: Select Target Policy for Enforce Ready Agents", "select_enforce_target": "Step 4: Select Target Policy for Enforce Ready Agents",
@@ -161,7 +162,7 @@ class QuietAgentWorkflowScreen(Screen):
# Initial policy selection for analysis # Initial policy selection for analysis
self.selected_policy = message.policy self.selected_policy = message.policy
logger.info(f"Selected policy for analysis: {self.selected_policy.name}") logger.info(f"Selected policy for analysis: {self.selected_policy.name}")
self._show_quiet_days_selection() self._show_history_days_selection()
elif self.workflow_stage == "select_enforce_target": elif self.workflow_stage == "select_enforce_target":
# Target policy selection for enforce ready agents # Target policy selection for enforce ready agents
self.enforce_ready_target_policy = message.policy self.enforce_ready_target_policy = message.policy
@@ -177,48 +178,167 @@ class QuietAgentWorkflowScreen(Screen):
) )
self._show_migration_confirmation() self._show_migration_confirmation()
def _show_quiet_days_selection(self) -> None: def _show_history_days_selection(self) -> None:
"""Show the quiet days selection screen.""" """Show the history days and quiet days selection screen."""
self.workflow_stage = "select_quiet_days" self.workflow_stage = "select_history_days"
content = self.query_one("#content_area", Vertical) content = self.query_one("#content_area", Vertical)
content.remove_children() content.remove_children()
# Create info text # Create info text
info_widget = Static( info_widget = Static(
f"Policy Selected: {self.selected_policy.name}\n\n" f"Policy Selected: {self.selected_policy.name}\n\n"
f"History Period: {self.history_days} days\n\n" "Configure Analysis Parameters:",
"Select quiet time period (days without untrusted execution):", id="analysis_params_info",
id="quiet_days_info",
) )
info_widget.styles.margin = (0, 0, 2, 0) info_widget.styles.margin = (0, 0, 2, 0)
content.mount(info_widget) content.mount(info_widget)
# Create button container and mount it first # Create input container
button_container = Vertical(id="quiet_days_buttons") input_container = Vertical(id="analysis_params_input_container")
button_container.styles.height = "auto" input_container.styles.height = "auto"
content.mount(button_container) content.mount(input_container)
# Now add buttons to the mounted container # History days label
for days in [15, 30, 45, 60]: history_label = Static("History Period (days of execution history to pull):")
btn = Button( history_label.styles.margin = (0, 0, 1, 0)
f"{days} days {'(Default)' if days == 45 else ''}", input_container.mount(history_label)
id=f"quiet_days_{days}",
classes="quiet_day_btn", # Add history days input field
history_input = Input(
placeholder="Enter days (1-365, default: 150)",
value="150",
id="history_days_input",
)
history_input.styles.width = "50"
history_input.styles.margin = (0, 0, 2, 0)
input_container.mount(history_input)
# Quiet days label
quiet_label = Static(
"Quiet Time Period (days without execution to be considered quiet):"
)
quiet_label.styles.margin = (0, 0, 1, 0)
input_container.mount(quiet_label)
# Add quiet days input field
quiet_input = Input(
placeholder="Enter days (1-365, default: 45)",
value="45",
id="quiet_days_input",
)
quiet_input.styles.width = "50"
quiet_input.styles.margin = (0, 0, 2, 0)
input_container.mount(quiet_input)
# Add submit button
submit_btn = Button(
"Continue",
id="analysis_params_submit",
variant="primary",
)
submit_btn.styles.width = "50"
submit_btn.styles.margin = (1, 0, 0, 0)
input_container.mount(submit_btn)
# Focus the first input field
history_input.focus()
def _validate_and_submit_history_days(self) -> None:
"""Validate and submit the history days and quiet days inputs."""
try:
history_input = self.query_one("#history_days_input", Input)
quiet_input = self.query_one("#quiet_days_input", Input)
history_value = history_input.value.strip()
quiet_value = quiet_input.value.strip()
# Validate history days
if not history_value:
self.app.notify(
"Please enter a history period value", severity="error", timeout=3
)
history_input.focus()
return
try:
history_days = int(history_value)
except ValueError:
self.app.notify(
"Please enter a valid number for history period",
severity="error",
timeout=3,
)
history_input.focus()
return
if history_days < 1 or history_days > 365:
self.app.notify(
"History period must be between 1 and 365 days",
severity="error",
timeout=3,
)
history_input.focus()
return
# Validate quiet days
if not quiet_value:
self.app.notify(
"Please enter a quiet time period value",
severity="error",
timeout=3,
)
quiet_input.focus()
return
try:
quiet_days = int(quiet_value)
except ValueError:
self.app.notify(
"Please enter a valid number for quiet time period",
severity="error",
timeout=3,
)
quiet_input.focus()
return
if quiet_days < 1 or quiet_days > 365:
self.app.notify(
"Quiet time period must be between 1 and 365 days",
severity="error",
timeout=3,
)
quiet_input.focus()
return
# Check that quiet days doesn't exceed history days
if quiet_days > history_days:
self.app.notify(
"Quiet time period cannot exceed history period",
severity="error",
timeout=3,
)
quiet_input.focus()
return
# All validation passed
self.history_days = history_days
self.quiet_days = quiet_days
logger.info(
f"Selected history days: {history_days}, quiet days: {quiet_days}"
) )
btn.styles.width = "100%" self._start_analysis()
btn.styles.margin = (0, 0, 1, 0)
button_container.mount(btn) except Exception as e:
logger.error(f"Error validating analysis parameters: {e}")
self.app.notify(f"Error: {str(e)}", severity="error", timeout=3)
def on_button_pressed(self, event: Button.Pressed) -> None: def on_button_pressed(self, event: Button.Pressed) -> None:
"""Handle button press events.""" """Handle button press events."""
button_id = event.button.id button_id = event.button.id
# Quiet days selection buttons # Analysis parameters submit button
if button_id and button_id.startswith("quiet_days_"): if button_id == "analysis_params_submit":
days = int(button_id.split("_")[-1]) self._validate_and_submit_history_days()
self.quiet_days = days
logger.info(f"Selected quiet days: {days}")
self._start_analysis()
return return
# Navigation buttons # Navigation buttons
@@ -258,46 +378,44 @@ class QuietAgentWorkflowScreen(Screen):
self._show_policy_selection() self._show_policy_selection()
return return
def on_input_submitted(self, event: Input.Submitted) -> None:
"""Handle input submission (Enter key pressed)."""
if event.input.id in ["history_days_input", "quiet_days_input"]:
self._validate_and_submit_history_days()
def _start_analysis(self) -> None: def _start_analysis(self) -> None:
"""Start the agent activity analysis.""" """Start the agent activity analysis."""
self.workflow_stage = "analyzing" # Show notification that analysis is starting
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( self.app.notify(
"Starting analysis - this may take several minutes for large policies", "Starting analysis - this may take several minutes for large policies",
severity="information", severity="information",
timeout=5, timeout=5,
) )
# Perform the analysis asynchronously # Clear the screen to provide a blank canvas for Rust progress output
self.call_later(self._perform_analysis) # (Rust output displays over the TUI, so we clear everything except header/footer)
try:
# Clear title
title_widget = self.query_one("#workflow_title", Static)
title_widget.update("")
def _perform_analysis(self) -> None: # Clear status
status_widget = self.query_one("#workflow_status", Static)
status_widget.update("")
# Clear content area
content = self.query_one("#content_area", Vertical)
content.remove_children()
except Exception as e:
logger.debug(f"Could not clear screen for analysis: {e}")
# Delay the analysis start to ensure UI refresh completes first
# This prevents Rust output from starting before the screen is cleared
self.set_timer(0.5, self._perform_analysis_worker)
def _perform_analysis_worker(self) -> None:
"""Perform the actual agent activity analysis.""" """Perform the actual agent activity analysis."""
try: try:
# Update status: Fetching agents
self._update_analysis_status("Step 1/4: Fetching agents from policy...")
# Get agents in the selected policy # Get agents in the selected policy
agents = self.api.agents_find_by_group(self.selected_policy.groupid) agents = self.api.agents_find_by_group(self.selected_policy.groupid)
@@ -310,32 +428,11 @@ class QuietAgentWorkflowScreen(Screen):
self._show_policy_selection() self._show_policy_selection()
return 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) # Get execution history (this shows progress bars in terminal via airlock_libs)
policy_exec_history = getPolicyInfo( policy_exec_history = getPolicyInfo(
self.api, self.selected_policy, [1, 2, 6, 7], self.history_days 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: if policy_exec_history.empty:
logger.info( logger.info(
"No execution history found for the selected policy and time range." "No execution history found for the selected policy and time range."
@@ -381,9 +478,6 @@ class QuietAgentWorkflowScreen(Screen):
lambda x: True if pd.isna(x) or x > self.quiet_days else False 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 # Sort agents
agents = agents.sort_values( agents = agents.sort_values(
by=["execution_count", "hostname"], ascending=[True, True] by=["execution_count", "hostname"], ascending=[True, True]
@@ -394,7 +488,7 @@ class QuietAgentWorkflowScreen(Screen):
# Categorize agents into DataFrames # Categorize agents into DataFrames
self.enforce_ready_df = agents[agents["enforce_ready"]].copy() self.enforce_ready_df = agents[agents["enforce_ready"]].copy()
self.non_enforce_ready_df = agents[not agents["enforce_ready"]].copy() self.non_enforce_ready_df = agents[~agents["enforce_ready"]].copy()
logger.info( logger.info(
f"Analysis complete: {len(self.enforce_ready_df)} enforce ready, " f"Analysis complete: {len(self.enforce_ready_df)} enforce ready, "
@@ -416,27 +510,6 @@ class QuietAgentWorkflowScreen(Screen):
self.app.notify(f"Analysis failed: {str(e)}", severity="error", timeout=5) self.app.notify(f"Analysis failed: {str(e)}", severity="error", timeout=5)
self._show_policy_selection() 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: def _show_results(self) -> None:
"""Show the categorized results.""" """Show the categorized results."""
self.workflow_stage = "view_results" self.workflow_stage = "view_results"
@@ -821,7 +894,7 @@ class QuietAgentWorkflowScreen(Screen):
# Depending on stage, go back to previous stage or exit # Depending on stage, go back to previous stage or exit
if self.workflow_stage in ["select_policy", "view_results", "complete"]: if self.workflow_stage in ["select_policy", "view_results", "complete"]:
self.app.pop_screen() self.app.pop_screen()
elif self.workflow_stage == "select_quiet_days": elif self.workflow_stage == "select_history_days":
self._show_policy_selection() self._show_policy_selection()
elif self.workflow_stage == "select_enforce_target": elif self.workflow_stage == "select_enforce_target":
self._show_results() self._show_results()
+4 -4
View File
@@ -92,15 +92,15 @@ class MainMenuScreen(Screen):
BUTTON_DEFS = { BUTTON_DEFS = {
"agent_actions": [ "agent_actions": [
( (
"🖥️ - Find, Move, or Generate OTP for Agents", "🖥️ - Find agent, Move agent, or Generate One Time Pass",
"move_agent_workflow_button", "move_agent_workflow_button",
), ),
("🎫 - Review and appove OTP Activities", "otp_activities_button"), ("🎫 - Review and approve OTP Activities", "otp_activities_button"),
("🔕 - Find and Move Quiet Hosts to Enforcement", "find_quiet_button"), ("🛑 - Revoke Active OTP Session", "otp_revoke_button"),
], ],
"policy": [ "policy": [
("⚖️ - Prepare Policy For Enforcement", "policy_prep_button"), ("⚖️ - Prepare Policy For Enforcement", "policy_prep_button"),
("🛑 - Revoke OTPs", "otp_revoke_button"), ("🔕 - Find and Move Quiet Hosts to Enforcement", "find_quiet_button"),
], ],
} }
+24 -24
View File
@@ -88,9 +88,9 @@ def sortHashes(
): ):
working_dir = load_env("WORKING_DIR") working_dir = load_env("WORKING_DIR")
history_days = Selector.select_value( history_days = Selector.select_value(
prompt="Enter how many days of history to pull (1150): ", prompt="Enter how many days of history to pull (1-365): ",
value_type=int, value_type=int,
valid_range=(1, 150), valid_range=(1, 365),
) )
logger.debug(f"{history_days} day selected for history") logger.debug(f"{history_days} day selected for history")
@@ -655,7 +655,7 @@ def section_header(title):
def printEnforceChecklist(selected_policies, destination_policy, destination_allowlist): def printEnforceChecklist(selected_policies, destination_policy, destination_allowlist):
working_dir = load_env("WORKING_DIR") working_dir = load_env("WORKING_DIR")
section_header("Prepare to Enforce Policy ") section_header("Prepare to Enforce Policy")
print( print(
colorText( colorText(
"\nSequentially follow these steps to prepare a policy for enforcement:", "\nSequentially follow these steps to prepare a policy for enforcement:",
@@ -670,11 +670,11 @@ def printEnforceChecklist(selected_policies, destination_policy, destination_all
) )
) )
if not selected_policies: if not selected_policies:
print(colorText(" [] No policies have been chosen", "red")) print(colorText(" [❌] No policies have been chosen", "red"))
else: else:
print(colorText("The following policies have been chosen:", "green")) print(colorText("The following policies have been chosen:", "green"))
for policy in selected_policies: for policy in selected_policies:
print(colorText(f" [] {policy.name}", "green")) print(colorText(f" [✅] {policy.name}", "green"))
# Step 2: Destination Policy and Allowlist # Step 2: Destination Policy and Allowlist
print( print(
@@ -683,22 +683,22 @@ def printEnforceChecklist(selected_policies, destination_policy, destination_all
if destination_policy: if destination_policy:
print( print(
colorText( colorText(
f" [] {destination_policy[0].name} has been selected as the destination policy", f" [✅] {destination_policy[0].name} has been selected as the destination policy",
"green", "green",
) )
) )
else: else:
print(colorText(" [] No destination policy has been chosen", "red")) print(colorText(" [❌] No destination policy has been chosen", "red"))
if destination_allowlist: if destination_allowlist:
print( print(
colorText( colorText(
f" [] {destination_allowlist[0].name} has been selected as allowlist", f" [✅] {destination_allowlist[0].name} has been selected as allowlist",
"green", "green",
) )
) )
else: else:
print(colorText(" [] No allowlist has been chosen", "red")) print(colorText(" [❌] No allowlist has been chosen", "red"))
# Step 3: Data Preparation # Step 3: Data Preparation
print( print(
@@ -713,9 +713,9 @@ def printEnforceChecklist(selected_policies, destination_policy, destination_all
print( print(
colorText( colorText(
( (
" [] Data has been fetched" " [✅] Data has been fetched"
if os.path.exists(review_path) if os.path.exists(review_path)
else " [] Data has not been fetched" else " [❌] Data has not been fetched"
), ),
"green" if os.path.exists(review_path) else "red", "green" if os.path.exists(review_path) else "red",
) )
@@ -723,7 +723,7 @@ def printEnforceChecklist(selected_policies, destination_policy, destination_all
else: else:
print( print(
colorText( colorText(
" [] No policies selected, cannot check data fetch status", "red" " [❌] No policies selected, cannot check data fetch status", "red"
) )
) )
@@ -756,9 +756,9 @@ def printEnforceChecklist(selected_policies, destination_policy, destination_all
print( print(
colorText( colorText(
( (
" [] Reviewed hashes have been loaded" " [✅] Reviewed hashes have been loaded"
if os.path.exists(approved_path) if os.path.exists(approved_path)
else " [] Reviewed hashes have not been loaded" else " [❌] Reviewed hashes have not been loaded"
), ),
"green" if os.path.exists(approved_path) else "red", "green" if os.path.exists(approved_path) else "red",
) )
@@ -766,9 +766,9 @@ def printEnforceChecklist(selected_policies, destination_policy, destination_all
print( print(
colorText( colorText(
( (
" [] Path review list created" " [✅] Path review list created"
if os.path.exists(second_review_path) if os.path.exists(second_review_path)
else " [] Path review list has not been created" else " [❌] Path review list has not been created"
), ),
"green" if os.path.exists(second_review_path) else "red", "green" if os.path.exists(second_review_path) else "red",
) )
@@ -776,7 +776,7 @@ def printEnforceChecklist(selected_policies, destination_policy, destination_all
else: else:
print( print(
colorText( colorText(
" [] No policies selected, cannot check reviewed hashes or path list", " [❌] No policies selected, cannot check reviewed hashes or path list",
"red", "red",
) )
) )
@@ -812,9 +812,9 @@ def printEnforceChecklist(selected_policies, destination_policy, destination_all
print( print(
colorText( colorText(
( (
" [] Reviewed path list detected" " [✅] Reviewed path list detected"
if os.path.exists(reviewed_path) if os.path.exists(reviewed_path)
else " [] Path review list has not been detected" else " [❌] Path review list has not been detected"
), ),
"green" if os.path.exists(reviewed_path) else "red", "green" if os.path.exists(reviewed_path) else "red",
) )
@@ -825,9 +825,9 @@ def printEnforceChecklist(selected_policies, destination_policy, destination_all
print( print(
colorText( colorText(
( (
" [] Preflight Path Exclusion List has been generated" " [✅] Preflight Path Exclusion List has been generated"
if preflight_ready if preflight_ready
else " [] Preflight Path Exclusion List has not been generated" else " [❌] Preflight Path Exclusion List has not been generated"
), ),
"green" if preflight_ready else "red", "green" if preflight_ready else "red",
) )
@@ -835,7 +835,7 @@ def printEnforceChecklist(selected_policies, destination_policy, destination_all
else: else:
print( print(
colorText( colorText(
" [] No policies selected, cannot check preflight status", "red" " [❌] No policies selected, cannot check preflight status", "red"
) )
) )
@@ -866,5 +866,5 @@ def printEnforceChecklist(selected_policies, destination_policy, destination_all
print(colorText(" Apply approved hashes to allowlist", "cyan")) print(colorText(" Apply approved hashes to allowlist", "cyan"))
# Utility Options # Utility Options
print(colorText("F. Open Working Directory", "cyan")) print(colorText("F. Open Working Directory", "cyan"))
print(colorText("B. Back", "cyan")) print(colorText("B. Back", "cyan"))
+355 -884
View File
File diff suppressed because it is too large Load Diff
+11 -13
View File
@@ -1,33 +1,31 @@
[package] [package]
name = "airlock_libs" name = "signoz_test"
version = "5.1.2" version = "6.0.0"
edition = "2024" edition = "2024"
[lib]
crate-type = ["cdylib"]
[dependencies] [dependencies]
chrono = "0.4.42" chrono = "0.4.42"
indicatif = "0.18.2" indicatif = "0.18.2"
mongodb = "3.3.0" mongodb = "3.3.0"
opentelemetry = { version = "0.18.0", features = ["rt-tokio", "metrics", "trace"] } opentelemetry = { version = "0.27.0", features = ["logs", "metrics", "trace"] }
opentelemetry-otlp = { version = "0.11.0", features = ["trace", "metrics"] } opentelemetry-otlp = { version = "0.27.0", features = ["trace", "metrics", "grpc-tonic", "http-proto", "tls", "reqwest-client", "reqwest-rustls"] }
opentelemetry-semantic-conventions = { version = "0.10.0" } opentelemetry-semantic-conventions = { version = "0.27.0" }
opentelemetry-proto = { version = "0.1.0"} opentelemetry-proto = { version = "0.27.0"}
pyo3 = { version = "0.27.0", features = ["extension-module", "generate-import-lib"] } pyo3 = { version = "0.27.0", features = ["extension-module", "generate-import-lib"] }
reqwest = { version = "0.12.24", features = ["json", "native-tls"] } reqwest = { version = "0.12.24", features = ["json", "native-tls", "rustls-tls"] }
serde = "1.0.228" serde = "1.0.228"
serde-pyobject = "0.8.0" serde-pyobject = "0.8.0"
serde_json = "1.0.145" serde_json = "1.0.145"
tokio = { version = "1.48.0", features = ["full"] } tokio = { version = "1.48.0", features = ["full"] }
tonic = { version = "0.8.2", features = ["tls-roots"] } tonic = { version = "0.12.3", features = ["tls-roots"] }
tracing = "0.1.41" tracing = "0.1.41"
tracing-subscriber = "0.3.20" tracing-subscriber = "0.3.20"
tracing-opentelemetry = "0.32.0" tracing-opentelemetry = "0.32.0"
pyo3-async-runtimes = { version = "0.27.0", features = ["async-std", "tokio"] }
crossbeam = "0.8.4" crossbeam = "0.8.4"
log = "0.4.29" log = "0.4.29"
flexi_logger = "0.31.7" flexi_logger = "0.31.7"
opentelemetry-appender-log = "0.27.0"
opentelemetry_sdk = { version = "0.27.0", features = ["rt-tokio", "trace"] }
[package.metadata.maturin] [package.metadata.maturin]
generate-abi-stubs = true generate-abi-stubs = true
@@ -40,4 +38,4 @@ codegen-units = 1
panic = 'abort' panic = 'abort'
strip = true strip = true
debug-assertions = false debug-assertions = false
overflow-checks = false overflow-checks = true
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "maturin"
[project] [project]
name = "airlock_libs" name = "airlock_libs"
version = "5.1.2" version = "6.0.0"
description = "Airlock Digital API Wrapper" description = "Airlock Digital API Wrapper"
readme = "README.md" readme = "README.md"
license = { text = "AGPL-3.0-only" } license = { text = "AGPL-3.0-only" }
+21 -27
View File
@@ -64,36 +64,30 @@ pub struct Group {
pub(crate) localip: String, pub(crate) localip: String,
} }
pub enum ExtractedValues { pub struct PyData {
Headers(reqwest::header::HeaderMap), pub headers: reqwest::header::HeaderMap,
BaseUrl(String), pub base_url: String,
} }
pub trait Converter { impl PyData {
fn convert(py: Python<'_>, py_self: &Py<PyAny>, extract_headers: bool) -> ExtractedValues; pub fn extract_data(py: Python<'_>, obj: &Py<PyAny>) -> Self {
} let headers_raw = obj.getattr(py, "headers").unwrap().to_string();
let headers_json = headers_raw.replace('\'', "\"");
pub struct PyData; let parsed: Value = serde_json::from_str(&headers_json).unwrap();
let mut header_map = HeaderMap::new();
impl Converter for PyData { if let Some(obj) = parsed.as_object() {
fn convert(py: Python<'_>, py_self: &Py<PyAny>, extract_headers: bool) -> ExtractedValues { for (key, val) in obj {
if extract_headers { if let Some(v) = val.as_str() {
let headers = py_self.getattr(py, "headers").unwrap().to_string(); let header_name = HeaderName::from_str(key).unwrap();
let headers_replace = headers.replace('\'', "\""); let header_value: HeaderValue = HeaderValue::from_str(v).unwrap();
let parsed: Value = serde_json::from_str(headers_replace.as_str()).unwrap(); header_map.insert(header_name, header_value);
let mut header_map = HeaderMap::new();
if let Some(obj) = parsed.as_object() {
for (_key, value) in obj {
if let Some(v) = value.as_str() {
let val = HeaderValue::from_str(v).unwrap();
header_map.insert(HeaderName::from_str("X-APIKey").unwrap(), val);
}
} }
} }
ExtractedValues::Headers(header_map) }
} else { let base_url = obj.getattr(py, "base_url").unwrap().to_string();
let base_url = py_self.getattr(py, "base_url").unwrap().to_string(); Self {
ExtractedValues::BaseUrl(base_url) headers: header_map,
base_url,
} }
} }
} }
@@ -109,4 +103,4 @@ impl SkipBack {
let objectid_hex = format!("{}0000000000000000", hex_timestamp); let objectid_hex = format!("{}0000000000000000", hex_timestamp);
ObjectId::parse_str(&objectid_hex).expect("Invalid ObjectId hex") ObjectId::parse_str(&objectid_hex).expect("Invalid ObjectId hex")
} }
} }
+6 -6
View File
@@ -1,15 +1,15 @@
pub use chrono::{Duration, Local, NaiveDate}; pub use chrono::{Duration, Local, NaiveDate};
pub use indicatif::{MultiProgress, ProgressBar, ProgressDrawTarget, ProgressStyle}; pub use indicatif::{MultiProgress, ProgressBar, ProgressDrawTarget, ProgressStyle};
pub use mongodb::bson::oid::ObjectId; pub use mongodb::bson::oid::ObjectId;
pub use opentelemetry::global::shutdown_tracer_provider; pub use opentelemetry::global::GlobalTracerProvider;
pub use opentelemetry::sdk::Resource;
pub use opentelemetry::trace::noop::NoopTracerProvider; pub use opentelemetry::trace::noop::NoopTracerProvider;
pub use opentelemetry::trace::{Status, TraceContextExt, TraceError}; pub use opentelemetry::trace::{Status, TraceContextExt, Tracer};
pub use opentelemetry::{Context, KeyValue, sdk::trace as sdktrace, trace::Tracer}; pub use opentelemetry::*;
pub use opentelemetry::{Key, global}; pub use opentelemetry_otlp::ExportConfig;
pub use opentelemetry_otlp::WithExportConfig; pub use opentelemetry_otlp::WithExportConfig;
pub use opentelemetry_sdk::Resource;
pub use opentelemetry_sdk::trace::{Config, TracerProvider};
pub use pyo3::{prelude::*, types::PyString}; pub use pyo3::{prelude::*, types::PyString};
pub use pyo3_async_runtimes::async_std;
pub use reqwest::{ pub use reqwest::{
Client, Client,
header::{HeaderMap, HeaderName, HeaderValue}, header::{HeaderMap, HeaderName, HeaderValue},
+67 -47
View File
@@ -1,7 +1,10 @@
use std::thread;
use crossbeam::channel::unbounded;
use crate::modules::datatypes::*; use crate::modules::datatypes::*;
use crate::prelude::*; use crate::prelude::*;
use crossbeam::channel::unbounded;
use opentelemetry_otlp::WithTonicConfig;
use std::sync::{Arc, Mutex};
use std::thread;
use tonic::transport::{Channel, ClientTlsConfig};
#[pyfunction] #[pyfunction]
pub fn pull_policy_exec_histories( pub fn pull_policy_exec_histories(
py: Python<'_>, py: Python<'_>,
@@ -10,14 +13,10 @@ pub fn pull_policy_exec_histories(
exec_types: String, exec_types: String,
days: i64, days: i64,
) -> Py<PyString> { ) -> Py<PyString> {
let headers: HeaderMap = match PyData::convert(py, &py_self, true) { println!();
ExtractedValues::Headers(h) => h, let data: PyData = PyData::extract_data(py, &py_self);
ExtractedValues::BaseUrl(_) => std::process::abort(), let headers: HeaderMap = data.headers;
}; let base_url: String = data.base_url;
let base_url: String = match PyData::convert(py, &py_self, false) {
ExtractedValues::Headers(_) => std::process::abort(),
ExtractedValues::BaseUrl(b) => b,
};
let handle: thread::JoinHandle<String> = std::thread::spawn(move || { let handle: thread::JoinHandle<String> = std::thread::spawn(move || {
let rt: tokio::runtime::Runtime = match tokio::runtime::Runtime::new() { let rt: tokio::runtime::Runtime = match tokio::runtime::Runtime::new() {
Ok(rt) => rt, Ok(rt) => rt,
@@ -26,10 +25,9 @@ pub fn pull_policy_exec_histories(
std::process::abort(); std::process::abort();
} }
}; };
rt.block_on(async { let tracer_provider = rt.block_on(async { init_tracer() });
let _ = init_tracer(); global::set_tracer_provider(tracer_provider.clone());
}); let tracer: global::BoxedTracer = global::tracer("tracer");
let tracer: global::BoxedTracer = global::tracer("global_tracer");
let _cx: Context = Context::new(); let _cx: Context = Context::new();
let file_path: PathBuf = format!( let file_path: PathBuf = format!(
"{}\\cache\\chunkinator.json", "{}\\cache\\chunkinator.json",
@@ -72,15 +70,16 @@ pub fn pull_policy_exec_histories(
} }
} }
let mut checkpoint_number: String = SkipBack::find_checkpoint(days).to_string(); let mut checkpoint_number: String = SkipBack::find_checkpoint(days).to_string();
let multi_progress: MultiProgress = MultiProgress::new(); let progress_bar = Arc::new(Mutex::new(ProgressBar::new(100)));
multi_progress.set_draw_target(ProgressDrawTarget::stderr()); progress_bar
let progress_bar: ProgressBar = multi_progress.add(ProgressBar::new(100)); .lock()
progress_bar.set_style( .unwrap()
.set_draw_target(ProgressDrawTarget::stderr());
progress_bar.lock().unwrap().set_style(
ProgressStyle::default_bar() ProgressStyle::default_bar()
.template("Total Completion: {spinner:.green} [{elapsed_precise}] [{bar:40.green/blue}] {pos}/{len} {message}") .template("Total Completion: {spinner:.green} [{elapsed_precise}] [{bar:40.green/blue}] {pos}/{len} {message}")
.unwrap(), .unwrap(),
); );
progress_bar.enable_steady_tick(std::time::Duration::from_millis(100));
let client: Client = tracer.in_span("Building HTTP Client", |cx| { let client: Client = tracer.in_span("Building HTTP Client", |cx| {
let client_result: Result<Client, reqwest::Error> = build_client(headers); let client_result: Result<Client, reqwest::Error> = build_client(headers);
match client_result { match client_result {
@@ -109,8 +108,10 @@ pub fn pull_policy_exec_histories(
} }
} }
}); });
let cutoff: chrono::NaiveDateTime = Local::now().naive_local() - Duration::days(days); let cutoff: chrono::NaiveDateTime =
Local::now().naive_local() - chrono::Duration::days(days);
let (tx, rx) = unbounded::<Vec<Group>>(); let (tx, rx) = unbounded::<Vec<Group>>();
let pb_clone = progress_bar.clone();
thread::spawn(move || { thread::spawn(move || {
let mut seen: HashMap<(String, String, String), Group> = if writeable_filepath.exists() let mut seen: HashMap<(String, String, String), Group> = if writeable_filepath.exists()
{ {
@@ -178,8 +179,15 @@ pub fn pull_policy_exec_histories(
}); });
let mut first_date: Option<NaiveDate> = None; let mut first_date: Option<NaiveDate> = None;
tracer.in_span("Airlock Data Retreival", |cx| { tracer.in_span("Airlock Data Retreival", |cx| {
pb_clone
.lock()
.unwrap()
.enable_steady_tick(std::time::Duration::from_millis(100));
let span: opentelemetry::trace::SpanRef<'_> = cx.span(); let span: opentelemetry::trace::SpanRef<'_> = cx.span();
span.set_attribute(Key::new("Days").string(days.to_string())); //span.set_attribute(Key::new("Days").string(days.to_string()));
//span.set_attribute(Key::new("Days"));
//span.set_attribute(KeyValue::new("Policy Name", policy_names.clone()));
span.set_attribute(KeyValue::new("Days", days));
span.set_attribute(KeyValue::new("Policy Name", policy_names.clone())); span.set_attribute(KeyValue::new("Policy Name", policy_names.clone()));
loop { loop {
let execution_histories = tracer.in_span(checkpoint_number.to_string(), |cx| { let execution_histories = tracer.in_span(checkpoint_number.to_string(), |cx| {
@@ -213,16 +221,21 @@ pub fn pull_policy_exec_histories(
} }
if let Some(base_date) = first_date { if let Some(base_date) = first_date {
let date_diff: chrono::TimeDelta = last_date - base_date; let date_diff: chrono::TimeDelta = last_date - base_date;
let total_span: i64 = (Local::now().naive_local().date() - base_date).num_days(); let total_span: i64 =
let percentage: u64 = ((date_diff.num_days() as f64 / total_span as f64) * 100.0) (Local::now().naive_local().date() - base_date).num_days();
let percentage: u64 = ((date_diff.num_days() as f64 / total_span as f64)
* 100.0)
.clamp(0.0, 100.0) .clamp(0.0, 100.0)
.round() as u64; .round() as u64;
progress_bar.set_position(percentage); pb_clone.lock().unwrap().set_position(percentage);
} }
} }
} }
}); });
progress_bar.finish_with_message("All Checkpoints Complete"); progress_bar
.lock()
.unwrap()
.finish_with_message("All Checkpoints Complete");
let return_data: String = match fs::read_to_string(file_path.clone()) { let return_data: String = match fs::read_to_string(file_path.clone()) {
Ok(return_data) => return_data, Ok(return_data) => return_data,
Err(e) => { Err(e) => {
@@ -230,8 +243,10 @@ pub fn pull_policy_exec_histories(
std::process::abort(); std::process::abort();
} }
}; };
tracer_provider
.shutdown()
.expect("Failed to Shutdown Tracer Provdier");
drop(tx); drop(tx);
shutdown_tracer_provider();
return_data.to_string() return_data.to_string()
}); });
let gil_value: String = handle.join().unwrap(); let gil_value: String = handle.join().unwrap();
@@ -298,29 +313,34 @@ pub fn get_base_directory() -> PathBuf {
.unwrap_or_else(|| home.join("AppData").join("Roaming")); .unwrap_or_else(|| home.join("AppData").join("Roaming"));
appdata.join("Loxide") appdata.join("Loxide")
} }
_ => home.join(".local").join("share").join("Loxide"), "linux" => home.join(".local").join("share").join("Loxide"),
_ => {
println!("{} is currently not compatible with LoxideLibs", os);
std::process::abort();
}
} }
} }
fn init_tracer() -> Result<Option<sdktrace::Tracer>, TraceError> { fn init_tracer() -> opentelemetry_sdk::trace::TracerProvider {
let cfg: TelemetryConfig = TelemetryConfig::load(); let cfg: TelemetryConfig = TelemetryConfig::load();
if !cfg.TELEMETRY { let endpoint = cfg.TELEM_URL.unwrap_or_default().clone();
global::set_tracer_provider(NoopTracerProvider::new()); let channel_endpoint = endpoint.clone();
return Ok(None); let channel = Channel::from_shared(channel_endpoint.clone())
} .unwrap()
let endpoint: String = cfg.TELEM_URL.unwrap_or_default(); .tls_config(ClientTlsConfig::new().with_native_roots())
let tracer: sdktrace::Tracer = .unwrap()
opentelemetry_otlp::new_pipeline() .connect_lazy();
.tracing() let exporter = opentelemetry_otlp::SpanExporter::builder()
.with_exporter( .with_tonic()
opentelemetry_otlp::new_exporter() .with_endpoint(endpoint.clone())
.tonic() .with_channel(channel)
.with_endpoint(endpoint), .build()
) .expect("Failed to build exporter");
.with_trace_config(sdktrace::config().with_resource(Resource::new(vec![ opentelemetry_sdk::trace::TracerProvider::builder()
KeyValue::new("service.name", "LoxideLibs"), .with_simple_exporter(exporter)
]))) .with_resource(Resource::new(vec![KeyValue::new(
.install_simple() "service.name",
.unwrap(); "LoxideLibs",
Ok(Some(tracer)) )]))
.build()
} }
+1 -1
View File
@@ -11,4 +11,4 @@ urllib3==2.5.0
pyperclip==1.11.0 pyperclip==1.11.0
--extra-index-url https://git.racooncity.org/api/packages/brotoskyj/pypi/simple/ --extra-index-url https://git.racooncity.org/api/packages/brotoskyj/pypi/simple/
airlock_libs==5.1.2 airlock_libs==6.0.0
+15 -15
View File
@@ -38,9 +38,9 @@ logger = logging.getLogger(__name__)
def devicehistory(api: AirlockAPIWrapper, outputjson: bool): def devicehistory(api: AirlockAPIWrapper, outputjson: bool):
agents = selectAgents(api) agents = selectAgents(api)
history_days = Selector.select_value( history_days = Selector.select_value(
prompt="Enter how many days of history to pull (1150): ", prompt="Enter how many days of history to pull (1–365): ",
value_type=int, value_type=int,
valid_range=(1, 150), valid_range=(1, 365),
) )
if not agents or not history_days: if not agents or not history_days:
@@ -60,7 +60,7 @@ def devicehistory(api: AirlockAPIWrapper, outputjson: bool):
except Exception as e: except Exception as e:
print( print(
colorText( colorText(
f" Error retrieving history for {agent.hostname}: {e}", "red" f"❌ Error retrieving history for {agent.hostname}: {e}", "red"
) )
) )
continue continue
@@ -139,7 +139,7 @@ def findAgents(api, return_dataframe):
print( print(
colorText( colorText(
f"\n Matched devices exported to: {working_dir}\\{filename}", f"\n✓ Matched devices exported to: {working_dir}\\{filename}",
"green", "green",
) )
) )
@@ -148,7 +148,7 @@ def findAgents(api, return_dataframe):
def collect_device_names() -> List[str]: def collect_device_names() -> List[str]:
print(colorText("🖥Â Device Search", "cyan")) print(colorText("🖥��Â Device Search", "cyan"))
print( print(
colorText( colorText(
"Enter the device hostnames you'd like to search for, one per line.", "cyan" "Enter the device hostnames you'd like to search for, one per line.", "cyan"
@@ -185,7 +185,7 @@ def collect_device_names() -> List[str]:
else: else:
print( print(
colorText( colorText(
f"⚠️ Invalid input: '{stripped_line}'  only letters, numbers, underscores, spaces, and hyphens are allowed.", f"⚠️ Invalid input: '{stripped_line}' — only letters, numbers, underscores, spaces, and hyphens are allowed.",
"yellow", "yellow",
) )
) )
@@ -235,8 +235,8 @@ def show_unmatched(
] ]
if unmatched: if unmatched:
logger.debug(f"⚠️ No matches for: {', '.join(unmatched)}") logger.debug(f"⚠️ No matches for: {', '.join(unmatched)}")
print(colorText(f"⚠️ No matches for: {', '.join(unmatched)}", "yellow")) print(colorText(f"⚠️ No matches for: {', '.join(unmatched)}", "yellow"))
def enrich_agents(agents: List["Agent"], policies: List["Policy"]): def enrich_agents(agents: List["Agent"], policies: List["Policy"]):
@@ -248,7 +248,7 @@ def selectAgents(api: "AirlockAPIWrapper") -> List["Agent"]:
device_names = collect_device_names() device_names = collect_device_names()
if not device_names: if not device_names:
logger.debug("No device names entered") logger.debug("No device names entered")
print(colorText("⚠️ No device names entered.", "red")) print(colorText("⚠️ No device names entered.", "red"))
return [] return []
use_exact = choose_match_type() use_exact = choose_match_type()
@@ -261,11 +261,11 @@ def selectAgents(api: "AirlockAPIWrapper") -> List["Agent"]:
show_unmatched(device_names, matched_agents, use_exact) show_unmatched(device_names, matched_agents, use_exact)
if not matched_agents: if not matched_agents:
logger.debug(" No matching devices found.") logger.debug("❌ No matching devices found.")
print(colorText(" No matching devices found.", "red")) print(colorText("❌ No matching devices found.", "red"))
return [] return []
print(colorText(f" Found {len(matched_agents)} matching device(s).", "green")) print(colorText(f"✓ Found {len(matched_agents)} matching device(s).", "green"))
logger.info("Matched agent hostnames:") logger.info("Matched agent hostnames:")
rows = (len(matched_agents) + 2) // 3 # 3 columns rows = (len(matched_agents) + 2) // 3 # 3 columns
for row in range(rows): for row in range(rows):
@@ -283,8 +283,8 @@ def selectAgents(api: "AirlockAPIWrapper") -> List["Agent"]:
) )
if not matched_agents: if not matched_agents:
logger.debug(" No matching devices remain after refinement.") logger.debug("❌ No matching devices remain after refinement.")
print(colorText(" No matching devices remain after refinement.", "red")) print(colorText("❌ No matching devices remain after refinement.", "red"))
return [] return []
enrich_agents(matched_agents, policies) enrich_agents(matched_agents, policies)
@@ -302,7 +302,7 @@ def moveAgentToRelatedPolicy(
Args: Args:
api: AirlockAPIWrapper instance. api: AirlockAPIWrapper instance.
agent: Agent object. agent: Agent object.
policy_relationship_map: Dict mapping enforcement â  audit. policy_relationship_map: Dict mapping enforcement âââ¬Â ââ¬â¢ audit.
mode: 'audit' to move to audit, 'enforcement' to move to enforcement. mode: 'audit' to move to audit, 'enforcement' to move to enforcement.
""" """
policy_relationship_map = get_system_json("POLICY_MAP_ENF_AUD", "{}") policy_relationship_map = get_system_json("POLICY_MAP_ENF_AUD", "{}")