Cleaning up menu, added Move to Audit/Enforcement

This commit is contained in:
2025-10-10 17:20:07 -04:00
parent b74f77a9db
commit 21370898a4
13 changed files with 213 additions and 468 deletions
+1
View File
@@ -10,3 +10,4 @@ jobs.json
securitytest.py securitytest.py
*.toml *.toml
system_config.json system_config.json
Devel_unused/
+4 -4
View File
@@ -64,11 +64,11 @@ def main():
logger.error(f"Configuration error: {e}", exc_info=True) logger.error(f"Configuration error: {e}", exc_info=True)
raise raise
api = AirlockAPIWrapper( api = AirlockAPIWrapper(
base_url=str(os.getenv("URL")), base_url=str(os.getenv("URL")),
api_key = getAPI(username, "AirlockTools"), api_key = getAPI(username, "AirlockTools"),
) )
logger.info("Running non-interactively to start monitoring Airlock Changes") logger.info("Running non-interactively to start monitoring Airlock Changes")
-352
View File
@@ -1,352 +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 json
import logging
import os
import time
from typing import Any, Callable, Dict, List
import schedule
logger = logging.getLogger(__name__)
# TODO - move this File where all jobs are persisted
JOBS_FILE = f"{os.getenv('WORKING_DIR')}\\scheduling\\jobs.json"
# Ensure directory exists
os.makedirs(os.path.dirname(JOBS_FILE), exist_ok=True)
# Registry of functions that can be scheduled
FUNCTION_MAP: Dict[str, Callable] = {}
# -------------------------------
# Function Registration
# -------------------------------
def register_function(name: str, func: Callable):
"""
Register a function so it can be called by name later.
Example:
register_function("say_hello", say_hello)
"""
FUNCTION_MAP[name] = func
# -------------------------------
# Persistence Helpers
# -------------------------------
def load_jobs() -> List[Dict[str, Any]]:
"""Load jobs from the JSON file, or return [] if none exist."""
if not os.path.exists(JOBS_FILE):
return []
with open(JOBS_FILE, "r") as f:
return json.load(f)
def _atomic_save(path: str, data: Any):
"""Write JSON atomically to avoid partial writes."""
tmp = f"{path}.tmp"
with open(tmp, "w") as f:
json.dump(data, f, indent=4)
os.replace(tmp, path)
def save_jobs(jobs: List[Dict[str, Any]]):
"""Save jobs to the JSON file (overwrite)."""
_atomic_save(JOBS_FILE, jobs)
# -------------------------------
# Uniqueness Helpers
# -------------------------------
def job_in_store(job_id: str) -> bool:
"""Check if a job id exists in the persisted JSON file."""
return any(j.get("id") == job_id for j in load_jobs())
def job_in_scheduler(job_id: str) -> bool:
"""
Check if a job with this tag exists in the in-memory scheduler.
Uses schedule.get_jobs(tag=...) if available, otherwise scans tags.
"""
try:
jobs = schedule.get_jobs(tag=job_id) # schedule >= 1.2.0
return len(jobs) > 0
except TypeError:
# Fallback for older versions
return any(job_id in getattr(j, "tags", set()) for j in schedule.jobs)
def ensure_unique(job_id: str, on_conflict: str = "skip") -> bool:
"""
Ensure the job_id is unique across persistence and in-memory schedule.
on_conflict:
- "error": raise ValueError if exists.
- "skip" : print and return False.
- "replace": remove existing (in-memory + JSON), then continue.
"""
exists = job_in_store(job_id) or job_in_scheduler(job_id)
if not exists:
return True
if on_conflict == "error":
raise ValueError(f"Job id '{job_id}' already exists.")
elif on_conflict == "skip":
logger.info(f"Job '{job_id}' already exists. Skipping creation.")
return False
elif on_conflict == "replace":
# Clear from scheduler
schedule.clear(job_id)
# Remove from persistence
jobs = [j for j in load_jobs() if j.get("id") != job_id]
save_jobs(jobs)
return True
else:
raise ValueError(f"Unsupported on_conflict policy: {on_conflict}")
# -------------------------------
# Internal scheduling (no persistence)
# -------------------------------
def _schedule_once(job_id: str, func_name: str, run_at_timestamp: float, args=None, kwargs=None):
args = args or []
kwargs = kwargs or {}
def job_wrapper():
"""Executes the job once, then removes it."""
if func_name not in FUNCTION_MAP:
logger.error(f"Function '{func_name}' is not registered.")
return
FUNCTION_MAP[func_name](*args, **kwargs)
# Remove from persistence
jobs = load_jobs()
jobs = [j for j in jobs if j["id"] != job_id]
save_jobs(jobs)
# Clear from in-memory schedule
schedule.clear(job_id)
delay_seconds = run_at_timestamp - time.time()
if delay_seconds <= 0:
logger.info(f"Job {job_id} scheduled in the past. Skipping.")
return
# Schedule via schedule library
schedule.every(int(delay_seconds)).seconds.do(job_wrapper).tag(job_id)
def _schedule_recurring(
job_id: str, func_name: str, interval: int, unit: str, args=None, kwargs=None
):
args = args or []
kwargs = kwargs or {}
def job_wrapper():
if func_name not in FUNCTION_MAP:
logger.error(f"Function '{func_name}' is not registered.")
return
FUNCTION_MAP[func_name](*args, **kwargs)
if unit == "seconds":
schedule.every(interval).seconds.do(job_wrapper).tag(job_id)
elif unit == "minutes":
schedule.every(interval).minutes.do(job_wrapper).tag(job_id)
elif unit == "hours":
schedule.every(interval).hours.do(job_wrapper).tag(job_id)
elif unit == "days":
schedule.every(interval).days.do(job_wrapper).tag(job_id)
else:
raise ValueError(f"Unsupported unit: {unit}")
# -------------------------------
# Public APIs (with uniqueness + persistence)
# -------------------------------
def run_once_job(
job_id: str,
func_name: str,
run_at_timestamp: float,
args=None,
kwargs=None,
*,
replace: bool = False,
persist: bool = True,
):
"""
Schedule a job to run once at a specific timestamp.
replace: if True, replace existing job with same id; otherwise print and skip.
persist: if False, do not write to JSON (used by reload_jobs()).
"""
if persist:
policy = "replace" if replace else "skip"
if not ensure_unique(job_id, on_conflict=policy):
return
elif job_in_scheduler(job_id):
schedule.clear(job_id)
_schedule_once(job_id, func_name, run_at_timestamp, args, kwargs)
if persist:
jobs = [j for j in load_jobs() if j["id"] != job_id]
jobs.append(
{
"id": job_id,
"type": "once",
"run_at": run_at_timestamp,
"function": func_name,
"args": args or [],
"kwargs": kwargs or {},
}
)
save_jobs(jobs)
def recurring_job(
job_id: str,
func_name: str,
interval: int,
unit: str,
args=None,
kwargs=None,
*,
replace: bool = False,
persist: bool = True,
):
"""
Schedule a recurring job.
replace: if True, replace existing job with same id; otherwise print and skip.
persist: if False, do not write to JSON (used by reload_jobs()).
"""
if persist:
policy = "replace" if replace else "skip"
if not ensure_unique(job_id, on_conflict=policy):
return
elif job_in_scheduler(job_id):
schedule.clear(job_id)
_schedule_recurring(job_id, func_name, interval, unit, args, kwargs)
if persist:
jobs = [j for j in load_jobs() if j["id"] != job_id]
jobs.append(
{
"id": job_id,
"type": "recurring",
"interval": interval,
"unit": unit,
"function": func_name,
"args": args or [],
"kwargs": kwargs or {},
}
)
save_jobs(jobs)
def find_and_prioritize_jobs_by_pid(pid_substring: str, new_delay_seconds: float = 1.0):
"""
Find all jobs whose ID contains the given PID substring and reschedule them to run sooner.
"""
jobs = load_jobs()
matched_jobs = [job for job in jobs if pid_substring in job.get("id", "")]
if not matched_jobs:
logger.info(f"No jobs found containing PID substring '{pid_substring}'.")
return
logger.info(f"Found {len(matched_jobs)} job(s) containing '{pid_substring}':")
for job in matched_jobs:
job_id = job["id"]
logger.debug(f" - Prioritizing job: {job_id}")
# Clear existing job from scheduler
schedule.clear(job_id)
# Reschedule based on job type
if job["type"] == "once":
run_once_job(
job_id,
job["function"],
time.time() + new_delay_seconds,
job.get("args"),
job.get("kwargs"),
replace=True,
persist=True,
)
elif job["type"] == "recurring":
recurring_job(
job_id,
job["function"],
job["interval"],
job["unit"],
job.get("args"),
job.get("kwargs"),
replace=True,
persist=True,
)
else:
logger.warning(f"Unknown job type for job '{job_id}'")
# -------------------------------
# Reload Saved Jobs
# -------------------------------
def reload_jobs():
"""Reload jobs from JSON and reschedule them (no re-persist)."""
jobs = load_jobs()
for job in jobs:
if job["type"] == "once":
if job["run_at"] > time.time():
run_once_job(
job["id"],
job["function"],
job["run_at"],
job.get("args"),
job.get("kwargs"),
persist=False,
)
elif job["type"] == "recurring":
recurring_job(
job["id"],
job["function"],
job["interval"],
job["unit"],
job.get("args"),
job.get("kwargs"),
persist=False,
)
# -------------------------------
# Scheduler Loop
# -------------------------------
def start_scheduler():
"""
Start the scheduler loop (blocking).
Call this once in main to begin.
"""
try:
while True:
schedule.run_pending()
time.sleep(0.5)
except KeyboardInterrupt:
logger.critical("Scheduler stopped.")
+6 -8
View File
@@ -27,11 +27,9 @@ from utils.setup import get_base_directory
from models.agent import Agent from models.agent import Agent
from services.agenthandler import findAllAgents, moveAgentToRelatedPolicy, selectAgents from services.agenthandler import findAllAgents, moveAgentToRelatedPolicy, selectAgents
from services.API import AirlockAPIWrapper from services.API import AirlockAPIWrapper
from Server.scheduler import (
register_function, from utils.utils import colorText
run_once_job, from utils.configmanager import get_protected_json, load_env, load_env_json
)
from utils.utils import colorText, load_env, load_env_json
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -68,7 +66,7 @@ def getLocalApprovals(api: AirlockAPIWrapper):
def scheduleAddingLAHashes(api: AirlockAPIWrapper): def scheduleAddingLAHashes(api: AirlockAPIWrapper):
policy_relationship_map = load_env_json("POLICY_MAP_ENF_AUD","{}") policy_relationship_map = get_protected_json("POLICY_MAP_ENF_AUD","{}")
bad_publisher_list = load_env_json("BAD_PUBLISHER", "[]") bad_publisher_list = load_env_json("BAD_PUBLISHER", "[]")
pups = load_env_json("PUPS", "[]") pups = load_env_json("PUPS", "[]")
threat_tolerance_constant = load_env("VT_THREAT_TOLERANCE", cast_type = int) threat_tolerance_constant = load_env("VT_THREAT_TOLERANCE", cast_type = int)
@@ -168,7 +166,7 @@ def returnFromLocalApproval(api, device_df, policy_relationship_map, bad_publish
#TODO finish logic for adding hashes #TODO finish logic for adding hashes
""" """
working_dir = load_env("WORKING_DIR") working_dir = load_env("WORKING_DIR")
policy_relationship_map = load_env_json("POLICY_MAP_ENF_AUD","{}") policy_relationship_map = get_protected_json("POLICY_MAP_ENF_AUD","{}")
bad_publisher_list = load_env_json("BAD_PUBLISHER", "[]") bad_publisher_list = load_env_json("BAD_PUBLISHER", "[]")
pups = load_env_json("PUPS", "[]") pups = load_env_json("PUPS", "[]")
threat_tolerance_constant = load_env("VT_THREAT_TOLERANCE") threat_tolerance_constant = load_env("VT_THREAT_TOLERANCE")
@@ -225,7 +223,7 @@ def monitorAuditStatus(api: AirlockAPIWrapper):
last_agents = [] last_agents = []
if not last_agents: if not last_agents:
last_agents = current_agents last_agents = current_agents
policy_relationship_map = load_env_json("POLICY_MAP_ENF_AUD","{}") policy_relationship_map = get_protected_json("POLICY_MAP_ENF_AUD","{}")
# Reverse map for audit → enforcement # Reverse map for audit → enforcement
reverse_policy_map = {v: k for k, v in policy_relationship_map.items()} reverse_policy_map = {v: k for k, v in policy_relationship_map.items()}
+4 -5
View File
@@ -24,13 +24,12 @@ import pandas as pd
from models.execution import ExecutionHistoryRecord, Hash from models.execution import ExecutionHistoryRecord, Hash
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.selector import Selector from utils.selector import Selector
from utils.utils import ( from utils.utils import (
colorText, colorText,
formatHTML, formatHTML,
import_to_dataframe, import_to_dataframe,
load_env,
load_env_json,
regulator, regulator,
) )
@@ -250,8 +249,8 @@ def buildPreflights():
formatHTML(df, f"{working_dir}\\Preflight\\HTML\\{name}.html") formatHTML(df, f"{working_dir}\\Preflight\\HTML\\{name}.html")
def splitFilepathsGrouped(df, col="filename"): def splitFilepathsGrouped(df, col="filename"):
path_exclusion_constant = load_env("PATH_EXCLUSION_CONST", cast_type= int) path_exclusion_constant = get_protected_value("PATH_EXCLUSION_CONST", cast_type= int)
min_files_for_path = load_env("MIN_FILES_FOR_PATH", cast_type= int) min_files_for_path = get_protected_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)):
@@ -317,7 +316,7 @@ def calculatePath(approved_hashes, split):
dfs_by_policy = [approved_hashes] dfs_by_policy = [approved_hashes]
badpathparts = load_env_json("BAD_PATH_PARTS", "[]") badpathparts = load_env_json("BAD_PATH_PARTS", "[]")
min_files_for_path = load_env("MIN_FILES_FOR_PATH", cast_type = int) min_files_for_path = get_protected_value("MIN_FILES_FOR_PATH", cast_type = int)
processed_dfs = [] processed_dfs = []
+1 -1
View File
@@ -69,7 +69,7 @@ groupid_to_name = {policy.groupid: policy.name for policy in policies}
# Step 3: Enrich agents # Step 3: Enrich agents
for agent in agents: for agent in agents:
agent.enrich(groupid_to_name) agent.enrich_with_policies(groupid_to_name)
""" """
+5 -4
View File
@@ -26,7 +26,8 @@ import dotenv
import pandas as pd import pandas as pd
from services.policyhandler import pullPolicyExechistories from services.policyhandler import pullPolicyExechistories
from utils.utils import colorText, load_env, load_env_json, regulator from utils.configmanager import get_protected_value, load_env_json
from utils.utils import colorText, regulator
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -126,7 +127,7 @@ class Hash:
@classmethod @classmethod
def categorize_hashes(cls, hashes): def categorize_hashes(cls, hashes):
threat_tolerance = load_env("VT_THREAT_TOLERANCE", cast_type=int) threat_tolerance = get_protected_value("VT_THREAT_TOLERANCE", cast_type=int)
bad_publishers_pattern = regulator(load_env_json("BAD_PUBLISHERS", "[]")) bad_publishers_pattern = regulator(load_env_json("BAD_PUBLISHERS", "[]"))
pups_pattern = regulator(load_env_json("PUPS", "[]")) pups_pattern = regulator(load_env_json("PUPS", "[]"))
@@ -162,9 +163,9 @@ class Hash:
# 3. Approved or Unapproved based on threat level # 3. Approved or Unapproved based on threat level
try: try:
score = int(scannermatch) score = int(scannermatch) # pyright: ignore[reportArgumentType]
logger.debug(f"Parsed scannermatch score: {score}") logger.debug(f"Parsed scannermatch score: {score}")
if score > threat_tolerance: if score > threat_tolerance: # pyright: ignore[reportOperatorIssue]
logger.debug("Unapproved: Unsigned file with high threat score.") logger.debug("Unapproved: Unsigned file with high threat score.")
unapproved.append(hash_obj) unapproved.append(hash_obj)
else: else:
+3 -2
View File
@@ -27,8 +27,9 @@ import pandas as pd
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 load_env, load_env_json, get_protected_json
from utils.selector import Selector from utils.selector import Selector
from utils.utils import colorText, load_env, load_env_json from utils.utils import colorText
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -207,7 +208,7 @@ def moveAgentToRelatedPolicy(
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 = load_env_json("POLICY_MAP_ENF_AUD", "{}") policy_relationship_map = get_protected_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:
+3 -2
View File
@@ -27,8 +27,9 @@ from bson import ObjectId
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.setup import get_base_directory from utils.setup import get_base_directory
from utils.utils import colorText, load_env_json from utils.utils import colorText
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -230,7 +231,7 @@ def skipback(days):
def updateAuditPoliciesFromEnforcementPolices(api: AirlockAPIWrapper): def updateAuditPoliciesFromEnforcementPolices(api: AirlockAPIWrapper):
policy_relationship_map = load_env_json("POLICY_MAP_ENF_AUD", "{}") policy_relationship_map = get_protected_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")
+115
View File
@@ -0,0 +1,115 @@
import json
import logging
import os
import sys
from pathlib import Path
from typing import Callable, Optional, TypeVar
T = TypeVar("T")
logger = logging.getLogger(__name__)
PROTECTED_KEYS = [
"APPNAME",
"LOG_LEVEL",
"PATH_EXCLUSION_CONST",
"MIN_FILES_FOR_PATH",
"VT_THREAT_TOLERANCE",
"POLICY_MAP_ENF_AUD"
]
_protected_config = {}
def get_system_config_path() -> Path:
# Check inside bundled EXE directory first
bundled_dir = Path(getattr(sys, '_MEIPASS', ''))
bundled_path = bundled_dir / "system_config.json"
if bundled_path.exists():
return bundled_path
# Fallback to external location
return Path(__file__).parent.parent / "system_config.json"
def load_protected_config() -> dict:
global _protected_config
try:
with open(get_system_config_path(), "r") as f:
system_config = json.load(f)
except FileNotFoundError:
logging.warning("⚠️ system_config.json not found. Using built-in defaults.")
system_config = {
"APPNAME": "AirlockTools",
"PATH_EXCLUSION_CONST": 4,
"MIN_FILES_FOR_PATH": 4,
"VT_THREAT_TOLERANCE": 4,
"POLICY_MAP_ENF_AUD": {
"enforced_id": "audit_id"
}
}
_protected_config = {key: system_config[key] for key in PROTECTED_KEYS}
return _protected_config
def get_protected_value(key: str, cast_type: Callable[[str], T] = str, default: Optional[T] = None) -> Optional[T]:
value = _protected_config.get(key)
if value is None:
logging.warning(f"Protected config key '{key}' not found.")
return default
try:
if isinstance(value, str):
value = value.strip("'\"")
return cast_type(value)
except (ValueError, TypeError):
logging.warning(f"Invalid value for protected key '{key}': {value}. Expected type {cast_type.__name__}.")
return default
def get_protected_json(key: str, default: str = "{}") -> dict:
raw = _protected_config.get(key, default)
if isinstance(raw, dict):
return raw
try:
return json.loads(raw)
except json.JSONDecodeError:
try:
escaped = raw.encode('unicode_escape').decode('utf-8')
return json.loads(escaped)
except Exception as e:
logging.error(f"Failed to parse protected JSON key '{key}': {e}")
return json.loads(default)
def load_env_json(key: str, default: str):
raw = os.getenv(key, default)
try:
return json.loads(raw)
except json.JSONDecodeError:
try:
escaped = raw.encode('unicode_escape').decode('utf-8')
return json.loads(escaped)
except Exception as e:
logging.error(f"Failed to parse {key}: {e}")
return json.loads(default)
def load_env(key: str, cast_type: Callable[[str], T] = str, default: Optional[T] = None) -> Optional[T]:
"""
Safely retrieves an environment variable and casts it to the desired type.
Parameters:
key (str): The name of the environment variable.
cast_type (Callable[[str], T], optional): Function to cast the value. Defaults to str.
default (Optional[T], optional): Default value if the variable is not set or invalid.
Returns:
Optional[T]: The casted value or the default.
"""
value = os.getenv(key)
if value is None:
logger.warning(f"Environment variable '{key}' not set.")
return default
try:
value = value.strip("'\"") # Strip surrounding quotes
return cast_type(value)
except (ValueError, TypeError):
logger.warning(f"Invalid value for env var '{key}': {value}. Expected type {cast_type.__name__}.")
return default
+46 -22
View File
@@ -36,14 +36,14 @@ from flows.otp import (
otp_activities_by_agent, otp_activities_by_agent,
revoke revoke
) )
from utils.selector import Selector
from services.agenthandler import findAgents from services.agenthandler import findAgents, selectAgents, moveAgentToRelatedPolicy
from services.API import AirlockAPIWrapper from services.API import AirlockAPIWrapper
from utils.configmanager import load_env
from utils.utils import ( from utils.utils import (
areYouSure, areYouSure,
colorText, colorText,
displayIntro, displayIntro,
load_env,
open_directory, open_directory,
printEnforceChecklist, printEnforceChecklist,
) )
@@ -59,34 +59,30 @@ def menu_main(api: AirlockAPIWrapper):
while True: while True:
displayIntro() displayIntro()
# Add Settings, and give option to change working dir # Add Settings, and give option to change working dir
print(colorText("1. ➡️ - Move Device(s) to local approval", "yellow")) print(colorText("1. - Move Device(s) to local approval", "yellow"))
print(colorText("2. 🎫 - OTP", "yellow")) print(colorText("2. 🎫 - OTP", "yellow"))
print(colorText("3. 🔍 - Device Search", "yellow")) print(colorText("3. 🔄 - Move to Audit/Enforcement", "yellow"))
print(colorText("4. 🔇 - Find Quiet Hosts", "yellow")) print(colorText("4. 🔍 - Device Search", "yellow"))
print(colorText("5. 🔒 - Prepare Policy For Enforcement", "yellow")) print(colorText("5. 🛡️ - Policy Enforcement Tools", "yellow"))
print(colorText("6. 🔄 - Update Audit Policies from Enforcement Policies", "yellow"))
print(colorText("F. 📂 - Open Working Directory", "yellow"))
print(colorText("S. 🛠️ - Settings", "yellow"))
print(colorText("Q. 🔚 - Quit", "yellow"))
choice = input(colorText("\nEnter Menu Item: ", "white")) choice = input(colorText("\nEnter Menu Item: ", "white"))
if choice == "1": if choice == "1":
la.moveToLocalApproval(api) print("This Feature is still in development")
input("Press enter to continue")
elif choice == "2": elif choice == "2":
menu_otp(api) menu_otp(api)
elif choice == "3": elif choice == "3":
choices = ["audit", "enforcement"]
findAgents(api,False) direction = Selector.select_string(choices, False, False)
devices = selectAgents(api)
if direction and devices:
for device in devices:
moveAgentToRelatedPolicy(api,device, direction[0])
elif choice == "4": elif choice == "4":
findQuietAgents(api) findAgents(api,False)
elif choice == "5": elif choice == "5":
menu_policy_enforce(api) menu_policymanagment(api)
elif choice == "6":
areYouSure()
confirmation = input(colorText("Type 'I AGREE' to continue: ", "white"))
if confirmation.strip().upper() == "I AGREE":
policyh.updateAuditPoliciesFromEnforcementPolices(api)
elif choice == "F": elif choice == "F":
open_directory(working_dir) open_directory(working_dir)
elif choice == "S": elif choice == "S":
@@ -97,6 +93,7 @@ def menu_main(api: AirlockAPIWrapper):
print(colorText("Invalid choice. Please try again.", "red")) print(colorText("Invalid choice. Please try again.", "red"))
def menu_policy_enforce(api: AirlockAPIWrapper): def menu_policy_enforce(api: AirlockAPIWrapper):
selected_policies = [] selected_policies = []
destination_policy = [] destination_policy = []
@@ -273,8 +270,35 @@ def menu_otp(api: AirlockAPIWrapper):
elif choice == "Q": elif choice == "Q":
break break
def menu_policymanagment(api: AirlockAPIWrapper):
working_dir = load_env("WORKING_DIR")
while True:
print(colorText("1. 🔇 - Find Quiet Hosts", "yellow"))
print(colorText("2. 🔒 - Prepare Policy For Enforcement", "yellow"))
print(colorText("3. 🔄 - Update Audit Policies from Enforcement Policies", "yellow"))
print(colorText("F. 📂 - Open Working Directory", "yellow"))
print(colorText("S. 🛠️ - Settings", "yellow"))
print(colorText("Q. 🔚 - Quit", "yellow"))
choice = input(colorText("\nEnter Menu Item: ", "white"))
if choice == "1":
findQuietAgents(api)
elif choice == "2":
menu_policy_enforce(api)
elif choice == "3":
areYouSure()
confirmation = input(colorText("Type 'I AGREE' to continue: ", "white"))
if confirmation.strip().upper() == "I AGREE":
policyh.updateAuditPoliciesFromEnforcementPolices(api)
elif choice == "F":
open_directory(working_dir)
elif choice == "S":
menu_settings()
elif choice == "Q":
break
else:
print(colorText("Invalid choice. Please try again.", "red"))
def menu_settings(): def menu_settings():
while True: while True:
print(colorText("\n--- 🛠️ Settings Submenu 🛠️ ---", "cyan")) print(colorText("\n--- 🛠️ Settings Submenu 🛠️ ---", "cyan"))
+18 -26
View File
@@ -1,19 +1,17 @@
# Copyright (C) 2025 James Brotosky, Brandon Wickline # Copyright (C) 2025 James Brotosky, Brandon Wickline
# #
# This program is free software: you can redistribute it and/or modify # 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 # 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 # by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version. # (at your option) any later version.
# #
# This program is distributed in the hope that it will be useful, # This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of # but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details. # GNU Affero General Public License for more details.
# #
# 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 json
import logging import logging
@@ -22,16 +20,9 @@ import os
import platform import platform
import sys import sys
from pathlib import Path from pathlib import Path
from dotenv import load_dotenv, set_key from dotenv import load_dotenv, set_key
PROTECTED_KEYS = [ from utils.configmanager import PROTECTED_KEYS, load_protected_config
"APPNAME",
"PATH_EXCLUSION_CONST",
"MIN_FILES_FOR_PATH",
"VT_THREAT_TOLERANCE",
"POLICY_MAP_ENF_AUD"
]
def get_base_directory() -> Path: def get_base_directory() -> Path:
system = platform.system() system = platform.system()
@@ -73,7 +64,6 @@ def configure_logging(log_dir: Path, log_level: str = "DEBUG"):
logger.debug("✅ Logging configured.") logger.debug("✅ Logging configured.")
def get_system_config_path() -> Path: def get_system_config_path() -> Path:
base_path = Path(getattr(sys, '_MEIPASS', os.path.dirname(os.path.abspath(__file__)))) base_path = Path(getattr(sys, '_MEIPASS', os.path.dirname(os.path.abspath(__file__))))
return base_path.parent / "system_config.json" return base_path.parent / "system_config.json"
@@ -110,7 +100,10 @@ def load_user_config(config_dir: Path) -> dict:
return json.load(f) return json.load(f)
def write_config_to_env(config: dict, env_path: Path): def write_config_to_env(config: dict, env_path: Path):
from utils.configmanager import PROTECTED_KEYS
for key, value in config.items(): for key, value in config.items():
if key in PROTECTED_KEYS:
continue # Skip protected keys
try: try:
serialized = json.dumps(value) if isinstance(value, (list, dict)) else str(value) serialized = json.dumps(value) if isinstance(value, (list, dict)) else str(value)
set_key(env_path, key, serialized) set_key(env_path, key, serialized)
@@ -157,13 +150,13 @@ def setup() -> Path:
for subfolder in subfolders: for subfolder in subfolders:
subfolder_path = folder_path / subfolder subfolder_path = folder_path / subfolder
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']) user_config = load_user_config(dirs['config'])
merged_config = {**system_config, **user_config} merged_config = {**system_config, **user_config}
for key in PROTECTED_KEYS: protected_config = load_protected_config()
merged_config[key] = system_config.get(key, "") merged_config.update(protected_config)
# ✅ URL resolution order: system_config → .env → user prompt # ✅ URL resolution order: system_config → .env → user prompt
url = system_config.get("URL") url = system_config.get("URL")
@@ -171,7 +164,6 @@ def setup() -> Path:
url = os.getenv("URL") url = os.getenv("URL")
if not url: if not url:
url = input("🌐 Enter the service URL (e.g., https://example.com/api): ").strip() url = input("🌐 Enter the service URL (e.g., https://example.com/api): ").strip()
merged_config["URL"] = url merged_config["URL"] = url
set_key(env_path, "URL", url) set_key(env_path, "URL", url)
os.environ["URL"] = url os.environ["URL"] = url
+3 -38
View File
@@ -13,7 +13,7 @@
# 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 os import os
import platform import platform
@@ -22,48 +22,14 @@ import subprocess
import tempfile import tempfile
import tkinter as tk import tkinter as tk
from tkinter import filedialog, messagebox, simpledialog from tkinter import filedialog, messagebox, simpledialog
from typing import Callable, Optional, TypeVar from utils.configmanager import load_env
import pandas as pd import pandas as pd
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
T = TypeVar("T")
def load_env_json(key: str, default: str):
raw = os.getenv(key, default)
try:
return json.loads(raw)
except json.JSONDecodeError:
try:
escaped = raw.encode('unicode_escape').decode('utf-8')
return json.loads(escaped)
except Exception as e:
logging.error(f"Failed to parse {key}: {e}")
return json.loads(default)
def load_env(key: str, cast_type: Callable[[str], T] = str, default: Optional[T] = None) -> Optional[T]:
"""
Safely retrieves an environment variable and casts it to the desired type.
Parameters:
key (str): The name of the environment variable.
cast_type (Callable[[str], T], optional): Function to cast the value. Defaults to str.
default (Optional[T], optional): Default value if the variable is not set or invalid.
Returns:
Optional[T]: The casted value or the default.
"""
value = os.getenv(key)
if value is None:
logger.warning(f"Environment variable '{key}' not set.")
return default
try:
value = value.strip("'\"") # Strip surrounding quotes
return cast_type(value)
except (ValueError, TypeError):
logger.warning(f"Invalid value for env var '{key}': {value}. Expected type {cast_type.__name__}.")
return default
def import_to_dataframe(file_path: str) -> pd.DataFrame: def import_to_dataframe(file_path: str) -> pd.DataFrame:
@@ -171,7 +137,6 @@ def regulator(paths, case_insensitive=True):
print(f"Regulator is providing: {pattern}") print(f"Regulator is providing: {pattern}")
return pattern return pattern
def displayIntro(): def displayIntro():
print( print(
colorText( colorText(