mirror of
https://github.com/Pennyw0rth/NetExec
synced 2026-06-06 16:34:30 +00:00
908 lines
37 KiB
Python
Executable File
908 lines
37 KiB
Python
Executable File
import base64
|
|
import warnings
|
|
from datetime import datetime
|
|
from pathlib import Path
|
|
|
|
from sqlalchemy import MetaData, func, Table, select, delete
|
|
from sqlalchemy.dialects.sqlite import Insert # used for upsert
|
|
from sqlalchemy.exc import (
|
|
IllegalStateChangeError,
|
|
NoInspectionAvailable,
|
|
NoSuchTableError,
|
|
)
|
|
from sqlalchemy.exc import SAWarning
|
|
from sqlalchemy.orm import sessionmaker, scoped_session
|
|
|
|
from nxc.logger import nxc_logger
|
|
import sys
|
|
from typing import Optional
|
|
|
|
# if there is an issue with SQLAlchemy and a connection cannot be cleaned up properly it spews out annoying warnings
|
|
warnings.filterwarnings("ignore", category=SAWarning)
|
|
|
|
|
|
class database:
|
|
def __init__(self, db_engine):
|
|
self.HostsTable = None
|
|
self.UsersTable = None
|
|
self.GroupsTable = None
|
|
self.SharesTable = None
|
|
self.AdminRelationsTable = None
|
|
self.GroupRelationsTable = None
|
|
self.LoggedinRelationsTable = None
|
|
self.ConfChecksTable = None
|
|
self.ConfChecksResultsTable = None
|
|
self.DpapiBackupkey = None
|
|
self.DpapiSecrets = None
|
|
|
|
self.db_engine = db_engine
|
|
self.db_path = self.db_engine.url.database
|
|
self.protocol = Path(self.db_path).stem.upper()
|
|
self.metadata = MetaData()
|
|
self.reflect_tables()
|
|
session_factory = sessionmaker(bind=self.db_engine, expire_on_commit=True)
|
|
|
|
Session = scoped_session(session_factory)
|
|
# this is still named "conn" when it is the session object; TODO: rename
|
|
self.conn = Session()
|
|
|
|
@staticmethod
|
|
def db_schema(db_conn):
|
|
db_conn.execute(
|
|
"""CREATE TABLE "hosts" (
|
|
"id" integer PRIMARY KEY,
|
|
"ip" text,
|
|
"hostname" text,
|
|
"domain" text,
|
|
"os" text,
|
|
"dc" boolean,
|
|
"smbv1" boolean,
|
|
"signing" boolean,
|
|
"spooler" boolean,
|
|
"zerologon" boolean,
|
|
"petitpotam" boolean
|
|
)"""
|
|
)
|
|
db_conn.execute(
|
|
"""CREATE TABLE "conf_checks" (
|
|
"id" integer PRIMARY KEY,
|
|
"name" text,
|
|
"description" text
|
|
)"""
|
|
)
|
|
|
|
db_conn.execute(
|
|
"""CREATE TABLE "conf_checks_results" (
|
|
"id" integer PRIMARY KEY,
|
|
"host_id" integer,
|
|
"check_id" integer,
|
|
"secure" boolean,
|
|
"reasons" text,
|
|
FOREIGN KEY(host_id) REFERENCES hosts(id),
|
|
FOREIGN KEY(check_id) REFERENCES conf_checks(id)
|
|
)
|
|
"""
|
|
)
|
|
|
|
db_conn.execute(
|
|
"""CREATE TABLE "users" (
|
|
"id" integer PRIMARY KEY,
|
|
"domain" text,
|
|
"username" text,
|
|
"password" text,
|
|
"credtype" text,
|
|
"pillaged_from_hostid" integer,
|
|
FOREIGN KEY(pillaged_from_hostid) REFERENCES hosts(id)
|
|
)"""
|
|
)
|
|
db_conn.execute(
|
|
"""CREATE TABLE "groups" (
|
|
"id" integer PRIMARY KEY,
|
|
"domain" text,
|
|
"name" text,
|
|
"rid" text,
|
|
"member_count_ad" integer,
|
|
"last_query_time" text
|
|
)"""
|
|
)
|
|
# This table keeps track of which credential has admin access over which machine and vice-versa
|
|
db_conn.execute(
|
|
"""CREATE TABLE "admin_relations" (
|
|
"id" integer PRIMARY KEY,
|
|
"userid" integer,
|
|
"hostid" integer,
|
|
FOREIGN KEY(userid) REFERENCES users(id),
|
|
FOREIGN KEY(hostid) REFERENCES hosts(id)
|
|
)"""
|
|
)
|
|
db_conn.execute(
|
|
"""CREATE TABLE "group_relations" (
|
|
"id" integer PRIMARY KEY,
|
|
"userid" integer,
|
|
"groupid" integer,
|
|
FOREIGN KEY(userid) REFERENCES users(id),
|
|
FOREIGN KEY(groupid) REFERENCES groups(id)
|
|
)"""
|
|
)
|
|
db_conn.execute(
|
|
"""CREATE TABLE "shares" (
|
|
"id" integer PRIMARY KEY,
|
|
"hostid" text,
|
|
"userid" integer,
|
|
"name" text,
|
|
"remark" text,
|
|
"read" boolean,
|
|
"write" boolean,
|
|
FOREIGN KEY(userid) REFERENCES users(id)
|
|
UNIQUE(hostid, userid, name)
|
|
)"""
|
|
)
|
|
db_conn.execute(
|
|
"""CREATE TABLE "loggedin_relations" (
|
|
"id" integer PRIMARY KEY,
|
|
"userid" integer,
|
|
"hostid" integer,
|
|
FOREIGN KEY(userid) REFERENCES users(id),
|
|
FOREIGN KEY(hostid) REFERENCES hosts(id)
|
|
)"""
|
|
)
|
|
db_conn.execute(
|
|
"""CREATE TABLE "dpapi_secrets" (
|
|
"id" integer PRIMARY KEY,
|
|
"host" text,
|
|
"dpapi_type" text,
|
|
"windows_user" text,
|
|
"username" text,
|
|
"password" text,
|
|
"url" text,
|
|
UNIQUE(host, dpapi_type, windows_user, username, password, url)
|
|
)"""
|
|
)
|
|
db_conn.execute(
|
|
"""CREATE TABLE "dpapi_backupkey" (
|
|
"id" integer PRIMARY KEY,
|
|
"domain" text,
|
|
"pvk" text,
|
|
UNIQUE(domain)
|
|
)"""
|
|
)
|
|
# db_conn.execute('''CREATE TABLE "ntds_dumps" (
|
|
# "id" integer PRIMARY KEY,
|
|
# "hostid", integer,
|
|
# "domain" text,
|
|
# "username" text,
|
|
# "hash" text,
|
|
# FOREIGN KEY(hostid) REFERENCES hosts(id)
|
|
# )''')
|
|
|
|
def reflect_tables(self):
|
|
with self.db_engine.connect():
|
|
try:
|
|
self.HostsTable = Table("hosts", self.metadata, autoload_with=self.db_engine)
|
|
self.UsersTable = Table("users", self.metadata, autoload_with=self.db_engine)
|
|
self.GroupsTable = Table("groups", self.metadata, autoload_with=self.db_engine)
|
|
self.SharesTable = Table("shares", self.metadata, autoload_with=self.db_engine)
|
|
self.AdminRelationsTable = Table("admin_relations", self.metadata, autoload_with=self.db_engine)
|
|
self.GroupRelationsTable = Table("group_relations", self.metadata, autoload_with=self.db_engine)
|
|
self.LoggedinRelationsTable = Table("loggedin_relations", self.metadata, autoload_with=self.db_engine)
|
|
self.DpapiSecrets = Table("dpapi_secrets", self.metadata, autoload_with=self.db_engine)
|
|
self.DpapiBackupkey = Table("dpapi_backupkey", self.metadata, autoload_with=self.db_engine)
|
|
self.ConfChecksTable = Table("conf_checks", self.metadata, autoload_with=self.db_engine)
|
|
self.ConfChecksResultsTable = Table("conf_checks_results", self.metadata, autoload_with=self.db_engine)
|
|
except (NoInspectionAvailable, NoSuchTableError):
|
|
print(
|
|
f"""
|
|
[-] Error reflecting tables for the {self.protocol} protocol - this means there is a DB schema mismatch
|
|
[-] This is probably because a newer version of nxc is being run on an old DB schema
|
|
[-] Optionally save the old DB data (`cp {self.db_path} ~/nxc_{self.protocol.lower()}.bak`)
|
|
[-] Then remove the {self.protocol} DB (`rm -f {self.db_path}`) and run nxc to initialize the new DB"""
|
|
)
|
|
sys.exit()
|
|
|
|
def shutdown_db(self):
|
|
try:
|
|
self.conn.close()
|
|
# due to the async nature of nxc, sometimes session state is a bit messy and this will throw:
|
|
# Method 'close()' can't be called here; method '_connection_for_bind()' is already in progress and
|
|
# this would cause an unexpected state change to <SessionTransactionState.CLOSED: 5>
|
|
except IllegalStateChangeError as e:
|
|
nxc_logger.debug(f"Error while closing session db object: {e}")
|
|
|
|
def clear_database(self):
|
|
for table in self.metadata.sorted_tables:
|
|
self.conn.execute(table.delete())
|
|
|
|
# pull/545
|
|
def add_host(
|
|
self,
|
|
ip,
|
|
hostname,
|
|
domain,
|
|
os,
|
|
smbv1,
|
|
signing,
|
|
spooler=None,
|
|
zerologon=None,
|
|
petitpotam=None,
|
|
dc=None,
|
|
):
|
|
"""Check if this host has already been added to the database, if not, add it in."""
|
|
hosts = []
|
|
updated_ids = []
|
|
|
|
q = select(self.HostsTable).filter(self.HostsTable.c.ip == ip)
|
|
results = self.conn.execute(q).all()
|
|
|
|
# create new host
|
|
if not results:
|
|
new_host = {
|
|
"ip": ip,
|
|
"hostname": hostname,
|
|
"domain": domain,
|
|
"os": os if os is not None else "",
|
|
"dc": dc,
|
|
"smbv1": smbv1,
|
|
"signing": signing,
|
|
"spooler": spooler,
|
|
"zerologon": zerologon,
|
|
"petitpotam": petitpotam,
|
|
}
|
|
hosts = [new_host]
|
|
# update existing hosts data
|
|
else:
|
|
for host in results:
|
|
host_data = host._asdict()
|
|
# only update column if it is being passed in
|
|
if ip is not None:
|
|
host_data["ip"] = ip
|
|
if hostname is not None:
|
|
host_data["hostname"] = hostname
|
|
if domain is not None:
|
|
host_data["domain"] = domain
|
|
if os is not None:
|
|
host_data["os"] = os
|
|
if smbv1 is not None:
|
|
host_data["smbv1"] = smbv1
|
|
if signing is not None:
|
|
host_data["signing"] = signing
|
|
if spooler is not None:
|
|
host_data["spooler"] = spooler
|
|
if zerologon is not None:
|
|
host_data["zerologon"] = zerologon
|
|
if petitpotam is not None:
|
|
host_data["petitpotam"] = petitpotam
|
|
if dc is not None:
|
|
host_data["dc"] = dc
|
|
# only add host to be updated if it has changed
|
|
if host_data not in hosts:
|
|
hosts.append(host_data)
|
|
updated_ids.append(host_data["id"])
|
|
nxc_logger.debug(f"Update Hosts: {hosts}")
|
|
|
|
# TODO: find a way to abstract this away to a single Upsert call
|
|
q = Insert(self.HostsTable) # .returning(self.HostsTable.c.id)
|
|
update_columns = {col.name: col for col in q.excluded if col.name not in "id"}
|
|
q = q.on_conflict_do_update(index_elements=self.HostsTable.primary_key, set_=update_columns)
|
|
|
|
self.conn.execute(q, hosts) # .scalar()
|
|
# we only return updated IDs for now - when RETURNING clause is allowed we can return inserted
|
|
if updated_ids:
|
|
nxc_logger.debug(f"add_host() - Host IDs Updated: {updated_ids}")
|
|
return updated_ids
|
|
|
|
def add_credential(self, credtype, domain, username, password, group_id=None, pillaged_from=None):
|
|
"""Check if this credential has already been added to the database, if not add it in."""
|
|
credentials = []
|
|
groups = []
|
|
|
|
if (group_id and not self.is_group_valid(group_id)) or (pillaged_from and not self.is_host_valid(pillaged_from)):
|
|
nxc_logger.debug("Invalid group or host")
|
|
return
|
|
|
|
q = select(self.UsersTable).filter(
|
|
func.lower(self.UsersTable.c.domain) == func.lower(domain),
|
|
func.lower(self.UsersTable.c.username) == func.lower(username),
|
|
func.lower(self.UsersTable.c.credtype) == func.lower(credtype),
|
|
)
|
|
results = self.conn.execute(q).all()
|
|
|
|
# add new credential
|
|
if not results:
|
|
new_cred = {
|
|
"credtype": credtype,
|
|
"domain": domain,
|
|
"username": username,
|
|
"password": password,
|
|
"groupid": group_id,
|
|
"pillaged_from": pillaged_from,
|
|
}
|
|
credentials = [new_cred]
|
|
# update existing cred data
|
|
else:
|
|
for creds in results:
|
|
# this will include the id, so we don't touch it
|
|
cred_data = creds._asdict()
|
|
# only update column if it is being passed in
|
|
if credtype is not None:
|
|
cred_data["credtype"] = credtype
|
|
if domain is not None:
|
|
cred_data["domain"] = domain
|
|
if username is not None:
|
|
cred_data["username"] = username
|
|
if password is not None:
|
|
cred_data["password"] = password
|
|
if group_id is not None:
|
|
cred_data["groupid"] = group_id
|
|
groups.append({"userid": cred_data["id"], "groupid": group_id})
|
|
if pillaged_from is not None:
|
|
cred_data["pillaged_from"] = pillaged_from
|
|
# only add cred to be updated if it has changed
|
|
if cred_data not in credentials:
|
|
credentials.append(cred_data)
|
|
|
|
# TODO: find a way to abstract this away to a single Upsert call
|
|
q_users = Insert(self.UsersTable) # .returning(self.UsersTable.c.id)
|
|
update_columns_users = {col.name: col for col in q_users.excluded if col.name not in "id"}
|
|
q_users = q_users.on_conflict_do_update(index_elements=self.UsersTable.primary_key, set_=update_columns_users)
|
|
nxc_logger.debug(f"Adding credentials: {credentials}")
|
|
|
|
self.conn.execute(q_users, credentials) # .scalar()
|
|
|
|
if groups:
|
|
q_groups = Insert(self.GroupRelationsTable)
|
|
|
|
self.conn.execute(q_groups, groups)
|
|
|
|
def remove_credentials(self, creds_id):
|
|
"""Removes a credential ID from the database"""
|
|
del_hosts = []
|
|
for cred_id in creds_id:
|
|
q = delete(self.UsersTable).filter(self.UsersTable.c.id == cred_id)
|
|
del_hosts.append(q)
|
|
self.conn.execute(q)
|
|
|
|
def add_admin_user(self, credtype, domain, username, password, host, user_id=None):
|
|
add_links = []
|
|
|
|
creds_q = select(self.UsersTable)
|
|
creds_q = creds_q.filter(self.UsersTable.c.id == user_id) if user_id else creds_q.filter(func.lower(self.UsersTable.c.credtype) == func.lower(credtype), func.lower(self.UsersTable.c.domain) == func.lower(domain), func.lower(self.UsersTable.c.username) == func.lower(username), self.UsersTable.c.password == password)
|
|
users = self.conn.execute(creds_q)
|
|
hosts = self.get_hosts(host)
|
|
|
|
if users and hosts:
|
|
for user, host in zip(users, hosts):
|
|
user_id = user[0]
|
|
host_id = host[0]
|
|
link = {"userid": user_id, "hostid": host_id}
|
|
admin_relations_select = select(self.AdminRelationsTable).filter(
|
|
self.AdminRelationsTable.c.userid == user_id,
|
|
self.AdminRelationsTable.c.hostid == host_id,
|
|
)
|
|
links = self.conn.execute(admin_relations_select).all()
|
|
|
|
if not links:
|
|
add_links.append(link)
|
|
|
|
admin_relations_insert = Insert(self.AdminRelationsTable)
|
|
|
|
if add_links:
|
|
self.conn.execute(admin_relations_insert, add_links)
|
|
|
|
def get_admin_relations(self, user_id=None, host_id=None):
|
|
if user_id:
|
|
q = select(self.AdminRelationsTable).filter(self.AdminRelationsTable.c.userid == user_id)
|
|
elif host_id:
|
|
q = select(self.AdminRelationsTable).filter(self.AdminRelationsTable.c.hostid == host_id)
|
|
else:
|
|
q = select(self.AdminRelationsTable)
|
|
|
|
return self.conn.execute(q).all()
|
|
|
|
def remove_admin_relation(self, user_ids=None, host_ids=None):
|
|
q = delete(self.AdminRelationsTable)
|
|
if user_ids:
|
|
for user_id in user_ids:
|
|
q = q.filter(self.AdminRelationsTable.c.userid == user_id)
|
|
elif host_ids:
|
|
for host_id in host_ids:
|
|
q = q.filter(self.AdminRelationsTable.c.hostid == host_id)
|
|
self.conn.execute(q)
|
|
|
|
def is_credential_valid(self, credential_id):
|
|
"""Check if this credential ID is valid."""
|
|
q = select(self.UsersTable).filter(
|
|
self.UsersTable.c.id == credential_id,
|
|
self.UsersTable.c.password is not None,
|
|
)
|
|
results = self.conn.execute(q).all()
|
|
return len(results) > 0
|
|
|
|
def get_credentials(self, filter_term=None, cred_type=None):
|
|
"""Return credentials from the database."""
|
|
# if we're returning a single credential by ID
|
|
if self.is_credential_valid(filter_term):
|
|
q = select(self.UsersTable).filter(self.UsersTable.c.id == filter_term)
|
|
elif cred_type:
|
|
q = select(self.UsersTable).filter(self.UsersTable.c.credtype == cred_type)
|
|
# if we're filtering by username
|
|
elif filter_term and filter_term != "":
|
|
like_term = func.lower(f"%{filter_term}%")
|
|
q = select(self.UsersTable).filter(func.lower(self.UsersTable.c.username).like(like_term))
|
|
# otherwise return all credentials
|
|
else:
|
|
q = select(self.UsersTable)
|
|
|
|
return self.conn.execute(q).all()
|
|
|
|
def get_credential(self, cred_type, domain, username, password):
|
|
q = select(self.UsersTable).filter(
|
|
self.UsersTable.c.domain == domain,
|
|
self.UsersTable.c.username == username,
|
|
self.UsersTable.c.password == password,
|
|
self.UsersTable.c.credtype == cred_type,
|
|
)
|
|
results = self.conn.execute(q).first()
|
|
return results.id
|
|
|
|
def is_credential_local(self, credential_id):
|
|
q = select(self.UsersTable.c.domain).filter(self.UsersTable.c.id == credential_id)
|
|
user_domain = self.conn.execute(q).all()
|
|
|
|
if user_domain:
|
|
q = select(self.HostsTable).filter(func.lower(self.HostsTable.c.id) == func.lower(user_domain))
|
|
results = self.conn.execute(q).all()
|
|
return len(results) > 0
|
|
|
|
def is_host_valid(self, host_id):
|
|
"""Check if this host ID is valid."""
|
|
q = select(self.HostsTable).filter(self.HostsTable.c.id == host_id)
|
|
results = self.conn.execute(q).all()
|
|
return len(results) > 0
|
|
|
|
def get_hosts(self, filter_term=None, domain=None):
|
|
"""Return hosts from the database."""
|
|
q = select(self.HostsTable)
|
|
|
|
# if we're returning a single host by ID
|
|
if self.is_host_valid(filter_term):
|
|
q = q.filter(self.HostsTable.c.id == filter_term)
|
|
results = self.conn.execute(q).first()
|
|
# all() returns a list, so we keep the return format the same so consumers don't have to guess
|
|
return [results]
|
|
# if we're filtering by domain controllers
|
|
elif filter_term == "dc":
|
|
q = q.filter(self.HostsTable.c.dc is True)
|
|
if domain:
|
|
q = q.filter(func.lower(self.HostsTable.c.domain) == func.lower(domain))
|
|
elif filter_term == "signing":
|
|
# generally we want hosts that are vulnerable, so signing disabled
|
|
q = q.filter(self.HostsTable.c.signing is False)
|
|
elif filter_term == "spooler":
|
|
q = q.filter(self.HostsTable.c.spooler is True)
|
|
elif filter_term == "zerologon":
|
|
q = q.filter(self.HostsTable.c.zerologon is True)
|
|
elif filter_term == "petitpotam":
|
|
q = q.filter(self.HostsTable.c.petitpotam is True)
|
|
elif filter_term is not None and filter_term.startswith("domain"):
|
|
domain = filter_term.split()[1]
|
|
like_term = func.lower(f"%{domain}%")
|
|
q = q.filter(self.HostsTable.c.domain.like(like_term))
|
|
# if we're filtering by ip/hostname
|
|
elif filter_term and filter_term != "":
|
|
like_term = func.lower(f"%{filter_term}%")
|
|
q = q.filter(self.HostsTable.c.ip.like(like_term) | func.lower(self.HostsTable.c.hostname).like(like_term))
|
|
results = self.conn.execute(q).all()
|
|
nxc_logger.debug(f"smb hosts() - results: {results}")
|
|
return results
|
|
|
|
def is_group_valid(self, group_id):
|
|
"""Check if this group ID is valid."""
|
|
q = select(self.GroupsTable).filter(self.GroupsTable.c.id == group_id)
|
|
results = self.conn.execute(q).first()
|
|
|
|
valid = bool(results)
|
|
nxc_logger.debug(f"is_group_valid(groupID={group_id}) => {valid}")
|
|
|
|
return valid
|
|
|
|
def add_group(self, domain, name, rid=None, member_count_ad=None):
|
|
results = self.get_groups(group_name=name, group_domain=domain)
|
|
|
|
groups = []
|
|
updated_ids = []
|
|
|
|
group_data = {
|
|
"domain": domain,
|
|
"name": name,
|
|
"rid": rid,
|
|
"member_count_ad": member_count_ad,
|
|
"last_query_time": None,
|
|
}
|
|
|
|
if not results:
|
|
if member_count_ad is not None:
|
|
group_data["member_count_ad"] = member_count_ad
|
|
today = datetime.now()
|
|
iso_date = today.isoformat()
|
|
group_data["last_query_time"] = iso_date
|
|
groups = [group_data]
|
|
|
|
# insert the group and get the returned id right away, this can be refactored when we can use RETURNING
|
|
q = Insert(self.GroupsTable)
|
|
|
|
self.conn.execute(q, groups)
|
|
new_group_data = self.get_groups(group_name=group_data["name"], group_domain=group_data["domain"])
|
|
returned_id = [new_group_data[0].id]
|
|
nxc_logger.debug(f"Inserted group with ID: {returned_id[0]}")
|
|
return returned_id
|
|
else:
|
|
for group in results:
|
|
g_data = group._asdict()
|
|
if domain is not None:
|
|
g_data["domain"] = domain
|
|
if name is not None:
|
|
g_data["name"] = name
|
|
if rid is not None:
|
|
g_data["rid"] = rid
|
|
if member_count_ad is not None:
|
|
g_data["member_count_ad"] = member_count_ad
|
|
today = datetime.now()
|
|
iso_date = today.isoformat()
|
|
g_data["last_query_time"] = iso_date
|
|
# only add it to the upsert query if it's changed to save query execution time
|
|
if g_data not in groups:
|
|
groups.append(g_data)
|
|
updated_ids.append(g_data["id"])
|
|
|
|
nxc_logger.debug(f"Update Groups: {groups}")
|
|
|
|
# TODO: find a way to abstract this away to a single Upsert call
|
|
q = Insert(self.GroupsTable) # .returning(self.GroupsTable.c.id)
|
|
update_columns = {col.name: col for col in q.excluded if col.name not in "id"}
|
|
q = q.on_conflict_do_update(index_elements=self.GroupsTable.primary_key, set_=update_columns)
|
|
|
|
self.conn.execute(q, groups)
|
|
# TODO: always return a list and fix code references to not expect a single integer
|
|
#
|
|
if updated_ids:
|
|
nxc_logger.debug(f"Updated groups with IDs: {updated_ids}")
|
|
return updated_ids
|
|
|
|
def get_groups(self, filter_term=None, group_name=None, group_domain=None):
|
|
"""Return groups from the database"""
|
|
if filter_term and self.is_group_valid(filter_term):
|
|
q = select(self.GroupsTable).filter(self.GroupsTable.c.id == filter_term)
|
|
results = self.conn.execute(q).first()
|
|
# all() returns a list, so we keep the return format the same so consumers don't have to guess
|
|
return [results]
|
|
elif group_name and group_domain:
|
|
q = select(self.GroupsTable).filter(
|
|
func.lower(self.GroupsTable.c.name) == func.lower(group_name),
|
|
func.lower(self.GroupsTable.c.domain) == func.lower(group_domain),
|
|
)
|
|
elif filter_term and filter_term != "":
|
|
like_term = func.lower(f"%{filter_term}%")
|
|
q = select(self.GroupsTable).filter(self.GroupsTable.c.name.like(like_term))
|
|
else:
|
|
q = select(self.GroupsTable).filter()
|
|
|
|
results = self.conn.execute(q).all()
|
|
|
|
nxc_logger.debug(f"get_groups(filter_term={filter_term}, groupName={group_name}, groupDomain={group_domain}) => {results}")
|
|
return results
|
|
|
|
def get_group_relations(self, user_id=None, group_id=None):
|
|
if user_id and group_id:
|
|
q = select(self.GroupRelationsTable).filter(
|
|
self.GroupRelationsTable.c.id == user_id,
|
|
self.GroupRelationsTable.c.groupid == group_id,
|
|
)
|
|
elif user_id:
|
|
q = select(self.GroupRelationsTable).filter(self.GroupRelationsTable.c.id == user_id)
|
|
elif group_id:
|
|
q = select(self.GroupRelationsTable).filter(self.GroupRelationsTable.c.groupid == group_id)
|
|
|
|
return self.conn.execute(q).all()
|
|
|
|
def remove_group_relations(self, user_id=None, group_id=None):
|
|
q = delete(self.GroupRelationsTable)
|
|
if user_id:
|
|
q = q.filter(self.GroupRelationsTable.c.userid == user_id)
|
|
elif group_id:
|
|
q = q.filter(self.GroupRelationsTable.c.groupid == group_id)
|
|
self.conn.execute(q)
|
|
|
|
def is_user_valid(self, user_id):
|
|
"""Check if this User ID is valid."""
|
|
q = select(self.UsersTable).filter(self.UsersTable.c.id == user_id)
|
|
results = self.conn.execute(q).all()
|
|
return len(results) > 0
|
|
|
|
def get_users(self, filter_term=None):
|
|
q = select(self.UsersTable)
|
|
|
|
if self.is_user_valid(filter_term):
|
|
q = q.filter(self.UsersTable.c.id == filter_term)
|
|
# if we're filtering by username
|
|
elif filter_term and filter_term != "":
|
|
like_term = func.lower(f"%{filter_term}%")
|
|
q = q.filter(func.lower(self.UsersTable.c.username).like(like_term))
|
|
return self.conn.execute(q).all()
|
|
|
|
def get_user(self, domain, username):
|
|
q = select(self.UsersTable).filter(
|
|
func.lower(self.UsersTable.c.domain) == func.lower(domain),
|
|
func.lower(self.UsersTable.c.username) == func.lower(username),
|
|
)
|
|
return self.conn.execute(q).all()
|
|
|
|
def get_domain_controllers(self, domain=None):
|
|
return self.get_hosts(filter_term="dc", domain=domain)
|
|
|
|
def is_share_valid(self, share_id):
|
|
"""Check if this share ID is valid."""
|
|
q = select(self.SharesTable).filter(self.SharesTable.c.id == share_id)
|
|
results = self.conn.execute(q).all()
|
|
|
|
nxc_logger.debug(f"is_share_valid(shareID={share_id}) => {len(results) > 0}")
|
|
return len(results) > 0
|
|
|
|
def add_share(self, host_id, user_id, name, remark, read, write):
|
|
share_data = {
|
|
"hostid": host_id,
|
|
"userid": user_id,
|
|
"name": name,
|
|
"remark": remark,
|
|
"read": read,
|
|
"write": write,
|
|
}
|
|
self.conn.execute(
|
|
Insert(self.SharesTable).on_conflict_do_nothing(), # .returning(self.SharesTable.c.id),
|
|
share_data,
|
|
) # .scalar_one()
|
|
|
|
def get_shares(self, filter_term=None):
|
|
if self.is_share_valid(filter_term):
|
|
q = select(self.SharesTable).filter(self.SharesTable.c.id == filter_term)
|
|
elif filter_term:
|
|
like_term = func.lower(f"%{filter_term}%")
|
|
q = select(self.SharesTable).filter(self.SharesTable.c.name.like(like_term))
|
|
else:
|
|
q = select(self.SharesTable)
|
|
return self.conn.execute(q).all()
|
|
|
|
def get_shares_by_access(self, permissions, share_id=None):
|
|
permissions = permissions.lower()
|
|
q = select(self.SharesTable)
|
|
if share_id:
|
|
q = q.filter(self.SharesTable.c.id == share_id)
|
|
if "r" in permissions:
|
|
q = q.filter(self.SharesTable.c.read == 1)
|
|
if "w" in permissions:
|
|
q = q.filter(self.SharesTable.c.write == 1)
|
|
return self.conn.execute(q).all()
|
|
|
|
def get_users_with_share_access(self, host_id, share_name, permissions):
|
|
permissions = permissions.lower()
|
|
q = select(self.SharesTable.c.userid).filter(self.SharesTable.c.name == share_name, self.SharesTable.c.hostid == host_id)
|
|
if "r" in permissions:
|
|
q = q.filter(self.SharesTable.c.read == 1)
|
|
if "w" in permissions:
|
|
q = q.filter(self.SharesTable.c.write == 1)
|
|
return self.conn.execute(q).all()
|
|
|
|
|
|
def add_domain_backupkey(self, domain: str, pvk: bytes):
|
|
"""
|
|
Add domain backupkey
|
|
:domain is the domain fqdn
|
|
:pvk is the domain backupkey
|
|
"""
|
|
q = select(self.DpapiBackupkey).filter(func.lower(self.DpapiBackupkey.c.domain) == func.lower(domain))
|
|
results = self.conn.execute(q).all()
|
|
|
|
if not len(results):
|
|
pvk_encoded = base64.b64encode(pvk)
|
|
backup_key = {"domain": domain, "pvk": pvk_encoded}
|
|
try:
|
|
# TODO: find a way to abstract this away to a single Upsert call
|
|
q = Insert(self.DpapiBackupkey) # .returning(self.DpapiBackupkey.c.id)
|
|
|
|
self.conn.execute(q, [backup_key]) # .scalar()
|
|
nxc_logger.debug(f"add_domain_backupkey(domain={domain}, pvk={pvk_encoded})")
|
|
except Exception as e:
|
|
nxc_logger.debug(f"Issue while inserting DPAPI Backup Key: {e}")
|
|
|
|
def get_domain_backupkey(self, domain: Optional[str] = None):
|
|
"""
|
|
Get domain backupkey
|
|
:domain is the domain fqdn
|
|
"""
|
|
q = select(self.DpapiBackupkey)
|
|
if domain is not None:
|
|
q = q.filter(func.lower(self.DpapiBackupkey.c.domain) == func.lower(domain))
|
|
results = self.conn.execute(q).all()
|
|
|
|
nxc_logger.debug(f"get_domain_backupkey(domain={domain}) => {results}")
|
|
|
|
if len(results) > 0:
|
|
results = [(id_key, domain, base64.b64decode(pvk)) for id_key, domain, pvk in results]
|
|
return results
|
|
|
|
def is_dpapi_secret_valid(self, dpapi_secret_id):
|
|
"""
|
|
Check if this group ID is valid.
|
|
:dpapi_secret_id is a primary id
|
|
"""
|
|
q = select(self.DpapiSecrets).filter(func.lower(self.DpapiSecrets.c.id) == dpapi_secret_id)
|
|
results = self.conn.execute(q).first()
|
|
valid = results is not None
|
|
nxc_logger.debug(f"is_dpapi_secret_valid(groupID={dpapi_secret_id}) => {valid}")
|
|
return valid
|
|
|
|
def add_dpapi_secrets(
|
|
self,
|
|
host: str,
|
|
dpapi_type: str,
|
|
windows_user: str,
|
|
username: str,
|
|
password: str,
|
|
url: str = "",
|
|
):
|
|
"""Add dpapi secrets to nxcdb"""
|
|
secret = {
|
|
"host": host,
|
|
"dpapi_type": dpapi_type,
|
|
"windows_user": windows_user,
|
|
"username": username,
|
|
"password": password,
|
|
"url": url,
|
|
}
|
|
q = Insert(self.DpapiSecrets).on_conflict_do_nothing() # .returning(self.DpapiSecrets.c.id)
|
|
|
|
self.conn.execute(q, [secret]) # .scalar()
|
|
|
|
|
|
nxc_logger.debug(f"add_dpapi_secrets(host={host}, dpapi_type={dpapi_type}, windows_user={windows_user}, username={username}, password={password}, url={url})")
|
|
|
|
def get_dpapi_secrets(
|
|
self,
|
|
filter_term=None,
|
|
host: Optional[str] = None,
|
|
dpapi_type: Optional[str] = None,
|
|
windows_user: Optional[str] = None,
|
|
username: Optional[str] = None,
|
|
url: Optional[str] = None,
|
|
):
|
|
"""Get dpapi secrets from nxcdb"""
|
|
q = select(self.DpapiSecrets)
|
|
|
|
if self.is_dpapi_secret_valid(filter_term):
|
|
q = q.filter(self.DpapiSecrets.c.id == filter_term)
|
|
results = self.conn.execute(q).first()
|
|
# all() returns a list, so we keep the return format the same so consumers don't have to guess
|
|
return [results]
|
|
elif host:
|
|
q = q.filter(self.DpapiSecrets.c.host == host)
|
|
results = self.conn.execute(q).first()
|
|
# all() returns a list, so we keep the return format the same so consumers don't have to guess
|
|
return [results]
|
|
elif dpapi_type:
|
|
q = q.filter(func.lower(self.DpapiSecrets.c.dpapi_type) == func.lower(dpapi_type))
|
|
elif windows_user:
|
|
like_term = func.lower(f"%{windows_user}%")
|
|
q = q.filter(func.lower(self.DpapiSecrets.c.windows_user).like(like_term))
|
|
elif username:
|
|
like_term = func.lower(f"%{username}%")
|
|
q = q.filter(func.lower(self.DpapiSecrets.c.windows_user).like(like_term))
|
|
elif url:
|
|
q = q.filter(func.lower(self.DpapiSecrets.c.url) == func.lower(url))
|
|
results = self.conn.execute(q).all()
|
|
|
|
nxc_logger.debug(f"get_dpapi_secrets(filter_term={filter_term}, host={host}, dpapi_type={dpapi_type}, windows_user={windows_user}, username={username}, url={url}) => {results}")
|
|
return results
|
|
|
|
def add_loggedin_relation(self, user_id, host_id):
|
|
relation_query = select(self.LoggedinRelationsTable).filter(
|
|
self.LoggedinRelationsTable.c.userid == user_id,
|
|
self.LoggedinRelationsTable.c.hostid == host_id,
|
|
)
|
|
results = self.conn.execute(relation_query).all()
|
|
|
|
# only add one if one doesn't already exist
|
|
if not results:
|
|
relation = {"userid": user_id, "hostid": host_id}
|
|
try:
|
|
nxc_logger.debug(f"Inserting loggedin_relations: {relation}")
|
|
# TODO: find a way to abstract this away to a single Upsert call
|
|
q = Insert(self.LoggedinRelationsTable) # .returning(self.LoggedinRelationsTable.c.id)
|
|
|
|
self.conn.execute(q, [relation]) # .scalar()
|
|
inserted_id_results = self.get_loggedin_relations(user_id, host_id)
|
|
nxc_logger.debug(f"Checking if relation was added: {inserted_id_results}")
|
|
return inserted_id_results[0].id
|
|
except Exception as e:
|
|
nxc_logger.debug(f"Error inserting LoggedinRelation: {e}")
|
|
|
|
def get_loggedin_relations(self, user_id=None, host_id=None):
|
|
q = select(self.LoggedinRelationsTable) # .returning(self.LoggedinRelationsTable.c.id)
|
|
if user_id:
|
|
q = q.filter(self.LoggedinRelationsTable.c.userid == user_id)
|
|
if host_id:
|
|
q = q.filter(self.LoggedinRelationsTable.c.hostid == host_id)
|
|
return self.conn.execute(q).all()
|
|
|
|
def remove_loggedin_relations(self, user_id=None, host_id=None):
|
|
q = delete(self.LoggedinRelationsTable)
|
|
if user_id:
|
|
q = q.filter(self.LoggedinRelationsTable.c.userid == user_id)
|
|
elif host_id:
|
|
q = q.filter(self.LoggedinRelationsTable.c.hostid == host_id)
|
|
self.conn.execute(q)
|
|
|
|
def get_checks(self):
|
|
q = select(self.ConfChecksTable)
|
|
return self.conn.execute(q).all()
|
|
|
|
def get_check_results(self):
|
|
q = select(self.ConfChecksResultsTable)
|
|
return self.conn.execute(q).all()
|
|
|
|
def insert_data(self, table, select_results=None, **new_row):
|
|
"""
|
|
Insert a new row in the given table.
|
|
Basically it's just a more generic version of add_host
|
|
"""
|
|
if select_results is None:
|
|
select_results = []
|
|
results = []
|
|
updated_ids = []
|
|
|
|
# Create new row
|
|
if not select_results:
|
|
results = [new_row]
|
|
# Update existing row data
|
|
else:
|
|
for row in select_results:
|
|
row_data = row._asdict()
|
|
for column, value in new_row.items():
|
|
row_data[column] = value
|
|
|
|
# Only add data to be updated if it has changed
|
|
if row_data not in results:
|
|
results.append(row_data)
|
|
updated_ids.append(row_data["id"])
|
|
|
|
nxc_logger.debug(f"Update data: {results}")
|
|
# TODO: find a way to abstract this away to a single Upsert call
|
|
q = Insert(table) # .returning(table.c.id)
|
|
update_column = {col.name: col for col in q.excluded if col.name not in "id"}
|
|
q = q.on_conflict_do_update(index_elements=table.primary_key, set_=update_column)
|
|
self.conn.execute(q, results) # .scalar()
|
|
# we only return updated IDs for now - when RETURNING clause is allowed we can return inserted
|
|
return updated_ids
|
|
|
|
def add_check(self, name, description):
|
|
"""Check if this check item has already been added to the database, if not, add it in."""
|
|
q = select(self.ConfChecksTable).filter(self.ConfChecksTable.c.name == name)
|
|
select_results = self.conn.execute(q).all()
|
|
context = locals()
|
|
new_row = {column: context[column] for column in ("name", "description")}
|
|
updated_ids = self.insert_data(self.ConfChecksTable, select_results, **new_row)
|
|
|
|
if updated_ids:
|
|
nxc_logger.debug(f"add_check() - Checks IDs Updated: {updated_ids}")
|
|
return updated_ids
|
|
|
|
def add_check_result(self, host_id, check_id, secure, reasons):
|
|
"""Check if this check result has already been added to the database, if not, add it in."""
|
|
q = select(self.ConfChecksResultsTable).filter(self.ConfChecksResultsTable.c.host_id == host_id, self.ConfChecksResultsTable.c.check_id == check_id)
|
|
select_results = self.conn.execute(q).all()
|
|
context = locals()
|
|
new_row = {column: context[column] for column in ("host_id", "check_id", "secure", "reasons")}
|
|
updated_ids = self.insert_data(self.ConfChecksResultsTable, select_results, **new_row)
|
|
|
|
if updated_ids:
|
|
nxc_logger.debug(f"add_check_result() - Check Results IDs Updated: {updated_ids}")
|
|
return updated_ids
|