Scan and Output ACLs to Excel in Python

I am working to solve an issue that my company is currently having, We have an extremely large share drive that is over 20 years old. There are so many security groups attached to some of these folder as well as individual users that no longer exist and appear as scrambled text.

We would like to scan through the entire directory and output all of the permissions to an excel file.

I have come up with this solution, however, I don't believe I have done it as efficiently as I believe it can be.

Here is the entire script:

import os
import subprocess
import xlsxwriter
import win32com.client as win32

# *** IMPORTANT ****
# MUST INSTALL XLSXWRITER AND PYWIN32 IN VENV
# PIP INSTALL XLSXWRITER
# PIP INSTALL PYWIN32

def CheckPerms(t, save_dir):

global firstDir

# LOOP THROUGH ALL PATHS IN ROOT DIRECTORY
for path in os.listdir(t):

# VARIABLE HOLDS FULL PATH OF THE ROOT DIRECTORY
full_root_path = os.path.join(t, path)

# CHECKS IF PATH IS A FILE, IF IT IS A FILE, SKIP AND CONTINUE WITH SCAN
if not os.path.isfile(full_root_path):

# VARIABLE HOLDS ROOT NAME OF FULL ROOT PATH
# EXAMPLE: INPUT - C:\TEST, RETURNS - C:\
firstDir = os.path.split(full_root_path)

# PRINTS CURRENT DIRECTORY THAT IS BEING SCANNED
print("Working on: " + firstDir[1])

# PREPARE EXCEL WORKBOOK IN SPECIFIED LOCATION
# SAVES AS NAME OF DIR BEING SCANNED
workbook = xlsxwriter.Workbook(f'{save_dir}\\{firstDir[1]}.xlsx')
worksheet.write("A1", "Directory Path")
worksheet.write("B1", "Security Groups")
row = 1
col = 0

# LOOP THOUGH A WALK OF EACH ROOT PATH
# THIS WILL SCAN FRONT TO BACK, TOP TO BOTTOM OF THE ENTIRE TREE
for r, d, f in os.walk(full_root_path):

# CAPTURE THE OUTPUT OF THE SYSTEM CMD ICALCS COMMAND ON DIRECTORY BEING SCANNED
sub_return = subprocess.check_output(["icacls", r])
try:
# TRY TO DECODE OUTPUT AS UTF-8
sub_return = sub_return.decode('utf-8')
except:
# SOMETIMES CANNOT DECODE, THIS WILL CATCH ERROR AND CONTINUE
print("Decode Error: Skipping a line")
continue

# SPLIT THE LINES OF THE RETURNED STRING
split_icacl_lines = sub_return.splitlines()

# ICACLS RETURNS A STATUS LINE AFTER COMPLETE
# THIS WILL REMOVE THE LAST LINE, EXAMPLE:
# "Successfully processed 1 files; Failed processing 0 files"
del split_icacl_lines[-1:]

# FIRST LINE OF ICACLS INCLUDES THE DIRECTORY AS WELL AS FIRST LINE OF ICACLS
# THIS WILL REVERSE SPLIT BY FIRST EMPTY SPACE TO SEPARATE THE LINES
# AND DELETE IT FROM THE ORIGINAL LIST
firstLine = split_icacl_lines[0].rsplit(" ", 1)
del split_icacl_lines[0]

# THERE HAPPENS TO BE AN EMPTY LINE, SO WE REMOVE IT HERE
del split_icacl_lines[-1:]

# ADD THE FIRST ELEMENT OF THE FIRST LINE BACK INTO THE BEGINNING OF THE LIST
split_icacl_lines.insert(0, firstLine[0].lstrip())
# APPEND THE SECOND ELEMENT OF THE FIRST LINE TO END OF LIST
split_icacl_lines.append(firstLine[1].lstrip())

# FIRST ELEMENT OF LIST IS THE TARGET DIRECTORY
target_directory = split_icacl_lines[0]

# DELETE TARGET DIRECTORY FROM LIST
del split_icacl_lines[0]

# ADD TARGET DIRECTORY TO EXCEL FILE
worksheet.write(row, col, target_directory)
# MOVE OVER EXCEL COLUMN BY 1
col += 1

# LOOP THROUGH EACH LINE IN THE FINAL ICACL DATA AND
# OUTPUT IT TO EXCEL FILE NEXT TO THE DIRECTORY IT
# BELONGS TO
for lines in split_icacl_lines:
# STRIP LINES OF ALL ABNORMAL CHARACTERS
output = lines.lstrip()
# INSERT LINE INTO WORKBOOK
worksheet.write(row, col, output)
row += 1
# EMPTY LINE BETWEEN EACH SCAN OUTPUT
row += 1
# RESET COLUMN TO 0
col = 0
# CLOSE WORKBOOK, SAVING IT
workbook.close()

