1
\$\begingroup\$

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 = workbook.add_worksheet()
            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:\\
C:\\ BUILTIN\Administrators:(OI)(CI)(F)
     NT AUTHORITY\SYSTEM:(OI)(CI)(F)
     BUILTIN\Users:(OI)(CI)(RX)
     NT AUTHORITY\Authenticated Users:(OI)(CI)(IO)(M)
     NT AUTHORITY\Authenticated Users:(AD)
     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:

image of excel output file

Can anyone suggest a better way to do this?

It would be very much appreciated.

\$\endgroup\$
1
\$\begingroup\$

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)
                   BUILTIN\Administrators:(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()
    lead = len(lines[1]) - len(lines[1].lstrip())

    for line in lines:
        if not line:
            break
        yield line[lead:]


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

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

        row = 0
        for col, header in enumerate(headers):
            worksheet.write(row, col, header)
        row += 1

        longest_dir = len(headers[0])
        longest_group = len(headers[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'))
\$\endgroup\$
1
  • 1
    \$\begingroup\$ It runs a lot faster, I thank you for your tips. I am trying to learn more and this helps a ton. Much appreciated. \$\endgroup\$ Jan 14 at 2:42

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.