- Consolidated all system/user config logic into configmanager.py - Removed duplicate loaders from setup.py and TUI.py - Eliminated .env redundancy; now only stores WORKING_DIR - Clarified boundaries: system config immutable, user config mutable - Updated TUI to use save_user_config() - Removed all deprecated/legacy config functions and aliases
This commit is contained in:
+15
-42
@@ -4,7 +4,6 @@ import sys
|
|||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
import dotenv
|
import dotenv
|
||||||
from dotenv import set_key
|
|
||||||
from textual.app import App, ComposeResult
|
from textual.app import App, ComposeResult
|
||||||
from textual.containers import Vertical
|
from textual.containers import Vertical
|
||||||
from textual.message import Message
|
from textual.message import Message
|
||||||
@@ -38,8 +37,8 @@ from TUI.resultsdisplay import ResultsDisplay
|
|||||||
from TUI.theme_amber_terminal import get_amber_terminal_theme
|
from TUI.theme_amber_terminal import get_amber_terminal_theme
|
||||||
from TUI.theme_retro_terminal import get_retro_terminal_theme
|
from TUI.theme_retro_terminal import get_retro_terminal_theme
|
||||||
from TUI.themeselector import ThemeSelector
|
from TUI.themeselector import ThemeSelector
|
||||||
from utils.configmanager import load_env
|
from utils.configmanager import get_user_value, load_env, save_user_config
|
||||||
from utils.setup import get_base_directory, load_user_config
|
from utils.setup import get_base_directory
|
||||||
from utils.utils import open_directory
|
from utils.utils import open_directory
|
||||||
|
|
||||||
dotenv.load_dotenv()
|
dotenv.load_dotenv()
|
||||||
@@ -58,47 +57,17 @@ logger = logging.getLogger(__name__)
|
|||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
def _persist_user_theme(theme_name: str) -> None:
|
def _persist_user_theme(theme_name: str) -> None:
|
||||||
"""
|
"""
|
||||||
Store the chosen Textual theme in the user's config:
|
Store the chosen Textual theme in the user's config using the config manager.
|
||||||
<base>/config/user_config.json
|
No need to touch .env - config manager handles everything.
|
||||||
and also mirror to <base>/.env so load_env(...) sees it.
|
|
||||||
"""
|
"""
|
||||||
base_dir = get_base_directory()
|
base_dir = get_base_directory()
|
||||||
config_dir = base_dir / "config"
|
config_dir = base_dir / "config"
|
||||||
user_config_path = config_dir / "user_config.json"
|
|
||||||
env_path = base_dir / ".env"
|
|
||||||
|
|
||||||
# ensure dirs / files exist similarly to setup()
|
|
||||||
config_dir.mkdir(parents=True, exist_ok=True)
|
|
||||||
if not user_config_path.exists():
|
|
||||||
# minimal default like your load_user_config does
|
|
||||||
user_config_path.write_text(
|
|
||||||
'{"URL": "", "LOG_LEVEL": "INFO"}\n', encoding="utf-8"
|
|
||||||
)
|
|
||||||
|
|
||||||
# load existing user config
|
|
||||||
user_conf = load_user_config(config_dir)
|
|
||||||
user_conf["TEXTUAL_THEME"] = theme_name
|
|
||||||
|
|
||||||
# write it back
|
|
||||||
user_config_path.write_text(
|
|
||||||
# pretty print so it stays human-readable
|
|
||||||
__import__("json").dumps(user_conf, indent=4),
|
|
||||||
encoding="utf-8",
|
|
||||||
)
|
|
||||||
logger.debug("Updated user_config.json with TEXTUAL_THEME=%s", theme_name)
|
|
||||||
|
|
||||||
# mirror to .env (like setup.write_config_to_env does)
|
|
||||||
env_path.parent.mkdir(parents=True, exist_ok=True)
|
|
||||||
if not env_path.exists():
|
|
||||||
env_path.touch()
|
|
||||||
try:
|
try:
|
||||||
set_key(str(env_path), "TEXTUAL_THEME", theme_name)
|
save_user_config(config_dir, {"TEXTUAL_THEME": theme_name})
|
||||||
except Exception as exc: # keep going even if .env write fails
|
logger.debug("Updated user config with TEXTUAL_THEME=%s", theme_name)
|
||||||
logger.warning("Failed to mirror TEXTUAL_THEME to .env: %s", exc)
|
except Exception as exc:
|
||||||
|
logger.error("Failed to save TEXTUAL_THEME: %s", exc)
|
||||||
# reload so load_env(...) sees the new value right now
|
|
||||||
dotenv.load_dotenv(dotenv_path=env_path, override=True)
|
|
||||||
logger.debug("Reloaded .env from %s", env_path)
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -115,7 +84,7 @@ class MainMenuScreen(Screen):
|
|||||||
"move_agent_workflow_button",
|
"move_agent_workflow_button",
|
||||||
),
|
),
|
||||||
("📊 - Review and appove OTP Activities", "otp_activities_button"),
|
("📊 - Review and appove OTP Activities", "otp_activities_button"),
|
||||||
("🔇 - Find and Move Quiet Hosts to Enforcement", "find_quiet_button"),
|
("📇 - Find and Move Quiet Hosts to Enforcement", "find_quiet_button"),
|
||||||
],
|
],
|
||||||
"policy": [
|
"policy": [
|
||||||
("🔒 - Prepare Policy For Enforcement", "policy_prep_button"),
|
("🔒 - Prepare Policy For Enforcement", "policy_prep_button"),
|
||||||
@@ -126,7 +95,7 @@ class MainMenuScreen(Screen):
|
|||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.extras = load_env("EXTRAS")
|
self.extras = get_user_value("EXTRAS", str, "NOTTODAY")
|
||||||
wd = load_env("WORKING_DIR") or os.getcwd()
|
wd = load_env("WORKING_DIR") or os.getcwd()
|
||||||
if not os.path.isdir(wd):
|
if not os.path.isdir(wd):
|
||||||
wd = os.getcwd()
|
wd = os.getcwd()
|
||||||
@@ -380,10 +349,11 @@ class Loxide(App[Message]):
|
|||||||
BINDINGS = [
|
BINDINGS = [
|
||||||
("q", "quit", "Quit"),
|
("q", "quit", "Quit"),
|
||||||
("f", "open_fe", "Launch Explorer"),
|
("f", "open_fe", "Launch Explorer"),
|
||||||
|
("r", "refresh", "Refresh"),
|
||||||
]
|
]
|
||||||
|
|
||||||
def __init__(self, api: AirlockAPIWrapper):
|
def __init__(self, api: AirlockAPIWrapper):
|
||||||
self._textual_theme = load_env("TEXTUAL_THEME") or "nord"
|
self._textual_theme = get_user_value("TEXTUAL_THEME", str, "nord")
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.api = api
|
self.api = api
|
||||||
wd = load_env("WORKING_DIR") or os.getcwd()
|
wd = load_env("WORKING_DIR") or os.getcwd()
|
||||||
@@ -421,6 +391,9 @@ class Loxide(App[Message]):
|
|||||||
self.theme = self._textual_theme
|
self.theme = self._textual_theme
|
||||||
self.push_screen(MainMenuScreen())
|
self.push_screen(MainMenuScreen())
|
||||||
|
|
||||||
|
def action_refresh(self) -> None:
|
||||||
|
self.refresh_data()
|
||||||
|
|
||||||
def action_quit(self) -> None:
|
def action_quit(self) -> None:
|
||||||
global _PENDING_JOB
|
global _PENDING_JOB
|
||||||
_PENDING_JOB = None
|
_PENDING_JOB = None
|
||||||
|
|||||||
+26
-26
@@ -183,21 +183,21 @@ class AgentMoveOperations(Widget):
|
|||||||
f"Operation: {operation_name}",
|
f"Operation: {operation_name}",
|
||||||
f"{'=' * 50}",
|
f"{'=' * 50}",
|
||||||
"",
|
"",
|
||||||
f"✅ Successful ({len(successful)}):",
|
f"✅ Successful ({len(successful)}):",
|
||||||
]
|
]
|
||||||
|
|
||||||
if successful:
|
if successful:
|
||||||
for agent, result in successful:
|
for agent, result in successful:
|
||||||
results_lines.append(f" ✅ {agent.hostname}")
|
results_lines.append(f" ✅ {agent.hostname}")
|
||||||
else:
|
else:
|
||||||
results_lines.append(" (none)")
|
results_lines.append(" (none)")
|
||||||
|
|
||||||
results_lines.append("")
|
results_lines.append("")
|
||||||
results_lines.append(f"❌ Failed ({len(unsuccessful)}):")
|
results_lines.append(f"⌠Failed ({len(unsuccessful)}):")
|
||||||
|
|
||||||
if unsuccessful:
|
if unsuccessful:
|
||||||
for agent, error in unsuccessful:
|
for agent, error in unsuccessful:
|
||||||
results_lines.append(f" ❌ {agent.hostname}: {error}")
|
results_lines.append(f" ⌠{agent.hostname}: {error}")
|
||||||
else:
|
else:
|
||||||
results_lines.append(" (none)")
|
results_lines.append(" (none)")
|
||||||
|
|
||||||
@@ -232,9 +232,9 @@ class AgentMoveOperations(Widget):
|
|||||||
- Operations panel: 1/3 width
|
- Operations panel: 1/3 width
|
||||||
- Results area: Initially hidden, shown after operation completion
|
- Results area: Initially hidden, shown after operation completion
|
||||||
"""
|
"""
|
||||||
yield Header(show_clock=True, icon="⚙")
|
yield Header(show_clock=True, icon="âš™")
|
||||||
title_text = Static(
|
title_text = Static(
|
||||||
f"🖥️ Agent Operations - {len(self.agents)} device(s) selected",
|
f"ðŸ–¥ï¸ Agent Operations - {len(self.agents)} device(s) selected",
|
||||||
id="move_ops_title",
|
id="move_ops_title",
|
||||||
)
|
)
|
||||||
title_text.styles.margin = (0, 0, 1, 0)
|
title_text.styles.margin = (0, 0, 1, 0)
|
||||||
@@ -269,32 +269,32 @@ class AgentMoveOperations(Widget):
|
|||||||
yield operations_label
|
yield operations_label
|
||||||
|
|
||||||
# Operation buttons
|
# Operation buttons
|
||||||
export_csv_btn = Button("📈 Export CSV", id="export_csv_btn")
|
export_csv_btn = Button("📈 Export CSV", id="export_csv_btn")
|
||||||
export_csv_btn.styles.width = "100%"
|
export_csv_btn.styles.width = "100%"
|
||||||
export_csv_btn.styles.margin = (0, 0, 1, 0)
|
export_csv_btn.styles.margin = (0, 0, 1, 0)
|
||||||
yield export_csv_btn
|
yield export_csv_btn
|
||||||
|
|
||||||
local_approval_btn = Button(
|
local_approval_btn = Button(
|
||||||
"✔️ Local Approval Mode", id="local_approval_btn"
|
"âœ”ï¸ Local Approval Mode", id="local_approval_btn"
|
||||||
)
|
)
|
||||||
local_approval_btn.styles.width = "100%"
|
local_approval_btn.styles.width = "100%"
|
||||||
local_approval_btn.styles.margin = (0, 0, 1, 0)
|
local_approval_btn.styles.margin = (0, 0, 1, 0)
|
||||||
yield local_approval_btn
|
yield local_approval_btn
|
||||||
|
|
||||||
otp_gen_btn = Button("🎫 Generate One Time Passes", id="otp_gen_btn")
|
otp_gen_btn = Button("🎫 Generate One Time Passes", id="otp_gen_btn")
|
||||||
otp_gen_btn.styles.width = "100%"
|
otp_gen_btn.styles.width = "100%"
|
||||||
otp_gen_btn.styles.margin = (0, 0, 1, 0)
|
otp_gen_btn.styles.margin = (0, 0, 1, 0)
|
||||||
yield otp_gen_btn
|
yield otp_gen_btn
|
||||||
|
|
||||||
toggle_enforcement_btn = Button(
|
toggle_enforcement_btn = Button(
|
||||||
"🔄 Toggle Audit/Enforcement", id="toggle_enforcement_btn"
|
"🔄 Toggle Audit/Enforcement", id="toggle_enforcement_btn"
|
||||||
)
|
)
|
||||||
toggle_enforcement_btn.styles.width = "100%"
|
toggle_enforcement_btn.styles.width = "100%"
|
||||||
toggle_enforcement_btn.styles.margin = (0, 0, 1, 0)
|
toggle_enforcement_btn.styles.margin = (0, 0, 1, 0)
|
||||||
yield toggle_enforcement_btn
|
yield toggle_enforcement_btn
|
||||||
|
|
||||||
other_policy_btn = Button(
|
other_policy_btn = Button(
|
||||||
"🔀 Move to Other Policy", id="other_policy_btn"
|
"🔀 Move to Other Policy", id="other_policy_btn"
|
||||||
)
|
)
|
||||||
other_policy_btn.styles.width = "100%"
|
other_policy_btn.styles.width = "100%"
|
||||||
other_policy_btn.styles.margin = (0, 0, 1, 0)
|
other_policy_btn.styles.margin = (0, 0, 1, 0)
|
||||||
@@ -305,7 +305,7 @@ class AgentMoveOperations(Widget):
|
|||||||
status_label.styles.margin = (2, 0, 0, 0)
|
status_label.styles.margin = (2, 0, 0, 0)
|
||||||
yield status_label
|
yield status_label
|
||||||
|
|
||||||
back_button = Button("← Back", id="back_button")
|
back_button = Button("↠Back", id="back_button")
|
||||||
back_button.styles.width = "50%"
|
back_button.styles.width = "50%"
|
||||||
back_button.styles.margin = (0, 1, 1, 0)
|
back_button.styles.margin = (0, 1, 1, 0)
|
||||||
yield back_button
|
yield back_button
|
||||||
@@ -369,17 +369,17 @@ class AgentMoveOperations(Widget):
|
|||||||
|
|
||||||
pyperclip.copy(results_text.text)
|
pyperclip.copy(results_text.text)
|
||||||
self.app.notify(
|
self.app.notify(
|
||||||
"📋✅ Results copied to clipboard!",
|
"📋✅ Results copied to clipboard!",
|
||||||
severity="information",
|
severity="information",
|
||||||
timeout=2,
|
timeout=2,
|
||||||
)
|
)
|
||||||
except ImportError:
|
except ImportError:
|
||||||
self.app.notify(
|
self.app.notify(
|
||||||
"❌ pyperclip not installed. Run: pip install pyperclip",
|
"⌠pyperclip not installed. Run: pip install pyperclip",
|
||||||
severity="warning",
|
severity="warning",
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.app.notify(f"⌠Failed to copy: {str(e)}", severity="error")
|
self.app.notify(f"âÂÅ’ Failed to copy: {str(e)}", severity="error")
|
||||||
event.stop()
|
event.stop()
|
||||||
elif btn_id == "export_csv_btn":
|
elif btn_id == "export_csv_btn":
|
||||||
self._start_export_csv_operation()
|
self._start_export_csv_operation()
|
||||||
@@ -427,7 +427,7 @@ class AgentMoveOperations(Widget):
|
|||||||
self.operation_in_progress = True
|
self.operation_in_progress = True
|
||||||
|
|
||||||
status_label = self.query_one("#status_label", Static)
|
status_label = self.query_one("#status_label", Static)
|
||||||
status_label.update("✔️ Moving agents to local approval...")
|
status_label.update("âœ”ï¸ Moving agents to local approval...")
|
||||||
|
|
||||||
# Get API from app
|
# Get API from app
|
||||||
api = self.app.api
|
api = self.app.api
|
||||||
@@ -462,12 +462,12 @@ class AgentMoveOperations(Widget):
|
|||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error during local approval operation: {e}")
|
logger.error(f"Error during local approval operation: {e}")
|
||||||
status_label.update(f"❌ Error: {str(e)}")
|
status_label.update(f"⌠Error: {str(e)}")
|
||||||
self.operation_in_progress = False
|
self.operation_in_progress = False
|
||||||
return
|
return
|
||||||
|
|
||||||
self.operation_in_progress = False
|
self.operation_in_progress = False
|
||||||
status_label.update("✅ Operation complete!")
|
status_label.update("✅ Operation complete!")
|
||||||
|
|
||||||
# Display results in the widget
|
# Display results in the widget
|
||||||
self._display_results("Local Approval Mode", successful, unsuccessful)
|
self._display_results("Local Approval Mode", successful, unsuccessful)
|
||||||
@@ -511,9 +511,9 @@ class AgentMoveOperations(Widget):
|
|||||||
file_path = os.path.join(str(path), filename)
|
file_path = os.path.join(str(path), filename)
|
||||||
df.to_csv(file_path, index=False)
|
df.to_csv(file_path, index=False)
|
||||||
successful.append(file_path)
|
successful.append(file_path)
|
||||||
status_label.update(f"✅ Exported to {file_path}")
|
status_label.update(f"✅ Exported to {file_path}")
|
||||||
except Exception:
|
except Exception:
|
||||||
status_label.update("❌ Failed")
|
status_label.update("⌠Failed")
|
||||||
|
|
||||||
self.operation_in_progress = False
|
self.operation_in_progress = False
|
||||||
|
|
||||||
@@ -557,7 +557,7 @@ class AgentMoveOperations(Widget):
|
|||||||
self.operation_in_progress = True
|
self.operation_in_progress = True
|
||||||
|
|
||||||
status_label = self.query_one("#status_label", Static)
|
status_label = self.query_one("#status_label", Static)
|
||||||
status_label.update("â³ Toggling enforcement mode...")
|
status_label.update("â³ Toggling enforcement mode...")
|
||||||
|
|
||||||
# Get API from app
|
# Get API from app
|
||||||
api = self.app.api
|
api = self.app.api
|
||||||
@@ -567,9 +567,9 @@ class AgentMoveOperations(Widget):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
from services.agenthandler import moveAgentToRelatedPolicy
|
from services.agenthandler import moveAgentToRelatedPolicy
|
||||||
from utils.configmanager import get_protected_json
|
from utils.configmanager import get_system_json
|
||||||
|
|
||||||
policy_relationship_map = get_protected_json("POLICY_MAP_ENF_AUD", "{}")
|
policy_relationship_map = get_system_json("POLICY_MAP_ENF_AUD", "{}")
|
||||||
|
|
||||||
for agent in self.agents:
|
for agent in self.agents:
|
||||||
try:
|
try:
|
||||||
@@ -593,12 +593,12 @@ class AgentMoveOperations(Widget):
|
|||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error during toggle enforcement operation: {e}")
|
logger.error(f"Error during toggle enforcement operation: {e}")
|
||||||
status_label.update(f"❌ Error: {str(e)}")
|
status_label.update(f"⌠Error: {str(e)}")
|
||||||
self.operation_in_progress = False
|
self.operation_in_progress = False
|
||||||
return
|
return
|
||||||
|
|
||||||
self.operation_in_progress = False
|
self.operation_in_progress = False
|
||||||
status_label.update("✅ Operation complete!")
|
status_label.update("✅ Operation complete!")
|
||||||
|
|
||||||
# Display results in the widget
|
# Display results in the widget
|
||||||
self._display_results("Toggle Audit/Enforcement", successful, unsuccessful)
|
self._display_results("Toggle Audit/Enforcement", successful, unsuccessful)
|
||||||
@@ -663,7 +663,7 @@ class AgentMoveOperations(Widget):
|
|||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error loading policies: {e}")
|
logger.error(f"Error loading policies: {e}")
|
||||||
status_label.update(f"❌ Error: {str(e)}")
|
status_label.update(f"⌠Error: {str(e)}")
|
||||||
self.operation_in_progress = False
|
self.operation_in_progress = False
|
||||||
self.selected_operation = ""
|
self.selected_operation = ""
|
||||||
self.app.notify(f"Failed to load policies: {str(e)}", severity="error")
|
self.app.notify(f"Failed to load policies: {str(e)}", severity="error")
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ def history_logging(
|
|||||||
checkpoint_number: str,
|
checkpoint_number: str,
|
||||||
policy_names: str,
|
policy_names: str,
|
||||||
) -> List[Dict[str, Any]]:
|
) -> List[Dict[str, Any]]:
|
||||||
"""
|
"""
|
||||||
Query execution history logs from the Airlock API.
|
Query execution history logs from the Airlock API.
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
|
|||||||
@@ -2,15 +2,38 @@
|
|||||||
"APPNAME": "Loxide",
|
"APPNAME": "Loxide",
|
||||||
"URL": "https://server:3129",
|
"URL": "https://server:3129",
|
||||||
"LOG_LEVEL": "INFO",
|
"LOG_LEVEL": "INFO",
|
||||||
"BAD_PATH_PARTS": ["users","wwwroot","windows\\temp","windows\\task","windows\\system32","startup", "windows\\fonts","Recycle.Bin","AppData","programdata", "Solarwinds","kaseya"],
|
"BAD_PATH_PARTS": [
|
||||||
"BAD_PUBLISHERS": ["Brave", "Zoom", "GlavSoft", "VNC"],
|
"users",
|
||||||
"PUPS":["logmein","invalid","nmap","LTSvc","VNC","Kaseya","Solarwinds","mRemoteNG"],
|
"wwwroot",
|
||||||
|
"windows\\temp",
|
||||||
|
"windows\\task",
|
||||||
|
"windows\\system32",
|
||||||
|
"startup",
|
||||||
|
"windows\\fonts",
|
||||||
|
"Recycle.Bin",
|
||||||
|
"AppData",
|
||||||
|
"programdata",
|
||||||
|
"Solarwinds",
|
||||||
|
"kaseya"
|
||||||
|
],
|
||||||
|
"BAD_PUBLISHERS": [
|
||||||
|
"Brave",
|
||||||
|
"Zoom",
|
||||||
|
"GlavSoft",
|
||||||
|
"VNC"
|
||||||
|
],
|
||||||
|
"PUPS": [
|
||||||
|
"logmein",
|
||||||
|
"invalid",
|
||||||
|
"nmap",
|
||||||
|
"LTSvc",
|
||||||
|
"VNC",
|
||||||
|
"Kaseya",
|
||||||
|
"Solarwinds",
|
||||||
|
"mRemoteNG"
|
||||||
|
],
|
||||||
"PATH_EXCLUSION_CONST": 4,
|
"PATH_EXCLUSION_CONST": 4,
|
||||||
"MIN_FILES_FOR_PATH": 4,
|
"MIN_FILES_FOR_PATH": 4,
|
||||||
"VT_THREAT_TOLERANCE": 4,
|
"VT_THREAT_TOLERANCE": 4,
|
||||||
"TELEMETRY": "FALSE",
|
"POLICY_MAP_ENF_AUD": {}
|
||||||
"TELEM_URL": "",
|
|
||||||
"POLICY_MAP_ENF_AUD": {
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
+26
-25
@@ -10,7 +10,7 @@ from typing import List, Optional
|
|||||||
from models.agent import Agent
|
from models.agent import Agent
|
||||||
from services.agenthandler import moveAgentToRelatedPolicy, selectAgents
|
from services.agenthandler import moveAgentToRelatedPolicy, selectAgents
|
||||||
from services.API import AirlockAPIWrapper
|
from services.API import AirlockAPIWrapper
|
||||||
from utils.configmanager import get_protected_json
|
from utils.configmanager import get_system_json
|
||||||
from utils.utils import colorText, get_sanitized_input
|
from utils.utils import colorText, get_sanitized_input
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -28,7 +28,7 @@ class LocalApprovalRequestor:
|
|||||||
username: Username creating the approvals (for tracking)
|
username: Username creating the approvals (for tracking)
|
||||||
"""
|
"""
|
||||||
self.api = api
|
self.api = api
|
||||||
self.policy_map = get_protected_json("POLICY_MAP_ENF_AUD", "{}")
|
self.policy_map = get_system_json("POLICY_MAP_ENF_AUD", "{}")
|
||||||
self.username = (
|
self.username = (
|
||||||
username or os.getenv("USERNAME") or os.getenv("USER") or "unknown"
|
username or os.getenv("USERNAME") or os.getenv("USER") or "unknown"
|
||||||
)
|
)
|
||||||
@@ -51,7 +51,7 @@ class LocalApprovalRequestor:
|
|||||||
batch_id = int(time.time())
|
batch_id = int(time.time())
|
||||||
|
|
||||||
purpose = (
|
purpose = (
|
||||||
f"🎫 Local Approval 🎫 - {duration_minutes} mins - "
|
f"🎫 Local Approval 🎫 - {duration_minutes} mins - "
|
||||||
f"batch:{batch_id} Client:{agent_id} User:{self.username}"
|
f"batch:{batch_id} Client:{agent_id} User:{self.username}"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -103,10 +103,10 @@ class LocalApprovalRequestor:
|
|||||||
success_count = 0
|
success_count = 0
|
||||||
failure_count = 0
|
failure_count = 0
|
||||||
|
|
||||||
print(colorText(f"\n📦 Processing batch {batch_id}...", "cyan"))
|
print(colorText(f"\n📦 Processing batch {batch_id}...", "cyan"))
|
||||||
print(colorText(f"👤 Requested by: {self.username}", "cyan"))
|
print(colorText(f"👤 Requested by: {self.username}", "cyan"))
|
||||||
print(
|
print(
|
||||||
colorText(f"📊 Moving {len(agents)} agent(s) to local approval\n", "cyan")
|
colorText(f"📊 Moving {len(agents)} agent(s) to local approval\n", "cyan")
|
||||||
)
|
)
|
||||||
|
|
||||||
for agent in agents:
|
for agent in agents:
|
||||||
@@ -125,11 +125,11 @@ class LocalApprovalRequestor:
|
|||||||
if not move_success:
|
if not move_success:
|
||||||
raise Exception("Failed to move to audit policy")
|
raise Exception("Failed to move to audit policy")
|
||||||
|
|
||||||
print(colorText(f"✓ {agent.hostname}", "green"))
|
print(colorText(f"✓ {agent.hostname}", "green"))
|
||||||
success_count += 1
|
success_count += 1
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(colorText(f"✗ {agent.hostname}: {e}", "red"))
|
print(colorText(f"✗ {agent.hostname}: {e}", "red"))
|
||||||
logger.error(f"Error processing agent {agent.hostname}: {e}")
|
logger.error(f"Error processing agent {agent.hostname}: {e}")
|
||||||
failure_count += 1
|
failure_count += 1
|
||||||
|
|
||||||
@@ -152,7 +152,7 @@ class LocalApprovalRequestor:
|
|||||||
]
|
]
|
||||||
|
|
||||||
# Display duration options
|
# Display duration options
|
||||||
print(colorText("\n⏱️ Select Local Approval Duration:", "white"))
|
print(colorText("\nâ±ï¸ Select Local Approval Duration:", "white"))
|
||||||
print(colorText("=" * 50, "white"))
|
print(colorText("=" * 50, "white"))
|
||||||
|
|
||||||
for i, (minutes, label) in enumerate(duration_options, start=1):
|
for i, (minutes, label) in enumerate(duration_options, start=1):
|
||||||
@@ -166,36 +166,36 @@ class LocalApprovalRequestor:
|
|||||||
|
|
||||||
if 1 <= choice <= len(duration_options):
|
if 1 <= choice <= len(duration_options):
|
||||||
duration_minutes, duration_label = duration_options[choice - 1]
|
duration_minutes, duration_label = duration_options[choice - 1]
|
||||||
print(colorText(f"✓ Selected: {duration_label}", "green"))
|
print(colorText(f"✓ Selected: {duration_label}", "green"))
|
||||||
logger.info(f"User selected duration: {duration_minutes} minutes")
|
logger.info(f"User selected duration: {duration_minutes} minutes")
|
||||||
else:
|
else:
|
||||||
print(colorText("❌ Invalid choice.", "red"))
|
print(colorText("⌠Invalid choice.", "red"))
|
||||||
logger.warning("Invalid duration choice")
|
logger.warning("Invalid duration choice")
|
||||||
return
|
return
|
||||||
|
|
||||||
except ValueError:
|
except ValueError:
|
||||||
print(colorText("❌ Invalid input. Please enter a number.", "red"))
|
print(colorText("⌠Invalid input. Please enter a number.", "red"))
|
||||||
logger.warning("Invalid input for duration selection")
|
logger.warning("Invalid input for duration selection")
|
||||||
return
|
return
|
||||||
|
|
||||||
# Select agents
|
# Select agents
|
||||||
print(colorText("\n🎯 Select Agents for Local Approval:", "white"))
|
print(colorText("\n🎯 Select Agents for Local Approval:", "white"))
|
||||||
agents = selectAgents(self.api)
|
agents = selectAgents(self.api)
|
||||||
|
|
||||||
if not agents:
|
if not agents:
|
||||||
print(colorText("❌ No agents found or error retrieving agents.", "red"))
|
print(colorText("⌠No agents found or error retrieving agents.", "red"))
|
||||||
logger.warning("No agents selected or error retrieving agents")
|
logger.warning("No agents selected or error retrieving agents")
|
||||||
return
|
return
|
||||||
|
|
||||||
# Confirm with user
|
# Confirm with user
|
||||||
print(colorText("\n📋 Summary:", "cyan"))
|
print(colorText("\n📋 Summary:", "cyan"))
|
||||||
print(colorText(f" Duration: {duration_label}", "white"))
|
print(colorText(f" Duration: {duration_label}", "white"))
|
||||||
print(colorText(f" Agents: {len(agents)}", "white"))
|
print(colorText(f" Agents: {len(agents)}", "white"))
|
||||||
|
|
||||||
confirm = get_sanitized_input("\nProceed? (y/n): ").lower()
|
confirm = get_sanitized_input("\nProceed? (y/n): ").lower()
|
||||||
|
|
||||||
if confirm != "y":
|
if confirm != "y":
|
||||||
print(colorText("❌ Operation cancelled.", "yellow"))
|
print(colorText("⌠Operation cancelled.", "yellow"))
|
||||||
return
|
return
|
||||||
|
|
||||||
# Process the batch
|
# Process the batch
|
||||||
@@ -219,24 +219,25 @@ class LocalApprovalRequestor:
|
|||||||
failure_count: Number of failed operations
|
failure_count: Number of failed operations
|
||||||
"""
|
"""
|
||||||
print(colorText(f"\n{'=' * 60}", "white"))
|
print(colorText(f"\n{'=' * 60}", "white"))
|
||||||
print(colorText("📊 Local Approval Summary", "cyan"))
|
print(colorText("📊 Local Approval Summary", "cyan"))
|
||||||
print(colorText("=" * 60, "white"))
|
print(colorText("=" * 60, "white"))
|
||||||
|
|
||||||
print(colorText(f"✓ Successfully processed: {success_count}", "green"))
|
print(colorText(f"✓ Successfully processed: {success_count}", "green"))
|
||||||
|
|
||||||
if failure_count > 0:
|
if failure_count > 0:
|
||||||
print(colorText(f"✗ Failed: {failure_count}", "red"))
|
print(colorText(f"✗ Failed: {failure_count}", "red"))
|
||||||
|
|
||||||
print(colorText(f"\n📦 Batch ID: {batch_id}", "cyan"))
|
print(colorText(f"\n📦 Batch ID: {batch_id}", "cyan"))
|
||||||
print(colorText(f"⏱️ Duration: {duration_label}", "cyan"))
|
print(colorText(f"â±ï¸ Duration: {duration_label}", "cyan"))
|
||||||
|
|
||||||
print(colorText("=" * 60, "white"))
|
print(colorText("=" * 60, "white"))
|
||||||
print(colorText("\n💡 Next Steps:", "yellow"))
|
print(colorText("\n💡 Next Steps:", "yellow"))
|
||||||
print(colorText(" • Agents have been moved to audit policies", "white"))
|
print(colorText(" • Agents have been moved to audit policies", "white"))
|
||||||
print(colorText(" • Local approvals are active", "white"))
|
print(colorText(" • Local approvals are active", "white"))
|
||||||
print(
|
print(
|
||||||
colorText(
|
colorText(
|
||||||
f" • Agents will return to enforcement after {duration_label}", "white"
|
f" • Agents will return to enforcement after {duration_label}",
|
||||||
|
"white",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
print(colorText("=" * 60 + "\n", "white"))
|
print(colorText("=" * 60 + "\n", "white"))
|
||||||
|
|||||||
+29
-29
@@ -25,7 +25,7 @@ import pandas as pd
|
|||||||
from models.execution import ExecutionHistoryRecord
|
from models.execution import ExecutionHistoryRecord
|
||||||
from models.policy import Allowlist, Policy
|
from models.policy import Allowlist, Policy
|
||||||
from services.API import AirlockAPIWrapper
|
from services.API import AirlockAPIWrapper
|
||||||
from utils.configmanager import get_protected_value, load_env, load_env_json
|
from utils.configmanager import get_system_list, get_system_value, load_env
|
||||||
from utils.selector import Selector
|
from utils.selector import Selector
|
||||||
from utils.utils import (
|
from utils.utils import (
|
||||||
areYouSure,
|
areYouSure,
|
||||||
@@ -88,7 +88,7 @@ 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 (1–150): ",
|
prompt="Enter how many days of history to pull (1–150): ",
|
||||||
value_type=int,
|
value_type=int,
|
||||||
valid_range=(1, 150),
|
valid_range=(1, 150),
|
||||||
)
|
)
|
||||||
@@ -157,7 +157,7 @@ def buildPathsandPublishers(selected_policies: List[Policy], split):
|
|||||||
f"{working_dir}\\Approved\\{selected_policies[0].name}_approved_executions.csv"
|
f"{working_dir}\\Approved\\{selected_policies[0].name}_approved_executions.csv"
|
||||||
)
|
)
|
||||||
path2 = f"{working_dir}\\Approved\\{selected_policies[0].name}_needs_review_executions.csv"
|
path2 = f"{working_dir}\\Approved\\{selected_policies[0].name}_needs_review_executions.csv"
|
||||||
path_exclusion_constant = get_protected_value("PATH_EXCLUSION_CONST", cast_type=int)
|
path_exclusion_constant = get_system_value("PATH_EXCLUSION_CONST", cast_type=int)
|
||||||
|
|
||||||
if os.path.exists(path1):
|
if os.path.exists(path1):
|
||||||
df1 = pd.read_csv(path1)
|
df1 = pd.read_csv(path1)
|
||||||
@@ -227,7 +227,7 @@ def buildPathsandPublishers(selected_policies: List[Policy], split):
|
|||||||
all_approved_hashes["publisher"] != "Not Signed"
|
all_approved_hashes["publisher"] != "Not Signed"
|
||||||
].drop_duplicates(subset=["publisher"])
|
].drop_duplicates(subset=["publisher"])
|
||||||
# Remove Bad publisher if somehow they made it this far
|
# Remove Bad publisher if somehow they made it this far
|
||||||
pattern = regulator(load_env_json("BAD_PUBLISHERS", "[]"))
|
pattern = regulator(get_system_list("BAD_PUBLISHERS"))
|
||||||
publist = publist[~publist["publisher"].str.contains(pattern, na=False)]
|
publist = publist[~publist["publisher"].str.contains(pattern, na=False)]
|
||||||
publist = publist[["publisher"]]
|
publist = publist[["publisher"]]
|
||||||
publist.sort_values(by="publisher", inplace=True)
|
publist.sort_values(by="publisher", inplace=True)
|
||||||
@@ -313,7 +313,7 @@ def buildPreflights(selected_policies: List[Policy]):
|
|||||||
|
|
||||||
|
|
||||||
def splitFilepathsGrouped(df, path_exclusion_constant, col="filename"):
|
def splitFilepathsGrouped(df, path_exclusion_constant, col="filename"):
|
||||||
min_files_for_path = get_protected_value("MIN_FILES_FOR_PATH", cast_type=int)
|
min_files_for_path = get_system_value("MIN_FILES_FOR_PATH", cast_type=int)
|
||||||
|
|
||||||
def clean_split(path):
|
def clean_split(path):
|
||||||
if not isinstance(path, (str, bytes, os.PathLike)):
|
if not isinstance(path, (str, bytes, os.PathLike)):
|
||||||
@@ -385,8 +385,8 @@ def calculatePath(approved_hashes, path_exclusion_constant, split):
|
|||||||
else:
|
else:
|
||||||
dfs_by_policy = [approved_hashes]
|
dfs_by_policy = [approved_hashes]
|
||||||
|
|
||||||
badpathparts = load_env_json("BAD_PATH_PARTS", "[]")
|
badpathparts = get_system_list("BAD_PATH_PARTS")
|
||||||
min_files_for_path = get_protected_value("MIN_FILES_FOR_PATH", cast_type=int)
|
min_files_for_path = get_system_value("MIN_FILES_FOR_PATH", cast_type=int)
|
||||||
|
|
||||||
processed_dfs = []
|
processed_dfs = []
|
||||||
|
|
||||||
@@ -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"))
|
||||||
|
|||||||
@@ -1,134 +0,0 @@
|
|||||||
# 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 <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
import datetime
|
|
||||||
import logging
|
|
||||||
|
|
||||||
import dotenv
|
|
||||||
import pandas as pd
|
|
||||||
|
|
||||||
from flows.prepPolicy import selectPolicies
|
|
||||||
from services.API import AirlockAPIWrapper
|
|
||||||
from services.policyhandler import getPolicyInfo
|
|
||||||
from utils.configmanager import load_env
|
|
||||||
from utils.selector import Selector
|
|
||||||
from utils.utils import colorText, get_sanitized_input
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
dotenv.load_dotenv()
|
|
||||||
|
|
||||||
|
|
||||||
def findQuietAgents(api: AirlockAPIWrapper):
|
|
||||||
working_dir = load_env("WORKING_DIR")
|
|
||||||
# Get policy selection and agent list
|
|
||||||
selected_policy = selectPolicies(api, False)
|
|
||||||
if selected_policy:
|
|
||||||
agents = api.agents_find_by_group(selected_policy[0].groupid)
|
|
||||||
|
|
||||||
# Prompt user for history range
|
|
||||||
history_days = Selector.select_value(
|
|
||||||
prompt="Enter how many days of history to pull (1–150): ",
|
|
||||||
value_type=int,
|
|
||||||
valid_range=(1, 150),
|
|
||||||
)
|
|
||||||
required_quiet = Selector.select_value(
|
|
||||||
prompt="Enter how many days without an untrusted execution before these are considered ready for enforcement? (1–365): ",
|
|
||||||
value_type=int,
|
|
||||||
valid_range=(1, 150),
|
|
||||||
)
|
|
||||||
|
|
||||||
confirm = Selector.confirm(
|
|
||||||
f"Do you wish to proceed to pull history for {selected_policy[0].name}? Y/N : "
|
|
||||||
)
|
|
||||||
# Get execution history as a DataFrame
|
|
||||||
if confirm:
|
|
||||||
policy_exec_history = getPolicyInfo(
|
|
||||||
api, selected_policy[0], [1, 2, 6, 7], history_days
|
|
||||||
)
|
|
||||||
|
|
||||||
if policy_exec_history.empty:
|
|
||||||
logging.info(
|
|
||||||
"No execution history found for the selected policy and time range."
|
|
||||||
)
|
|
||||||
get_sanitized_input("Press enter to continue")
|
|
||||||
return
|
|
||||||
|
|
||||||
# Convert 'datetime' column to timezone-aware datetime objects
|
|
||||||
policy_exec_history["datetime"] = pd.to_datetime(
|
|
||||||
policy_exec_history["datetime"], format="%Y-%m-%dT%H:%M:%SZ", utc=True
|
|
||||||
)
|
|
||||||
|
|
||||||
# Get current UTC time
|
|
||||||
now = datetime.datetime.now(datetime.timezone.utc)
|
|
||||||
|
|
||||||
# Calculate days ago
|
|
||||||
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()
|
|
||||||
|
|
||||||
# Map execution counts to agents
|
|
||||||
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"] = required_quiet
|
|
||||||
agents["enforce_ready"] = agents["days_since"].apply(
|
|
||||||
lambda x: True if pd.isna(x) or x > required_quiet else False
|
|
||||||
)
|
|
||||||
|
|
||||||
# Sort agents by execution count and hostname
|
|
||||||
agents = agents.sort_values(
|
|
||||||
by=["execution_count", "hostname"], ascending=[True, True]
|
|
||||||
)
|
|
||||||
|
|
||||||
# Save to CSV
|
|
||||||
filename = f"{working_dir}\\{selected_policy[0].name}_agents_last_{history_days}_days.csv"
|
|
||||||
logging.debug(f"Saving CSV to {filename}")
|
|
||||||
print(colorText(f"Saving CSV to {filename}", "green"))
|
|
||||||
agents.to_csv(filename, index=False)
|
|
||||||
|
|
||||||
# Summary statistics
|
|
||||||
total_agents = len(agents)
|
|
||||||
ready_agents = agents["enforce_ready"].sum()
|
|
||||||
not_ready_agents = total_agents - ready_agents
|
|
||||||
ready_percentage = (ready_agents / total_agents) * 100
|
|
||||||
|
|
||||||
# Print results
|
|
||||||
|
|
||||||
message = (
|
|
||||||
f"Total agents: {total_agents}\n"
|
|
||||||
f"Agents marked as 'enforce_ready': {ready_agents}\n"
|
|
||||||
f"Agents not ready: {not_ready_agents}\n"
|
|
||||||
f"Percentage ready for enforcement: {ready_percentage:.2f}%"
|
|
||||||
)
|
|
||||||
logger.debug(message)
|
|
||||||
colorText(message, "green")
|
|
||||||
get_sanitized_input("Press enter to continue")
|
|
||||||
+11
-11
@@ -28,7 +28,7 @@ import pandas as pd
|
|||||||
|
|
||||||
import airlock_libs
|
import airlock_libs
|
||||||
from services.API import AirlockAPIWrapper
|
from services.API import AirlockAPIWrapper
|
||||||
from utils.configmanager import get_protected_value, load_env_json
|
from utils.configmanager import get_system_list, get_system_value
|
||||||
from utils.utils import colorText, regulator
|
from utils.utils import colorText, regulator
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -90,9 +90,9 @@ class Hash:
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def categorize_hashes(cls, hashes):
|
def categorize_hashes(cls, hashes):
|
||||||
threat_tolerance = get_protected_value("VT_THREAT_TOLERANCE", cast_type=int)
|
threat_tolerance = get_system_value("VT_THREAT_TOLERANCE", cast_type=int)
|
||||||
bad_publishers_pattern = regulator(load_env_json("BAD_PUBLISHERS", "[]"))
|
bad_publishers_pattern = regulator(get_system_list("BAD_PUBLISHERS"))
|
||||||
pups_pattern = regulator(load_env_json("PUPS", "[]"))
|
pups_pattern = regulator(get_system_list("PUPS"))
|
||||||
|
|
||||||
approved_count = 0
|
approved_count = 0
|
||||||
unapproved_count = 0
|
unapproved_count = 0
|
||||||
@@ -145,13 +145,13 @@ class Hash:
|
|||||||
approved_count += 1
|
approved_count += 1
|
||||||
except (ValueError, TypeError):
|
except (ValueError, TypeError):
|
||||||
logger.debug(
|
logger.debug(
|
||||||
"Needs Review: Scannermatch score is missing or invalid. — {e}"
|
"Needs Review: Scannermatch score is missing or invalid. — {e}"
|
||||||
)
|
)
|
||||||
hash_obj.at_decision = "needs_review"
|
hash_obj.at_decision = "needs_review"
|
||||||
needs_review_count += 1
|
needs_review_count += 1
|
||||||
|
|
||||||
logger.debug(
|
logger.debug(
|
||||||
f"Final counts — Needs Review: {needs_review_count}, Approved: {approved_count}, Unapproved: {unapproved_count}"
|
f"Final counts — Needs Review: {needs_review_count}, Approved: {approved_count}, Unapproved: {unapproved_count}"
|
||||||
)
|
)
|
||||||
return hashes
|
return hashes
|
||||||
|
|
||||||
@@ -384,9 +384,9 @@ class ExecutionHistoryRecord:
|
|||||||
Returns:
|
Returns:
|
||||||
List[ExecutionHistoryRecord]: The same list, with hash_obj.at_decision updated.
|
List[ExecutionHistoryRecord]: The same list, with hash_obj.at_decision updated.
|
||||||
"""
|
"""
|
||||||
threat_tolerance = get_protected_value("VT_THREAT_TOLERANCE", cast_type=int)
|
threat_tolerance = get_system_value("VT_THREAT_TOLERANCE", cast_type=int)
|
||||||
bad_publishers_pattern = regulator(load_env_json("BAD_PUBLISHERS", "[]"))
|
bad_publishers_pattern = regulator(get_system_list("BAD_PUBLISHERS"))
|
||||||
pups_pattern = regulator(load_env_json("PUPS", "[]"))
|
pups_pattern = regulator(get_system_list("PUPS"))
|
||||||
|
|
||||||
approved_count = 0
|
approved_count = 0
|
||||||
unapproved_count = 0
|
unapproved_count = 0
|
||||||
@@ -443,13 +443,13 @@ class ExecutionHistoryRecord:
|
|||||||
approved_count += 1
|
approved_count += 1
|
||||||
except (ValueError, TypeError) as e:
|
except (ValueError, TypeError) as e:
|
||||||
logger.debug(
|
logger.debug(
|
||||||
f"Needs Review: Scannermatch score is missing or invalid. — {e}"
|
f"Needs Review: Scannermatch score is missing or invalid. — {e}"
|
||||||
)
|
)
|
||||||
hash_obj.at_decision = "needs_review"
|
hash_obj.at_decision = "needs_review"
|
||||||
needs_review_count += 1
|
needs_review_count += 1
|
||||||
|
|
||||||
logger.debug(
|
logger.debug(
|
||||||
f"Final counts — Needs Review: {needs_review_count}, "
|
f"Final counts — Needs Review: {needs_review_count}, "
|
||||||
f"Approved: {approved_count}, Unapproved: {unapproved_count}"
|
f"Approved: {approved_count}, Unapproved: {unapproved_count}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
+16
-16
@@ -28,7 +28,7 @@ from flows.prepPolicy import selectPolicies
|
|||||||
from models.agent import Agent
|
from models.agent import Agent
|
||||||
from models.policy import Policy
|
from models.policy import Policy
|
||||||
from services.API import AirlockAPIWrapper
|
from services.API import AirlockAPIWrapper
|
||||||
from utils.configmanager import get_protected_json, load_env
|
from utils.configmanager import get_system_json, load_env
|
||||||
from utils.selector import Selector
|
from utils.selector import Selector
|
||||||
from utils.utils import colorText, get_sanitized_input
|
from utils.utils import colorText, get_sanitized_input
|
||||||
|
|
||||||
@@ -38,7 +38,7 @@ 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 (1–150): ",
|
prompt="Enter how many days of history to pull (1–150): ",
|
||||||
value_type=int,
|
value_type=int,
|
||||||
valid_range=(1, 150),
|
valid_range=(1, 150),
|
||||||
)
|
)
|
||||||
@@ -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,10 +302,10 @@ 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_protected_json("POLICY_MAP_ENF_AUD", "{}")
|
policy_relationship_map = get_system_json("POLICY_MAP_ENF_AUD", "{}")
|
||||||
|
|
||||||
if mode == "audit":
|
if mode == "audit":
|
||||||
if agent.groupid in policy_relationship_map:
|
if agent.groupid in policy_relationship_map:
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ import tqdm
|
|||||||
|
|
||||||
from models.policy import Policy
|
from models.policy import Policy
|
||||||
from services.API import AirlockAPIWrapper
|
from services.API import AirlockAPIWrapper
|
||||||
from utils.configmanager import get_protected_json
|
from utils.configmanager import get_system_json
|
||||||
from utils.setup import get_base_directory
|
from utils.setup import get_base_directory
|
||||||
from utils.utils import areYouSure, colorText, get_sanitized_input
|
from utils.utils import areYouSure, colorText, get_sanitized_input
|
||||||
|
|
||||||
@@ -236,7 +236,7 @@ def skipback(days):
|
|||||||
|
|
||||||
|
|
||||||
def updateAuditPoliciesFromEnforcementPolices(api: AirlockAPIWrapper):
|
def updateAuditPoliciesFromEnforcementPolices(api: AirlockAPIWrapper):
|
||||||
policy_relationship_map = get_protected_json("POLICY_MAP_ENF_AUD", "{}")
|
policy_relationship_map = get_system_json("POLICY_MAP_ENF_AUD", "{}")
|
||||||
for enforcement_policy, audit_policy in policy_relationship_map.items():
|
for enforcement_policy, audit_policy in policy_relationship_map.items():
|
||||||
api.policy_clone(enforcement_policy, audit_policy)
|
api.policy_clone(enforcement_policy, audit_policy)
|
||||||
api.policy_set_auditmode(audit_policy, "1")
|
api.policy_set_auditmode(audit_policy, "1")
|
||||||
|
|||||||
+246
-43
@@ -18,119 +18,279 @@ import logging
|
|||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import sys
|
import sys
|
||||||
from typing import Callable, Optional, TypeVar
|
from typing import Any, Callable, Optional, TypeVar
|
||||||
|
|
||||||
T = TypeVar("T")
|
T = TypeVar("T")
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
PROTECTED_KEYS = [
|
# System config keys - these are immutable and come from system_config.json (bundled in exe)
|
||||||
|
SYSTEM_CONFIG_KEYS = [
|
||||||
"URL",
|
"URL",
|
||||||
"TELEM_URL",
|
"TELEM_URL",
|
||||||
"APPNAME",
|
"APPNAME",
|
||||||
"LOG_LEVEL",
|
"LOG_LEVEL",
|
||||||
|
"BAD_PATH_PARTS",
|
||||||
|
"BAD_PUBLISHERS",
|
||||||
|
"PUPS",
|
||||||
"PATH_EXCLUSION_CONST",
|
"PATH_EXCLUSION_CONST",
|
||||||
"MIN_FILES_FOR_PATH",
|
"MIN_FILES_FOR_PATH",
|
||||||
"VT_THREAT_TOLERANCE",
|
"VT_THREAT_TOLERANCE",
|
||||||
"POLICY_MAP_ENF_AUD",
|
"POLICY_MAP_ENF_AUD",
|
||||||
]
|
]
|
||||||
|
|
||||||
_protected_config = {}
|
# User config keys - these can be changed by the end user
|
||||||
|
USER_CONFIG_KEYS = [
|
||||||
|
"TELEMETRY", # User opt-in/out for telemetry
|
||||||
|
"TEXTUAL_THEME", # UI theme preference
|
||||||
|
"EXTRAS", # Feature flags
|
||||||
|
]
|
||||||
|
|
||||||
|
# In-memory config storage
|
||||||
|
_system_config = {}
|
||||||
|
_user_config = {}
|
||||||
|
|
||||||
|
|
||||||
def get_system_config_path() -> Path:
|
def get_system_config_path() -> Path:
|
||||||
|
"""
|
||||||
|
Get path to system_config.json.
|
||||||
|
Priority:
|
||||||
|
1. Bundled in exe (_MEIPASS)
|
||||||
|
2. Next to this file (development)
|
||||||
|
"""
|
||||||
# Check inside bundled EXE directory first
|
# Check inside bundled EXE directory first
|
||||||
bundled_dir = Path(getattr(sys, "_MEIPASS", ""))
|
bundled_dir = Path(getattr(sys, "_MEIPASS", ""))
|
||||||
bundled_path = bundled_dir / "system_config.json"
|
bundled_path = bundled_dir / "system_config.json"
|
||||||
if bundled_path.exists():
|
if bundled_path.exists():
|
||||||
return bundled_path
|
return bundled_path
|
||||||
|
|
||||||
# Fallback to external location
|
# Fallback to development location (next to this file)
|
||||||
return Path(__file__).parent.parent / "system_config.json"
|
return Path(__file__).parent.parent / "system_config.json"
|
||||||
|
|
||||||
|
|
||||||
def load_protected_config() -> dict:
|
def load_system_config() -> dict:
|
||||||
global _protected_config
|
"""
|
||||||
|
Load system configuration from system_config.json.
|
||||||
|
This should only be called once at startup.
|
||||||
|
Returns the full system config dict.
|
||||||
|
"""
|
||||||
|
global _system_config
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with open(get_system_config_path(), "r") as f:
|
config_path = get_system_config_path()
|
||||||
system_config = json.load(f)
|
with open(config_path, "r") as f:
|
||||||
|
_system_config = json.load(f)
|
||||||
|
logger.debug(f"✅ Loaded system config from {config_path}")
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
logging.warning("⚠️ system_config.json not found. Using built-in defaults.")
|
logger.warning("⚠️ system_config.json not found. Using minimal defaults.")
|
||||||
system_config = {
|
# Minimal defaults for development without system_config.json
|
||||||
|
_system_config = {
|
||||||
"APPNAME": "Loxide",
|
"APPNAME": "Loxide",
|
||||||
|
"LOG_LEVEL": "INFO",
|
||||||
"PATH_EXCLUSION_CONST": 4,
|
"PATH_EXCLUSION_CONST": 4,
|
||||||
"MIN_FILES_FOR_PATH": 4,
|
"MIN_FILES_FOR_PATH": 4,
|
||||||
"VT_THREAT_TOLERANCE": 4,
|
"VT_THREAT_TOLERANCE": 4,
|
||||||
"POLICY_MAP_ENF_AUD": {"enforced_id": "audit_id"},
|
"POLICY_MAP_ENF_AUD": {},
|
||||||
}
|
}
|
||||||
|
|
||||||
_protected_config = {key: system_config[key] for key in PROTECTED_KEYS}
|
return _system_config
|
||||||
return _protected_config
|
|
||||||
|
|
||||||
|
|
||||||
def get_protected_value(
|
def get_system_value(
|
||||||
key: str, cast_type: Callable[[str], T] = str, default: Optional[T] = None
|
key: str, cast_type: Callable[[str], T] = str, default: Optional[T] = None
|
||||||
) -> Optional[T]:
|
) -> Optional[T]:
|
||||||
value = _protected_config.get(key)
|
"""
|
||||||
|
Get a value from system config (immutable).
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
key: The config key to retrieve
|
||||||
|
cast_type: Function to cast the value to desired type
|
||||||
|
default: Default value if key not found
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The config value cast to the desired type, or default
|
||||||
|
"""
|
||||||
|
value = _system_config.get(key)
|
||||||
if value is None:
|
if value is None:
|
||||||
logging.warning(f"Protected config key '{key}' not found.")
|
logger.warning(f"System config key '{key}' not found.")
|
||||||
return default
|
return default
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if isinstance(value, str):
|
if isinstance(value, str):
|
||||||
value = value.strip("'\"")
|
value = value.strip("'\"")
|
||||||
return cast_type(value)
|
return cast_type(value)
|
||||||
except (ValueError, TypeError):
|
except (ValueError, TypeError):
|
||||||
logging.warning(
|
logger.warning(
|
||||||
f"Invalid value for protected key '{key}': {value}. Expected type {cast_type.__name__}."
|
f"Invalid value for system key '{key}': {value}. Expected type {cast_type.__name__}."
|
||||||
)
|
)
|
||||||
return default
|
return default
|
||||||
|
|
||||||
|
|
||||||
def get_protected_json(key: str, default: str = "{}") -> dict:
|
def get_system_json(key: str, default: Optional[dict] = None) -> dict:
|
||||||
raw = _protected_config.get(key, default)
|
"""
|
||||||
|
Get a JSON/dict value from system config.
|
||||||
|
Handles both dict values and JSON strings.
|
||||||
|
"""
|
||||||
|
if default is None:
|
||||||
|
default = {}
|
||||||
|
|
||||||
|
raw = _system_config.get(key, default)
|
||||||
if isinstance(raw, dict):
|
if isinstance(raw, dict):
|
||||||
return raw
|
return raw
|
||||||
|
|
||||||
try:
|
try:
|
||||||
return json.loads(raw)
|
return json.loads(raw)
|
||||||
except json.JSONDecodeError:
|
except (json.JSONDecodeError, TypeError) as e:
|
||||||
try:
|
logger.error(f"Failed to parse system JSON key '{key}': {e}")
|
||||||
escaped = raw.encode("unicode_escape").decode("utf-8")
|
return default
|
||||||
return json.loads(escaped)
|
|
||||||
except Exception as e:
|
|
||||||
logging.error(f"Failed to parse protected JSON key '{key}': {e}")
|
|
||||||
return json.loads(default)
|
|
||||||
|
|
||||||
|
|
||||||
def load_env_json(key: str, default: str):
|
def get_system_list(key: str, default: Optional[list] = None) -> list:
|
||||||
raw = os.getenv(key, default)
|
"""
|
||||||
|
Get a list value from system config.
|
||||||
|
Handles both list values and JSON strings.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
key: The config key to retrieve
|
||||||
|
default: Default value if key not found or parsing fails
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The list value or default
|
||||||
|
"""
|
||||||
|
if default is None:
|
||||||
|
default = []
|
||||||
|
|
||||||
|
raw = _system_config.get(key, default)
|
||||||
|
if isinstance(raw, list):
|
||||||
|
return raw
|
||||||
|
|
||||||
try:
|
try:
|
||||||
return json.loads(raw)
|
result = json.loads(raw) if isinstance(raw, str) else raw
|
||||||
except json.JSONDecodeError:
|
if isinstance(result, list):
|
||||||
try:
|
return result
|
||||||
escaped = raw.encode("unicode_escape").decode("utf-8")
|
logger.warning(f"System config key '{key}' is not a list: {type(result)}")
|
||||||
return json.loads(escaped)
|
return default
|
||||||
except Exception as e:
|
except (json.JSONDecodeError, TypeError) as e:
|
||||||
logging.error(f"Failed to parse {key}: {e}")
|
logger.error(f"Failed to parse system list key '{key}': {e}")
|
||||||
return json.loads(default)
|
return default
|
||||||
|
|
||||||
|
|
||||||
|
def load_user_config(config_dir: Path) -> dict:
|
||||||
|
"""
|
||||||
|
Load user configuration from user_config.json.
|
||||||
|
Creates the file with defaults if it doesn't exist.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
config_dir: Directory containing user_config.json
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The user config dict
|
||||||
|
"""
|
||||||
|
global _user_config
|
||||||
|
|
||||||
|
user_config_path = config_dir / "user_config.json"
|
||||||
|
|
||||||
|
if not user_config_path.exists():
|
||||||
|
# Create default user config
|
||||||
|
default_user_config = {
|
||||||
|
"TELEMETRY": "false",
|
||||||
|
"TEXTUAL_THEME": "gruvbox",
|
||||||
|
"EXTRAS": "NOTTODAY",
|
||||||
|
}
|
||||||
|
user_config_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
with open(user_config_path, "w") as f:
|
||||||
|
json.dump(default_user_config, f, indent=4)
|
||||||
|
logger.debug(f"Created default user config at {user_config_path}")
|
||||||
|
_user_config = default_user_config
|
||||||
|
else:
|
||||||
|
with open(user_config_path, "r") as f:
|
||||||
|
_user_config = json.load(f)
|
||||||
|
logger.debug(f"✅ Loaded user config from {user_config_path}")
|
||||||
|
|
||||||
|
return _user_config
|
||||||
|
|
||||||
|
|
||||||
|
def save_user_config(config_dir: Path, updates: dict) -> None:
|
||||||
|
"""
|
||||||
|
Save updates to user configuration.
|
||||||
|
Only keys in USER_CONFIG_KEYS are allowed.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
config_dir: Directory containing user_config.json
|
||||||
|
updates: Dict of key-value pairs to update
|
||||||
|
"""
|
||||||
|
global _user_config
|
||||||
|
|
||||||
|
# Validate that only user-configurable keys are being updated
|
||||||
|
invalid_keys = [k for k in updates.keys() if k not in USER_CONFIG_KEYS]
|
||||||
|
if invalid_keys:
|
||||||
|
logger.error(f"Attempted to save invalid user config keys: {invalid_keys}")
|
||||||
|
raise ValueError(f"Cannot modify system config keys: {invalid_keys}")
|
||||||
|
|
||||||
|
# Update in-memory config
|
||||||
|
_user_config.update(updates)
|
||||||
|
|
||||||
|
# Write to file
|
||||||
|
user_config_path = config_dir / "user_config.json"
|
||||||
|
user_config_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
with open(user_config_path, "w") as f:
|
||||||
|
json.dump(_user_config, f, indent=4)
|
||||||
|
|
||||||
|
logger.debug(f"✅ Saved user config to {user_config_path}: {updates}")
|
||||||
|
|
||||||
|
|
||||||
|
def get_user_value(
|
||||||
|
key: str, cast_type: Callable[[str], T] = str, default: Optional[T] = None
|
||||||
|
) -> Optional[T]:
|
||||||
|
"""
|
||||||
|
Get a value from user config (mutable).
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
key: The config key to retrieve
|
||||||
|
cast_type: Function to cast the value to desired type
|
||||||
|
default: Default value if key not found
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The config value cast to the desired type, or default
|
||||||
|
"""
|
||||||
|
value = _user_config.get(key)
|
||||||
|
if value is None:
|
||||||
|
logger.warning(f"User config key '{key}' not found.")
|
||||||
|
return default
|
||||||
|
|
||||||
|
try:
|
||||||
|
if isinstance(value, str):
|
||||||
|
value = value.strip("'\"")
|
||||||
|
return cast_type(value)
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
logger.warning(
|
||||||
|
f"Invalid value for user key '{key}': {value}. Expected type {cast_type.__name__}."
|
||||||
|
)
|
||||||
|
return default
|
||||||
|
|
||||||
|
|
||||||
def load_env(
|
def load_env(
|
||||||
key: str, cast_type: Callable[[str], T] = str, default: Optional[T] = None
|
key: str, cast_type: Callable[[str], T] = str, default: Optional[T] = None
|
||||||
) -> Optional[T]:
|
) -> Optional[T]:
|
||||||
"""
|
"""
|
||||||
Safely retrieves an environment variable and casts it to the desired type.
|
Safely retrieves an environment variable from .env and casts it to the desired type.
|
||||||
|
This should ONLY be used for runtime/dynamic values like WORKING_DIR.
|
||||||
|
|
||||||
|
For system config, use get_system_value().
|
||||||
|
For user config, use get_user_value().
|
||||||
|
|
||||||
Parameters:
|
Parameters:
|
||||||
key (str): The name of the environment variable.
|
key: The name of the environment variable
|
||||||
cast_type (Callable[[str], T], optional): Function to cast the value. Defaults to str.
|
cast_type: Function to cast the value. Defaults to str
|
||||||
default (Optional[T], optional): Default value if the variable is not set or invalid.
|
default: Default value if the variable is not set or invalid
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Optional[T]: The casted value or the default.
|
The casted value or the default
|
||||||
"""
|
"""
|
||||||
value = os.getenv(key)
|
value = os.getenv(key)
|
||||||
if value is None:
|
if value is None:
|
||||||
logger.warning(f"Environment variable '{key}' not set.")
|
logger.debug(f"Environment variable '{key}' not set, using default.")
|
||||||
return default
|
return default
|
||||||
|
|
||||||
try:
|
try:
|
||||||
value = value.strip("'\"") # Strip surrounding quotes
|
value = value.strip("'\"") # Strip surrounding quotes
|
||||||
return cast_type(value)
|
return cast_type(value)
|
||||||
@@ -139,3 +299,46 @@ def load_env(
|
|||||||
f"Invalid value for env var '{key}': {value}. Expected type {cast_type.__name__}."
|
f"Invalid value for env var '{key}': {value}. Expected type {cast_type.__name__}."
|
||||||
)
|
)
|
||||||
return default
|
return default
|
||||||
|
|
||||||
|
|
||||||
|
def load_env_json(key: str, default: str = "[]") -> Any:
|
||||||
|
"""
|
||||||
|
Load a JSON value from environment or system config.
|
||||||
|
|
||||||
|
DEPRECATED: This function is kept for backward compatibility.
|
||||||
|
- For system config lists (BAD_PUBLISHERS, PUPS, BAD_PATH_PARTS), use get_system_list()
|
||||||
|
- For system config dicts, use get_system_json()
|
||||||
|
- For actual .env JSON values, parse manually
|
||||||
|
|
||||||
|
This function automatically redirects known system config keys to system config.
|
||||||
|
"""
|
||||||
|
# Known system config list keys - redirect to system config
|
||||||
|
system_list_keys = ["BAD_PUBLISHERS", "PUPS", "BAD_PATH_PARTS"]
|
||||||
|
if key in system_list_keys:
|
||||||
|
logger.debug(f"Redirecting load_env_json('{key}') to get_system_list()")
|
||||||
|
return get_system_list(key, json.loads(default) if default else [])
|
||||||
|
|
||||||
|
# Known system config dict keys - redirect to system config
|
||||||
|
system_dict_keys = ["POLICY_MAP_ENF_AUD"]
|
||||||
|
if key in system_dict_keys:
|
||||||
|
logger.debug(f"Redirecting load_env_json('{key}') to get_system_json()")
|
||||||
|
return get_system_json(key, json.loads(default) if default else {})
|
||||||
|
|
||||||
|
# Fall back to reading from .env (backward compatibility for unknown keys)
|
||||||
|
raw = os.getenv(key, default)
|
||||||
|
try:
|
||||||
|
return json.loads(raw)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
try:
|
||||||
|
escaped = raw.encode("unicode_escape").decode("utf-8")
|
||||||
|
return json.loads(escaped)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to parse {key}: {e}")
|
||||||
|
return json.loads(default)
|
||||||
|
|
||||||
|
|
||||||
|
# Backwards compatibility aliases (deprecated - use get_system_value instead)
|
||||||
|
get_protected_value = get_system_value
|
||||||
|
get_protected_json = get_system_json
|
||||||
|
load_protected_config = load_system_config
|
||||||
|
PROTECTED_KEYS = SYSTEM_CONFIG_KEYS # For backwards compatibility
|
||||||
|
|||||||
+29
-80
@@ -13,18 +13,20 @@
|
|||||||
# You should have received a copy of the GNU Affero General Public License
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
import json
|
|
||||||
import logging
|
import logging
|
||||||
import logging.config
|
import logging.config
|
||||||
import logging.handlers
|
import logging.handlers
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import platform
|
import platform
|
||||||
import sys
|
|
||||||
|
|
||||||
from dotenv import load_dotenv, set_key
|
from dotenv import load_dotenv, set_key
|
||||||
|
|
||||||
from utils.configmanager import PROTECTED_KEYS, load_protected_config
|
from utils.configmanager import (
|
||||||
|
get_system_value,
|
||||||
|
load_system_config,
|
||||||
|
load_user_config,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def get_base_directory() -> Path:
|
def get_base_directory() -> Path:
|
||||||
@@ -38,7 +40,7 @@ def get_base_directory() -> Path:
|
|||||||
return home / ".local" / "share" / "Loxide"
|
return home / ".local" / "share" / "Loxide"
|
||||||
|
|
||||||
|
|
||||||
def configure_logging(log_dir: Path, log_level: str = "DEBUG"):
|
def configure_logging(log_dir: Path, log_level: str = "INFO"):
|
||||||
log_file = log_dir / "Loxide.log"
|
log_file = log_dir / "Loxide.log"
|
||||||
|
|
||||||
config = {
|
config = {
|
||||||
@@ -62,12 +64,12 @@ def configure_logging(log_dir: Path, log_level: str = "DEBUG"):
|
|||||||
"interval": 1, # Every 1 day
|
"interval": 1, # Every 1 day
|
||||||
"backupCount": 7, # Keep 7 days of logs
|
"backupCount": 7, # Keep 7 days of logs
|
||||||
"encoding": "utf-8", # Ensure UTF-8 encoding
|
"encoding": "utf-8", # Ensure UTF-8 encoding
|
||||||
"level": "DEBUG", # Always log DEBUG and above
|
"level": "DEBUG", # Always log DEBUG and above to file
|
||||||
"formatter": "detailed", # Use detailed format
|
"formatter": "detailed", # Use detailed format
|
||||||
},
|
},
|
||||||
"console": {
|
"console": {
|
||||||
"class": "logging.StreamHandler",
|
"class": "logging.StreamHandler",
|
||||||
"level": log_level.upper(), # Configurable log level
|
"level": log_level.upper(), # System-configured level for console
|
||||||
"formatter": "simple", # Use simple format
|
"formatter": "simple", # Use simple format
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -95,59 +97,15 @@ def configure_logging(log_dir: Path, log_level: str = "DEBUG"):
|
|||||||
logging.getLogger().debug("✅ Logging configured.")
|
logging.getLogger().debug("✅ Logging configured.")
|
||||||
|
|
||||||
|
|
||||||
def get_system_config_path() -> Path:
|
|
||||||
base_path = Path(
|
|
||||||
getattr(sys, "_MEIPASS", os.path.dirname(os.path.abspath(__file__)))
|
|
||||||
)
|
|
||||||
return base_path.parent / "system_config.json"
|
|
||||||
|
|
||||||
|
|
||||||
def load_system_config() -> dict:
|
|
||||||
try:
|
|
||||||
config_path = get_system_config_path()
|
|
||||||
with open(config_path, "r") as f:
|
|
||||||
return json.load(f)
|
|
||||||
except FileNotFoundError:
|
|
||||||
logging.warning("⚠️ system_config.json not found. Using built-in defaults.")
|
|
||||||
return {
|
|
||||||
"APPNAME": "Loxide",
|
|
||||||
"LOG_LEVEL": "DEBUG",
|
|
||||||
"PATH_EXCLUSION_CONST": 4,
|
|
||||||
"MIN_FILES_FOR_PATH": 4,
|
|
||||||
"VT_THREAT_TOLERANCE": 4,
|
|
||||||
"POLICY_MAP_ENF_AUD": {"enforced_id": "audit_id"},
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def load_user_config(config_dir: Path) -> dict:
|
|
||||||
user_config_path = config_dir / "user_config.json"
|
|
||||||
if not user_config_path.exists():
|
|
||||||
default_user_config = {
|
|
||||||
"TELEMETRY": "FALSE",
|
|
||||||
"TEXTUAL_THEME": "gruvbox",
|
|
||||||
"EXTRAS": "NOTTODAY",
|
|
||||||
}
|
|
||||||
with open(user_config_path, "w") as f:
|
|
||||||
json.dump(default_user_config, f, indent=4)
|
|
||||||
logging.debug(f"Created user config at {user_config_path}")
|
|
||||||
with open(user_config_path, "r") as f:
|
|
||||||
return json.load(f)
|
|
||||||
|
|
||||||
|
|
||||||
def write_config_to_env(config: dict, env_path: Path):
|
|
||||||
for key, value in config.items():
|
|
||||||
if key in PROTECTED_KEYS:
|
|
||||||
continue # Skip protected keys
|
|
||||||
try:
|
|
||||||
serialized = (
|
|
||||||
json.dumps(value) if isinstance(value, (list, dict)) else str(value)
|
|
||||||
)
|
|
||||||
set_key(env_path, key, serialized)
|
|
||||||
except Exception as e:
|
|
||||||
logging.warning(f"Failed to write {key} to .env: {e}")
|
|
||||||
|
|
||||||
|
|
||||||
def setup():
|
def setup():
|
||||||
|
"""
|
||||||
|
Initialize the application environment:
|
||||||
|
1. Create directory structure
|
||||||
|
2. Load system config (immutable, from system_config.json)
|
||||||
|
3. Load user config (mutable, from user_config.json)
|
||||||
|
4. Configure logging
|
||||||
|
5. Set up .env with WORKING_DIR only
|
||||||
|
"""
|
||||||
base_dir = get_base_directory()
|
base_dir = get_base_directory()
|
||||||
dirs = {
|
dirs = {
|
||||||
"config": base_dir / "config",
|
"config": base_dir / "config",
|
||||||
@@ -159,20 +117,30 @@ def setup():
|
|||||||
path.mkdir(parents=True, exist_ok=True)
|
path.mkdir(parents=True, exist_ok=True)
|
||||||
logging.debug(f"{name.capitalize()} directory ensured at: {path}")
|
logging.debug(f"{name.capitalize()} directory ensured at: {path}")
|
||||||
|
|
||||||
|
# Load system config (immutable)
|
||||||
system_config = load_system_config()
|
system_config = load_system_config()
|
||||||
configure_logging(dirs["logs"], system_config.get("LOG_LEVEL", "DEBUG"))
|
|
||||||
|
|
||||||
|
# Configure logging with system-defined log level
|
||||||
|
log_level = get_system_value("LOG_LEVEL", str, "INFO")
|
||||||
|
configure_logging(dirs["logs"], log_level)
|
||||||
|
|
||||||
|
# Load user config (mutable)
|
||||||
|
user_config = load_user_config(dirs["config"])
|
||||||
|
|
||||||
|
# Set up .env file - ONLY for WORKING_DIR (runtime-configurable value)
|
||||||
env_path = base_dir / ".env"
|
env_path = base_dir / ".env"
|
||||||
if not env_path.exists():
|
if not env_path.exists():
|
||||||
env_path.touch()
|
env_path.touch()
|
||||||
load_dotenv(dotenv_path=env_path, override=True)
|
load_dotenv(dotenv_path=env_path, override=True)
|
||||||
|
|
||||||
|
# Set up working directory (only dynamic value in .env)
|
||||||
working_dir = Path(os.getenv("WORKING_DIR") or (base_dir / "data"))
|
working_dir = Path(os.getenv("WORKING_DIR") or (base_dir / "data"))
|
||||||
working_dir.mkdir(parents=True, exist_ok=True)
|
working_dir.mkdir(parents=True, exist_ok=True)
|
||||||
set_key(env_path, "WORKING_DIR", str(working_dir))
|
set_key(str(env_path), "WORKING_DIR", str(working_dir))
|
||||||
os.environ["WORKING_DIR"] = str(working_dir)
|
os.environ["WORKING_DIR"] = str(working_dir)
|
||||||
logging.debug(f"Working directory set to: {working_dir}")
|
logging.debug(f"Working directory set to: {working_dir}")
|
||||||
|
|
||||||
|
# Create folder structure in working directory
|
||||||
folders_structure = {
|
folders_structure = {
|
||||||
"Approved": [],
|
"Approved": [],
|
||||||
"Needs_Review": ["Review_First", "Review_Second", "HTML"],
|
"Needs_Review": ["Review_First", "Review_Second", "HTML"],
|
||||||
@@ -189,23 +157,4 @@ def setup():
|
|||||||
subfolder_path.mkdir(parents=True, exist_ok=True)
|
subfolder_path.mkdir(parents=True, exist_ok=True)
|
||||||
logging.debug(f" └─ '{subfolder}' subfolder created at: {subfolder_path}")
|
logging.debug(f" └─ '{subfolder}' subfolder created at: {subfolder_path}")
|
||||||
|
|
||||||
user_config = load_user_config(dirs["config"])
|
logging.info("✅ Setup complete")
|
||||||
merged_config = {**system_config, **user_config}
|
|
||||||
|
|
||||||
protected_config = load_protected_config()
|
|
||||||
merged_config.update(protected_config)
|
|
||||||
|
|
||||||
# ✅ URL resolution order: system_config → .env → user prompt
|
|
||||||
url = system_config.get("URL")
|
|
||||||
if not url:
|
|
||||||
url = os.getenv("URL")
|
|
||||||
if not url:
|
|
||||||
url = input(
|
|
||||||
"🌐 Enter the service URL (e.g., https://example.com/api): "
|
|
||||||
).strip()
|
|
||||||
merged_config["URL"] = url
|
|
||||||
set_key(env_path, "URL", url)
|
|
||||||
os.environ["URL"] = url
|
|
||||||
logging.debug(f"Service URL set to: {url}")
|
|
||||||
|
|
||||||
write_config_to_env(merged_config, env_path)
|
|
||||||
|
|||||||
Reference in New Issue
Block a user