# OPEN WORKBOOK IN WIN32, AUTO-FIT EACH COLUMN AND SAVE IT
excel = win32.gencache.EnsureDispatch('Excel.Application')
wb = excel.Workbooks.Open(f'C:\\Test\\{firstDir[1]}.xlsx')
ws = wb.Worksheets("Sheet1")
ws.Columns.AutoFit()
wb.Save()
excel.Application.Quit()

# REPEAT ALL ABOVE FOR EACH DIRECTORY

CheckPerms("C:\\", "C:\\Test")


The problem I have, is when ICACLS is run in windows, the first line it returns includes the directory as well as the first permission, example:

C:\Users\Michael>icacls C:\\
NT AUTHORITY\SYSTEM:(OI)(CI)(F)
BUILTIN\Users:(OI)(CI)(RX)
NT AUTHORITY\Authenticated Users:(OI)(CI)(IO)(M)
Mandatory Label\High Mandatory Level:(OI)(NP)(IO)(NW)


This is why I had to do some weird stuff with the output after I split it into a list.

After splitting the original output, I split the first line of that output by an empty space and added each element back into the list, however, for some files such as in the directory "Program Files" I get strange outputs like this:

Can anyone suggest a better way to do this?

It would be very much appreciated.

when ICACLS is run in windows, the first line it returns includes the directory as well as the first permission

That's because your parsing algorithm is incorrect. About the only way to parse this output is to look at the second line to see how far indented it is.

Otherwise:

• DON'T SHOUT IN YOUR COMMENTS; THIS IS ESPECIALLY IMPORTANT FOR PIP WHERE IT WILL NOT WORK ON CASE-SENSITIVE OPERATING SYSTEMS UNLESS IT'S LOWER-CASE
• Don't store firstDir as a global
• CheckPerms should be check_perms by PEP8
• Divide this out into more subroutines
• Prefer pathlib over os where possible
• Your firstDir[1] index is strange. I assume this is just the stem of the directory.
• Put your xlsx file path into a variable for reuse.
• You have inconsistent worksheet indexing, mixing A1 with row/column. Prefer the latter.
• Do not use single-letter variable names like r, d, f
• Don't decode yourself; pass encoding to subprocess functions
• Don't call workbook.close; put it in a with
• Don't open Excel just for the purpose of column auto-sizing; keep track of the longest string and use that as the column width
• Add a __main__ guard
• Never bare except:
• Strongly consider parsing the result of icacls /T which includes multiple files and will be more efficient. I have not shown this below.

Suggested

import os
import subprocess
from typing import Iterator

import xlsxwriter
from pathlib import Path

# must install xlsxwriter in venv
# pip install xlsxwriter

def parse_icacls(dir_path: str) -> Iterator[str]:
"""
Example output:

icacls some_long_file
some_long_file NT AUTHORITY\SYSTEM:(I)(OI)(CI)(F)
LAPTOP\me:(I)(OI)(CI)(F)

Successfully processed 1 files; Failed processing 0 files
"""

sub_return = subprocess.check_output(('icacls', dir_path), encoding='utf-8')
lines = sub_return.splitlines()

for line in lines:
if not line:
break

def write_workbook(full_root_path: Path, saved_to: Path) -> None:
with xlsxwriter.Workbook(saved_to) as workbook:

headers = ('Directory Path', 'Security Groups')

row = 0
row += 1

for dir_path, dir_names, file_names in os.walk(full_root_path):
try:
groups = parse_icacls(dir_path)
except UnicodeDecodeError:
print('Decode Error: Skipping a line')
continue

col = 0
worksheet.write(row, col, dir_path)
longest_dir = max(longest_dir, len(dir_path))
col += 1

for group in groups:
worksheet.write(row, col, group)
longest_group = max(longest_group, len(group))
row += 1

row += 1

worksheet.set_column(first_col=0, last_col=0, width=longest_dir)
worksheet.set_column(first_col=1, last_col=1, width=longest_group)

def check_perms(top: Path, save_dir: Path) -> None:
save_dir.mkdir(exist_ok=True)

for full_root_path in top.iterdir():
if full_root_path.is_dir():
saved_to = (save_dir / full_root_path.stem).with_suffix('.xlsx')
print(f'Working on: {full_root_path}; saving to {saved_to}')
write_workbook(full_root_path, saved_to)

if __name__ == '__main__':
check_perms(Path('.'), Path('Test'))

• It runs a lot faster, I thank you for your tips. I am trying to learn more and this helps a ton. Much appreciated. Jan 14 at 2:42