Cleaning up menu, added Move to Audit/Enforcement
This commit is contained in:
@@ -10,3 +10,4 @@ jobs.json
|
|||||||
securitytest.py
|
securitytest.py
|
||||||
*.toml
|
*.toml
|
||||||
system_config.json
|
system_config.json
|
||||||
|
Devel_unused/
|
||||||
@@ -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")
|
||||||
|
|
||||||
|
|||||||
@@ -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.")
|
|
||||||
@@ -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
@@ -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
@@ -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
@@ -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:
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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(
|
||||||
|
|||||||
Reference in New Issue
Block a user