Skip to content
Snippets Groups Projects
Commit 15bc0b1d authored by Daniele Nicolodi's avatar Daniele Nicolodi
Browse files

vorhabenACL: New tool to verify ACL set on folders in 4-3-Vorhaben

parent 04cfe00a
No related branches found
No related tags found
No related merge requests found
import os
import pathlib
import re
import subprocess
import sys
from ctypes import windll, byref
from ctypes.wintypes import DWORD
import click
import openpyxl
PATH = r'o:\4-3\4-3-Vorhaben'
DATA = r'o:\4-3\4-3-Vorhaben\Zugriffsliste.xlsx'
IDS = {
'luecke': 2,
'klose': 5,
'knigge': 2,
}
PHONETICS = str.maketrans({'ö': 'oe', 'ü': 'ue', 'ß': 'ss'})
def setup_console():
# Enable ANSI escape sequences.
kernel = windll.kernel32
mode = DWORD()
STD_OUTPUT_HANDLE = -11
ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004
stdout = kernel.GetStdHandle(STD_OUTPUT_HANDLE)
kernel.GetConsoleMode(stdout, byref(mode))
kernel.SetConsoleMode(stdout, mode.value | ENABLE_VIRTUAL_TERMINAL_PROCESSING)
class style:
black = '\033[30m'
red = '\033[31m'
green = '\033[32m'
yellow = '\033[33m'
blue = '\033[34m'
magenta = '\033[35m'
cyan = '\033[36m'
white = '\033[37m'
reset = '\033[39m'
bright_black = '\033[90m'
bright_red = '\033[91m'
bright_green = '\033[92m'
bright_yellow = '\033[93m'
bright_blue = '\033[94m'
bright_magenta = '\033[95m'
bright_cyan = '\033[96m'
bright_white = '\033[97m'
bold = '\033[1m'
dim = '\033[2m'
normal = '\033[22m'
reset = '\033[0m'
# ICACLS preserves the canonical ordering of ACE entries:
# Explicit denials
# Explicit grants
# Inherited denials
# Inherited grants
#
# perm is a permission mask and can be specified in one of two forms:
# a sequence of simple rights:
# N - no access
# F - full access
# M - modify access
# RX - read and execute access
# R - read-only access
# W - write-only access
# D - delete access
# a comma-separated list in parentheses of specific rights:
# DE - delete
# RC - read control
# WDAC - write DAC
# WO - write owner
# S - synchronize
# AS - access system security
# MA - maximum allowed
# GR - generic read
# GW - generic write
# GE - generic execute
# GA - generic all
# RD - read data/list directory
# WD - write data/add file
# AD - append data/add subdirectory
# REA - read extended attributes
# WEA - write extended attributes
# X - execute/traverse
# DC - delete child
# RA - read attributes
# WA - write attributes
# inheritance rights may precede either form and are applied
# only to directories:
# (OI) - object inherit
# (CI) - container inherit
# (IO) - inherit only
# (NP) - don't propagate inherit
# (I) - permission inherited from parent container
def get_acl(path):
r = subprocess.run(['icacls', os.fspath(path)], check=True, capture_output=True, text=True)
acl = {}
# Parse command output.
lines = [line for line in r.stdout.splitlines() if line.strip()]
m = re.match(r'^Successfully processed ([0-9]+) files; Failed processing ([0-9]+) files$', lines[-1])
if not m or int(m.group(1)) != 1 or int(m.group(2)) != 0:
raise RuntimeError(line)
# icacls uses the most stupid output format ever. This is the only
# way I found to reliebly enough split the path prefix from the
# ACL listing.
assert len(lines) > 2
l = re.match(r' +', lines[-2]).end()
for line in lines[:-1]:
user, permissions = line[l:].split(':')
acl[user] = permissions
return acl
def parse_permissions(s):
groups = re.findall(r'\([A-Z,]+\)', s)
*inheritance, permissions = groups
inherited = inheritance and inheritance[0] == '(I)'
return inherited, tuple(permissions[1:-1].split(','))
def get_group_members(group):
r = subprocess.run(['net', 'localgroup', '/domain', group], check=False, capture_output=True, text=True)
if r.returncode == 2:
return
lines = iter(r.stdout.splitlines())
# Skip header.
for line in lines:
if re.match(r'^-+$', line):
break
# Collect group members.
for line in lines:
if line == 'The command completed successfully.':
break
yield line.strip()
def collect_users(name, users=None):
if users is None:
users = set()
if not name.startswith('8GP'):
users.add(name)
return users
for member in get_group_members(name):
collect_users(member, users)
return users
def print_group_members(name, level=1):
prefix = ' ' * level
if name.startswith('8GP'):
for member in get_group_members(name):
print(f'{prefix}{member}')
print_group_members(member, level + 1)
def _print_acl(path, print_inherited=True, print_traversal=False):
print(f'{style.bright_green}{path}{style.reset}')
acl = get_acl(path)
for user, permissions in acl.items():
inherited, permissions = parse_permissions(permissions)
if inherited and not print_inherited:
continue
if not print_traversal and permissions == ('RX',):
continue
permissions = ','.join(permissions)
print(f'{user:50s}: {permissions}')
domain, name = user.split('\\')
print_group_members(name)
print()
def guess_username(name):
surname, _ = name.split(',', 1)
username = surname.lower().translate(PHONETICS)[:6]
assert username.isascii(), username
x = IDS.get(username, 1)
return f'{username}{x:02d}'
def parse_excel_sheet():
doc = openpyxl.open(DATA, read_only=True, data_only=True)
names = []
for x in doc['Zugriffsliste'][1][4:]:
if x.value == 'ENDE':
break
names.append(guess_username(x.value) if x.value and not x.value.startswith('AG ') else None)
acl = {}
for row in doc['Zugriffsliste'].iter_rows(min_row=4):
if not row[1].value:
break
path = row[1].value
acl[os.path.join(PATH, path)] = set(names[i] for i in range(len(names)) if row[i + 4].value)
return acl
@click.group()
def main():
setup_console()
@main.command()
@click.option('-v', '--verbose', count=True)
def verify(verbose):
"""Verify ACL on folders."""
errors = 0
master = parse_excel_sheet()
for entry in sorted(os.listdir(PATH)):
if entry.startswith('.'):
continue
path = os.path.join(PATH, entry)
acl = get_acl(path)
users = set()
for user, permissions in acl.items():
inherited, permissions = parse_permissions(permissions)
if inherited:
continue
assert permissions == ('RX', 'W', 'DC'), permissions
domain, name = user.split('\\')
users = collect_users(name, users)
authorized = master.get(path, set())
if not os.path.isdir(path):
assert not authorized
assert not users
continue
if verbose:
print(f'. {path}')
def _style(name):
if name not in authorized:
return f'{style.red}{name}{style.reset}'
if name not in users:
return f'{style.yellow}{name}{style.reset}'
return name
s = ', '.join(_style(user) for user in sorted(authorized.union(users)))
print(f' {s}')
else:
not_authorized = users - authorized
if not_authorized:
print(f'. {style.reset}')
s = ', '.join(f'{style.red}{name}{style.reset}' for name in sorted(not_authorized))
print(f' {s}')
errors = 1
sys.exit(errors)
@main.command()
@click.option('--json', is_flag=True)
def dump(json):
"""Dump ACL defined in Excel sheet."""
acl = parse_excel_sheet()
if json:
import json
class Encoder(json.JSONEncoder):
def default(self, obj):
if isinstance(obj, set):
return list(obj)
return super().default(self, obj)
print(json.dumps(acl, cls=Encoder))
else:
for path, users in acl.items():
print(f'. {path}')
users = ', '.join(users)
print(f' {users}')
print()
if __name__ == '__main__':
main()
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment