Major Refactor now allows multiple policies to be selected

This commit is contained in:
2025-09-26 14:57:12 -04:00
parent 74b3f7b2e1
commit 0a26ed71c1
8 changed files with 777 additions and 1133 deletions
+13 -378
View File
@@ -15,15 +15,11 @@
import argparse import argparse
import dotenv import dotenv
import os import os
import re
import pandas as pd
import urllib3 import urllib3
import utils.clientfunctions import utils.clientfunctions
import utils.hashfunctions
import utils.otpfunctions import utils.otpfunctions
import utils.pathfunctions
import utils.policyfunctions import utils.policyfunctions
import utils.pretty as ct import utils.utils as ct
from utils.perstscheduler import register_function, run_once_job, recurring_job, reload_jobs, start_scheduler from utils.perstscheduler import register_function, run_once_job, recurring_job, reload_jobs, start_scheduler
@@ -48,8 +44,6 @@ policy_relationship_map ={ #Enforcement : Audit
} }
def main(): def main():
parser = argparse.ArgumentParser(description="Your script description") parser = argparse.ArgumentParser(description="Your script description")
parser.add_argument('--monitorOTP', action='store_true', help='Run in non-interactive mode') parser.add_argument('--monitorOTP', action='store_true', help='Run in non-interactive mode')
@@ -65,7 +59,7 @@ def main():
os.makedirs("OTP/HTML", exist_ok=True) os.makedirs("OTP/HTML", exist_ok=True)
os.makedirs("OTP/PARQ", exist_ok=True) os.makedirs("OTP/PARQ", exist_ok=True)
apivalidation() ct.apivalidation()
register_function("monitorOTP", utils.otpfunctions.monitorOTP) register_function("monitorOTP", utils.otpfunctions.monitorOTP)
register_function("updateAudit", utils.policyfunctions.updateAuditPoliciesFromEnforcementPolices) register_function("updateAudit", utils.policyfunctions.updateAuditPoliciesFromEnforcementPolices)
@@ -79,55 +73,15 @@ def main():
else: else:
# Interactive logic # Interactive logic
apivalidation() ct.apivalidation()
menu_main() menu_main()
def apivalidation():
match os.getenv('APIKEY'):
case '':
print(ct.colorText("Please add your API Key to the .env file", "red"))
def tryToReadCSV(csv):
try:
if not os.path.exists(csv):
print(ct.colorText(f"Error: File '{csv}' does not exist.", "red"))
return pd.DataFrame() # Return empty DataFrame if file doesn't exist
df = pd.read_csv(csv)
if df.empty:
print(ct.colorText("Error: CSV file has headers but no data rows.", "red"))
else:
print(ct.colorText(f"Data loaded successfully from {csv}", "green"))
except pd.errors.EmptyDataError:
print(ct.colorText("Notice: CSV file is completely empty (no headers, no data), falling back to empty frame", "white"))
df = pd.DataFrame() # Create an empty DataFrame as fallback
return df
def tryToReadParquet(parquet):
try:
df = pd.read_parquet(parquet)
if df.empty:
print(ct.colorText("Error: Parquet file has headers but no data rows.", "red"))
else:
print(ct.colorText(f"Data loaded successfully from {parquet}", "green"))
except pd.errors.EmptyDataError:
print(ct.colorText("Notice : Parquet file is completely empty (no headers, no data), falling back to empty frame", "white"))
df = pd.DataFrame() # Create an empty DataFrame as fallback
return df
def deduplicate_list(lst):
seen = set()
return [x for x in lst if not (x in seen or seen.add(x))]
def menu_main(): def menu_main():
while True: while True:
ct.displayIntro(); ct.displayIntro();
print(ct.colorText("1. 🖥️ - Get All Events for Single Device", "yellow")) print(ct.colorText("1. 🖥️ - Get All Events for Single Device", "yellow"))
print(ct.colorText("2. 🎫 - OTP", "yellow")) print(ct.colorText("2. 🎫 - OTP", "yellow"))
print(ct.colorText("3. ⏱️ - Find Unexcluded from 24hr Execution", "yellow")) print(ct.colorText("3. ⏱️ - Placeholder", "yellow"))
print(ct.colorText("4. 🔒 - Prepare Policy For Enforcement", "yellow")) print(ct.colorText("4. 🔒 - Prepare Policy For Enforcement", "yellow"))
print(ct.colorText("5. 🔄 - Update Audit Policies from Enforcement Policies", "yellow")) print(ct.colorText("5. 🔄 - Update Audit Policies from Enforcement Policies", "yellow"))
print(ct.colorText("6 🔍 - Device Search", "yellow")) print(ct.colorText("6 🔍 - Device Search", "yellow"))
@@ -139,14 +93,19 @@ def menu_main():
elif choice == "2": elif choice == "2":
menu_otp() menu_otp()
elif choice == "3": elif choice == "3":
find_unexcluded() pass
elif choice == "4": elif choice == "4":
menu_prepare_to_enforce() utils.policyfunctions.prepare_to_enforce(url, bad_publisher_list, pups, badpathparts, threat_tolerance_constant, path_exclusion_constant, min_files_for_path)
elif choice == "5": elif choice == "5":
utils.policyfunctions.updateAuditPoliciesFromEnforcementPolices(url, policy_relationship_map) utils.policyfunctions.updateAuditPoliciesFromEnforcementPolices(url, policy_relationship_map)
elif choice == "6": elif choice == "6":
device_input_str = utils.clientfunctions.promptForDevices() devicelist = utils.clientfunctions.promptForDevices()
utils.clientfunctions.findAgents(url, device_input_str) utils.clientfunctions.findAgents(url, devicelist, False)
elif choice == "7":
devicelist = utils.clientfunctions.promptForDevices()
device_df = utils.clientfunctions.findAgents(url, devicelist, True)
utils.clientfunctions.returnToEnforcement(url, device_df, policy_relationship_map, bad_publisher_list, pups, badpathparts, threat_tolerance_constant, path_exclusion_constant, min_files_for_path)
elif choice == "Q": elif choice == "Q":
break break
else: else:
@@ -172,330 +131,6 @@ def menu_otp():
else: else:
print("Invalid choice. Please try again.") print("Invalid choice. Please try again.")
def find_unexcluded():
while True:
print("\n--- Follow Steps Sequentially, Files will land in directory named 'exclusions'")
print("1. Pull last 24-72 hours execution for all ATPolicys")
print("2. Generate Paths")
print("3. Merge")
print("4. Pull existing paths")
print("5. Divide By Excluded or not excluded by path")
print("Q. Exit")
if not os.path.exists("exclusions"): os.makedirs("exclusions")
choice = input("Enter your choice: ")
if choice == "1":
# Get the AT policies dictionary
atpolicies = utils.policyfunctions.listATPolicies(url)
# List to hold each policy's DataFrame
all_dfs = []
while True:
try:
history_days = int(input("Enter the how many days in of history do you want to pull - select a number between 1 and 150: "))
if 1 <= history_days <= 3:
break
else:
print("Invalid input. Please enter a number between 1 and 3.")
except ValueError:
print("Invalid input. Please enter a valid integer.")
# Loop through each policy name
for policy_name in atpolicies:
try:
# Get the policy info DataFrame
df = utils.policyfunctions.getPolicyInfo(url, policy_name, [1, 2, 6, 7], history_days, False)
# Add a column to indicate the policy name
df['PolicyName'] = policy_name
# Append to the list
all_dfs.append(df)
except Exception as e:
print(f"Error processing policy '{policy_name}': {e}")
# Combine all DataFrames into one
if all_dfs:
combined_df = pd.concat(all_dfs, ignore_index=True)
print("✅ Combined DataFrame created.")
else:
combined_df = pd.DataFrame()
print("⚠️ No data was retrieved.")
combined_df.to_csv("exclusions\\allATPolicyExecs.csv", index=False)
elif choice == "2":
utils.pathfunctions.allATpaths(url,pups, bad_publisher_list, badpathparts, threat_tolerance_constant, path_exclusion_constant, min_files_for_path)
elif choice == "3":
utils.pathfunctions.mergeTesting()
elif choice == "4":
# Get the AT policies dictionary
atpolicies = utils.policyfunctions.listATPolicies(url)
# List to hold each policy's path data
all_paths = []
# Loop through each policy name and group ID
for policy_name, group_id in atpolicies.items():
try:
# Get the list of paths using the group ID
paths = utils.pathfunctions.listPaths(url, group_id)
# Ensure each path is a string and append properly
for path in paths:
if isinstance(path, str):
all_paths.append({
'PolicyName': policy_name,
'GroupID': group_id,
'Path': path
})
except Exception as e:
print(f"Error retrieving paths for policy '{policy_name}': {e}")
# Convert to DataFrame
if all_paths:
paths_df = pd.DataFrame(all_paths)
print(paths_df.head()) # Optional: preview first few rows
print("✅ Paths DataFrame created.")
else:
paths_df = pd.DataFrame()
print("⚠️ No paths were retrieved.")
# Save to CSV
paths_df.to_csv("exclusions\\paths.csv", index=False)
elif choice == "5":
# Load and clean data
filenames_df = pd.read_csv('exclusions\\merged_output.csv') # Contains 'PolicyName' and 'filename'
exclusions_df = pd.read_csv('exclusions\\paths.csv') # Contains 'PolicyName' and 'Path'
# Clean and normalize columns
filenames_df['filename'] = filenames_df['filename'].fillna('').astype(str).str.strip()
filenames_df['PolicyName'] = filenames_df['PolicyName'].fillna('').astype(str).str.strip()
exclusions_df['Path'] = exclusions_df['Path'].fillna('').astype(str).str.strip()
exclusions_df['PolicyName'] = exclusions_df['PolicyName'].fillna('').astype(str).str.strip()
# Decode escaped backslashes in exclusion patterns
exclusions_df['Path'] = exclusions_df['Path'].apply(lambda p: p.encode('utf-8').decode('unicode_escape'))
# Build exclusion map: {PolicyName: [compiled regex patterns]}
exclusion_map = {}
for _, row in exclusions_df.iterrows():
policy = row['PolicyName']
raw_pattern = row['Path']
try:
regex = utils.pathfunctions.wildcardRegex(raw_pattern)
print(f"[EXCLUSION MAP] Policy: {policy}, Pattern: {raw_pattern} → Regex: {regex.pattern}")
exclusion_map.setdefault(policy, []).append(regex)
except Exception as e:
print(f"[ERROR] Failed to compile pattern for Policy: {policy}, Path: {raw_pattern}, Error: {e}")
# Function to check if a filename matches any exclusion pattern for its PolicyName
def is_excluded(row):
policy = row['PolicyName'].strip()
path = os.path.normpath(row['filename'].strip())
patterns = exclusion_map.get(policy, [])
for r in patterns:
if r.match(path):
print(f"[MATCH] {policy}: {path} matches {r.pattern}")
return True
print(f"[NO MATCH] {policy}: {path}")
return False
# Apply matching
excluded = filenames_df[filenames_df.apply(is_excluded, axis=1)]
not_excluded = filenames_df[~filenames_df.apply(is_excluded, axis=1)]
# Desired column order
column_order = [
'PolicyName', 'filename', 'longestcfp',
'pprocess', 'gprocess', 'sha256', 'publisher', 'description', 'productname','commandline', 'middle', 'filename_only',
'file_extension', 'unique_sha256_count', 'hostname', 'username', 'productversion',
'reputation_lastseen', 'reputation_scannermatch', 'reputation_scannercount',
'reputation_status', 'reputation_threatlevel', 'reputation_threatname', 'reputation_timestamp'
]
# Reorder columns (ignore missing ones)
excluded = excluded[[col for col in column_order if col in excluded.columns]]
not_excluded = not_excluded[[col for col in column_order if col in not_excluded.columns]]
# Save results
excluded.to_csv('exclusions\\excluded_filenames.csv', index=False)
not_excluded.to_csv('exclusions\\non_excluded_filenames.csv', index=False)
elif choice == "Q":
break
else:
print("Invalid choice. Please try again.")
def menu_prepare_to_enforce():
first_policy = " "
second_policy = " "
destination_name = " "
destination_id = " "
allowlist_parent_name = " "
allowlist_parent_id = " "
allowlist_child_name = " "
allowlist_child_id = " "
history_days = 1
#If the directorys where we're going to store our output dont exist, make them.
if not os.path.exists("parquet"): os.makedirs("parquet")
if not os.path.exists("needs_approved"): os.makedirs("needs_approved")
if not os.path.exists("approved"): os.makedirs("approved")
if not os.path.exists("preflight"): os.makedirs("preflight")
while True:
ct.printEnforceChecklist(first_policy, second_policy, allowlist_child_name, allowlist_parent_name, destination_name)
choice = input(ct.colorText("\nEnter your choice: ", "white"))
if choice == "1":
choice, policynames, policyid = utils.policyfunctions.listPolicies(url)
first_policy = policynames[choice]
while True:
answer = input(ct.colorText(f"{"Do you want to load a second policy?"} (yes/no): ", "white").strip().lower())
if answer in ("yes", "y"):
choice, policynames, policyid = utils.policyfunctions.listPolicies(url)
second_policy = policynames[choice]
break
elif answer in ("no", "n"):
second_policy = first_policy
break
else:
print(ct.colorText("Please answer with 'yes' or 'no'.", "red"))
elif choice == "2":
while True:
try:
history_days = int(input("Enter the how many days in of history do you want to pull - select a number between 1 and 150: "))
if 1 <= history_days <= 150:
break
else:
print("Invalid input. Please enter a number between 1 and 150.")
except ValueError:
print("Invalid input. Please enter a valid integer.")
if not os.path.exists(f"parquet\\execution_history_{first_policy}.parquet"):
utils.policyfunctions.getPolicyInfo(url, first_policy, [1,2,6,7], history_days)
if not os.path.exists(f"parquet\\execution_history_{second_policy}.parquet"):
utils.policyfunctions.getPolicyInfo(url, second_policy, [1,2,6,7], history_days)
if not os.path.exists(f"parquet\\combined_hashlist_{first_policy}_{second_policy}.parquet"):
utils.hashfunctions.combineHashes(url, first_policy, second_policy)
if not os.path.exists(f"parquet\\hashes_rep_unknown_{first_policy}_{second_policy}.parquet") and not os.path.exists(f"parquet\\hashes_rep_good_{first_policy}_{second_policy}.parquet") and not os.path.exists(f"parquet\\hashes_rep_bad_{first_policy}_{second_policy}.parquet"):
utils.hashfunctions.categorizeHashes(
first_policy,
second_policy,
pd.read_parquet(f"parquet\\combined_hashlist_{first_policy}_{second_policy}.parquet"),
threat_tolerance_constant,
bad_publisher_list,
pups
)
if not os.path.exists(f"parquet\\condensed_executions_{first_policy}_{second_policy}.parquet"):
utils.hashfunctions.condenseExecutions(first_policy,second_policy)
if (
os.path.exists(f"parquet\\hashes_rep_unknown_{first_policy}_{second_policy}.parquet") and
os.path.exists(f"parquet\\hashes_rep_good_{first_policy}_{second_policy}.parquet") and
os.path.exists(f"parquet\\hashes_rep_bad_{first_policy}_{second_policy}.parquet") and
not os.path.exists(f"needs_approved\\hashes_rep_unknown_{first_policy}_{second_policy}.csv")
):utils.hashfunctions.divideSortedHashExecutions(first_policy,second_policy,pups)
elif choice == "3":
if os.path.exists(f"approved\\hashes_rep_unknown_{first_policy}_{second_policy}.csv") and os.path.exists(f"approved\\hashes_rep_good_{first_policy}_{second_policy}.csv"):
utils.pathfunctions.generatePathReview(first_policy, second_policy, badpathparts,path_exclusion_constant, min_files_for_path)
utils.hashfunctions.generatePublist(first_policy,second_policy,bad_publisher_list)
else:
print(ct.colorText(f"Please manually approve hashes prior to this step","red"))
elif choice == "4":
if os.path.exists(f"approved\\path_needs_approved_{first_policy}_{second_policy}.csv"):
if not os.path.exists(f"parquet\\final_hash_approvals_{first_policy}_{second_policy}.parquet") and not os.path.exists(f"parquet\\final_path_exclusions_{first_policy}_{second_policy}.parquet"):
utils.hashfunctions.generatePreflights(first_policy, second_policy)
elif choice == "5":
print(ct.colorText(f"Please choose destination_name Policy for Path Exclusions","white"))
choice, policynames, policyid = utils.policyfunctions.listPolicies(url)
#print(allowlist_parent_tuple)
destination_name = policynames[choice]
destination_id = policyid[choice]
print(ct.colorText(f"Please choose Parent Allowlist for Known Hashes","white"))
choice, allowlists,allowid = utils.policyfunctions.listAllowlists(url)
#print(allowlist_parent_tuple)
allowlist_parent_name = allowlists[choice]
allowlist_parent_id = allowid[choice]
print(ct.colorText(f"Please choose Child Allowlist for Less-Known Hashes","white"))
choice, allowlists, allowid = utils.policyfunctions.listAllowlists(url)
#print(allowlist_child_tuple)
allowlist_child_name = allowlists[choice]
allowlist_child_id = allowid[choice]
elif choice == "6":
if os.path.exists(f"preflight\\final_path_exclusions_{first_policy}_{second_policy}.html") and os.path.exists(f"preflight\\final_hash_approvals_{first_policy}_{second_policy}.html") and allowlist_parent_name != " " and allowlist_child_name != " " and destination_name != " ":
utils.policyfunctions.sendToPolicyTest(
url,
first_policy,
second_policy,
destination_name,
destination_id,
allowlist_parent_name,
allowlist_parent_id,
allowlist_child_name,
allowlist_child_id
)
elif choice == "7":
if os.path.exists(f"preflight\\final_path_exclusions_{first_policy}_{second_policy}.html") and os.path.exists(f"preflight\\final_hash_approvals_{first_policy}_{second_policy}.html") and allowlist_parent_name != " " and allowlist_child_name != " " and destination_name != " ":
utils.policyfunctions.sendToPolicy(
url,
first_policy,
second_policy,
destination_name,
destination_id,
allowlist_parent_name,
allowlist_parent_id,
allowlist_child_name,
allowlist_child_id
)
elif choice == "R":
utils.pathfunctions.clean_folders_enforcement_prep()
elif choice == "Q":
break
else:
print(ct.colorText("Invalid choice. Please try again.", "red"))
if __name__ == "__main__": if __name__ == "__main__":
+86 -2
View File
@@ -14,7 +14,10 @@
# along with this program. If not, see <https://www.gnu.org/licenses/>. # along with this program. If not, see <https://www.gnu.org/licenses/>.
#Local Imports #Local Imports
import utils.pretty as ct import utils.hashfunctions as hashf
import utils.utils as ct
import utils.pathfunctions as pathf
import utils.policyfunctions as policyf
#Standard Libary Imports: #Standard Libary Imports:
import datetime import datetime
@@ -140,6 +143,7 @@ def devicehistory(url, outputjson: bool):
choice = input(ct.colorText("\nSelect Date Range: ", "white")) choice = input(ct.colorText("\nSelect Date Range: ", "white"))
today = datetime.date.today() today = datetime.date.today()
today = today.strftime("%Y-%m-%d") today = today.strftime("%Y-%m-%d")
date_selected = " "
if choice == '1': if choice == '1':
date_selected = today date_selected = today
elif choice == '2': elif choice == '2':
@@ -218,7 +222,7 @@ def findAllAgents(url):
data['status'] = data['status'].map(status_map) data['status'] = data['status'].map(status_map)
return(data) return(data)
def findAgents(url, device_input_str): def findAgents(url, device_input_str, return_dataframe):
os.makedirs("device_search", exist_ok=True) os.makedirs("device_search", exist_ok=True)
df = findAllAgents(url) df = findAllAgents(url)
@@ -240,6 +244,7 @@ def findAgents(url, device_input_str):
matched_df.to_csv(f"device_search\\{filename}", index=False) matched_df.to_csv(f"device_search\\{filename}", index=False)
print(ct.colorText(f"\n✅ Matched devices exported to: device_search\\{filename}","green")) print(ct.colorText(f"\n✅ Matched devices exported to: device_search\\{filename}","green"))
if return_dataframe : return matched_df
def promptForDevices(): def promptForDevices():
@@ -266,3 +271,82 @@ def promptForDevices():
return device_input_str return device_input_str
def returnToEnforcement(url, device_df, policy_relationship_map, bad_publisher_list, pups, badpathparts, threat_tolerance_constant, path_exclusion_constant, min_files_for_path):
policylist = sorted(device_df['policy_name'].unique().tolist())
type = [1, 2, 6, 7]
parq_base_dir = "prepare_policy\\parquet\\"
appr_base_dir = "prepare_policy\\approved\\"
needappr_base_dir = "prepare_policy\\needs_approved"
pflight_base_dir = "prepare_policy\\preflight"
#If the directorys where we're going to store our output dont exist, make them.
os.makedirs(parq_base_dir, exist_ok=True)
os.makedirs(needappr_base_dir, exist_ok=True)
os.makedirs(appr_base_dir, exist_ok=True)
os.makedirs(pflight_base_dir, exist_ok=True)
while True:
#ct.printDeviceEnforceChecklist()
choice = input(ct.colorText("\nEnter your choice: ", "white"))
if choice == "1":
policyf.buildExecHistory(url,
policylist,
parq_base_dir,
needappr_base_dir,
type,
threat_tolerance_constant,
bad_publisher_list,
pups,
)
elif choice == "2":
pathf.generateApprovables(needappr_base_dir, appr_base_dir, parq_base_dir, badpathparts, bad_publisher_list, path_exclusion_constant, min_files_for_path, True)
elif choice == "3":
policyf.savePreFlights(appr_base_dir, parq_base_dir, pflight_base_dir)
"""
elif choice == "4":
if os.path.exists(f"preflight\\final_path_exclusions.html") and os.path.exists(f"preflight\\final_hash_approvals.html") and allowlist_parent_name != " " and allowlist_child_name != " " and destination_name != " ":
sendToPolicyTest(
url,
first_policy,
second_policy,
destination_name,
destination_id,
allowlist_parent_name,
allowlist_parent_id,
allowlist_child_name,
allowlist_child_id
)
elif choice == "5":
if os.path.exists(f"preflight\\final_path_exclusions.html") and os.path.exists(f"preflight\\final_hash_approvals.html") and allowlist_parent_name != " " and allowlist_child_name != " " and destination_name != " ":
sendToPolicy(
url,
first_policy,
second_policy,
destination_name,
destination_id,
allowlist_parent_name,
allowlist_parent_id,
allowlist_child_name,
allowlist_child_id
)
elif choice == "R":
pathf.clean_folders(enforcement_prep)
elif choice == "Q":
break
else:
print(ct.colorText("Invalid choice. Please try again.", "red"))
"""
+68 -175
View File
@@ -14,10 +14,8 @@
# along with this program. If not, see <https://www.gnu.org/licenses/>. # along with this program. If not, see <https://www.gnu.org/licenses/>.
#Local Imports #Local Imports
import utils.hashfunctions as hashf
import utils.pathfunctions as pathf import utils.pathfunctions as pathf
import utils.pretty as ct import utils.utils as ct
from AirlockTools import tryToReadCSV
#Standard Libary Imports: #Standard Libary Imports:
import gc import gc
@@ -99,7 +97,7 @@ def augmentAggregatedHashes(url, agg_df: pd.DataFrame) -> pd.DataFrame:
df = agg_df.merge(df_api, on="sha256", how="left") df = agg_df.merge(df_api, on="sha256", how="left")
# Only include columns that exist to avoid KeyErrors # Only include columns that exist to avoid KeyErrors
expected_columns = ['sha256', 'filename_x', 'description', 'productname', 'productversion', expected_columns = ['policy','sha256', 'filename_x', 'description', 'productname', 'productversion',
'publisher_y', 'publisher_x', 'netdomain', 'hostname', 'username', 'publisher_y', 'publisher_x', 'netdomain', 'hostname', 'username',
'reputation_lastseen', 'reputation_scannermatch', 'reputation_scannercount', 'reputation_lastseen', 'reputation_scannermatch', 'reputation_scannercount',
'reputation_status', 'reputation_threatlevel', 'reputation_threatname', 'reputation_status', 'reputation_threatlevel', 'reputation_threatname',
@@ -110,7 +108,7 @@ def augmentAggregatedHashes(url, agg_df: pd.DataFrame) -> pd.DataFrame:
return aug_df return aug_df
def categorizeHashes(first_policy, second_policy, df: pd.DataFrame, threat_tolerance: int, untrusted_publishers, pups: list): def categorizeHashes(df: pd.DataFrame, threat_tolerance: int, untrusted_publishers, pups: list) -> tuple[pd.DataFrame, pd.DataFrame, pd.DataFrame]:
if untrusted_publishers is None: untrusted_publishers = [] if untrusted_publishers is None: untrusted_publishers = []
if pups is None: pups = [] if pups is None: pups = []
@@ -149,122 +147,56 @@ def categorizeHashes(first_policy, second_policy, df: pd.DataFrame, threat_toler
needsreview_df = df[mask_needsreview] needsreview_df = df[mask_needsreview]
approved_df = df[mask_approved] approved_df = df[mask_approved]
unapproved_df = df[~(mask_needsreview | mask_approved)] unapproved_df = df[~(mask_needsreview | mask_approved)]
return needsreview_df, approved_df, unapproved_df
needsreview_df.to_parquet(f"parquet\\hashes_rep_unknown_{first_policy}_{second_policy}.parquet", index=False) def combineHashAndHist(hash_path, condensed_path):
approved_df.to_parquet(f"parquet\\hashes_rep_good_{first_policy}_{second_policy}.parquet", index=False) # Load both datasets
unapproved_df.to_parquet(f"parquet\\hashes_rep_bad_{first_policy}_{second_policy}.parquet", index=False) condensed_combo = pd.read_parquet(condensed_path)
df = pd.read_parquet(hash_path)
del needsreview_df # Merge on sha256
del approved_df
del unapproved_df
gc.collect()
def explode_and_deduplicate(df):
df['sha256'] = df['sha256'].str.split(',')
df = df.explode('sha256')
return df.drop_duplicates().reset_index(drop=True)
def clean_sha256(df, column='sha256'):
"""Discard quotes, brackets, and whitespace from sha256 values."""
df[column] = df[column].astype(str).str.strip("'[]\" ")
return df
def destinationHashes(
df_approved_paths: pd.DataFrame,
df_approved_hashes: pd.DataFrame,
df_hashes_auto_approved: pd.DataFrame,
df_hashes_manually_approved: pd.DataFrame,
):
# Deduplicate and explode all input DataFrames
df_approved_paths = explode_and_deduplicate(df_approved_paths)
df_approved_hashes = explode_and_deduplicate(df_approved_hashes)
df_hashes_auto_approved = explode_and_deduplicate(df_hashes_auto_approved)
df_hashes_manually_approved = explode_and_deduplicate(df_hashes_manually_approved)
# Clean sha256 values in all relevant DataFrames
df_approved_hashes = clean_sha256(df_approved_hashes)
df_hashes_auto_approved = clean_sha256(df_hashes_auto_approved)
df_hashes_manually_approved = clean_sha256(df_hashes_manually_approved)
# Create sets for faster lookup
auto_approved_sha256 = set(df_hashes_auto_approved['sha256'].values)
manually_approved_sha256 = set(df_hashes_manually_approved['sha256'].values)
# Debug: Print unmatched hashes
unmatched = set(df_approved_hashes['sha256']) - (auto_approved_sha256 | manually_approved_sha256)
print(f"Unmatched hashes: {unmatched}")
# Process df_approved_paths
df_paths = df_approved_paths.assign(destination='Path Exclusion')
df_paths = df_paths[['sha256', 'description', 'destination', 'grouped_directory', 'filename']]
# Process df_approved_hashes
df_hashes = df_approved_hashes.copy()
df_hashes['destination'] = df_hashes['sha256'].apply(
lambda x: 'Parent Policy Baseline' if x in auto_approved_sha256
else ('Child Policy Allowlist' if x in manually_approved_sha256 else None)
)
df_hashes = df_hashes.dropna(subset=['destination'])
df_hashes = df_hashes.assign(grouped_directory=None)
# Use 'filename_x' only if it exists, otherwise fallback to 'filename'
filename_col = 'filename_x' if 'filename_x' in df_hashes.columns else 'filename'
selected_cols = ['sha256', 'description', 'destination', 'grouped_directory', filename_col]
df_hashes = df_hashes[selected_cols]
# Concatenate results
df_hashdestination = pd.concat([df_paths, df_hashes], ignore_index=True)
return df_hashdestination
def combineHashAndHist(path, first_policy, second_policy):
condensed_combo = pd.read_parquet(f"parquet\\condensed_executions_{first_policy}_{second_policy}.parquet")
df = pd.read_parquet(path)
#Pull hash info for the entries in the needs approval table
df = pd.merge(condensed_combo, df, on='sha256', how='inner') df = pd.merge(condensed_combo, df, on='sha256', how='inner')
df.to_csv("testing4.csv")
#Rename Publisher, Keep and reorder columns we want # Rename and reorder columns
df = df.rename(columns={'publisher_x': 'publisher'}) df = df.rename(columns={'publisher_x': 'publisher'})
df = df[['sha256', 'publisher', 'description', 'filename', 'hostname', 'username', 'productname', 'productversion','reputation_lastseen', 'reputation_scannermatch', 'reputation_scannercount','reputation_status', 'reputation_threatlevel', 'reputation_threatname','reputation_timestamp', 'pprocess', 'gprocess', 'commandline']] df = df.rename(columns={'policy_x': 'policy'})
df = df[['policy','sha256', 'publisher', 'description', 'filename', 'hostname', 'username',
'productname', 'productversion', 'reputation_lastseen', 'reputation_scannermatch',
'reputation_scannercount', 'reputation_status', 'reputation_threatlevel',
'reputation_threatname', 'reputation_timestamp', 'pprocess', 'gprocess', 'commandline']]
df = df.sort_values(by='filename') df = df.sort_values(by='filename')
df.to_parquet(path, index=False) # Overwrite the original hash file
df.to_parquet(hash_path, index=False)
# Cleanup
del df del df
del condensed_combo del condensed_combo
gc.collect() gc.collect()
def combineHashes(url, first_policy, second_policy): def combineHashes(url, parquet_files) -> pd.DataFrame:
combined_hashes = pd.DataFrame(columns=['sha256', 'publisher']) combined_hashes = pd.DataFrame()
hashes = [] hashes = []
try:
hash1 = pd.read_parquet(f"parquet\\execution_history_{first_policy}.parquet", columns=['sha256', 'publisher'])
pathf.inspect_parquet(f"parquet\\execution_history_{first_policy}.parquet")
if not hash1.empty:
hashes.append(hash1)
else:
print("⚠️ First dataframe is empty.")
except Exception as e:
print(f"❌ Error reading first Parquet file: {e}")
for file_path in parquet_files:
try: try:
hash2 = pd.read_parquet(f"parquet\\execution_history_{second_policy}.parquet", columns=['sha256', 'publisher']) hash_df = pd.read_parquet(file_path)
pathf.inspect_parquet(f"parquet\\execution_history_{second_policy}.parquet") pathf.inspect_parquet(file_path)
if not hash2.empty:
hashes.append(hash2) if not hash_df.empty:
hashes.append(hash_df)
else: else:
print("⚠️ Second dataframe is empty.") print(f"⚠️ Dataframe is empty: {file_path}")
except Exception as e: except Exception as e:
print(f"❌ Error reading second Parquet file: {e}") print(f"❌ Error reading Parquet file '{file_path}': {e}")
if hashes: if hashes:
combined_hashes = pd.concat(hashes, ignore_index=True) combined_hashes = pd.concat(hashes, ignore_index=True)
print(f"✅ Combined {len(combined_hashes)} hashes.") print(f"✅ Combined {len(combined_hashes)} hashes from {len(hashes)} files.")
else: else:
print("⚠️ No valid dataframes to combine.") print("⚠️ No valid dataframes to combine.")
combined_hashes = combined_hashes.drop_duplicates(subset=['sha256']) combined_hashes = combined_hashes.drop_duplicates(subset=['sha256'])
augmented_combo = hashf.augmentAggregatedHashes(url, combined_hashes) augmented_combo = augmentAggregatedHashes(url, combined_hashes)
numeric_reputation_cols = [ numeric_reputation_cols = [
'reputation_scannermatch', 'reputation_scannermatch',
@@ -282,62 +214,45 @@ def combineHashes(url, first_policy, second_policy):
'reputation_status', 'reputation_threatlevel', 'reputation_threatname', 'reputation_status', 'reputation_threatlevel', 'reputation_threatname',
'reputation_timestamp']] 'reputation_timestamp']]
augmented_combo = augmented_combo.sort_values(by=['publisher', 'description', 'productname']) augmented_combo = augmented_combo.sort_values(by=['publisher', 'description', 'productname'])
augmented_combo.to_parquet(f"parquet\\combined_hashlist_{first_policy}_{second_policy}.parquet", index=False)
del combined_hashes del combined_hashes
del augmented_combo
gc.collect() gc.collect()
print(ct.colorText("Hash reputation info added to dataframe", "green")) print(ct.colorText("Hash reputation info added to dataframe", "green"))
return augmented_combo
def condenseExecutions(first_policy,second_policy): def condenseExecutions(parquet_paths):
exe1 = pd.DataFrame() combined_df = pd.DataFrame()
exe2 = pd.DataFrame() valid_files = []
condensed_combo = pd.DataFrame()
for file_path in parquet_paths:
try: try:
exe1 = pd.read_parquet(f"parquet\\execution_history_{first_policy}.parquet") df = pd.read_parquet(file_path)
pathf.inspect_parquet(f"parquet\\execution_history_{first_policy}.parquet") # Optional: pathf.inspect_parquet(file_path)
if not exe1.empty: if not df.empty:
print() combined_df = pd.concat([combined_df, df], ignore_index=True)
valid_files.append(file_path)
print(f"✅ Loaded {len(df)} rows from {file_path}")
else: else:
print("⚠️ First dataframe is empty.") print(f"⚠️ DataFrame from '{file_path}' is empty.")
except Exception as e: except Exception as e:
print(f"❌ Error reading first Parquet file: {e}") print(f"❌ Error reading Parquet file '{file_path}': {e}")
try: if not combined_df.empty:
exe2 = pd.read_parquet(f"parquet\\execution_history_{second_policy}.parquet") print(f"✅ Combined {len(combined_df)} rows from {len(valid_files)} files.")
pathf.inspect_parquet(f"parquet\\execution_history_{second_policy}.parquet")
if not exe2.empty:
print()
else:
print("⚠️ Second dataframe is empty.")
except Exception as e:
print(f"❌ Error reading second Parquet file: {e}")
if not exe1.empty and not exe2.empty:
condensed_combo = pd.concat([exe1, exe2], ignore_index=True)
print(f"✅ Combined {len(condensed_combo)} hashes.")
elif exe1.empty:
condensed_combo = exe2
elif exe2.empty:
condensed_combo = exe1
else: else:
print("⚠️ No valid dataframes to combine.") print("⚠️ No valid dataframes to combine.")
condensed_combo.to_parquet(f"parquet\\condensed_executions_{first_policy}_{second_policy}.parquet", index=False) return combined_df
del condensed_combo
gc.collect()
def divideSortedHashExecutions(first_policy,second_policy, pups): def divideSortedHashExecutions(unknown_parq, good_parq, bad_parq, condensed_parq, pups) -> tuple[pd.DataFrame, pd.DataFrame, pd.DataFrame]:
# Run combineHashAndHist on each file
combineHashAndHist(unknown_parq, condensed_parq)
combineHashAndHist(good_parq, condensed_parq)
combineHashAndHist(bad_parq, condensed_parq)
combineHashAndHist(f"parquet\\hashes_rep_unknown_{first_policy}_{second_policy}.parquet", first_policy, second_policy) # Load data
combineHashAndHist(f"parquet\\hashes_rep_good_{first_policy}_{second_policy}.parquet", first_policy, second_policy) unknown = pd.read_parquet(unknown_parq)
combineHashAndHist(f"parquet\\hashes_rep_bad_{first_policy}_{second_policy}.parquet", first_policy, second_policy) good = pd.read_parquet(good_parq)
bad = pd.read_parquet(bad_parq)
unknown = pd.read_parquet(f"parquet\\hashes_rep_unknown_{first_policy}_{second_policy}.parquet")
good = pd.read_parquet(f"parquet\\hashes_rep_good_{first_policy}_{second_policy}.parquet")
bad = pd.read_parquet(f"parquet\\hashes_rep_bad_{first_policy}_{second_policy}.parquet")
# Build regex pattern once # Build regex pattern once
pattern = pathf.regulator(pups) pattern = pathf.regulator(pups)
@@ -353,52 +268,30 @@ def divideSortedHashExecutions(first_policy,second_policy, pups):
unknown = unknown[~unknown["filename"].str.contains(pattern, na=False)] unknown = unknown[~unknown["filename"].str.contains(pattern, na=False)]
good = good[~good["filename"].str.contains(pattern, na=False)] good = good[~good["filename"].str.contains(pattern, na=False)]
unknown.to_csv(f"needs_approved\\hashes_rep_unknown_{first_policy}_{second_policy}.csv",index=False) return unknown, good, bad
good.to_csv(f"needs_approved\\hashes_rep_good_{first_policy}_{second_policy}.csv",index=False)
bad.to_csv(f"needs_approved\\hashes_rep_bad_{first_policy}_{second_policy}.csv",index=False)
ct.style_dataframe_dark(unknown, f"needs_approved\\hashes_rep_unknown_{first_policy}_{second_policy}.html") def generatePreflights(hashes, primary_path, secondary_path):
ct.style_dataframe_dark(good, f"needs_approved\\hashes_rep_good_{first_policy}_{second_policy}.html") all_hashes = pd.read_parquet(hashes)
ct.style_dataframe_dark(bad, f"needs_approved\\hashes_rep_bad_{first_policy}_{second_policy}.html")
def generatePreflights(first_policy, second_policy): primarypathexclusions = ct.tryToReadCSV(primary_path)
allhashes = pd.read_parquet(f"parquet\\all_approved_hashes_{first_policy}_{second_policy}.parquet") secondarypathexclusions = ct.tryToReadCSV(secondary_path)
primarypathexclusions = tryToReadCSV(f"approved\\path_needs_approved_{first_policy}_{second_policy}.csv")
secondarypathexclusions = tryToReadCSV(f"approved\\secondary_paths_{first_policy}_{second_policy}.csv")
pathexclusions = pd.concat([primarypathexclusions, secondarypathexclusions], ignore_index=True) pathexclusions = pd.concat([primarypathexclusions, secondarypathexclusions], ignore_index=True)
publishers = tryToReadCSV(f"approved\\publishers_{first_policy}_{second_policy}.csv") allowbyhash = all_hashes[~all_hashes['sha256'].isin(pathexclusions['sha256'])]
publishers.to_parquet(f"parquet\\publishers_{first_policy}_{second_policy}.parquet", index=False)
allowbyhash = allhashes[~allhashes['sha256'].isin(pathexclusions['sha256'])]
allowbyhash.to_parquet(f"parquet\\final_hash_approvals_{first_policy}_{second_policy}.parquet", index=False)
allowbyhash.sort_values(by=["filename"]) allowbyhash.sort_values(by=["filename"])
ct.style_dataframe_dark(allowbyhash, f"preflight\\final_hash_approvals_{first_policy}_{second_policy}.html") return pathexclusions, allowbyhash
ct.style_dataframe_dark(pathexclusions, f"preflight\\final_path_exclusions_{first_policy}_{second_policy}.html")
ct.style_dataframe_dark(publishers, f"preflight\\publishers_{first_policy}_{second_policy}.html")
del allowbyhash def generatePublist(all_hashes, bad_publisher_list):
del pathexclusions all_approved_hashes = ct.tryToReadParquet(all_hashes)
gc.collect()
def generatePublist(first_policy,second_policy,bad_publisher_list):
try:
publist = pd.read_parquet(f"parquet\\all_approved_hashes_{first_policy}_{second_policy}.parquet", columns=['publisher'])
except Exception as e:
print(f"Error reading parquet file: {e}")
publist = pd.DataFrame()
#Drop all not signed, only keep unique values #Drop all not signed, only keep unique values
publist = publist[publist['publisher'] != "Not Signed"].drop_duplicates(subset='publisher') publist = all_approved_hashes[all_approved_hashes['publisher'] != "Not Signed"].drop_duplicates(subset='publisher')
#Remove Bad publisher if somehow they made it this far #Remove Bad publisher if somehow they made it this far
pattern = pathf.regulator(bad_publisher_list) pattern = pathf.regulator(bad_publisher_list)
publist = publist[~publist["publisher"].str.contains(pattern, na=False)] publist = publist[~publist["publisher"].str.contains(pattern, na=False)]
publist.to_csv(f"needs_approved\\publishers_{first_policy}_{second_policy}.csv", index=False) return publist
+1 -1
View File
@@ -17,7 +17,7 @@
import utils.clientfunctions as clientf import utils.clientfunctions as clientf
import utils.pathfunctions as pathf import utils.pathfunctions as pathf
import utils.policyfunctions as policyf import utils.policyfunctions as policyf
import utils.pretty as ct import utils.utils as ct
from utils.perstscheduler import register_function, run_once_job, recurring_job, reload_jobs, start_scheduler, find_and_prioritize_jobs_by_pid from utils.perstscheduler import register_function, run_once_job, recurring_job, reload_jobs, start_scheduler, find_and_prioritize_jobs_by_pid
#Standard Libary Imports: #Standard Libary Imports:
+71 -301
View File
@@ -16,8 +16,7 @@
#Local Imports #Local Imports
import utils.hashfunctions as hashf import utils.hashfunctions as hashf
import utils.pathfunctions as pathf import utils.pathfunctions as pathf
import utils.pretty as ct import utils.utils as ct
from AirlockTools import tryToReadCSV
#Standard Libary Imports: #Standard Libary Imports:
import ast import ast
@@ -77,51 +76,6 @@ def split_filepaths_grouped(df, col="filename", group_parts=4, min_parts=4):
return pd.DataFrame(new_rows).drop(columns=["group_key"]) return pd.DataFrame(new_rows).drop(columns=["group_key"])
def mask_from_csv(df, csv_path, filepath_col):
"""
Reads reviewed CSV of groups, keeps only files in approved groups.
"""
review_df = pd.read_csv(csv_path)
def parse_paths(val):
if isinstance(val, str):
try:
# Try to parse as a list
parsed = ast.literal_eval(val)
# If it's not a list, wrap it
return parsed if isinstance(parsed, list) else [parsed]
except (ValueError, SyntaxError):
# If parsing fails, treat it as a single path
return [val]
return [val]
review_df[filepath_col] = review_df[filepath_col].apply(parse_paths)
# Flatten all approved file paths into a set for masking
approved_files = set()
for paths in review_df[filepath_col]:
approved_files.update(paths)
# Keep only rows in df that are in approved_files
masked_df = df[df[filepath_col].isin(approved_files)].copy()
remainder = df[~df[filepath_col].isin(approved_files)].copy()
return remainder
def filter_and_drop(approved, eligiblepaths, min_hashes):
"""
Filters eligiblepaths to rows where all hashes are in approved,
then drops rows with fewer than min_hashes hashes.
"""
approved_hashes = set(approved['sha256'])
def all_hashes_approved(row):
return all(h in approved_hashes for h in row['sha256'])
filtered = eligiblepaths[eligiblepaths.apply(all_hashes_approved, axis=1)]
filtered = filtered[filtered['sha256'].apply(len) >= min_hashes]
return filtered
def inspect_parquet(path): def inspect_parquet(path):
try: try:
df = pd.read_parquet(path) df = pd.read_parquet(path)
@@ -133,7 +87,6 @@ def inspect_parquet(path):
print(f"❌ Error reading {path}: {e}") print(f"❌ Error reading {path}: {e}")
return pd.DataFrame() return pd.DataFrame()
def regulator(paths, case_insensitive=True): def regulator(paths, case_insensitive=True):
""" """
Build a regex pattern that matches any of the given Windows path fragments. Build a regex pattern that matches any of the given Windows path fragments.
@@ -145,269 +98,54 @@ def regulator(paths, case_insensitive=True):
print(f"Regulator is providing: {pattern}") print(f"Regulator is providing: {pattern}")
return pattern return pattern
def calculatePath(approved_hashes, badpathparts, path_exclusion_constant, min_files_for_path, split):
def generatePathReview(first_policy, second_policy, badpathparts, path_exclusion_constant, min_files_for_path): if split : dfs_by_policy = [group for _, group in approved_hashes.groupby('policy')]
else : dfs_by_policy = [approved_hashes]
if not os.path.exists(f"parquet\\all_approved_hashes_{first_policy}_{second_policy}.parquet"): processed_dfs = []
df1 = tryToReadCSV(f"approved\\hashes_rep_unknown_{first_policy}_{second_policy}.csv") for df in dfs_by_policy:
df2 = tryToReadCSV(f"approved\\hashes_rep_good_{first_policy}_{second_policy}.csv") haslcp = pathf.split_filepaths_grouped(df, "filename", path_exclusion_constant, min_files_for_path)
haslcp = haslcp.drop_duplicates()
forbidden = pathf.regulator(badpathparts, True)
forbidden_lcfp = haslcp["longestcfp"].str.contains(forbidden, na=False)
print(ct.colorText("Removing forbidden filepaths for path exceptions", "green"))
lcp_not_forbidden = haslcp[~forbidden_lcfp].copy()
lcp_not_forbidden_review = lcp_not_forbidden[['policy', 'longestcfp', 'middle', 'filename_only', 'file_extension', 'sha256']]
unique_sha_counts = lcp_not_forbidden_review.groupby('longestcfp')['sha256'].nunique().reset_index()
unique_sha_counts.columns = ['longestcfp', 'unique_sha256_count']
lcp_not_forbidden_review = lcp_not_forbidden_review.merge(unique_sha_counts, on='longestcfp', how='left')
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
def generatePathReview(unknown, good, badpathparts, path_exclusion_constant, min_files_for_path, split = False):
df1 = ct.tryToReadCSV(unknown)
df2 = ct.tryToReadCSV(good)
all_approved_hashes = pd.concat([df1 , df2], ignore_index=True).sort_values(by=['filename']) all_approved_hashes = pd.concat([df1 , df2], ignore_index=True).sort_values(by=['filename'])
print(ct.colorText(f"Approved hash lists have been combined","green")) primary_path_exclusions = calculatePath(all_approved_hashes, badpathparts, path_exclusion_constant, min_files_for_path, split)
all_approved_hashes.to_parquet(f"parquet\\all_approved_hashes_{first_policy}_{second_policy}.parquet", index=False) remaining_hashes = all_approved_hashes[~all_approved_hashes['sha256'].isin(primary_path_exclusions['sha256'])]
del all_approved_hashes
gc.collect()
if not os.path.exists(f"parquet\\path_needs_approved_{first_policy}_{second_policy}.parquet"): secondary_path_exclusions = calculatePath(remaining_hashes, badpathparts, 3, min_files_for_path, split)
all_approved_hashes = pd.read_parquet(f"parquet\\all_approved_hashes_{first_policy}_{second_policy}.parquet")
print(ct.colorText(f"Beginning calculating longest common filepaths for path exceptions","green"))
haslcp = pathf.split_filepaths_grouped(all_approved_hashes,"filename",path_exclusion_constant, min_files_for_path) remaining_hashes = remaining_hashes[~remaining_hashes['sha256'].isin(secondary_path_exclusions['sha256'])]
haslcp.drop_duplicates()
forbidden = pathf.regulator(badpathparts, True) return all_approved_hashes, primary_path_exclusions, secondary_path_exclusions, remaining_hashes
forbidden_lcfp = haslcp["longestcfp"].str.contains(forbidden, na=False)
def clean_folders(parq_base_dir, appr_base_dir, needappr_base_dir, pflight_base_dir):
print(ct.colorText("Removing forbidden filepaths for path exceptions", "green"))
# Make a real DataFrame copy before modifying
lcp_not_forbidden = haslcp[~forbidden_lcfp].copy()
#For the review, drop down to only the columns we care, and then group by the commmon file path, consolidating and dropping dupes
lcp_not_forbidden_review = lcp_not_forbidden[['longestcfp', 'middle', 'filename_only', 'file_extension', 'sha256']]
# Count unique sha256 per longestcfp
unique_sha_counts = lcp_not_forbidden_review.groupby('longestcfp')['sha256'].nunique().reset_index()
unique_sha_counts.columns = ['longestcfp', 'unique_sha256_count']
# Merge the count back into the original DataFrame
lcp_not_forbidden_review = lcp_not_forbidden_review.merge(unique_sha_counts, on='longestcfp', how='left')
lcp_not_forbidden_review = lcp_not_forbidden_review[lcp_not_forbidden_review['unique_sha256_count'] >= min_files_for_path]
lcp_not_forbidden_review.to_parquet(f"parquet\\path_needs_approved_{first_policy}_{second_policy}.parquet",index=False)
lcp_not_forbidden_review.to_csv(f"needs_approved\\path_needs_approved_{first_policy}_{second_policy}.csv",index=False)
ct.style_dataframe_dark(lcp_not_forbidden_review,f"needs_approved\\path_needs_approved_{first_policy}_{second_policy}.html", True)
del lcp_not_forbidden
del unique_sha_counts
del lcp_not_forbidden_review
if not os.path.exists(f"parquet\\secondary_paths_{first_policy}_{second_policy}.parquet"):
all_approved_hashes = pd.read_parquet(f"parquet\\all_approved_hashes_{first_policy}_{second_policy}.parquet")
recommended_paths = pd.read_parquet(f"parquet\\path_needs_approved_{first_policy}_{second_policy}.parquet")
remaining_hashes = all_approved_hashes[~all_approved_hashes['sha256'].isin(recommended_paths['sha256'])]
print(ct.colorText(f"Beginning calculating longest common filepaths for path exceptions","green"))
haslcp = pathf.split_filepaths_grouped(remaining_hashes,"filename", 3, min_files_for_path)
haslcp.drop_duplicates()
forbidden = pathf.regulator(badpathparts, True)
forbidden_lcfp = haslcp["longestcfp"].str.contains(forbidden, na=False)
print(ct.colorText("Removing forbidden filepaths for path exceptions", "green"))
# Make a real DataFrame copy before modifying
lcp_not_forbidden = haslcp[~forbidden_lcfp].copy()
#For the review, drop down to only the columns we care, and then group by the commmon file path, consolidating and dropping dupes
lcp_not_forbidden_review = lcp_not_forbidden[['longestcfp', 'middle', 'filename_only', 'file_extension', 'sha256']]
# Count unique sha256 per longestcfp
unique_sha_counts = lcp_not_forbidden_review.groupby('longestcfp')['sha256'].nunique().reset_index()
unique_sha_counts.columns = ['longestcfp', 'unique_sha256_count']
# Merge the count back into the original DataFrame
lcp_not_forbidden_review = lcp_not_forbidden_review.merge(unique_sha_counts, on='longestcfp', how='left')
lcp_not_forbidden_review = lcp_not_forbidden_review[lcp_not_forbidden_review['unique_sha256_count'] >= min_files_for_path]
lcp_not_forbidden_review.to_parquet(f"parquet\\secondary_paths_{first_policy}_{second_policy}.parquet",index=False)
lcp_not_forbidden_review.to_csv(f"needs_approved\\secondary_paths_{first_policy}_{second_policy}.csv",index=False)
ct.style_dataframe_dark(lcp_not_forbidden_review,f"needs_approved\\secondary_paths_{first_policy}_{second_policy}.html", True)
remaining_hashes = remaining_hashes[~remaining_hashes['sha256'].isin(lcp_not_forbidden_review['sha256'])]
remaining_hashes.to_csv(f"needs_approved\\not_covered_by_path_exclusion_{first_policy}_{second_policy}.csv",index=False)
del lcp_not_forbidden
del unique_sha_counts
del lcp_not_forbidden_review
def allATpaths(url, pups, untrusted_publishers, badpathparts, threat_tolerance, path_exclusion_constant, min_files_for_path):
import pandas as pd
# Load raw data
hashes = pd.read_csv("exclusions\\allATPolicyExecs.csv")
# Deduplicate hashes before augmentation
deduped_hashes = hashes.drop_duplicates(subset=['sha256']).copy()
# Save policy name mapping (before deduplication)
policyname_map = hashes[['hostname', 'PolicyName']].drop_duplicates()
# Handle None inputs
untrusted_publishers = untrusted_publishers or []
pups = pups or []
# Augment deduplicated hashes
augmented = hashf.augmentAggregatedHashes(url, deduped_hashes)
# Merge policy names
augmented = augmented.merge(policyname_map, on='hostname', how='left')
# Clean numeric reputation fields
for col in ['reputation_scannermatch', 'reputation_scannercount', 'reputation_threatlevel']:
if col in augmented.columns:
augmented[col] = pd.to_numeric(augmented[col].replace('N/A', pd.NA), errors='coerce')
# Rename and select relevant columns
augmented = augmented.rename(columns={'publisher_x': 'publisher'})
augmented = augmented[[
'PolicyName', 'sha256', 'publisher', 'description', 'productname', 'productversion',
'reputation_lastseen', 'reputation_scannermatch', 'reputation_scannercount',
'reputation_status', 'reputation_threatlevel', 'reputation_threatname',
'reputation_timestamp'
]].sort_values(by=['publisher', 'description', 'productname'])
# Merge with deduplicated hashes to enrich data
final_augmented = augmented.merge(deduped_hashes, on='sha256', how='left')
# Clean up column names before applying reputation logic
if 'publisher_y' in final_augmented.columns:
final_augmented = final_augmented.drop(columns=['publisher_y'])
if 'publisher_x' in final_augmented.columns:
final_augmented = final_augmented.rename(columns={'publisher_x': 'publisher'})
if 'PolicyName_x' in final_augmented.columns:
final_augmented = final_augmented.rename(columns={'PolicyName_x': 'PolicyName'})
final_augmented.to_csv("exclusions\\testing.csv", index=False)
# Reputation flag logic
def reputationtool(row):
val = row["reputation_scannermatch"]
if pd.isna(val):
return row["publisher"] == "Not Signed"
try:
return int(val) > threat_tolerance
except (ValueError, TypeError):
return row["publisher"] == "Not Signed"
df = final_augmented.copy()
df["reputation_flag"] = df.apply(reputationtool, axis=1)
# Filtering logic
mask_needsreview = (
((df["publisher"] == "Not Signed") & df["reputation_flag"]) |
(df["reputation_status"] == "UNKNOWN")
)
mask_approved = (
(
(df["publisher"] != "Not Signed") &
~df["publisher"].str.contains(pathf.regulator(untrusted_publishers), case=False, na=False) &
~df["reputation_status"].isna() &
~df["description"].str.contains(pathf.regulator(pups), case=False, na=False)
) |
(
(df["publisher"] == "Not Signed") &
~df["reputation_flag"] &
~df["publisher"].str.contains(pathf.regulator(untrusted_publishers), case=False, na=False) &
~df["reputation_status"].isna() &
~df["description"].str.contains(pathf.regulator(pups), case=False, na=False)
)
)
needsreview_df = df[mask_needsreview]
approved_df = df[mask_approved]
all_approved_hashes = pd.concat([needsreview_df, approved_df], ignore_index=True)
print(ct.colorText("Beginning calculating longest common filepaths for path exceptions", "green"))
# Path analysis
haslcp = pathf.split_filepaths_grouped(all_approved_hashes, "filename", path_exclusion_constant, min_files_for_path)
haslcp = haslcp.drop_duplicates()
# Remove forbidden paths
forbidden = pathf.regulator(badpathparts, True)
lcp_not_forbidden = haslcp[~haslcp["longestcfp"].str.contains(forbidden, na=False)].copy()
print(ct.colorText("Removing forbidden filepaths for path exceptions", "green"))
# Reviewable paths
review_df = lcp_not_forbidden[['longestcfp', 'middle', 'filename_only', 'file_extension', 'sha256']]
sha_counts = review_df.groupby('longestcfp')['sha256'].nunique().reset_index()
sha_counts.columns = ['longestcfp', 'unique_sha256_count']
review_df = review_df.merge(sha_counts, on='longestcfp', how='left')
review_df = review_df[review_df['unique_sha256_count'] >= min_files_for_path]
review_df.to_csv("exclusions\\ALL_AT_PATHS.csv", index=False)
# Cleanup
del lcp_not_forbidden, sha_counts, review_df
def mergeTesting():
# Load the two CSVs
testing_df = pd.read_csv("exclusions\\testing.csv")
paths_df = pd.read_csv("exclusions\\ALL_AT_PATHS.csv")
# Merge on 'sha256' with testing as the left DataFrame
merged_df = testing_df.merge(paths_df, on="sha256", how="left")
# Save the merged result
merged_df.to_csv("exclusions\\merged_output.csv", index=False)
print(f"Merged DataFrame saved with {len(merged_df)} rows.")
def listPaths(url, group):
endpoint = url + '/v1/group/policies'
print(ct.colorText("[+] Grabbing All Paths", "cyan"))
payload = {
"groupid": [group],
}
headers = {
"X-APIKey": os.getenv('APIKEY')
}
try:
response = requests.post(endpoint, headers=headers, data=payload, verify=False)
response.raise_for_status()
parse_text = response.json()
pathnames = []
print(parse_text) # Optional: for debugging
for item in parse_text.get('response', {}).get('paths', []):
path = item.get('name')
if path:
pathnames.append(path)
return pathnames
except requests.exceptions.RequestException as e:
print(ct.colorText(f"[!] Request failed: {e}", "red"))
return []
except (KeyError, json.JSONDecodeError) as e:
print(ct.colorText(f"[!] Failed to parse response: {e}", "red"))
return []
def wildcardRegex(pattern):
pattern = pattern.replace("\\", "\\\\")
pattern = pattern.replace("**", "___RECURSIVE___")
pattern = pattern.replace("*", "[^\\\\]*")
pattern = pattern.replace("?", ".")
pattern = pattern.replace("___RECURSIVE___", ".*")
return re.compile(f"^{pattern}$", re.IGNORECASE)
def clean_folders_enforcement_prep():
""" """
Prompts user to choose whether to delete all .parquet files or preserve execution_history ones. Prompts user to choose whether to delete all .parquet files or preserve execution_history ones.
Then deletes .csv, .html, and .parquet files accordingly from specified folders. Then deletes .csv, .html, and .parquet files accordingly from specified folders.
@@ -416,7 +154,7 @@ def clean_folders_enforcement_prep():
user_input = input("Do you want to delete *all* .parquet files including execution_history ones? (yes/y or no/n): ").strip().lower() user_input = input("Do you want to delete *all* .parquet files including execution_history ones? (yes/y or no/n): ").strip().lower()
delete_execution_hist = user_input in ["yes", "y"] delete_execution_hist = user_input in ["yes", "y"]
folders = ["approved", "exclusions", "needs_approved", "parquet", "preflight"] folders = [parq_base_dir, appr_base_dir, needappr_base_dir, pflight_base_dir]
for folder in folders: for folder in folders:
folder_path = os.path.abspath(folder) folder_path = os.path.abspath(folder)
@@ -443,3 +181,35 @@ def clean_folders_enforcement_prep():
if delete_execution_hist or not filename.startswith("execution_history"): if delete_execution_hist or not filename.startswith("execution_history"):
os.remove(file_path) os.remove(file_path)
def generateApprovables(needappr_base_dir, appr_base_dir, parq_base_dir, badpathparts, bad_publisher_list, path_exclusion_constant, min_files_for_path, split= False ):
if os.path.exists(f"{needappr_base_dir}unknown_hashes.csv") and os.path.exists(f"{needappr_base_dir}good_hashes.csv"):
all_hashes, primary_paths, secondary_paths, remaining = pathf.generatePathReview(f"{appr_base_dir}unknown_hashes.csv", f"{appr_base_dir}good_hashes.csv", badpathparts,path_exclusion_constant, min_files_for_path, split)
all_hashes.to_parquet(f"{parq_base_dir}all_hashes.parquet", index=False)
dataframes = {
"all_hashes" : all_hashes,
"primary_Paths": primary_paths,
"secondary_Paths": secondary_paths,
"remaining": remaining
}
for name, df in dataframes.items():
df.to_csv(f"{needappr_base_dir}{name}.csv", index=False)
df.to_parquet(f"{parq_base_dir}{name}.parquet", index=False)
ct.style_dataframe_dark(df, f"{needappr_base_dir}{name}.html")
publishers = hashf.generatePublist(f"{parq_base_dir}all_hashes.parquet",bad_publisher_list)
publishers.to_csv(f"{needappr_base_dir}publishers.csv", index=False)
publishers.to_parquet(f"{parq_base_dir}publishers.parquet", index=False)
ct.style_dataframe_dark(publishers, f"{needappr_base_dir}publishers.html")
else:
print(ct.colorText(f"Please manually approve hashes prior to this step","red"))
+1 -1
View File
@@ -123,7 +123,7 @@ def _schedule_once(job_id: str, func_name: str, run_at_timestamp: float, args=No
print(f"[WARN] Job {job_id} scheduled in the past. Skipping.") print(f"[WARN] Job {job_id} scheduled in the past. Skipping.")
return return
# Schedule via schedule library # Schedule via schedule library
schedule.every(delay_seconds).seconds.do(job_wrapper).tag(job_id) 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): def _schedule_recurring(job_id: str, func_name: str, interval: int, unit: str, args=None, kwargs=None):
args = args or [] args = args or []
+247 -42
View File
@@ -14,7 +14,11 @@
# along with this program. If not, see <https://www.gnu.org/licenses/>. # along with this program. If not, see <https://www.gnu.org/licenses/>.
#Local Imports #Local Imports
import utils.pretty as ct import utils.utils as ct
import utils.hashfunctions as hashf
import utils.pathfunctions as pathf
import utils.policyfunctions as policyf
#Standard Libary Imports: #Standard Libary Imports:
import datetime import datetime
import gc import gc
@@ -45,7 +49,6 @@ def addPub(url, policy, publist):
for p in publist: for p in publist:
print(p) print(p)
def addHashReal(url, allowlistID, hashlist): def addHashReal(url, allowlistID, hashlist):
endpoint = url + '/v1/hash/application/add' endpoint = url + '/v1/hash/application/add'
@@ -62,7 +65,6 @@ def addHashReal(url, allowlistID, hashlist):
parse_text = json.loads(response.text) parse_text = json.loads(response.text)
print(parse_text) print(parse_text)
def addPathReal(url, grouplistID, pathlist): def addPathReal(url, grouplistID, pathlist):
endpoint = url + '/v1/group/path/add' endpoint = url + '/v1/group/path/add'
payload = { payload = {
@@ -77,7 +79,6 @@ def addPathReal(url, grouplistID, pathlist):
response = requests.request("POST", endpoint, headers=headers, data=payload, verify=False) response = requests.request("POST", endpoint, headers=headers, data=payload, verify=False)
print(response.text) print(response.text)
def addPubReal(url, grouplistID, publist): def addPubReal(url, grouplistID, publist):
endpoint = url + '/v1/group/publisher/add' endpoint = url + '/v1/group/publisher/add'
payload = { payload = {
@@ -92,27 +93,29 @@ def addPubReal(url, grouplistID, publist):
response = requests.request("POST", endpoint, headers=headers, data=payload, verify=False) response = requests.request("POST", endpoint, headers=headers, data=payload, verify=False)
print(response.text) print(response.text)
def getPolicyInfo(url, policy, type, days, parquet=True): def getPolicyInfo(url, policy, type, days, parquet=True):
executionhist_policy = pd.DataFrame() executionhist_policy = pd.DataFrame()
exehist = pullPolicyExechistories(url, policy, type, days, True) exehist = pullPolicyExechistories(url, policy, type, days, True)
if exehist is not None:
data = json.loads(exehist) data = json.loads(exehist)
executionhist_policy = pd.DataFrame(data["response"]["exechistories"]) executionhist_policy = pd.DataFrame(data["response"]["exechistories"])
if not executionhist_policy.empty: if not executionhist_policy.empty:
executionhist_policy = executionhist_policy[['sha256', 'publisher', 'filename', 'hostname', 'username', 'pprocess', 'gprocess', 'commandline']] executionhist_policy = executionhist_policy[['sha256', 'publisher', 'filename', 'hostname', 'username', 'pprocess', 'gprocess', 'commandline']]
executionhist_policy['policy'] = policy # Add policy column here
executionhist_policy = executionhist_policy.drop_duplicates(subset=['sha256', 'filename', 'hostname']) executionhist_policy = executionhist_policy.drop_duplicates(subset=['sha256', 'filename', 'hostname'])
executionhist_policy = executionhist_policy.sort_values(by=['sha256', 'filename']) executionhist_policy = executionhist_policy.sort_values(by=['sha256', 'filename'])
if parquet: executionhist_policy.to_parquet(f"parquet\\execution_history_{policy}.parquet", index=False) if parquet:
executionhist_policy.to_parquet(f"prepare_policy\\parquet\\execution_history_{policy}.parquet", index=False)
print(ct.colorText(f"Staging of Execution history for policy: {policy} is complete", "green")) print(ct.colorText(f"Staging of Execution history for policy: {policy} is complete", "green"))
del data del data
del exehist del exehist
gc.collect() gc.collect()
return executionhist_policy return executionhist_policy
def sendToPolicy(url, first_policy, second_policy, destination_name, destination_id, allowlist_parent_name, allowlist_parent_id, allowlist_child_name, allowlist_child_id): def sendToPolicy(url, paths, hashes, publishers, destination_name, destination_id, allowlist_name, allowlist_id):
pathexclusions = pd.read_parquet(f"parquet\\final_path_exclusions_{first_policy}_{second_policy}.parquet") pathexclusions = pd.read_parquet(paths)
allowbyhash = pd.read_parquet(f"parquet\\final_hash_approvals_{first_policy}_{second_policy}.parquet") allowbyhash = pd.read_parquet(hashes)
publishers = pd.read_parquet(f"parquet\\publishers_{first_policy}_{second_policy}.parquet") publishers = ct.tryToReadCSV(publishers)
ct.areYouSure() ct.areYouSure()
confirmation = input(ct.colorText("Type 'I AGREE' to continue: ","white")) confirmation = input(ct.colorText("Type 'I AGREE' to continue: ","white"))
@@ -138,17 +141,17 @@ def sendToPolicy(url, first_policy, second_policy, destination_name, destination
print(ct.colorText(f"Adding publishers to {destination_name}", "yellow")) print(ct.colorText(f"Adding publishers to {destination_name}", "yellow"))
if publishers.empty:
print(ct.colorText("The publishers list is empty.", "red"))
else:
publisher_list = publishers['publisher'].tolist() publisher_list = publishers['publisher'].tolist()
addPubReal(url, destination_id, publisher_list) addPubReal(url, destination_id, publisher_list)
print(ct.colorText(f"Adding hashes to {allowlist_parent_name}", "yellow"))
allowlist_parenthashlist = allowbyhash[allowbyhash['reputation_status'] == 'KNOWN']['sha256'].unique().tolist() print(ct.colorText(f"These hashes would be added to {allowlist_name}", "yellow"))
addHashReal(url, allowlist_parent_id,allowlist_parenthashlist)
print(ct.colorText(f"Adding hashes to {allowlist_child_name}", "yellow")) allowlist = allowbyhash['sha256'].unique().tolist()
allowlist_childhashlist = allowbyhash[allowbyhash['reputation_status'] == 'UNKNOWN']['sha256'].unique().tolist() addHash(url, allowlist_id,allowlist)
addHashReal(url, allowlist_child_id, allowlist_childhashlist)
ct.locked() ct.locked()
@@ -281,8 +284,7 @@ def listATPolicies(url):
print(ct.colorText(f"[!] Failed to parse response: {e}", "red")) print(ct.colorText(f"[!] Failed to parse response: {e}", "red"))
return {} return {}
def listAllowlists(url: str) -> tuple[int, list, list]:
def listAllowlists(url):
endpoint = url + '/v1/application' endpoint = url + '/v1/application'
print(ct.colorText("[+] Grabbing All Allowlists", "cyan")) print(ct.colorText("[+] Grabbing All Allowlists", "cyan"))
payload = {} payload = {}
@@ -293,18 +295,23 @@ def listAllowlists(url):
parse_text = json.loads(response.text) parse_text = json.loads(response.text)
policiesnames = [] policiesnames = []
policyids = [] policyids = []
for index, list in enumerate(parse_text['response']['applications'], start=1):
for index, item in enumerate(parse_text['response']['applications'], start=1):
if index >= 38: if index >= 38:
print(ct.colorText(f"{index}. {list['name']}", "yellow")) print(ct.colorText(f"{index}. {item['name']}", "yellow"))
policiesnames.append(list['name']) policiesnames.append(item['name'])
policyids.append(list['applicationid']) policyids.append(item['applicationid'])
while True:
try:
choice = int(input(ct.colorText("Select allowlist: ", "white"))) choice = int(input(ct.colorText("Select allowlist: ", "white")))
if choice < 38: if choice < 38 or choice > len(parse_text['response']['applications']):
print(ct.colorText("Please only choose an allowlist designed for this use - '38+'","red")) print(ct.colorText("Please only choose an allowlist designed for this use - '38+'", "red"))
elif choice >= 38: else:
choice = choice - 38 adjusted_choice = choice - 38
return choice, policiesnames, policyids return adjusted_choice, policiesnames, policyids
#Need else and catch for upper bound except ValueError:
print(ct.colorText("Invalid input. Please enter a number.", "red"))
def skipback(days): def skipback(days):
""" """
@@ -318,10 +325,10 @@ def skipback(days):
objectid_hex = hex_timestamp + '0000000000000000' objectid_hex = hex_timestamp + '0000000000000000'
return ObjectId(objectid_hex) return ObjectId(objectid_hex)
def sendToPolicyTest(url, first_policy, second_policy, destination_name, destination_id, allowlist_parent_name, allowlist_parent_id, allowlist_child_name, allowlist_child_id): def sendToPolicyTest(url, paths, hashes, publishers, destination_name, destination_id, allowlist_id, allowlist_name):
pathexclusions = pd.read_parquet(f"parquet\\final_path_exclusions_{first_policy}_{second_policy}.parquet") pathexclusions = pd.read_parquet(paths)
allowbyhash = pd.read_parquet(f"parquet\\final_hash_approvals_{first_policy}_{second_policy}.parquet") allowbyhash = pd.read_parquet(hashes)
publishers = pd.read_parquet(f"parquet\\publishers_{first_policy}_{second_policy}.parquet") publishers = ct.tryToReadCSV(publishers)
print(ct.colorText(f"These path exclusions would be added to {destination_name}", "yellow")) print(ct.colorText(f"These path exclusions would be added to {destination_name}", "yellow"))
@@ -342,19 +349,16 @@ def sendToPolicyTest(url, first_policy, second_policy, destination_name, destina
print(ct.colorText(f"These publishers would added to {destination_name}", "yellow")) print(ct.colorText(f"These publishers would added to {destination_name}", "yellow"))
if publishers.empty:
print(ct.colorText("The publishers list is empty.", "red"))
else:
publisher_list = publishers['publisher'].tolist() publisher_list = publishers['publisher'].tolist()
addPub(url, destination_id, publisher_list) addPub(url, destination_id, publisher_list)
print(ct.colorText(f"These hashes would be added to {allowlist_parent_name}", "yellow")) print(ct.colorText(f"These hashes would be added to {allowlist_name}", "yellow"))
allowlist_parenthashlist = allowbyhash[allowbyhash['reputation_status'] == 'KNOWN']['sha256'].unique().tolist()
addHash(url, allowlist_parent_id,allowlist_parenthashlist)
print(ct.colorText(f"These hashes would be added to {allowlist_child_name}", "yellow"))
allowlist_childhashlist = allowbyhash[allowbyhash['reputation_status'] == 'UNKNOWN']['sha256'].unique().tolist()
addHash(url, allowlist_child_id, allowlist_childhashlist)
allowlist = allowbyhash['sha256'].unique().tolist()
addHash(url, allowlist_id,allowlist)
def updateAuditPoliciesFromEnforcementPolices(url, policy_relationship_map): def updateAuditPoliciesFromEnforcementPolices(url, policy_relationship_map):
for enforcement_policy, audit_policy in policy_relationship_map.items(): for enforcement_policy, audit_policy in policy_relationship_map.items():
@@ -376,7 +380,6 @@ def assignPoliciesfromGroup(url, source_policy_id, target_policy_id):
parse_text = json.loads(response.text) parse_text = json.loads(response.text)
print(parse_text) print(parse_text)
def turnOnAudit(url, policyid): def turnOnAudit(url, policyid):
endpoint = url + '/v1/group/settings/auditmode' endpoint = url + '/v1/group/settings/auditmode'
@@ -404,3 +407,205 @@ def agentsInPolicy(url, policyid):
response = requests.request("POST", endpoint, headers=headers, data=payload, verify=False) response = requests.request("POST", endpoint, headers=headers, data=payload, verify=False)
parse_text = json.loads(response.text) parse_text = json.loads(response.text)
print(parse_text) print(parse_text)
def prepare_to_enforce(url, bad_publisher_list, pups, badpathparts, threat_tolerance_constant = 4, path_exclusion_constant = 4, min_files_for_path = 4):
destination_name = " "
destination_id = " "
allowlist_name = " "
allowlist_id = " "
policylist = []
type = [1, 2, 6, 7]
parq_base_dir = "prepare_policy\\parquet\\"
appr_base_dir = "prepare_policy\\approved\\"
needappr_base_dir = "prepare_policy\\needs_approved\\"
pflight_base_dir = "prepare_policy\\preflight\\"
#If the directorys where we're going to store our output dont exist, make them.
os.makedirs(parq_base_dir, exist_ok=True)
os.makedirs(needappr_base_dir, exist_ok=True)
os.makedirs(appr_base_dir, exist_ok=True)
os.makedirs(pflight_base_dir, exist_ok=True)
while True:
ct.printEnforceChecklist(parq_base_dir,appr_base_dir, needappr_base_dir, pflight_base_dir, policylist, allowlist_name, destination_name)
choice = input(ct.colorText("\nEnter your choice: ", "white"))
if choice == "1":
while True:
choice, policynames, policyid = listPolicies(url)
selected_policy = policynames[choice]
if selected_policy not in policylist:
policylist.append(selected_policy)
while True:
answer = input(ct.colorText("Do you want to load another policy? (yes/no): ", "white")).strip().lower()
if answer in ("no", "n"):
break # Exit the inner loop and then the outer loop
elif answer in ("yes", "y"):
break # Exit the inner loop and continue the outer loop
else:
print(ct.colorText("Please answer with 'yes' or 'no'.", "red"))
if answer in ("no", "n"):
break
print(policylist)
elif choice == "2":
print(ct.colorText(f"Please choose destination_name Policy for Path Exclusions","white"))
choice, policynames, policyid = listPolicies(url)
#print(allowlist_parent_tuple)
destination_name = policynames[choice]
destination_id = policyid[choice]
print(ct.colorText(f"Please choose Allowlist for Hashes","white"))
choice, allowlists,allowid = listAllowlists(url)
#print(allowlist_parent_tuple)
allowlist_name = allowlists[choice]
allowlist_id = allowid[choice]
print(destination_name, allowlist_name)
elif choice == "3":
policyf.buildExecHistory(url,
policylist,
parq_base_dir,
needappr_base_dir,
type,
threat_tolerance_constant,
bad_publisher_list,
pups,
)
csvs = [f"{needappr_base_dir}unknown_hashes.csv", f"{needappr_base_dir}good_hashes.csv"]
#Since we want to build paths as if they were all in the same policy to begin with, lets group them that way
for csv in csvs:
df = ct.tryToReadCSV(csv)
df['policy'] = destination_name
df.to_csv(csv)
elif choice == "4":
pathf.generateApprovables(needappr_base_dir, appr_base_dir, parq_base_dir, badpathparts, bad_publisher_list, path_exclusion_constant, min_files_for_path)
elif choice == "5":
savePreFlights(appr_base_dir, parq_base_dir, pflight_base_dir)
elif choice == "6":
if os.path.exists(f"{pflight_base_dir}final_path_exclusions.html") and os.path.exists(f"{pflight_base_dir}\\final_hash_approvals.html") and allowlist_name != " " and destination_name != " ":
sendToPolicyTest(
url,
f"{parq_base_dir}final_path_exclusions.parquet",
f"{parq_base_dir}final_hash_approvals.parquet",
f"{appr_base_dir}publishers.parquet",
destination_name,
destination_id,
allowlist_name,
allowlist_id
)
elif choice == "7":
if os.path.exists(f"{pflight_base_dir}final_path_exclusions.html") and os.path.exists(f"{pflight_base_dir}\\final_hash_approvals.html") and allowlist_name != " " and destination_name != " ":
sendToPolicy(
url,
f"{parq_base_dir}final_path_exclusions.parquet",
f"{parq_base_dir}final_hash_approvals.parquet",
f"{appr_base_dir}publishers.parquet",
destination_name,
destination_id,
allowlist_name,
allowlist_id,
)
elif choice == "R":
pathf.clean_folders(parq_base_dir, appr_base_dir, needappr_base_dir, pflight_base_dir)
elif choice == "Q":
break
else:
print(ct.colorText("Invalid choice. Please try again.", "red"))
def buildExecHistory(url,
policylist,
parq_base_dir,
needappr_base_dir,
type,
threat_tolerance_constant,
bad_publisher_list,
pups
):
exe_hist_parq_list = []
while True:
try:
history_days = int(input("Enter the how many days in of history do you want to pull - select a number between 1 and 150: "))
if 1 <= history_days <= 150:
break
else:
print("Invalid input. Please enter a number between 1 and 150.")
except ValueError:
print("Invalid input. Please enter a valid integer.")
for policy in policylist:
policy_exec_history = policyf.getPolicyInfo(url, policy, type, history_days)
policy_exec_history['policy'] = policy
policy_exec_history.to_parquet(f"{parq_base_dir}Exec_Hist_{policy}.parquet")
exe_hist_parq_list.append(f"{parq_base_dir}Exec_Hist_{policy}.parquet")
augmented_hashlist = hashf.combineHashes(url, exe_hist_parq_list)
augmented_hashlist.to_parquet(f"{parq_base_dir}augmentedHashlist.parquet",index=False)
needsreview_df, approved_df, unapproved_df = hashf.categorizeHashes(augmented_hashlist, threat_tolerance_constant, bad_publisher_list, pups)
needsreview_df.to_parquet(f"{parq_base_dir}needsreview.parquet",index=False)
approved_df.to_parquet(f"{parq_base_dir}approved.parquet",index=False)
unapproved_df.to_parquet(f"{parq_base_dir}unapproved.parquet",index=False)
condensed_executions = hashf.condenseExecutions(exe_hist_parq_list)
condensed_executions.to_parquet(f"{parq_base_dir}condensed_executions.parquet", index=False)
unknown, good, bad = hashf.divideSortedHashExecutions(
f"{parq_base_dir}needsreview.parquet",
f"{parq_base_dir}approved.parquet",
f"{parq_base_dir}unapproved.parquet",
f"{parq_base_dir}condensed_executions.parquet",
pups
)
dataframes = {
"unknown_hashes" : unknown,
"good_hashes": good,
"bad_hashes": bad
}
for name, df in dataframes.items():
df.to_csv(f"{needappr_base_dir}{name}.csv", index=False)
df.to_parquet(f"{parq_base_dir}{name}.parquet", index=False)
ct.style_dataframe_dark(df, f"{needappr_base_dir}{name}.html")
def savePreFlights(appr_base_dir, parq_base_dir, pflight_base_dir):
if os.path.exists(f"{appr_base_dir}primary_Paths.csv"):
if not os.path.exists(f"{parq_base_dir}final_hash_approvals.parquet") and not os.path.exists(f"{parq_base_dir}final_path_exclusions.parquet"):
pathexclusions, allowbyhash = hashf.generatePreflights(
f"{parq_base_dir}all_hashes.parquet",
f"{appr_base_dir}primary_Paths.csv",
f"{appr_base_dir}secondary_Paths.csv")
dataframes = {
"final_path_exclusions" : pathexclusions,
"final_hash_approvals": allowbyhash
}
for name, df in dataframes.items():
df.to_csv(f"{pflight_base_dir}{name}.csv", index=False)
df.to_parquet(f"{parq_base_dir}{name}.parquet", index=False)
ct.style_dataframe_dark(df, f"{pflight_base_dir}{name}.html")
+110 -53
View File
@@ -15,6 +15,7 @@
#Standard Libary Imports: #Standard Libary Imports:
import os import os
import pandas as pd
def colorText(text: str, color: str) -> str: def colorText(text: str, color: str) -> str:
colors = { colors = {
@@ -193,7 +194,7 @@ def displayIntro():
print(colorText("======================== Welcome to the Airlock API Tool ========================", "cyan")) print(colorText("======================== Welcome to the Airlock API Tool ========================", "cyan"))
print(colorText("=================================================================================", "cyan")) print(colorText("=================================================================================", "cyan"))
def printEnforceChecklist(first_policy, second_policy, allowlist_child_name, allowlist_parent_name, destination_name): def printEnforceChecklist(parq_base_dir,appr_base_dir, needappr_base_dir, pflight_base_dir, policy_list, allowlist_name, destination_name):
print(colorText("\n --------------------------------------------------------------------", "cyan")) print(colorText("\n --------------------------------------------------------------------", "cyan"))
print(colorText(" ------------- 🛠️ 🔒 Prepare to Enforce Policy 🛠️ 🔒 ------------------", "cyan")) print(colorText(" ------------- 🛠️ 🔒 Prepare to Enforce Policy 🛠️ 🔒 ------------------", "cyan"))
@@ -201,100 +202,80 @@ def printEnforceChecklist(first_policy, second_policy, allowlist_child_name, all
print(colorText("\nSequentually follow these steps to prepare a policy for enforcement:", "white")) print(colorText("\nSequentually follow these steps to prepare a policy for enforcement:", "white"))
print(colorText("\n1. Choose which originating policy or policies to move to enforcement", "cyan")) print(colorText("\n1. Choose which originating policy or policies to move to enforcement", "cyan"))
if first_policy == " " and second_policy == " ": if not policy_list:
print(colorText(f" [✗] No policies have been chosen","red")) print(colorText(f" [✗] No policies have been chosen","red"))
elif first_policy != " " and second_policy is first_policy:
print(colorText(f" [✓] {first_policy} has been selected,", "green"))
elif first_policy != " " and second_policy != " ":
print(colorText(f" [✓] {first_policy} has been selected as Policy 1","green"))
print(colorText(f" [✓] {second_policy} has been selected as Policy 2","green"))
print(colorText("2. Pull and stage event history, combine the histories, add hash info, then categorize the hashes", "cyan"))
if os.path.exists(f"parquet\\execution_history_{first_policy}.parquet"):
print(colorText(f" [✓] Execution history has been compiled for {first_policy}","green"))
elif not os.path.exists(f"parquet\\execution_history_{first_policy}.parquet"):
print(colorText(f" [✗] Execution history has not been compiled for {first_policy}","red"))
elif second_policy is not first_policy and os.path.exists(f"parquet\\execution_history_{second_policy}.parquet"):
print(colorText(f" [✓] Execution history has been compiled for {second_policy}","green"))
elif second_policy is not first_policy and not os.path.exists(f"parquet\\execution_history_{second_policy}.parquet"):
print(colorText(f" [✗] Execution history has not been compiled for {second_policy}","red"))
if os.path.exists(f"parquet\\combined_hashlist_{first_policy}_{second_policy}.parquet"):
print(colorText(f" [✓] Hash Info has been added to the combined execution history", "green"))
else: else:
print(colorText(f" [✗] Hash Info has not been added to the combined execution history", "red")) print(colorText(f"The following policies have been choosen:", "green"))
for policy in policy_list:
print(colorText(f" [✓] {policy}","green"))
if os.path.exists(f"parquet\\hashes_rep_unknown_{first_policy}_{second_policy}.parquet") and os.path.exists(f"parquet\\hashes_rep_good_{first_policy}_{second_policy}.parquet") and os.path.exists(f"parquet\\hashes_rep_bad_{first_policy}_{second_policy}.parquet"):
print(colorText(f" [✓] Hashes have been cateogrized", "green")) print(colorText(f"2. Choose the destination policy and allowlist", "cyan"))
if allowlist_name == " ":
print(colorText(f" [✗] No allowlists have been chosen","red"))
elif allowlist_name != " " and allowlist_name != " " and allowlist_name is not allowlist_name:
print(colorText(f" [✓] {allowlist_name} has been selected as allowlist","green"))
if destination_name == " ":
print(colorText(f" [✗] No destination policy has been chosen","red"))
else: else:
print(colorText(f" [] Hashes have not been cateogrized", "red")) print(colorText(f" [] destination policy is {destination_name}","green"))
if os.path.exists(f"parquet\\condensed_executions_{first_policy}_{second_policy}.parquet"): print(colorText("3. Pull and stage event history, combine the histories, add hash info, then categorize the hashes", "cyan"))
print(colorText(f" [✓] Execution history has been_combined_for {first_policy} and_{second_policy}", "green")) if not policy_list:
print(colorText(f" [✗] No policies have been chosen","red"))
else: else:
print(colorText(f" [✗] Execution history has not been_combined_for {first_policy} and_{second_policy}", "red")) for policy in policy_list:
if os.path.exists(f"{parq_base_dir}Exec_Hist_{policy}.parquet"): print(colorText(f" [✓] Data for {policy} has been fetched","green"))
else: print(colorText(f" [✗] Data for {policy} has not been fetched","red"))
print(colorText(f"4. Manually review the files:","cyan"))
print(colorText(f"3. Manually review the files:","cyan")) print(colorText(" 'prepare_policy\\needs_approved\\good_hashes.csv' and 'prepare_policy\\needs_approved\\unknown_hashes.csv'\n", "cyan"))
print(colorText(" 'needs_approved\\hashes_rep_good_{first_policy}_{second_policy}.csv' and 'needs_approved\\hashes_rep_unknown_{first_policy}_{second_policy}.csv'", "cyan"))
print(colorText(" Remove the rows containing hashes you do not approve of, and those you would not approve of without metarules.", "cyan")) print(colorText(" Remove the rows containing hashes you do not approve of, and those you would not approve of without metarules.", "cyan"))
print(colorText(" If metarules need to be created, please make note of them, and remove the row from the csv.", "cyan")) print(colorText(" If metarules need to be created, please make note of them, and remove the row from the csv.", "cyan"))
print(colorText(" When complete, save both csv files to the directory 'approved' and choose this option.","cyan")) print(colorText(" When complete, save both csv files to the directory 'approved' and choose this option.","cyan"))
print(colorText(" This will combine these approved hashes with the automatically approved hashes and generate a list of paths to be reviewed", "cyan")) print(colorText(" This will combine these approved hashes with the automatically approved hashes and generate a list of paths to be reviewed", "cyan"))
if os.path.exists(f"approved\\hashes_rep_good_{first_policy}_{second_policy}.csv") and os.path.exists(f"approved\\hashes_rep_unknown_{first_policy}_{second_policy}.csv"): if os.path.exists(f"{appr_base_dir}good_hashes.csv") and os.path.exists(f"{appr_base_dir}unknown_hashes.csv"):
print(colorText(" [✓] Reviewed hashes have been loaded","green")) print(colorText(" [✓] Reviewed hashes have been loaded","green"))
else: else:
print(colorText(" [✗] Reviewed hashes have not been loaded","red")) print(colorText(" [✗] Reviewed hashes have not been loaded","red"))
if os.path.exists(f"parquet\\all_approved_hashes_{first_policy}_{second_policy}.parquet"): if os.path.exists(f"{parq_base_dir}all_hashes.parquet"):
print(colorText(" [✓] The combined approved hashes list has been generated","green")) print(colorText(" [✓] The combined approved hashes list has been generated","green"))
else: else:
print(colorText(" [✗] The combined approved hashes list has not been generated","red")) print(colorText(" [✗] The combined approved hashes list has not been generated","red"))
if os.path.exists(f"needs_approved\\path_needs_approved_{first_policy}_{second_policy}.csv"): if os.path.exists(f"{needappr_base_dir}primary_Paths.csv"):
print(colorText(" [✓] Path review list created","green")) print(colorText(" [✓] Path review list created","green"))
else: else:
print(colorText(" [✗] Path review list has not been created","red")) print(colorText(" [✗] Path review list has not been created","red"))
print(colorText(f"4. Manually review the file 'needs_approved\\paths_needing_review_{first_policy}_{second_policy}.csv'", "cyan")) print(colorText(f"5. Manually review the files \n 'prepare_policy\\needs_approved\\primary_Paths.csv'\n 'prepare_policy\\needs_approved\\secondary_Paths.csv'", "cyan"))
print(colorText(" Remove the rows containing path exclusions you do not approve of" , "cyan")) print(colorText(" Remove the rows containing path exclusions you do not approve of. The secondary list can be not added at all if nothing is useful" , "cyan"))
print(colorText(" When complete, save the csv file to the directory 'approved'", "cyan")) print(colorText(" When complete, save the csv file to the directory 'approved'", "cyan"))
print(colorText(" Do the same process with the list of publishers forthe same directories", "cyan")) print(colorText(" Do the same process with the list of publishers forthe same directories", "cyan"))
print(colorText(" Preflight Lists will be generated", "cyan")) print(colorText(" Preflight Lists will be generated", "cyan"))
if os.path.exists(f"approved\\path_needs_approved_{first_policy}_{second_policy}.csv"): if os.path.exists(f"{appr_base_dir}primary_Paths.csv"):
print(colorText(" [✓] Reviewed path list detected","green")) print(colorText(" [✓] Reviewed path list detected","green"))
else: else:
print(colorText(" [✗] Path review list has not been detected","red")) print(colorText(" [✗] Path review list has not been detected","red"))
if os.path.exists(f"preflight\\final_path_exclusions_{first_policy}_{second_policy}.html"): if os.path.exists(f"{pflight_base_dir}final_path_exclusions.csv"):
print(colorText(" [✓] Preflight Path Exclusion List has been generated","green")) print(colorText(" [✓] Preflight Path Exclusion List has been generated","green"))
else: else:
print(colorText(" [✗] Preflight Path Exclusion List has not been generated","red")) print(colorText(" [✗] Preflight Path Exclusion List has not been generated","red"))
if os.path.exists(f"preflight\\final_hash_approvals_{first_policy}_{second_policy}.html"): if os.path.exists(f"{pflight_base_dir}final_hash_approvals.csv"):
print(colorText(" [✓] Preflight hash approval list has been generated","green")) print(colorText(" [✓] Preflight hash approval list has been generated","green"))
else: else:
print(colorText(" [✗] Preflight hash approval list has not been generated","red")) print(colorText(" [✗] Preflight hash approval list has not been generated","red"))
print(colorText(f"5. Choose the destination policy and parent and child allow list", "cyan"))
if allowlist_child_name == " " and allowlist_parent_name== " ":
print(colorText(f" [✗] No allowlists have been chosen","red"))
elif allowlist_parent_name != " " and allowlist_child_name != " " and allowlist_parent_name is allowlist_child_name:
print(colorText(f" [✓] [✗] Only {allowlist_parent_name} has been selected this is unusual, but potentially valid case, double check before proceeding,", "yellow"))
elif allowlist_parent_name != " " and allowlist_child_name != " " and allowlist_parent_name is not allowlist_child_name:
print(colorText(f" [✓] {allowlist_parent_name} has been selected as Parent Policy","green"))
print(colorText(f" [✓] {allowlist_child_name} has been selected as Child Policy","green"))
if destination_name == " ":
print(colorText(f" [✗] No destination policy has been chosen","red"))
else:
print(colorText(f" [✓] destination policy is {destination_name}","green"))
print(colorText(f"6. Test ------------------------------------------------------", "cyan")) print(colorText(f"6. Test ------------------------------------------------------", "cyan"))
print(colorText(f" Print rather than apply selected data.", "cyan")) print(colorText(f" Print rather than apply selected data.", "cyan"))
@@ -342,3 +323,79 @@ def locked():
""", "yellow")) """, "yellow"))
def printDeviceEnforceChecklist():
print(colorText("\n --------------------------------------------------------------------", "cyan"))
print(colorText(" ------------- 🛠️ 🔒 Prepare to Enforce Policy 🛠️ 🔒 ------------------", "cyan"))
print(colorText(" --------------------------------------------------------------------", "cyan"))
print(colorText("\nSequentually follow these steps to prepare a policy for enforcement:", "white"))
print(colorText("\n1. Choose which originating policy or policies to move to enforcement", "cyan"))
print(colorText("2. Pull and stage event history, combine the histories, add hash info, then categorize the hashes", "cyan"))
print(colorText(f"3. Manually review the files:","cyan"))
print(colorText(" 'needs_approved\\good_{first_policy}_{second_policy}.csv' and 'needs_approved\\unknown_{first_policy}_{second_policy}.csv'", "cyan"))
print(colorText(" Remove the rows containing hashes you do not approve of, and those you would not approve of without metarules.", "cyan"))
print(colorText(" If metarules need to be created, please make note of them, and remove the row from the csv.", "cyan"))
print(colorText(" When complete, save both csv files to the directory 'approved' and choose this option.","cyan"))
print(colorText(" This will combine these approved hashes with the automatically approved hashes and generate a list of paths to be reviewed", "cyan"))
print(colorText(f"4. Manually review the file 'needs_approved\\paths_needing_review.csv'", "cyan"))
print(colorText(" Remove the rows containing path exclusions you do not approve of" , "cyan"))
print(colorText(" When complete, save the csv file to the directory 'approved'", "cyan"))
print(colorText(" Do the same process with the list of publishers forthe same directories", "cyan"))
print(colorText(" Preflight Lists will be generated", "cyan"))
print(colorText(f"5. Choose the destination policy and parent and child allow list", "cyan"))
print(colorText(f"6. Test ------------------------------------------------------", "cyan"))
print(colorText(f" Print rather than apply selected data.", "cyan"))
print(colorText(f"7. Liftoff ------------------------------------------------------", "cyan"))
print(colorText(f" Apply path exclusions according to allowed and approved paths", "cyan"))
print(colorText(f" Apply signed or attested hashes to Parent Allow List", "cyan"))
print(colorText(f" Apply approved, but unsigned hashes to the Child Allow List", "cyan"))
print(colorText("R. Remove/Reset Generated data - will prompt to allow keeping execution history", "cyan"))
print(colorText("Q. Quit", "cyan"))
def apivalidation():
match os.getenv('APIKEY'):
case '':
print(colorText("Please add your API Key to the .env file", "red"))
def tryToReadCSV(csv):
try:
if not os.path.exists(csv):
print(colorText(f"Error: File '{csv}' does not exist.", "red"))
return pd.DataFrame() # Return empty DataFrame if file doesn't exist
df = pd.read_csv(csv)
if df.empty:
print(colorText("Error: CSV file has headers but no data rows.", "red"))
else:
print(colorText(f"Data loaded successfully from {csv}", "green"))
except pd.errors.EmptyDataError:
print(colorText("Notice: CSV file is completely empty (no headers, no data), falling back to empty frame", "white"))
df = pd.DataFrame() # Create an empty DataFrame as fallback
return df
def tryToReadParquet(parquet):
try:
df = pd.read_parquet(parquet)
if df.empty:
print(colorText("Error: Parquet file has headers but no data rows.", "red"))
else:
print(colorText(f"Data loaded successfully from {parquet}", "green"))
except pd.errors.EmptyDataError:
print(colorText("Notice : Parquet file is completely empty (no headers, no data), falling back to empty frame", "white"))
df = pd.DataFrame() # Create an empty DataFrame as fallback
return df
def deduplicate_list(lst):
seen = set()
return [x for x in lst if not (x in seen or seen.add(x))]