326 lines
12 KiB
Python
326 lines
12 KiB
Python
# 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/>.
|
|
|
|
#Local Imports
|
|
import utils.clientfunctions as clientf
|
|
import utils.pathfunctions as pathf
|
|
import utils.policyfunctions as policyf
|
|
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
|
|
|
|
#Standard Libary Imports:
|
|
import json
|
|
import os
|
|
import re
|
|
import time
|
|
import datetime
|
|
|
|
#3rd Party Imports:
|
|
import pandas as pd
|
|
import numpy as np
|
|
import requests
|
|
|
|
def getLocalApprovals(url):
|
|
|
|
endpoint = url + f'/v1/otp/usage'
|
|
payload = {
|
|
"status" : "0"
|
|
}
|
|
|
|
headers = {"X-APIKey": os.getenv('APIKEY')}
|
|
payload = json.dumps(payload)
|
|
|
|
response = requests.post(endpoint, headers=headers, data=payload, verify=False)
|
|
result = json.loads(response.text)
|
|
local_approval = pd.DataFrame(result["response"]["otpusage"])
|
|
if os.path.exists("Local_Approval\\PARQ\\newest_local_approval.parquet"):
|
|
previous_run = pd.read_parquet("Local_Approval\\PARQ\\newest_local_approval.parquet")
|
|
previous_run.to_parquet("Local_Approval\\PARQ\\last_local_approval.parquet", index=False)
|
|
os.remove("Local_Approval\\PARQ\\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:
|
|
ct.style_dataframe_dark(local_approval, f"Local_Approval\\HTML\\newest_local_approval.html")
|
|
local_approval.to_parquet("Local_Approval\\PARQ\\newest_local_approval.parquet", index=False)
|
|
|
|
return local_approval
|
|
|
|
def scheduleAddingLAHashes(url, policy_relationship_map, bad_publisher_list, pups, threat_tolerance_constant):
|
|
try:
|
|
register_function("add_hash", returnFromLocalApproval)
|
|
register_function("move_device", clientf.moveAgentToEnforcement)
|
|
except Exception as e:
|
|
print(f"[ERROR] Failed to register functions: {e}")
|
|
return
|
|
|
|
try:
|
|
approvals_df = getNewLocalApprovals(url)
|
|
if approvals_df.empty:
|
|
print("[INFO] No new local approvals found. Nothing to schedule.")
|
|
return
|
|
batches = approvals_df.groupby('batchid')
|
|
except Exception as e:
|
|
print(f"[ERROR] 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,
|
|
[url, batch_df, policy_relationship_map, bad_publisher_list, pups, threat_tolerance_constant],
|
|
None
|
|
)
|
|
print(f"[INFO] Scheduled add_hash for batch {batchid} at {early_time}")
|
|
except Exception as e:
|
|
print(f"[ERROR] Failed to schedule add_hash for batch {batchid}: {e}")
|
|
|
|
# Schedule move_device jobs
|
|
devices = batch_df['agentid'].drop_duplicates().tolist()
|
|
for device in devices:
|
|
try:
|
|
run_once_job(
|
|
f"move_device_{device}_{batchid}",
|
|
"move_device",
|
|
run_timestamp,
|
|
[url, device, policy_relationship_map],
|
|
None
|
|
)
|
|
print(f"[INFO] Scheduled move_device for device {device} in batch {batchid} at {run_time}")
|
|
except Exception as e:
|
|
print(f"[ERROR] Failed to schedule move_device for device {device} in batch {batchid}: {e}")
|
|
|
|
except Exception as e:
|
|
print(f"[ERROR] Failed to process batch {batchid}: {e}")
|
|
|
|
def returnFromLocalApproval(url, 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 = policyf.getPolicyDataframe(url)
|
|
|
|
# Define policy types
|
|
policy_types = [1, 2, 6, 7]
|
|
|
|
# Define output directories
|
|
parq_base_dir = "Local_Approval\\PARQ\\"
|
|
needappr_base_dir = "Local_Approval\\PARQ\\"
|
|
|
|
# Ensure output directories exist
|
|
os.makedirs(parq_base_dir, exist_ok=True)
|
|
os.makedirs(needappr_base_dir, exist_ok=True)
|
|
|
|
# Build execution history
|
|
policyf.buildExecHistory(
|
|
url,
|
|
policies_in_devicelist,
|
|
parq_base_dir,
|
|
needappr_base_dir,
|
|
policy_types,
|
|
threat_tolerance_constant,
|
|
bad_publisher_list,
|
|
pups,
|
|
)
|
|
|
|
# Read hash data
|
|
unknown_hashes = ct.tryToReadCSV(f"{needappr_base_dir}unknown_hashes.csv")
|
|
good_hashes = ct.tryToReadCSV(f"{needappr_base_dir}good_hashes.csv")
|
|
hashes = pd.concat([unknown_hashes, good_hashes], ignore_index=True)
|
|
|
|
# Process each policy
|
|
for policy in policies_in_devicelist:
|
|
# Filter for matching policy
|
|
matching_rows = all_policies[all_policies['name'] == policy]
|
|
|
|
if matching_rows.empty:
|
|
print(f"Warning: No group ID found for policy '{policy}'. Skipping.")
|
|
continue
|
|
|
|
# Extract group ID
|
|
policy_id = matching_rows['groupid'].values[0]
|
|
|
|
# Map to destination ID
|
|
destination_id = inverse_map.get(policy_id)
|
|
if destination_id is None:
|
|
print(f"Warning: No corresponding enforcement policy found for group ID '{policy_id}'. Skipping.")
|
|
continue
|
|
|
|
allowlist = policyf.getDestAllowlist(url, destination_id)
|
|
|
|
policyf.addHash(url, allowlist, hashes[hashes['group'] == policy_id])
|
|
|
|
def moveToLocalApproval(url, policy_relationship_map):
|
|
|
|
possible_durations = [15, 60, 360, 1440, 10080]
|
|
duration_selected = None
|
|
|
|
print(ct.colorText("Please select a duration:", "white"))
|
|
for i, option in enumerate(possible_durations, start=1):
|
|
print(f"{i}. {option}")
|
|
|
|
try:
|
|
choice = int(input("Enter the number of your choice: "))
|
|
if 1 <= choice <= len(possible_durations):
|
|
duration_selected = possible_durations[choice - 1]
|
|
print(ct.colorText(f"You selected: {duration_selected}", "yellow"))
|
|
else:
|
|
print(ct.colorText("❌ Invalid choice.", "red"))
|
|
return
|
|
except ValueError:
|
|
print(ct.colorText("❌ Invalid input. Please enter a number.", "red"))
|
|
return
|
|
|
|
devicelist = clientf.promptForDevices()
|
|
device_df = clientf.findAgents(url, devicelist, True)
|
|
|
|
batch = int(time.time())
|
|
|
|
if device_df is None or device_df.empty:
|
|
print(ct.colorText("❌ No agents found or error retrieving agents.", "red"))
|
|
return
|
|
|
|
for row in device_df.itertuples(index=False):
|
|
try:
|
|
addLocalApproval(url, batch, duration_selected, row.agentid)
|
|
clientf.moveAgentToAudit(url, row.agentid, policy_relationship_map)
|
|
except Exception as e:
|
|
print(ct.colorText(f"❌ Error processing agent {row.agentid}: {e}", "red"))
|
|
|
|
def addLocalApproval(url, batchid, duration_selected, agentid):
|
|
purpose = f"🎫 Local Approval 🎫 - {duration_selected} mins - batch:{batchid} Client:{agentid}"
|
|
endpoint = url + '/v1/otp/retrieve'
|
|
payload = {
|
|
"duration": str(duration_selected),
|
|
"agentid": str(agentid),
|
|
"purpose": purpose
|
|
}
|
|
|
|
headers = {"X-APIKey": os.getenv('APIKEY')}
|
|
response = requests.post(endpoint, headers=headers, data=json.dumps(payload), verify=False)
|
|
|
|
try:
|
|
result = response.json()
|
|
otpcode = result["response"]["otpcode"]
|
|
print(ct.colorText(f"The OTP code is: {otpcode}", "yellow"))
|
|
except Exception as e:
|
|
print(ct.colorText(f"An unexpected error occurred for agent {agentid}: {str(e)}", "red"))
|
|
|
|
def monitorAuditStatus(url: str, policy_relationship_map: dict):
|
|
|
|
# Simulated current agent list
|
|
current_agent_list = clientf.findAllAgents(url)
|
|
|
|
# Load old agent list
|
|
old_agent_path = "Local_Approval\\PARQ\\last_agent_list.parquet"
|
|
if os.path.exists(old_agent_path):
|
|
old_agent_list = pd.read_parquet(old_agent_path)
|
|
else:
|
|
old_agent_list = pd.DataFrame(columns=current_agent_list.columns)
|
|
|
|
# Merge on hostname
|
|
merged = pd.merge(
|
|
old_agent_list[['hostname', 'groupid']],
|
|
current_agent_list[['hostname', 'groupid']],
|
|
on='hostname',
|
|
how='outer',
|
|
suffixes=('_old', '_current'),
|
|
indicator=True
|
|
)
|
|
|
|
# Reverse map for 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())
|
|
|
|
# 1. Newly added
|
|
newly_added = merged[merged['_merge'] == 'right_only']
|
|
|
|
# 2. Same policy
|
|
same_policy = merged[
|
|
(merged['_merge'] == 'both') &
|
|
(merged['groupid_old'] == merged['groupid_current'])
|
|
]
|
|
|
|
# 3. Moved to audit
|
|
moved_to_audit = merged[
|
|
(merged['_merge'] == 'both') &
|
|
(merged['groupid_old'].isin(policy_relationship_map)) &
|
|
(merged['groupid_current'] == merged['groupid_old'].map(policy_relationship_map))
|
|
]
|
|
|
|
# 4. Moved to enforcement
|
|
moved_to_enforcement = merged[
|
|
(merged['_merge'] == 'both') &
|
|
(merged['groupid_old'].isin(reverse_policy_map)) &
|
|
(merged['groupid_current'] == merged['groupid_old'].map(reverse_policy_map))
|
|
]
|
|
|
|
# 5. Unusual moves
|
|
unusual_move = merged[
|
|
(merged['_merge'] == 'both') &
|
|
(merged['groupid_old'] != merged['groupid_current']) &
|
|
merged.apply(lambda row: (row['groupid_old'], row['groupid_current']) not in known_transitions, axis=1)
|
|
]
|
|
|
|
current_agent_list.to_parquet("Local_Approval\\PARQ\\last_agent_list.parquet", index=False)
|
|
|
|
# Return all five DataFrames
|
|
return newly_added, same_policy, moved_to_audit, moved_to_enforcement, unusual_move
|
|
|
|
def getNewLocalApprovals(url):
|
|
current_la = getLocalApprovals(url)
|
|
|
|
# Load old approval list
|
|
old_la_path = "Local_Approval\\PARQ\\last_la.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 |