summaryrefslogtreecommitdiffstats
path: root/libcli/security/tests/windows/windows-sddl-tests.py
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--libcli/security/tests/windows/windows-sddl-tests.py181
1 files changed, 181 insertions, 0 deletions
diff --git a/libcli/security/tests/windows/windows-sddl-tests.py b/libcli/security/tests/windows/windows-sddl-tests.py
new file mode 100644
index 0000000..38acb44
--- /dev/null
+++ b/libcli/security/tests/windows/windows-sddl-tests.py
@@ -0,0 +1,181 @@
+# Test SDDL strings on Windows
+#
+#
+# Copyright (c) 2023 Catalyst IT
+#
+# GPLv3+.
+#
+# This uses the Python win32 module to access
+# ConvertStringSecurityDescriptorToSecurityDescriptor and the like. To
+# install this, you need to go
+#
+# pip install pywin32
+#
+# or something like that.
+
+import argparse
+from difflib import SequenceMatcher
+from collections import defaultdict
+import sys
+import json
+
+try:
+ import win32security as w
+except ImportError:
+ print("This test script is meant to be run on Windows using the pywin32 module.")
+ print("To install this module, try:\n")
+ print("pip install pywin32")
+ sys.exit(1)
+
+
+# This is necessary for ANSI colour escapes to work in Powershell.
+import os
+os.system('')
+
+RED = "\033[1;31m"
+GREEN = "\033[1;32m"
+DARK_YELLOW = "\033[0;33m"
+C_NORMAL = "\033[0m"
+
+def c_RED(s):
+ return f"{RED}{s}{C_NORMAL}"
+def c_GREEN(s):
+ return f"{GREEN}{s}{C_NORMAL}"
+def c_DY(s):
+ return f"{DARK_YELLOW}{s}{C_NORMAL}"
+
+
+def read_strings(files):
+ """Try to read as JSON a JSON dictionary first, then secondly in the bespoke
+ sddl-in -> sddl-out
+ format used by other Samba SDDL test programs on Windows.
+ """
+ pairs = []
+ for filename in files:
+ with open(filename) as f:
+ try:
+ data = json.load(f)
+ print(f"loading {filename} as JSON")
+ for k, v in data.items():
+ if not v or not isinstance(v, str):
+ v = k
+ pairs.append((k, v))
+ continue
+ except json.JSONDecodeError:
+ pass
+
+ print(f"loading {filename} as 'a -> b' style")
+ f.seek(0)
+ for line in f:
+ line = line.rstrip()
+ if line.startswith('#') or line == '':
+ continue
+ # note: if the line does not have ' -> ', we expect a
+ # perfect round trip.
+ o, _, c = line.partition(' -> ')
+ if c == '':
+ c = o
+ pairs.append((o, c))
+
+ return pairs
+
+
+def colourdiff(a, b):
+ out = []
+ a = a.replace(' ', '␠')
+ b = b.replace(' ', '␠')
+
+ s = SequenceMatcher(None, a, b)
+ for op, al, ar, bl, br in s.get_opcodes():
+ if op == 'equal':
+ out.append(a[al: ar])
+ elif op == 'delete':
+ out.append(c_RED(a[al: ar]))
+ elif op == 'insert':
+ out.append(c_GREEN(b[bl: br]))
+ elif op == 'replace':
+ out.append(c_RED(a[al: ar]))
+ out.append(c_GREEN(b[bl: br]))
+ else:
+ print(f'unknown op {op}!')
+
+ return ''.join(out)
+
+
+def no_print(*args, **kwargs):
+ pass
+
+
+def main():
+ parser = argparse.ArgumentParser()
+ parser.add_argument('--export-bytes', const='sddl_bytes.json', nargs='?',
+ help='write JSON file containing SD bytes')
+ parser.add_argument('--quiet', action='store_true',
+ help='avoid printing to stdout')
+ parser.add_argument('files', nargs='+', help='read these files')
+
+ args = parser.parse_args()
+
+ if args.quiet:
+ global print
+ print = no_print
+
+ cases = read_strings(args.files)
+ parseable_cases = []
+ unparseable_cases = []
+ unserializeable_cases = []
+ round_trip_failures = []
+ exceptions = defaultdict(list)
+ bytes_json = {}
+
+ print(f"{len(set(cases))}/{len(cases)} unique pairs, "
+ f"{len(set(x[0] for x in cases))}/{len(cases)} unique strings")
+
+ for a, b in sorted(set(cases)):
+ try:
+ sd = w.ConvertStringSecurityDescriptorToSecurityDescriptor(a, 1)
+ except Exception as e:
+ print(a)
+ exceptions[f"{e} parse"].append(a)
+ print(c_RED(e))
+ unparseable_cases.append(a)
+ continue
+
+ parseable_cases.append(a)
+
+ try:
+ # maybe 0xffff is an incorrect guess -- it gives use v2 (NT), not v4 (AD)
+ c = w.ConvertSecurityDescriptorToStringSecurityDescriptor(sd, 1, 0xffff)
+ except Exception as e:
+ print(f"could not serialize '{sd}': {e}")
+ print(f" derived from '{a}'")
+ exceptions[f"{e} serialize"].append(a)
+ unserializeable_cases.append(a)
+ continue
+
+ if args.export_bytes:
+ bytes_json[c] = list(bytes(sd))
+
+ if c != b:
+ round_trip_failures.append((a, b, c))
+ exceptions["mismatch"].append(a)
+ #print(f"{c_GREEN(a)} -> {c_DY(c)}")
+ print(colourdiff(b, c))
+ print(c_DY(f"{b} -> {c}"))
+
+ for k, v in exceptions.items():
+ print(f"{k}: {len(v)}")
+
+ print(f"{len(unparseable_cases)} failed to parse")
+ print(f"{len(parseable_cases)} successfully parsed")
+ print(f"{len(unserializeable_cases)} of these failed to re-serialize")
+ print(f"{len(round_trip_failures)} of these failed to round trip")
+ #for p in parseable_cases:
+ # print(f"«{c_GREEN(p)}»")
+
+ if args.export_bytes:
+ with open(args.export_bytes, 'w') as f:
+ json.dump(bytes_json, f)
+ print(f"wrote bytes to {args.export_bytes}")
+
+main()