First Round Async. Much work left to do, dont trust results of hash categorization presently.
This commit is contained in:
@@ -1,291 +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 os
|
||||
import re
|
||||
import time
|
||||
|
||||
import dotenv
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
from models.agent import Agent
|
||||
from services.agenthandler import findAllAgents, moveAgentToRelatedPolicy, selectAgents
|
||||
from services.API import AirlockAPIWrapper
|
||||
from utils.configmanager import get_protected_json, load_env, load_env_json
|
||||
from utils.setup import get_base_directory
|
||||
from utils.utils import colorText, get_sanitized_input
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
dotenv.load_dotenv()
|
||||
|
||||
|
||||
def getLocalApprovals(api: AirlockAPIWrapper):
|
||||
base_dir = get_base_directory
|
||||
result = api.otp_find_awaiting()
|
||||
local_approval = pd.DataFrame(result["response"]["otpusage"])
|
||||
if os.path.exists(f"{base_dir}\\cache\\newest_local_approval.parquet"):
|
||||
previous_run = pd.read_parquet(f"{base_dir}\\cache\\newest_local_approval.parquet")
|
||||
previous_run.to_parquet(
|
||||
f"{base_dir}\\cache\\last_local_approval.parquet", index=False
|
||||
)
|
||||
os.remove(f"{base_dir}\\cache\\newest_local_approval.parquet")
|
||||
|
||||
# Only keep rows presumably created by the generate local approval function
|
||||
local_approval = local_approval[
|
||||
local_approval["purpose"].str.startswith("🎫 Local Approval 🎫")
|
||||
]
|
||||
|
||||
local_approval["batchid"] = local_approval["purpose"].apply(
|
||||
lambda x: (match := re.search(r"batch:(\S+)", str(x))) and match.group(1)
|
||||
)
|
||||
|
||||
if not local_approval.empty:
|
||||
local_approval.to_parquet(
|
||||
f"{base_dir}\\cache\\newest_local_approval.parquet", index=False
|
||||
)
|
||||
|
||||
return local_approval
|
||||
|
||||
|
||||
def scheduleAddingLAHashes(api: AirlockAPIWrapper):
|
||||
|
||||
policy_relationship_map = get_protected_json("POLICY_MAP_ENF_AUD","{}")
|
||||
bad_publisher_list = load_env_json("BAD_PUBLISHER", "[]")
|
||||
pups = load_env_json("PUPS", "[]")
|
||||
threat_tolerance_constant = load_env("VT_THREAT_TOLERANCE", cast_type = int)
|
||||
|
||||
try:
|
||||
register_function("add_hash", returnFromLocalApproval)
|
||||
register_function("move_device", moveAgentToRelatedPolicy)
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to register functions: {e}")
|
||||
return
|
||||
|
||||
try:
|
||||
approvals_df = getNewLocalApprovals(api)
|
||||
if approvals_df.empty:
|
||||
logger.debug("No new local approvals found. Nothing to schedule.")
|
||||
return
|
||||
batches = approvals_df.groupby("batchid")
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to retrieve or group local approvals: {e}")
|
||||
return
|
||||
|
||||
for batchid, batch_df in batches:
|
||||
try:
|
||||
duration_minutes = int(batch_df["duration"].iloc[0])
|
||||
start_time = datetime.datetime.now()
|
||||
run_time = start_time + datetime.timedelta(minutes=duration_minutes)
|
||||
early_time = start_time + datetime.timedelta(minutes=np.floor(duration_minutes * 0.95))
|
||||
|
||||
early_timestamp = early_time.timestamp()
|
||||
run_timestamp = run_time.timestamp()
|
||||
|
||||
# Schedule add_hash job
|
||||
try:
|
||||
run_once_job(
|
||||
f"add_hash_{batchid}",
|
||||
"add_hash",
|
||||
early_timestamp,
|
||||
[
|
||||
api,
|
||||
batch_df,
|
||||
policy_relationship_map,
|
||||
bad_publisher_list,
|
||||
pups,
|
||||
threat_tolerance_constant,
|
||||
],
|
||||
None,
|
||||
)
|
||||
logger.debug(f"Scheduled add_hash for batch {batchid} at {early_time}")
|
||||
except Exception:
|
||||
logger.debug("Failed to schedule add_hash for batch {batchid}: {e}")
|
||||
|
||||
# Schedule move_device jobs
|
||||
devices = batch_df["agentid"].drop_duplicates().tolist()
|
||||
agents = []
|
||||
|
||||
for device in devices:
|
||||
rows = api.agent_find_by_hostname(device).iterrows()
|
||||
agents += [Agent(**row["data"]) for _, row in rows]
|
||||
|
||||
for agent in agents:
|
||||
try:
|
||||
run_once_job(
|
||||
f"move_device_{agent.hostame}_{batchid}",
|
||||
"move_device",
|
||||
run_timestamp,
|
||||
[api, agent, policy_relationship_map],
|
||||
"enforcement",
|
||||
)
|
||||
|
||||
print(
|
||||
f"Scheduled move_device for device {agent.hostname} in batch {batchid} at {run_time}"
|
||||
)
|
||||
except Exception as e:
|
||||
print(
|
||||
f"Failed to schedule move_device for device {agent.hostname} in batch {batchid}: {e}"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to process batch {batchid}: {e}")
|
||||
|
||||
|
||||
def returnFromLocalApproval(api, device_df, policy_relationship_map, bad_publisher_list, pups, threat_tolerance_constant
|
||||
):
|
||||
"""
|
||||
# Get unique policy names from device list
|
||||
policies_in_devicelist = sorted(device_df['policy_name'].unique().tolist())
|
||||
|
||||
# Create inverse map to go from Audit to Enforcement
|
||||
inverse_map = {v: k for k, v in policy_relationship_map.items()}
|
||||
|
||||
# Fetch all policies
|
||||
all_policies = [Policy(row['groupid'], row['hidden'], row['name'], row['parent']) for _, row in api.policy_find_all().iterrows()]
|
||||
|
||||
# Define policy types
|
||||
policy_types = [1, 2, 6, 7]
|
||||
|
||||
#TODO finish logic for adding hashes
|
||||
"""
|
||||
working_dir = load_env("WORKING_DIR")
|
||||
policy_relationship_map = get_protected_json("POLICY_MAP_ENF_AUD","{}")
|
||||
bad_publisher_list = load_env_json("BAD_PUBLISHER", "[]")
|
||||
pups = load_env_json("PUPS", "[]")
|
||||
threat_tolerance_constant = load_env("VT_THREAT_TOLERANCE")
|
||||
print(f"{working_dir}, {policy_relationship_map}, {bad_publisher_list}, {pups}, {threat_tolerance_constant}")
|
||||
|
||||
def moveToLocalApproval(api: AirlockAPIWrapper):
|
||||
possible_durations = [15, 60, 360, 1440, 10080]
|
||||
duration_selected = None
|
||||
|
||||
print(colorText("Please select a duration:", "white"))
|
||||
for i, option in enumerate(possible_durations, start=1):
|
||||
print(f"{i}. {option}")
|
||||
|
||||
try:
|
||||
|
||||
choice = int(get_sanitized_input("Enter the number of your choice:"))
|
||||
if 1 <= choice <= len(possible_durations):
|
||||
duration_selected = possible_durations[choice - 1]
|
||||
print(colorText(f"You selected: {duration_selected}", "yellow"))
|
||||
logger.debug(f"You selected: {duration_selected}")
|
||||
else:
|
||||
print(colorText("❌ Invalid choice.", "red"))
|
||||
logger.debug("Invalid Input")
|
||||
return
|
||||
except ValueError:
|
||||
print(colorText("❌ Invalid input. Please enter a number.", "red"))
|
||||
logger.debug("Invalid Input")
|
||||
return
|
||||
|
||||
agents = selectAgents(api)
|
||||
batch = int(time.time())
|
||||
|
||||
if not agents:
|
||||
print(colorText("❌ No agents found or error retrieving agents.", "red"))
|
||||
logger.debug("No agents found or error retrieving agents")
|
||||
return
|
||||
|
||||
for agent in agents:
|
||||
try:
|
||||
addLocalApproval(api, batch, duration_selected, agent.agentid)
|
||||
moveAgentToRelatedPolicy(api, agent, "audit")
|
||||
except Exception as e:
|
||||
print(colorText(f"❌ Error processing agent {agent.hostname}: {e}", "red"))
|
||||
|
||||
|
||||
def addLocalApproval(api: AirlockAPIWrapper, batchid, duration_selected, agentid):
|
||||
|
||||
purpose = f"🎫 Local Approval 🎫 - {duration_selected} mins - batch:{batchid} Client:{agentid}"
|
||||
api.otp_generate(agentid, duration_selected, purpose)
|
||||
|
||||
|
||||
|
||||
def monitorAuditStatus(api: AirlockAPIWrapper):
|
||||
current_agents = findAllAgents(api)
|
||||
last_agents = []
|
||||
if not last_agents:
|
||||
last_agents = current_agents
|
||||
policy_relationship_map = get_protected_json("POLICY_MAP_ENF_AUD","{}")
|
||||
|
||||
# Reverse map for audit → enforcement
|
||||
reverse_policy_map = {v: k for k, v in policy_relationship_map.items()}
|
||||
known_transitions = set(policy_relationship_map.items()) | set(reverse_policy_map.items())
|
||||
|
||||
# Index last_agents by hostname for quick lookup
|
||||
last_agent_map = {agent.hostname: agent for agent in last_agents}
|
||||
|
||||
# Result buckets
|
||||
newly_added = []
|
||||
same_policy = []
|
||||
moved_to_audit = []
|
||||
moved_to_enforcement = []
|
||||
unusual_move = []
|
||||
|
||||
for current in current_agents:
|
||||
previous = last_agent_map.get(current.hostname)
|
||||
|
||||
if not previous:
|
||||
newly_added.append(current)
|
||||
continue
|
||||
|
||||
if current.groupid == previous.groupid:
|
||||
same_policy.append(current)
|
||||
elif (previous.groupid, current.groupid) in known_transitions:
|
||||
moved_to_audit.append(current)
|
||||
elif (current.groupid, previous.groupid) in known_transitions:
|
||||
moved_to_enforcement.append(current)
|
||||
else:
|
||||
unusual_move.append(current)
|
||||
|
||||
# Return all five DataFrames
|
||||
return newly_added, same_policy, moved_to_audit, moved_to_enforcement, unusual_move
|
||||
|
||||
|
||||
def getNewLocalApprovals(api: AirlockAPIWrapper):
|
||||
|
||||
working_dir = load_env("WORKING_DIR")
|
||||
current_la = getLocalApprovals(api)
|
||||
|
||||
# Load old approval list
|
||||
old_la_path = f"{working_dir}\\Scheduling\\last_local_approval.parquet"
|
||||
if os.path.exists(old_la_path):
|
||||
old_la = pd.read_parquet(old_la_path)
|
||||
else:
|
||||
old_la = pd.DataFrame(columns=current_la.columns)
|
||||
|
||||
# Create composite keys
|
||||
current_la["key"] = current_la["clientid"].astype(str) + "_" + current_la["granted"].astype(str)
|
||||
old_la["key"] = old_la["clientid"].astype(str) + "_" + old_la["granted"].astype(str)
|
||||
|
||||
# Find new entries
|
||||
new_entries = current_la[~current_la["key"].isin(old_la["key"])]
|
||||
|
||||
# Convert 'granted' to datetime and filter by last 10 minutes
|
||||
new_entries["granted"] = pd.to_datetime(new_entries["granted"], utc=True, errors="coerce")
|
||||
ten_minutes_ago = datetime.datetime.now(datetime.timezone.utc) - datetime.timedelta(minutes=10)
|
||||
recent_entries = new_entries[new_entries["granted"] > ten_minutes_ago]
|
||||
|
||||
# Save current approvals for next run
|
||||
current_la.drop(columns=["key"], inplace=True)
|
||||
current_la.to_parquet(old_la_path, index=False)
|
||||
|
||||
return recent_entries
|
||||
+32
-40
@@ -1,69 +1,61 @@
|
||||
# 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 asyncio
|
||||
import logging
|
||||
|
||||
from services.agenthandler import selectAgents
|
||||
from services.API import AirlockAPIWrapper
|
||||
from utils.selector import Selector
|
||||
from utils.Selector import Selector
|
||||
from utils.utils import colorText, get_sanitized_input
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
|
||||
|
||||
def generate(api: AirlockAPIWrapper):
|
||||
async def generate(api: AirlockAPIWrapper):
|
||||
otp_dict = {}
|
||||
agents = selectAgents(api)
|
||||
print(colorText("Would you like to continue with these devices?","white"))
|
||||
agents = await selectAgents(api)
|
||||
|
||||
print(colorText("Would you like to continue with these devices?", "white"))
|
||||
for agent in agents:
|
||||
print(agent.hostname)
|
||||
confirm = Selector.confirm()
|
||||
|
||||
confirm = await Selector.confirm()
|
||||
if agents and confirm:
|
||||
requester = get_sanitized_input("Who is requesting the OTP: ")
|
||||
because = get_sanitized_input("Why/What work are they doing?: ")
|
||||
|
||||
requester = await get_sanitized_input("Who is requesting the OTP: ")
|
||||
because = await get_sanitized_input("Why/What work are they doing?: ")
|
||||
|
||||
purpose = f"Requester: {requester} - for : {because}"
|
||||
possible_durations = [15, 60, 360, 1440, 10080]
|
||||
|
||||
print(colorText("Please select a duration in minutes: ", "white"))
|
||||
print(colorText("15 mins, 60 mins, 360 mins(6 Hours), 1440 mins (24 Hours), 10080 mins (7 Days):", "white"))
|
||||
duration_selected = Selector.select_int(possible_durations)
|
||||
duration_selected = await Selector.select_int(possible_durations)
|
||||
|
||||
if duration_selected:
|
||||
for agent in agents:
|
||||
otp_code = api.otp_generate(agent.agentid, duration_selected, purpose)
|
||||
async def generate_otp(agent):
|
||||
otp_code = await api.otp_generate(agent.agentid, duration_selected, purpose) # pyright: ignore[reportArgumentType]
|
||||
logger.info(f"Generated OTP for {agent.hostname}: {otp_code}")
|
||||
otp_dict[agent.hostname] = otp_code
|
||||
return agent.hostname, otp_code
|
||||
|
||||
results = await asyncio.gather(*(generate_otp(agent) for agent in agents))
|
||||
otp_dict = dict(results)
|
||||
|
||||
return otp_dict
|
||||
|
||||
def otp_activities_by_agent(api: AirlockAPIWrapper):
|
||||
agents = selectAgents(api)
|
||||
|
||||
async def otp_activities_by_agent(api: AirlockAPIWrapper):
|
||||
agents = await selectAgents(api)
|
||||
otp_dict = {}
|
||||
|
||||
for agent in agents:
|
||||
otp_info = api.otp_find_by_agent(agent.agentid)
|
||||
otp_info = await api.otp_find_by_agent(agent.agentid)
|
||||
otp_dict[agent.hostname] = otp_info
|
||||
|
||||
return otp_dict
|
||||
|
||||
def revoke(api: AirlockAPIWrapper):
|
||||
otp_dict = otp_activities_by_agent(api)
|
||||
list_to_revoke = [entry["otpid"] for entry in otp_dict]
|
||||
async def revoke(api: AirlockAPIWrapper):
|
||||
otp_dict = await otp_activities_by_agent(api)
|
||||
list_to_revoke = [entry["otpid"] for entry in otp_dict.values() if entry]
|
||||
|
||||
if otp_dict and list_to_revoke:
|
||||
for revokee in list_to_revoke:
|
||||
api.otp_revoke(revokee)
|
||||
async def revoke_otp(otpid):
|
||||
await api.otp_revoke(otpid)
|
||||
|
||||
await asyncio.gather(*(revoke_otp(otpid) for otpid in list_to_revoke))
|
||||
|
||||
+181
-185
@@ -13,10 +13,11 @@
|
||||
# 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 asyncio
|
||||
import logging
|
||||
import os
|
||||
import os.path
|
||||
from typing import List
|
||||
from typing import List, Optional
|
||||
|
||||
import dotenv
|
||||
import pandas as pd
|
||||
@@ -24,12 +25,12 @@ import pandas as pd
|
||||
from models.execution import ExecutionHistoryRecord, Hash
|
||||
from models.policy import Allowlist, Policy
|
||||
from services.API import AirlockAPIWrapper
|
||||
from services.TaskQueue import AsyncTaskQueue, run_sync_task_in_thread
|
||||
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 (
|
||||
colorText,
|
||||
formatHTML,
|
||||
import_to_dataframe,
|
||||
regulator,
|
||||
)
|
||||
|
||||
@@ -37,79 +38,86 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
dotenv.load_dotenv()
|
||||
|
||||
async def selectPolicies(api: AirlockAPIWrapper, allow_multiple=True) -> List[Policy]:
|
||||
df = await api.policy_find_all()
|
||||
policies = [Policy(**row.to_dict()) for _, row in df.iterrows()]
|
||||
|
||||
|
||||
def selectPolicies(api: AirlockAPIWrapper, allow_multiple=True) -> List[Policy]:
|
||||
|
||||
policies = [Policy(**row.to_dict()) for _, row in api.policy_find_all().iterrows()]
|
||||
logger.debug("Prompting for Policies")
|
||||
print(colorText("Please select policy/policies", "white"))
|
||||
selected = Selector.select_objects(policies, allow_multiple, prompt_each=True)
|
||||
|
||||
selected = await Selector.select_objects(policies, allow_multiple, prompt_each=True)
|
||||
|
||||
if selected is None:
|
||||
return []
|
||||
|
||||
# Normalize to always return a list
|
||||
logger.debug("Returning {selected.dict}")
|
||||
logger.debug("Returning selected policies")
|
||||
return selected if isinstance(selected, list) else [selected]
|
||||
|
||||
|
||||
def selectAllowlists(api: AirlockAPIWrapper, policy = all, allow_multiple=True) -> List[Allowlist]:
|
||||
if policy == "all": allowlists = [Allowlist(**row.to_dict()) for _, row in api.allowlist_find_all().iterrows()]
|
||||
else: allowlists = [Allowlist(**row.to_dict()) for _, row in api.policy_list_allowlists(policy[0].groupid).iterrows()]
|
||||
async def selectAllowlists(api: AirlockAPIWrapper, policy="all", allow_multiple=True) -> List[Allowlist]:
|
||||
if policy == "all":
|
||||
df = await api.allowlist_find_all()
|
||||
else:
|
||||
df = await api.policy_list_allowlists(policy[0].groupid) # pyright: ignore[reportAttributeAccessIssue]
|
||||
|
||||
allowlists = [Allowlist(**row.to_dict()) for _, row in df.iterrows()]
|
||||
|
||||
logger.debug("Prompting for Allowlist(s)")
|
||||
print(colorText("Please select allowlist(s)", "white"))
|
||||
selected = Selector.select_objects(allowlists, allow_multiple, prompt_each=True)
|
||||
|
||||
selected = await Selector.select_objects(allowlists, allow_multiple, prompt_each=True)
|
||||
|
||||
if selected is None:
|
||||
return []
|
||||
|
||||
# Normalize to always return a list
|
||||
logger.debug(f"Returning {selected}")
|
||||
return selected if isinstance(selected, list) else [selected]
|
||||
|
||||
|
||||
def sortHashes(
|
||||
async def sortHashes(
|
||||
api: AirlockAPIWrapper,
|
||||
queue: AsyncTaskQueue,
|
||||
selected_policies: List[Policy],
|
||||
type=[1, 2, 6, 7]
|
||||
type=[1, 2, 6, 7],
|
||||
history_days: Optional[int] = None
|
||||
):
|
||||
working_dir = load_env("WORKING_DIR")
|
||||
history_days = Selector.select_value(
|
||||
prompt="Enter how many days of history to pull (1–150): ",
|
||||
value_type=int,
|
||||
valid_range=(1, 150),
|
||||
)
|
||||
|
||||
if history_days is None:
|
||||
history_days = await Selector.select_value(
|
||||
prompt="Enter how many days of history to pull (1–150): ",
|
||||
value_type=int,
|
||||
valid_range=(1, 150),
|
||||
)
|
||||
|
||||
logger.debug(f"{history_days} day selected for history")
|
||||
|
||||
|
||||
if history_days is None:
|
||||
logging.warning("No history range selected. Aborting.")
|
||||
return
|
||||
|
||||
executions = []
|
||||
hashes = []
|
||||
working_dir = await load_env("WORKING_DIR")
|
||||
|
||||
# Pull execution histories for each policy
|
||||
|
||||
policy_executions = ExecutionHistoryRecord.from_policies(
|
||||
policy_executions = await ExecutionHistoryRecord.from_policies(
|
||||
api, selected_policies, type_=type, history_days=history_days
|
||||
)
|
||||
|
||||
logger.debug(f"Policy_executions is {policy_executions}")
|
||||
|
||||
logger.debug(f"Policy_executions is {policy_executions}")
|
||||
executions.extend(policy_executions)
|
||||
logger.debug(f"Executions contains {executions}")
|
||||
|
||||
if executions:
|
||||
hashes = [Hash(sha256=row["sha256"], **row["data"]) for _, row in api.hash_query([record.sha256 for record in executions]).iterrows()
|
||||
sha_list = [record.sha256 for record in executions]
|
||||
hash_df = await api.hash_query(sha_list)
|
||||
hashes = [
|
||||
Hash(sha256=row["sha256"], **row["data"])
|
||||
for _, row in hash_df.iterrows()
|
||||
]
|
||||
|
||||
if hashes:
|
||||
unique_hashes = Hash.deduplicate(hashes)
|
||||
|
||||
needs_review, approved, unapproved = Hash.categorize_hashes(
|
||||
hashes=unique_hashes
|
||||
)
|
||||
needs_review, approved, unapproved = await Hash.categorize_hashes(hashes=unique_hashes)
|
||||
|
||||
categories = {
|
||||
"needs_review": needs_review,
|
||||
@@ -118,157 +126,158 @@ def sortHashes(
|
||||
}
|
||||
|
||||
for label, category in categories.items():
|
||||
csv_path = f"{working_dir}\\Needs_Review\\Review_First\\{label}_executions.csv"
|
||||
html_path = f"{working_dir}\\Needs_Review\\HTML\\{label}.html"
|
||||
csv_path = f"{working_dir}/Needs_Review/Review_First/{selected_policies[0].name}_{label}_executions.csv"
|
||||
html_path = f"{working_dir}/Needs_Review/HTML/{selected_policies[0].name}_{label}.html"
|
||||
|
||||
ExecutionHistoryRecord.enrich_with_hashes_and_export(
|
||||
executions, category, f"{working_dir}\\Needs_Review\\Review_First", label=label
|
||||
)
|
||||
df = import_to_dataframe(csv_path)
|
||||
formatHTML(df, html_path)
|
||||
df = await ExecutionHistoryRecord.enrich_with_hashes(executions, category)
|
||||
|
||||
asyncio.create_task(queue.enqueue(
|
||||
f"DF TO CSV {selected_policies[0].name}_{label}",
|
||||
run_sync_task_in_thread,
|
||||
df.to_csv,
|
||||
csv_path,
|
||||
index=False,
|
||||
encoding='utf-8'
|
||||
))
|
||||
|
||||
def buildPathsandPublishers(split):
|
||||
working_dir = load_env("WORKING_DIR")
|
||||
df1 = pd.DataFrame()
|
||||
df2 = pd.DataFrame()
|
||||
all_approved_hashes = pd.DataFrame()
|
||||
path1 = f"{working_dir}\\Approved\\approved_executions.csv"
|
||||
path2 = f"{working_dir}\\Approved\\needs_review_executions.csv"
|
||||
|
||||
if os.path.exists(path1):
|
||||
df1 = pd.read_csv(path1)
|
||||
else:
|
||||
logger.warning(f"File not found: {path1}")
|
||||
asyncio.create_task(queue.enqueue(
|
||||
f"DF TO HTML {selected_policies[0].name}_{label}",
|
||||
run_sync_task_in_thread,
|
||||
formatHTML,
|
||||
df,
|
||||
html_path
|
||||
))
|
||||
|
||||
if os.path.exists(path2):
|
||||
df2 = pd.read_csv(path2)
|
||||
else:
|
||||
logger.warning(f"File not found: {path2}")
|
||||
print("sortHashes completed successfully.")
|
||||
|
||||
|
||||
async def buildPathsandPublishers(queue: AsyncTaskQueue, split):
|
||||
working_dir = await load_env("WORKING_DIR")
|
||||
path1 = f"{working_dir}/Approved/approved_executions.csv"
|
||||
path2 = f"{working_dir}/Approved/needs_review_executions.csv"
|
||||
|
||||
df1 = await asyncio.to_thread(pd.read_csv, path1) if os.path.exists(path1) else pd.DataFrame()
|
||||
if df1.empty:
|
||||
logger.warning(f"File not found or empty: {path1}")
|
||||
|
||||
df2 = await asyncio.to_thread(pd.read_csv, path2) if os.path.exists(path2) else pd.DataFrame()
|
||||
if df2.empty:
|
||||
logger.warning(f"File not found or empty: {path2}")
|
||||
|
||||
if df1.empty and df2.empty:
|
||||
logger.warning("Both DataFrames are empty. Skipping sort.")
|
||||
all_approved_hashes = pd.DataFrame()
|
||||
logger.debug(all_approved_hashes.head)
|
||||
logger.debug(all_approved_hashes.head())
|
||||
return
|
||||
|
||||
all_approved_hashes = pd.concat([df1, df2], ignore_index=True)
|
||||
if "filename_exec" in all_approved_hashes.columns:
|
||||
all_approved_hashes = all_approved_hashes.sort_values(by="filename_exec")
|
||||
else:
|
||||
all_approved_hashes = pd.concat([df1, df2], ignore_index=True)
|
||||
if "filename_exec" in all_approved_hashes.columns:
|
||||
all_approved_hashes = all_approved_hashes.sort_values(by="filename_exec")
|
||||
else:
|
||||
logger.warning("Warning: 'filename_exec' column not found in concatenated DataFrame.")
|
||||
logger.warning("'filename_exec' column not found in concatenated DataFrame.")
|
||||
|
||||
if not all_approved_hashes.empty:
|
||||
primary_path_exclusions = calculatePath(
|
||||
all_approved_hashes,
|
||||
split,
|
||||
)
|
||||
remaining_hashes = all_approved_hashes[
|
||||
~all_approved_hashes["sha256"].isin(primary_path_exclusions["sha256"])
|
||||
]
|
||||
secondary_path_exclusions = calculatePath(
|
||||
remaining_hashes, split
|
||||
)
|
||||
remaining_hashes = remaining_hashes[
|
||||
~remaining_hashes["sha256"].isin(secondary_path_exclusions["sha256"])
|
||||
]
|
||||
dataframes = {
|
||||
"primary_Paths": primary_path_exclusions,
|
||||
"secondary_Paths": secondary_path_exclusions,
|
||||
"hashes_to_add": remaining_hashes,
|
||||
}
|
||||
logger.debug("Preparing to sort dataframes")
|
||||
for name, df in dataframes.items():
|
||||
logger.debug(f" DataFrame headers: {list(df.columns)}")
|
||||
if name == "hashes_to_add": df.sort_values(by="filename_exec", inplace=True)
|
||||
else: df.sort_values(by="longestcfp", inplace=True)
|
||||
|
||||
df.to_csv(f"{working_dir}\\Needs_Review\\Review_Second\\{name}.csv", index=False)
|
||||
formatHTML(df, f"{working_dir}\\Needs_Review\\HTML\\{name}.html")
|
||||
primary_path_exclusions = await calculatePath(all_approved_hashes, split)
|
||||
remaining_hashes = all_approved_hashes[~all_approved_hashes["sha256"].isin(primary_path_exclusions["sha256"])]
|
||||
secondary_path_exclusions = await calculatePath(remaining_hashes, split)
|
||||
remaining_hashes = remaining_hashes[~remaining_hashes["sha256"].isin(secondary_path_exclusions["sha256"])]
|
||||
|
||||
if not all_approved_hashes.empty:
|
||||
# Drop all not signed, only keep unique values
|
||||
publist = all_approved_hashes[
|
||||
all_approved_hashes["publisher_hash"] != "Not Signed"
|
||||
].drop_duplicates(subset=["publisher_hash"])
|
||||
# Remove Bad publisher if somehow they made it this far
|
||||
pattern = regulator(load_env_json("BAD_PUBLISHERS","[]"))
|
||||
publist = publist[~publist["publisher_hash"].str.contains(pattern, na=False)]
|
||||
publist = publist[["publisher_hash"]]
|
||||
publist.sort_values(by="publisher_hash", inplace=True)
|
||||
publist.to_csv(f"{working_dir}\\Needs_Review\\Review_Second\\publishers.csv", index=False)
|
||||
|
||||
def buildPreflights():
|
||||
working_dir = load_env("WORKING_DIR")
|
||||
|
||||
df1 = pd.DataFrame()
|
||||
df2 = pd.DataFrame()
|
||||
approved_hashes = pd.DataFrame()
|
||||
approved_publishers = pd.DataFrame()
|
||||
|
||||
hash = f"{working_dir}\\Approved\\hashes_to_add.csv"
|
||||
path1 = f"{working_dir}\\Approved\\primary_Paths.csv"
|
||||
path2 = f"{working_dir}\\Approved\\secondary_Paths.csv"
|
||||
publishers = f"{working_dir}\\Approved\\publishers.csv"
|
||||
|
||||
if os.path.exists(hash):
|
||||
approved_hashes = pd.read_csv(hash)
|
||||
|
||||
else:
|
||||
logger.warning(f"File not found: {hash}")
|
||||
|
||||
if os.path.exists(path1):
|
||||
df1 = pd.read_csv(path1)
|
||||
else:
|
||||
logger.warning(f"File not found: {path1}")
|
||||
|
||||
if os.path.exists(path2):
|
||||
df2 = pd.read_csv(path2)
|
||||
else:
|
||||
logger.warning(f"File not found: {path2}")
|
||||
|
||||
if df1.empty and df2.empty:
|
||||
logger.warning("Both DataFrames are empty. Skipping sort.")
|
||||
approved_paths = pd.DataFrame()
|
||||
else:
|
||||
approved_paths = pd.concat([df1, df2], ignore_index=True)
|
||||
|
||||
if os.path.exists(publishers):
|
||||
approved_publishers = pd.read_csv(publishers)
|
||||
|
||||
else:
|
||||
logger.warning(f"File not found: {publishers}")
|
||||
|
||||
dataframes = {"approved_paths": approved_paths, "approved_hashes": approved_hashes, "approved_publishers": approved_publishers}
|
||||
dataframes = {
|
||||
"primary_Paths": primary_path_exclusions,
|
||||
"secondary_Paths": secondary_path_exclusions,
|
||||
"hashes_to_add": remaining_hashes,
|
||||
}
|
||||
|
||||
for name, df in dataframes.items():
|
||||
logger.debug(f" DataFrame headers: {list(df.columns)}")
|
||||
if name == "approved_paths":df.sort_values(by="longestcfp", inplace=True)
|
||||
elif name == "approved_hashes":df.sort_values(by="filename_exec", inplace=True)
|
||||
elif name == "approved_publishers" : df.sort_values(by="publisher_hash", inplace=True)
|
||||
|
||||
df.to_csv(f"{working_dir}\\Preflight\\{name}.csv", index=False)
|
||||
formatHTML(df, f"{working_dir}\\Preflight\\HTML\\{name}.html")
|
||||
logger.debug(f"DataFrame headers for {name}: {list(df.columns)}")
|
||||
sort_column = "filename_exec" if name == "hashes_to_add" else "longestcfp"
|
||||
df.sort_values(by=sort_column, inplace=True)
|
||||
csv_path = f"{working_dir}/Needs_Review/Review_Second/{name}.csv"
|
||||
html_path = f"{working_dir}/Needs_Review/HTML/{name}.html"
|
||||
await asyncio.to_thread(df.to_csv, csv_path, index=False)
|
||||
await asyncio.to_thread(formatHTML, df, html_path)
|
||||
|
||||
def splitFilepathsGrouped(df, col="filename"):
|
||||
path_exclusion_constant = get_protected_value("PATH_EXCLUSION_CONST", cast_type= int)
|
||||
min_files_for_path = get_protected_value("MIN_FILES_FOR_PATH", cast_type= int)
|
||||
publist = all_approved_hashes[all_approved_hashes["publisher_hash"] != "Not Signed"].drop_duplicates(subset=["publisher_hash"])
|
||||
pattern = regulator(await load_env_json("BAD_PUBLISHERS", "[]"))
|
||||
publist = publist[~publist["publisher_hash"].str.contains(pattern, na=False)]
|
||||
publist = publist[["publisher_hash"]]
|
||||
publist.sort_values(by="publisher_hash", inplace=True)
|
||||
pub_csv_path = f"{working_dir}/Needs_Review/Review_Second/publishers.csv"
|
||||
await asyncio.to_thread(publist.to_csv, pub_csv_path, index=False)
|
||||
|
||||
print("buildPathsandPublishers completed asynchronously.")
|
||||
|
||||
|
||||
async def buildPreflights():
|
||||
working_dir = await load_env("WORKING_DIR")
|
||||
|
||||
hash_path = f"{working_dir}/Approved/hashes_to_add.csv"
|
||||
path1 = f"{working_dir}/Approved/primary_Paths.csv"
|
||||
path2 = f"{working_dir}/Approved/secondary_Paths.csv"
|
||||
publishers_path = f"{working_dir}/Approved/publishers.csv"
|
||||
|
||||
df1 = await asyncio.to_thread(pd.read_csv, path1) if os.path.exists(path1) else pd.DataFrame()
|
||||
if df1.empty:
|
||||
logger.warning(f"File not found or empty: {path1}")
|
||||
|
||||
df2 = await asyncio.to_thread(pd.read_csv, path2) if os.path.exists(path2) else pd.DataFrame()
|
||||
if df2.empty:
|
||||
logger.warning(f"File not found or empty: {path2}")
|
||||
|
||||
approved_hashes = await asyncio.to_thread(pd.read_csv, hash_path) if os.path.exists(hash_path) else pd.DataFrame()
|
||||
if approved_hashes.empty:
|
||||
logger.warning(f"File not found or empty: {hash_path}")
|
||||
|
||||
approved_publishers = await asyncio.to_thread(pd.read_csv, publishers_path) if os.path.exists(publishers_path) else pd.DataFrame()
|
||||
if approved_publishers.empty:
|
||||
logger.warning(f"File not found or empty: {publishers_path}")
|
||||
|
||||
approved_paths = pd.concat([df1, df2], ignore_index=True) if not (df1.empty and df2.empty) else pd.DataFrame()
|
||||
|
||||
dataframes = {
|
||||
"approved_paths": approved_paths,
|
||||
"approved_hashes": approved_hashes,
|
||||
"approved_publishers": approved_publishers
|
||||
}
|
||||
|
||||
for name, df in dataframes.items():
|
||||
logger.debug(f"DataFrame headers for {name}: {list(df.columns)}")
|
||||
if name == "approved_paths":
|
||||
df.sort_values(by="longestcfp", inplace=True)
|
||||
elif name == "approved_hashes":
|
||||
df.sort_values(by="filename_exec", inplace=True)
|
||||
elif name == "approved_publishers":
|
||||
df.sort_values(by="publisher_hash", inplace=True)
|
||||
|
||||
csv_path = f"{working_dir}/Preflight/{name}.csv"
|
||||
html_path = f"{working_dir}/Preflight/HTML/{name}.html"
|
||||
|
||||
await asyncio.to_thread(df.to_csv, csv_path, index=False)
|
||||
await asyncio.to_thread(formatHTML, df, html_path)
|
||||
|
||||
print("buildPreflights completed asynchronously.")
|
||||
|
||||
|
||||
async def splitFilepathsGrouped(df, col="filename"):
|
||||
path_task = asyncio.create_task(get_protected_value("PATH_EXCLUSION_CONST", int))
|
||||
min_files_task = asyncio.create_task(get_protected_value("MIN_FILES_FOR_PATH", int))
|
||||
|
||||
path_exclusion_constant = await path_task
|
||||
min_files_for_path = await min_files_task
|
||||
|
||||
def clean_split(path):
|
||||
if not isinstance(path, (str, bytes, os.PathLike)):
|
||||
return []
|
||||
parts = os.path.normpath(path).split(os.sep)
|
||||
parts = [p for p in parts if p] # Remove empty strings
|
||||
return parts
|
||||
return [p for p in parts if p]
|
||||
|
||||
# Diagnostic: log any non-string entries
|
||||
non_string_entries = df[~df[col].apply(lambda x: isinstance(x, (str, bytes, os.PathLike)))]
|
||||
if not non_string_entries.empty:
|
||||
print(f"[WARNING] Non-string entries found in column '{col}':")
|
||||
print(non_string_entries)
|
||||
logger.warning(f"Non-string entries found in column '{col}':")
|
||||
logger.debug(non_string_entries)
|
||||
|
||||
df = df.copy()
|
||||
split_paths = df[col].apply(clean_split)
|
||||
|
||||
# Filter out paths with fewer than `min_files_for_path` components
|
||||
df = df[split_paths.apply(lambda parts: len(parts) >= min_files_for_path)].copy()
|
||||
split_paths = split_paths[df.index]
|
||||
|
||||
@@ -295,11 +304,7 @@ def splitFilepathsGrouped(df, col="filename"):
|
||||
|
||||
for i, parts in enumerate(split_parts):
|
||||
filename = parts[-1]
|
||||
middle = (
|
||||
os.sep.join(parts[len(common_prefix):-1])
|
||||
if len(parts) > len(common_prefix) + 1
|
||||
else ""
|
||||
)
|
||||
middle = os.sep.join(parts[len(common_prefix):-1]) if len(parts) > len(common_prefix) + 1 else ""
|
||||
row = group_df.iloc[i].copy()
|
||||
row["longestcfp"] = prefix_str
|
||||
row["middle"] = middle
|
||||
@@ -309,19 +314,16 @@ def splitFilepathsGrouped(df, col="filename"):
|
||||
|
||||
return pd.DataFrame(new_rows).drop(columns=["group_key"])
|
||||
|
||||
def calculatePath(approved_hashes, split):
|
||||
if split:
|
||||
dfs_by_policy = [group for _, group in approved_hashes.groupby("policy")]
|
||||
else:
|
||||
dfs_by_policy = [approved_hashes]
|
||||
|
||||
badpathparts = load_env_json("BAD_PATH_PARTS", "[]")
|
||||
min_files_for_path = get_protected_value("MIN_FILES_FOR_PATH", cast_type = int)
|
||||
async def calculatePath(approved_hashes, split):
|
||||
dfs_by_policy = [group for _, group in approved_hashes.groupby("policy")] if split else [approved_hashes]
|
||||
badpathparts = await asyncio.to_thread(load_env_json, "BAD_PATH_PARTS", "[]")
|
||||
min_files_for_path = await asyncio.to_thread(get_protected_value, "MIN_FILES_FOR_PATH", int)
|
||||
|
||||
processed_dfs = []
|
||||
|
||||
for df in dfs_by_policy:
|
||||
haslcp = splitFilepathsGrouped(df, "filename_exec")
|
||||
haslcp = await splitFilepathsGrouped(df, "filename_exec")
|
||||
haslcp = haslcp.drop_duplicates()
|
||||
|
||||
forbidden = regulator(badpathparts, True)
|
||||
@@ -329,18 +331,12 @@ def calculatePath(approved_hashes, split):
|
||||
|
||||
logger.debug("Removing forbidden filepaths for path exceptions")
|
||||
print(colorText("Removing forbidden filepaths for path exceptions", "green"))
|
||||
|
||||
lcp_not_forbidden = haslcp[~forbidden_lcfp].copy()
|
||||
|
||||
lcp_not_forbidden_review = lcp_not_forbidden[
|
||||
[
|
||||
"policyname",
|
||||
"longestcfp",
|
||||
"middle",
|
||||
"filename_only",
|
||||
"file_extension",
|
||||
"sha256",
|
||||
]
|
||||
]
|
||||
lcp_not_forbidden_review = lcp_not_forbidden[[
|
||||
"policyname", "longestcfp", "middle", "filename_only", "file_extension", "sha256"
|
||||
]]
|
||||
|
||||
unique_sha_counts = (
|
||||
lcp_not_forbidden_review.groupby("longestcfp")["sha256"].nunique().reset_index()
|
||||
@@ -353,8 +349,8 @@ def calculatePath(approved_hashes, split):
|
||||
lcp_not_forbidden_review = lcp_not_forbidden_review[
|
||||
lcp_not_forbidden_review["unique_sha256_count"] >= min_files_for_path
|
||||
]
|
||||
|
||||
processed_dfs.append(lcp_not_forbidden_review)
|
||||
|
||||
pathExclusions = pd.concat(processed_dfs, ignore_index=True)
|
||||
|
||||
return pathExclusions
|
||||
return pathExclusions
|
||||
+14
-54
@@ -1,124 +1,84 @@
|
||||
# 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.selector import Selector
|
||||
from services.PolicyHandler import getPolicyInfo
|
||||
from utils.Selector import Selector
|
||||
from utils.utils import colorText, load_env
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
dotenv.load_dotenv()
|
||||
|
||||
|
||||
def findQuietAgents(api: AirlockAPIWrapper):
|
||||
async 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)
|
||||
selected_policy = await selectPolicies(api, False)
|
||||
|
||||
# Prompt user for history range
|
||||
history_days = Selector.select_value(
|
||||
if selected_policy:
|
||||
agents = await api.agents_find_by_group(selected_policy[0].groupid)
|
||||
|
||||
history_days = await 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): ",
|
||||
required_quiet = await Selector.select_value(
|
||||
prompt="Enter how many days without an untrusted execution before these are considered ready for enforcement? (1–150): ",
|
||||
value_type=int,
|
||||
valid_range=(1, 150),
|
||||
)
|
||||
|
||||
# Get execution history as a DataFrame
|
||||
policy_exec_history = getPolicyInfo(
|
||||
policy_exec_history = await 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.")
|
||||
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")
|
||||
logger.info(message)
|
||||
colorText(message, "green")
|
||||
@@ -0,0 +1,108 @@
|
||||
"""
|
||||
MAJOR WORK IN PROGRESS
|
||||
|
||||
from flows.prepPolicy import (
|
||||
buildPathsandPublishers,
|
||||
buildPreflights,
|
||||
selectAllowlists,
|
||||
selectPolicies,
|
||||
sortHashes,
|
||||
)
|
||||
|
||||
|
||||
|
||||
#Pull History for last 24 hours, Make a list of unique policy names that had exectutions
|
||||
|
||||
sortHashes(api,selected_policies, type=[1, 2, 6, 7], history_days=150)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
buildPathsandPublishers(False)
|
||||
|
||||
buildPreflights()
|
||||
else:
|
||||
print("File not found. Please make sure it's saved correctly and try again.")
|
||||
|
||||
|
||||
|
||||
pathexclusions = pd.read_csv(f"{working_dir}\\Preflight\\approved_paths.csv")
|
||||
hashes = pd.read_csv(f"{working_dir}\\Preflight\\approved_hashes.csv")
|
||||
|
||||
unique_combinations = pathexclusions[["longestcfp", "file_extension"]].drop_duplicates()
|
||||
|
||||
drive_letter_pattern = re.compile(r"^[a-zA-Z]:\\")
|
||||
processed_paths = [
|
||||
(path if drive_letter_pattern.match(path) else f"\\\\{path}") + f"\\**{ext}"
|
||||
for path, ext in unique_combinations.itertuples(index=False, name=None)
|
||||
]
|
||||
|
||||
print(processed_paths)
|
||||
print(colorText("These publishers would added", "yellow"))
|
||||
|
||||
if os.path.exists(f"{working_dir}\\Preflight\\approved_publishers.csv"):
|
||||
publishers = pd.read_csv(f"{working_dir}\\Preflight\\approved_hashes.csv")
|
||||
if publishers.empty:
|
||||
print(colorText("The publishers list is empty.", "red"))
|
||||
else:
|
||||
processed_publishers = (
|
||||
publishers[publishers["publisher_hash"] != "Not Signed"]
|
||||
["publisher_hash"]
|
||||
.drop_duplicates()
|
||||
.tolist()
|
||||
)
|
||||
print(processed_publishers)
|
||||
|
||||
print(colorText("These hashes would be added to:", "yellow"))
|
||||
print(destination_allowlist)
|
||||
|
||||
processed_hashes = hashes["sha256"].unique().tolist()
|
||||
print(processed_hashes)
|
||||
|
||||
if processed_paths and processed_hashes:
|
||||
tested = True
|
||||
else:
|
||||
# Log which condition(s) failed
|
||||
missing_items = []
|
||||
if not os.path.exists(f"{working_dir}\\Preflight\\approved_paths.csv"):
|
||||
missing_items.append("approved_paths.csv not found")
|
||||
if not os.path.exists(f"{working_dir}\\Preflight\\approved_hashes.csv"):
|
||||
missing_items.append("approved_hashes.csv not found")
|
||||
if not destination_policy:
|
||||
missing_items.append("destination_policy is empty or None")
|
||||
if not destination_allowlist:
|
||||
missing_items.append("destination_allowlist is empty or None")
|
||||
|
||||
logger.error("Preflight check failed due to the following:")
|
||||
for item in missing_items:
|
||||
logger.error(f" - {item}")
|
||||
|
||||
|
||||
elif choice == "7":
|
||||
areYouSure()
|
||||
confirmation = get_sanitized_input("Type 'I AGREE' to continue: ")
|
||||
if (
|
||||
tested
|
||||
and destination_policy
|
||||
and destination_allowlist
|
||||
and confirmation.strip() == "I AGREE"
|
||||
):
|
||||
print(colorText("Proceeding with the code...", "yellow"))
|
||||
api.hash_add_to_allowlist(destination_allowlist[0].applicationid, processed_hashes)
|
||||
api.policy_add_path_exclusions(destination_policy[0].groupid, processed_paths)
|
||||
if processed_publishers:
|
||||
api.policy_add_publishers(destination_policy[0].groupid, processed_publishers)
|
||||
else:
|
||||
logger.error("Confirmation block failed. Reasons:")
|
||||
if not tested:
|
||||
logger.error(" - Preflight checks were not completed successfully (`tested` is False).")
|
||||
if not destination_policy:
|
||||
logger.error(" - `destination_policy` is missing or invalid.")
|
||||
if not destination_allowlist:
|
||||
logger.error(" - `destination_allowlist` is missing or invalid.")
|
||||
if confirmation.strip() != "I AGREE":
|
||||
logger.error(" - User did not confirm with 'I AGREE'. Received: '%s'", confirmation.strip())
|
||||
|
||||
"""
|
||||
Reference in New Issue
Block a user