From 52cde9918c1ea6612523fd4e1b9e274bdb9c2600 Mon Sep 17 00:00:00 2001 From: Brigs Date: Sun, 27 Sep 2026 17:07:12 -0400 Subject: [PATCH 1/2] Read the chunks a dirty event log's header does not count python-evtx reads only as many chunks as an event log's file header counts. On a log marked dirty that count can be lower than the chunks the file holds, and the newest records sit in the chunks past it. log_records also reads those chunks when they carry the chunk signature and both checksums match, skipping any record number already read, and every event log artifact now reads through it. On LoneWolf and Szechuan the System and Security logs, and on PC-MUS-001 the System log, held 255 to 4,173 records in chunks their headers did not count. The notes and sample_data of the 20 artifacts whose rows changed are updated to match. Co-Authored-By: Claude Opus 5.5 --- .../test_windows_evtx_uncounted_chunks.py | 205 ++++++++++++++++++ scripts/artifacts/windowsAccountManagement.py | 3 +- scripts/artifacts/windowsBitsEvents.py | 12 +- .../artifacts/windowsCompatibilityEvents.py | 17 +- scripts/artifacts/windowsDefenderEvents.py | 6 +- scripts/artifacts/windowsErrorReporting.py | 43 ++-- scripts/artifacts/windowsMsiInstaller.py | 4 +- .../artifacts/windowsNetworkProfileEvents.py | 9 +- scripts/artifacts/windowsPowerShellEvents.py | 16 +- scripts/artifacts/windowsRdpSessions.py | 3 +- scripts/artifacts/windowsSecurityLogons.py | 7 +- scripts/artifacts/windowsServiceInstalls.py | 9 +- scripts/artifacts/windowsShellCoreRunKeys.py | 6 +- .../artifacts/windowsStorageDeviceEvents.py | 4 +- scripts/artifacts/windowsSystemPowerEvents.py | 59 ++--- .../artifacts/windowsTaskSchedulerEvents.py | 16 +- scripts/artifacts/windowsVhdmpEvents.py | 4 +- scripts/artifacts/windowsWlanEvents.py | 9 +- scripts/artifacts/windowsWmiActivity.py | 22 +- scripts/windows_evtx.py | 86 +++++++- 20 files changed, 415 insertions(+), 125 deletions(-) create mode 100644 admin/test/scripts/test_windows_evtx_uncounted_chunks.py diff --git a/admin/test/scripts/test_windows_evtx_uncounted_chunks.py b/admin/test/scripts/test_windows_evtx_uncounted_chunks.py new file mode 100644 index 00000000..d4603a08 --- /dev/null +++ b/admin/test/scripts/test_windows_evtx_uncounted_chunks.py @@ -0,0 +1,205 @@ +"""Pin how log_records reads the chunks a dirty event log's header does not count.""" +import inspect +import pathlib +import re +import sys +import unittest +from unittest import mock + +REPO_ROOT = pathlib.Path(__file__).resolve().parents[3] +sys.path.insert(0, str(REPO_ROOT)) + +# pylint: disable=wrong-import-position +from scripts import windows_evtx +# pylint: enable=wrong-import-position + + +class FakeRecord: + def __init__(self, number): + self.number = number + + def record_num(self): + return self.number + + def xml(self): + return ('' + f'7{self.number}' + '') + + +class FakeChunk: + """A chunk holding records with the given numbers.""" + + def __init__(self, numbers, magic=True, header_ok=True, data_ok=True, fail_at=None): + self.numbers = numbers + self.magic = magic + self.header_ok = header_ok + self.data_ok = data_ok + self.fail_at = fail_at + + def check_magic(self): + return self.magic + + def header_checksum(self): + return 11 + + def calculate_header_checksum(self): + return 11 if self.header_ok else 12 + + def data_checksum(self): + return 21 + + def calculate_data_checksum(self): + return 21 if self.data_ok else 22 + + def records(self): + for index, number in enumerate(self.numbers): + if index == self.fail_at: + raise ValueError('record does not parse') + yield FakeRecord(number) + + +class FakeHeader: + def __init__(self, chunks, counted, dirty): + self._chunks = chunks + self.counted = counted + self.dirty = dirty + + def chunk_count(self): + return self.counted + + def is_dirty(self): + return self.dirty + + def chunks(self, include_inactive=False): + return iter(self._chunks if include_inactive else self._chunks[:self.counted]) + + +class FakeLog: + def __init__(self, chunks, counted, dirty): + self.header = FakeHeader(chunks, counted, dirty) + + def get_file_header(self): + return self.header + + +def read(chunks, counted, dirty): + """The record numbers log_records yields, and the run log lines it writes.""" + lines = [] + with mock.patch.object(windows_evtx, 'logfunc', lines.append): + numbers = [record.record_num() for record in + windows_evtx.log_records(FakeLog(chunks, counted, dirty), 'Label', 'vol/System.evtx')] + return numbers, lines + + +class LogRecordsTest(unittest.TestCase): + def test_a_dirty_log_is_read_past_the_counted_chunks(self): + numbers, lines = read([FakeChunk([1, 2]), FakeChunk([3, 4]), FakeChunk([5, 6])], 2, True) + self.assertEqual(numbers, [1, 2, 3, 4, 5, 6]) + self.assertEqual(lines, ['Label: vol/System.evtx is marked dirty; 2 record(s) were read ' + 'from 1 chunk(s) after the 2 its header counts']) + + def test_a_log_not_marked_dirty_is_read_as_python_evtx_reads_it(self): + numbers, lines = read([FakeChunk([1, 2]), FakeChunk([3, 4]), FakeChunk([5, 6])], 2, False) + self.assertEqual(numbers, [1, 2, 3, 4]) + self.assertEqual(lines, []) + + def test_the_counted_chunks_are_read_without_the_checks_later_chunks_get(self): + numbers, _lines = read([FakeChunk([1], magic=False, header_ok=False), FakeChunk([2])], 2, True) + self.assertEqual(numbers, [1, 2]) + + def test_a_later_chunk_without_the_signature_or_a_matching_checksum_is_not_read(self): + chunks = [FakeChunk([1]), FakeChunk([2], magic=False), FakeChunk([3], header_ok=False), + FakeChunk([4], data_ok=False), FakeChunk([5])] + numbers, lines = read(chunks, 1, True) + self.assertEqual(numbers, [1, 5]) + self.assertEqual(lines, ['Label: vol/System.evtx is marked dirty; 1 record(s) were read ' + 'from 1 chunk(s) after the 1 its header counts, and 2 chunk(s) ' + 'after them failed their checksums and were not read']) + + def test_a_record_number_already_read_is_not_read_again(self): + numbers, lines = read([FakeChunk([1, 2]), FakeChunk([2, 3]), FakeChunk([3, 4])], 1, True) + self.assertEqual(numbers, [1, 2, 3, 4]) + self.assertIn('2 record(s) were read from 2 chunk(s)', lines[0]) + + def test_a_later_chunk_that_stops_parsing_keeps_what_it_read_and_the_next_chunk_is_read(self): + numbers, lines = read([FakeChunk([1]), FakeChunk([2, 3, 4], fail_at=2), FakeChunk([5])], 1, True) + self.assertEqual(numbers, [1, 2, 3, 5]) + self.assertTrue(lines[0].endswith('; 1 of those chunk(s) stopped parsing part way through')) + + def test_a_dirty_log_with_nothing_after_the_counted_chunks_writes_no_line(self): + numbers, lines = read([FakeChunk([1]), FakeChunk([], magic=False)], 1, True) + self.assertEqual(numbers, [1]) + self.assertEqual(lines, []) + + +class FakeEvtxModule: + """Stands in for python-evtx's Evtx module: Evtx(path) opens the given fake log.""" + + def __init__(self, log): + self.log = log + + def Evtx(self, _path): # pylint: disable=invalid-name + log = self.log + + class _Open: + def __enter__(self): + return log + + def __exit__(self, *_exc): + return False + return _Open() + + +class FakeContext: + @staticmethod + def get_files_found(): + return ['/case/data/vol/Windows/System32/winevt/Logs/System.evtx'] + + @staticmethod + def get_relative_path(path): + return path.split('/data/', 1)[1] + + +class ReadEventRecordsTest(unittest.TestCase): + def test_the_shared_reader_returns_the_records_a_dirty_header_does_not_count(self): + log = FakeLog([FakeChunk([1, 2]), FakeChunk([3])], 1, True) + lines = [] + with mock.patch.object(windows_evtx, 'evtx', FakeEvtxModule(log)), \ + mock.patch.object(windows_evtx, 'logfunc', lines.append): + records, sources = windows_evtx.read_event_records(FakeContext(), 'system.evtx', 'Label') + self.assertEqual([r.record_id for r in records], ['1', '2', '3']) + self.assertEqual(len(sources), 1) + self.assertIn('1 record(s) were read from 1 chunk(s) after the 1 its header counts', lines[0]) + + +class NoDirectReadsTest(unittest.TestCase): + def test_no_module_reads_a_log_with_python_evtx_records(self): + offenders = [] + for path in sorted((REPO_ROOT / 'scripts').rglob('*.py')): + if path.name == 'windows_evtx.py': + continue + text = path.read_text(encoding='utf-8', errors='replace') + if 'Evtx' in text and re.search(r'\blog\.records\(\)|\.Evtx\([^)]*\)\.records\(\)', text): + offenders.append(str(path.relative_to(REPO_ROOT))) + self.assertEqual(offenders, []) + + +@unittest.skipUnless(windows_evtx.evtx is not None, 'python-evtx is not installed') +class PythonEvtxInterfaceTest(unittest.TestCase): + def test_python_evtx_can_list_the_chunks_its_header_does_not_count(self): + # chunk_count, header_checksum, data_checksum and record_num are fields python-evtx + # declares on each instance, so they are checked on instances built over zeroed bytes. + header = windows_evtx.evtx.FileHeader(bytearray(0x1000), 0) + self.assertIn('include_inactive', inspect.signature(header.chunks).parameters) + for name in ('chunk_count', 'is_dirty'): + self.assertTrue(callable(getattr(header, name))) + chunk = windows_evtx.evtx.ChunkHeader(bytearray(0x10000), 0) + for name in ('check_magic', 'header_checksum', 'calculate_header_checksum', 'data_checksum', + 'calculate_data_checksum', 'records'): + self.assertTrue(callable(getattr(chunk, name))) + self.assertTrue(callable(windows_evtx.evtx.Record(bytearray(0x100), 0, chunk).record_num)) + + +if __name__ == '__main__': + unittest.main() diff --git a/scripts/artifacts/windowsAccountManagement.py b/scripts/artifacts/windowsAccountManagement.py index 7bdd00ed..4d612790 100644 --- a/scripts/artifacts/windowsAccountManagement.py +++ b/scripts/artifacts/windowsAccountManagement.py @@ -18,6 +18,7 @@ evtx = None from scripts.ilapfuncs import artifact_processor, logfunc +from scripts.windows_evtx import log_records # The Security event log records local account and group administration under # the account-management audit subcategories: an account being created, @@ -177,7 +178,7 @@ def accountManagement(context): rows_here = 0 try: with evtx.Evtx(source) as log: - for record in log.records(): + for record in log_records(log, 'Windows Account Management', relative_source): try: row = _account_row(record.xml()) except ElementTree.ParseError: diff --git a/scripts/artifacts/windowsBitsEvents.py b/scripts/artifacts/windowsBitsEvents.py index 39402933..ff72ba02 100644 --- a/scripts/artifacts/windowsBitsEvents.py +++ b/scripts/artifacts/windowsBitsEvents.py @@ -34,7 +34,7 @@ "record stores them.", "author": "@AlexisBrignoni, Claude", "creation_date": "2026-09-23", - "last_update_date": "2026-09-24", + "last_update_date": "2026-09-27", "requirements": "python-evtx", "category": "Windows", "notes": "Read from Microsoft-Windows-Bits-Client%4Operational.evtx, named in the " @@ -54,11 +54,11 @@ "Count are jobOwner, User, processPath, processId, bytesTotal, " "bytesTransferred and fileCount; Status Code is hr, which the manifest " "formats as hexadecimal, shown as hex with the stored decimal. Every other " - "value is reported as stored. Local File was filled on the 173 16403 rows " + "value is reported as stored. Local File was filled on the 192 16403 rows " "of pc_mus_001_win11 and is empty on af_case2_win10 and lonewolf_win10, " "which carry no 16403 records. Bytes Total held 18446744073709551615, the " "largest unsigned 64-bit number, on 79 of 116 rows on af_case2_win10, 20 " - "of 871 on pc_mus_001_win11 and 43 of 156 on lonewolf_win10; the manifest " + "of 967 on pc_mus_001_win11 and 88 of 421 on lonewolf_win10; the manifest " "dump gives no description of that value. Event Time (UTC) is the " "record's TimeCreated SystemTime, which python-evtx renders from the " "FILETIME the record stores, counted in UTC (python-evtx 0.8.1, " @@ -72,7 +72,7 @@ "or whose XML does not parse, is counted in the run log and not reported; " "every record in this log rendered on the registered images. On the job " "created (3) rows, Job Owner was an NT AUTHORITY account on 10 of 18 on " - "af_case2_win10, 13 of 174 on pc_mus_001_win11 and 10 of 28 on " + "af_case2_win10, 13 of 192 on pc_mus_001_win11 and 14 of 86 on " "lonewolf_win10; a row does not by itself establish that a person started " "the transfer. Reading needs the python-evtx package (pip install " "python-evtx).", @@ -80,9 +80,9 @@ "output_types": ["standard"], "artifact_icon": "download", "sample_data": { - "pc_mus_001_win11": "Windows 11 22H2 build 22621 | 871 rows", + "pc_mus_001_win11": "Windows 11 22H2 build 22621 | 967 rows", "af_case2_win10": "Windows 10 1809 build 17763 | 116 rows", - "lonewolf_win10": "Windows 10 Education build 16299 | 156 rows", + "lonewolf_win10": "Windows 10 Education build 16299 | 421 rows", }, }, } diff --git a/scripts/artifacts/windowsCompatibilityEvents.py b/scripts/artifacts/windowsCompatibilityEvents.py index 7fd4243d..39f7a8bc 100644 --- a/scripts/artifacts/windowsCompatibilityEvents.py +++ b/scripts/artifacts/windowsCompatibilityEvents.py @@ -29,7 +29,7 @@ "stores.", "author": "@AlexisBrignoni, Claude", "creation_date": "2026-09-26", - "last_update_date": "2026-09-26", + "last_update_date": "2026-09-27", "requirements": "python-evtx", "category": "Windows", "notes": "Read from Microsoft-Windows-Application-Experience%4Program-Telemetry.evtx, named in the " @@ -45,12 +45,15 @@ "ExePath, Process ID is ProcessId, and Fix Name, Fix ID and Flags are FixName, FixID and " "Flags, all as stored; the manifest does not define the Flags bits. Every 505 row of the " "tested images carried Flags 0x80010101, and every 500 row 0x00010101 except one " - "pc_mus_001_win11 row with 0x00010205. Process Start Time (UTC) is StartTime, a FILETIME " + "pc_mus_001_win11 row with 0x00010205 and two lonewolf_win10 rows with 0x00040102. Process " + "Start Time (UTC) is StartTime, a FILETIME " "that python-evtx renders counted in UTC; on every row of the tested images it was " - "between 0.004 and 3 seconds before the record's own time. User SID is the SID the " + "between 0.004 and 9.939 seconds before the record's own time, and under 3 seconds on every " + "row but one on pc_mus_001_win11. User SID is the SID the " "record's Security element stores: an account SID (S-1-5-21-...) on every row of the " - "tested images except one pc_mus_001_win11 500 row that carried S-1-5-18, and it held one " - "value on every row of lonewolf_win10. Event Time (UTC) is the record's TimeCreated " + "tested images except one pc_mus_001_win11 500 row that carried S-1-5-18 and two " + "lonewolf_win10 500 rows that carried S-1-5-20, and the other rows of lonewolf_win10 held " + "one account SID. Event Time (UTC) is the record's TimeCreated " "SystemTime, which python-evtx renders from the FILETIME the record stores, counted in " "UTC (python-evtx 0.8.1, " "https://github.com/williballenthin/python-evtx/blob/cab997af04b6caae68b306e5c2c40b3aa751454e/Evtx/BinaryParser.py#L105-L113). " @@ -69,8 +72,8 @@ "artifact_icon": "tool", "sample_data": { "af_case2_win10": "Windows 10 1809 build 17763 | 1 row", - "lonewolf_win10": "Windows 10 Education build 16299 | 196 rows", - "pc_mus_001_win11": "Windows 11 22H2 build 22621 | 203 rows", + "lonewolf_win10": "Windows 10 Education build 16299 | 1563 rows", + "pc_mus_001_win11": "Windows 11 22H2 build 22621 | 221 rows", "szechuan_win10": "Windows 10 2004 build 19041 | 32 rows", }, }, diff --git a/scripts/artifacts/windowsDefenderEvents.py b/scripts/artifacts/windowsDefenderEvents.py index 2599c65e..9d23e1da 100644 --- a/scripts/artifacts/windowsDefenderEvents.py +++ b/scripts/artifacts/windowsDefenderEvents.py @@ -301,7 +301,7 @@ "account of each record and the duration of each finished scan.", "author": "@AlexisBrignoni, Claude", "creation_date": "2026-09-23", - "last_update_date": "2026-09-24", + "last_update_date": "2026-09-27", "requirements": "python-evtx; pefile to give parameter references their text", "category": "Windows", "notes": "Read from Microsoft-Windows-Windows Defender%4Operational.evtx, named in " @@ -402,8 +402,8 @@ "artifact_icon": "search", "sample_data": { "af_case2_win10": "Windows 10 1809 build 17763 | 2 rows", - "pc_mus_001_win11": "Windows 11 22H2 build 22621 | 10 rows", - "lonewolf_win10": "Windows 10 Education build 16299 | 4 rows", + "pc_mus_001_win11": "Windows 11 22H2 build 22621 | 12 rows", + "lonewolf_win10": "Windows 10 Education build 16299 | 7 rows", "defender_evtx_attack_samples": "Defender Operational log only, Defender platform 4.18.1906.3 | 0 rows (the log holds only 1116 and 1117 records)", "defender_evtx_to_mitre": "Defender Operational log only | 0 rows (python-evtx 0.8.1 renders none of the log's 6 records)", }, diff --git a/scripts/artifacts/windowsErrorReporting.py b/scripts/artifacts/windowsErrorReporting.py index f7f5b1f4..8e11f41b 100644 --- a/scripts/artifacts/windowsErrorReporting.py +++ b/scripts/artifacts/windowsErrorReporting.py @@ -146,7 +146,7 @@ "report's files and the report identifier.", "author": "@AlexisBrignoni, Claude", "creation_date": "2026-09-26", - "last_update_date": "2026-09-26", + "last_update_date": "2026-09-27", "requirements": "python-evtx", "category": "Windows", "notes": "Read from Application.evtx, named in the report's located-at line; only records of " @@ -163,13 +163,12 @@ "Guid %23 from build 17763 (af_case2_win10) on. Event Name is EventName, Problem " "Signature lists P1 to P10 where they hold a value, Attached Files is AttachedFiles, " "Store Path is StorePath, Report ID is ReportId, Report Status is ReportStatus as " - "stored and Hashed Bucket is HashedBucket. On the 65 records whose report is on the " - "image (18 on lonewolf_win10, 47 on pc_mus_001_win11), P1 to P10 equalled that " + "stored and Hashed Bucket is HashedBucket. On the 90 records whose report is on the image (31 on lonewolf_win10, 59 on " + "pc_mus_001_win11), P1 to P10 equalled that " "Report.wer's Sig[0] to Sig[9] values, which its Problem Signature names. One report " - "can be logged more than once: 18 records named 8 report IDs on lonewolf_win10, and " - "53 named 31 on pc_mus_001_win11. Report ID matched a Report.wer's ReportIdentifier " - "for 7 of those IDs on each image and its IntegratorReportIdentifier for 1 " - "(lonewolf_win10) and 18 (pc_mus_001_win11). User SID is the SID in the record's " + "can be logged more than once: 33 records named 17 report IDs on lonewolf_win10, and 68 named 40 on pc_mus_001_win11. Report ID " + "matched a Report.wer's ReportIdentifier for 11 of those IDs on lonewolf_win10 and 7 on pc_mus_001_win11, and its " + "IntegratorReportIdentifier for 4 (lonewolf_win10) and 24 (pc_mus_001_win11). User SID is the SID in the record's " "Security element, and it is blank on every lonewolf_win10 row, because those records " "carry none. Event Time (UTC) is the record's TimeCreated SystemTime, which " "python-evtx renders from the FILETIME the record stores, counted in UTC " @@ -181,8 +180,8 @@ "artifact_icon": "alert-triangle", "sample_data": { "af_case2_win10": "Windows 10 1809 build 17763 | 0 rows (no Windows Error Reporting 1001 record)", - "lonewolf_win10": "Windows 10 Education build 16299 | 18 rows", - "pc_mus_001_win11": "Windows 11 22H2 build 22621 | 53 rows", + "lonewolf_win10": "Windows 10 Education build 16299 | 33 rows", + "pc_mus_001_win11": "Windows 11 22H2 build 22621 | 68 rows", }, }, "appCrashHangEvents": { @@ -193,7 +192,7 @@ "report identifier.", "author": "@AlexisBrignoni, Claude", "creation_date": "2026-09-26", - "last_update_date": "2026-09-26", + "last_update_date": "2026-09-27", "requirements": "python-evtx", "category": "Windows", "notes": "Read from Application.evtx, named in the report's located-at line; only Application " @@ -211,26 +210,26 @@ "%2, time stamp: 0x%3' through 'Faulting package-relative application ID: %15', and " "'The program %1 version %2', Process ID %3, Start Time %4, Termination Time %5, " "Application Path %6, Report Id %7, package %8 and %9, and Hang type %10, which " - "message 1002 on build 16299 lacks. lonewolf_win10 holds one such unnamed 1000 " - "record; no unnamed 1002 record was found, so that reading is unexercised. App Path " + "message 1002 on build 16299 lacks. lonewolf_win10 holds four such unnamed 1000 records; no unnamed 1002 record was found, so " + "that reading is unexercised. Event held one value, Application Error (1000), on every lonewolf_win10 row. App Path " "is AppPath (1000) or ExeFileName (1002). Process ID is ProcessId in decimal, with " "its hexadecimal form: the named records store it with 0x, and message 1000 prints " "the unnamed value after 0x, so that value is read as hexadecimal too; an unnamed " "1002 value is shown as stored. Process Start Time (UTC) is ProcessCreationTime " "(1000) or StartTime (1002) read as a hexadecimal FILETIME counted in UTC. Both " "machines were set to Eastern time, and it fell 0 to 2,059 seconds before the " - "record's own time on all 18 pc_mus_001_win11 records and 1,585 seconds before it on " - "the lonewolf_win10 record, where a local-time reading would put each start after the " + "record's own time on all 24 pc_mus_001_win11 records and 3.7 to 1,585 seconds before it on the four lonewolf_win10 records, " + "where a local-time reading would put each start after the " "crash or hang it precedes. Exception Code and Fault Offset are ExceptionCode and " "FaultingOffset with a 0x prefix, as message 1000 prints them; they and Module Name, " "Module Version and Module Path are blank on 1002 rows, as Hang Type is on 1000 rows. " - "Report ID is IntegratorReportId (1000) or ReportId (1002); each of the 18 on " - "pc_mus_001_win11 and the one on lonewolf_win10 matched the " + "Report ID is IntegratorReportId (1000) or ReportId (1002); each of the 24 on pc_mus_001_win11 and the four on lonewolf_win10 " + "matched the " "IntegratorReportIdentifier of a Report.wer and the Report ID of a Windows Error " - "Reporting Events row. Package Full Name is PackageFullName, blank on 14 of the 18 " - "pc_mus_001_win11 rows and on the lonewolf_win10 row. User SID is the SID in the " - "record's Security element, and it is blank on the lonewolf_win10 row, because that " - "record carries none. Event Time (UTC) is the record's TimeCreated SystemTime, which " + "Reporting Events row. Package Full Name is PackageFullName, blank on 20 of the 24 pc_mus_001_win11 rows and on every " + "lonewolf_win10 row. User SID is the SID in the " + "record's Security element, and it is blank on every lonewolf_win10 row, because those records carry none. Event Time (UTC) is " + "the record's TimeCreated SystemTime, which " "python-evtx renders from the FILETIME the record stores, counted in UTC " "(https://github.com/williballenthin/python-evtx/blob/cab997af04b6caae68b306e5c2c40b3aa751454e/Evtx/BinaryParser.py#L105-L113), " "and Record ID is the record's EventRecordID. Not reported: AppTimeStamp, " @@ -240,8 +239,8 @@ "artifact_icon": "alert-triangle", "sample_data": { "af_case2_win10": "Windows 10 1809 build 17763 | 0 rows (no Application Error 1000 or Application Hang 1002 record)", - "lonewolf_win10": "Windows 10 Education build 16299 | 1 row", - "pc_mus_001_win11": "Windows 11 22H2 build 22621 | 18 rows", + "lonewolf_win10": "Windows 10 Education build 16299 | 4 rows", + "pc_mus_001_win11": "Windows 11 22H2 build 22621 | 24 rows", }, }, } diff --git a/scripts/artifacts/windowsMsiInstaller.py b/scripts/artifacts/windowsMsiInstaller.py index 0e195a1e..ab928015 100644 --- a/scripts/artifacts/windowsMsiInstaller.py +++ b/scripts/artifacts/windowsMsiInstaller.py @@ -42,7 +42,7 @@ "where the record stores them.", "author": "@AlexisBrignoni, Claude", "creation_date": "2026-09-23", - "last_update_date": "2026-09-24", + "last_update_date": "2026-09-27", "requirements": "python-evtx", "category": "Windows", "notes": "Read from Application.evtx, named in the report's located-at line; only " @@ -90,7 +90,7 @@ "sample_data": { "af_case2_win10": "Windows 10 1809 build 17763 | 39 rows", "pc_mus_001_win11": "Windows 11 22H2 build 22621 | 8 rows", - "lonewolf_win10": "Windows 10 Education build 16299 | 18 rows", + "lonewolf_win10": "Windows 10 Education build 16299 | 22 rows", }, }, } diff --git a/scripts/artifacts/windowsNetworkProfileEvents.py b/scripts/artifacts/windowsNetworkProfileEvents.py index 307a2041..7273ca48 100644 --- a/scripts/artifacts/windowsNetworkProfileEvents.py +++ b/scripts/artifacts/windowsNetworkProfileEvents.py @@ -28,7 +28,7 @@ "name, description and profile GUID each record stores.", "author": "@AlexisBrignoni, Claude", "creation_date": "2026-09-23", - "last_update_date": "2026-09-23", + "last_update_date": "2026-09-27", "requirements": "python-evtx", "category": "Windows", "notes": "Read from Microsoft-Windows-NetworkProfile%4Operational.evtx, named in " @@ -43,8 +43,9 @@ "Guid fields. Type (as stored), State (as stored) and Category (as " "stored) are the Type, State and Category numbers, for which the " "published manifest dump carries no names. Description held the same text " - "as Network Name on every row of the three registered images (39, 173 and " - "22 rows); whether the two can differ was not established. Type held one " + "as Network Name on every row of af_case2_win10, pc_mus_001_win11 and " + "lonewolf_win10 (39, 178 and 22 rows); whether the two can differ was not " + "established. Type held one " "value on every row of each registered image, and Category held one value " "on every row of pc_mus_001_win11 and lonewolf_win10. Event Time (UTC) is " "the record's TimeCreated SystemTime, which python-evtx renders from the " @@ -65,7 +66,7 @@ "artifact_icon": "globe", "sample_data": { "af_case2_win10": "Windows 10 1809 build 17763 | 39 rows", - "pc_mus_001_win11": "Windows 11 22H2 build 22621 | 173 rows", + "pc_mus_001_win11": "Windows 11 22H2 build 22621 | 178 rows", "lonewolf_win10": "Windows 10 Education build 16299 | 22 rows", }, }, diff --git a/scripts/artifacts/windowsPowerShellEvents.py b/scripts/artifacts/windowsPowerShellEvents.py index 98177464..ae44ceb4 100644 --- a/scripts/artifacts/windowsPowerShellEvents.py +++ b/scripts/artifacts/windowsPowerShellEvents.py @@ -67,7 +67,7 @@ "line and engine version each event stores.", "author": "@AlexisBrignoni, Claude", "creation_date": "2026-09-23", - "last_update_date": "2026-09-24", + "last_update_date": "2026-09-27", "requirements": "python-evtx", "category": "Windows", "notes": "Read from Windows PowerShell.evtx, named in the report's located-at line. " @@ -130,7 +130,7 @@ "sample_data": { "pc_mus_001_win11": "Windows 11 22H2 build 22621 | 157 rows", "af_case2_win10": "Windows 10 1809 build 17763 | 154 rows", - "lonewolf_win10": "Windows 10 Education build 16299 | 43 rows", + "lonewolf_win10": "Windows 10 Education build 16299 | 48 rows", }, }, "powershellPipelineExecution": { @@ -193,7 +193,7 @@ "joined in order.", "author": "@AlexisBrignoni, Claude", "creation_date": "2026-09-23", - "last_update_date": "2026-09-23", + "last_update_date": "2026-09-27", "requirements": "python-evtx", "category": "Windows", "notes": "Read from Microsoft-Windows-PowerShell%4Operational.evtx, named in the " @@ -220,8 +220,7 @@ "One row per ScriptBlockId: Script Block Text is the ScriptBlockText of " "its parts joined in MessageNumber order, and Parts is how many parts " "were found of the MessageTotal the parts store. Checked against a " - "separate join of the raw records, the text matched for the 47, 3 and 1 " - "script blocks on af_case2_win10, pc_mus_001_win11 and lonewolf_win10, " + "separate join of the raw records, the text matched for the 47, 4 and 1 script blocks on af_case2_win10, pc_mus_001_win11 and lonewolf_win10, " "and every block had every part; af_case2_win10 had 9 blocks split into 2 " "to 12 parts. First Part Time (UTC) and Last Part Time (UTC) are the " "earliest and latest TimeCreated SystemTime of the parts, which " @@ -229,7 +228,7 @@ "(python-evtx 0.8.1, " "https://github.com/williballenthin/python-evtx/blob/cab997af04b6caae68b306e5c2c40b3aa751454e/Evtx/BinaryParser.py#L105-L113). " "First Part Time and Last Part Time are the same for a block stored in " - "one part, as on all 3 rows of pc_mus_001_win11. Level is the record's " + "one part, as on all 4 rows of pc_mus_001_win11. Level is the record's " "level with Microsoft's name for it " "(https://github.com/MicrosoftDocs/win32/blob/e103fa4e8810bd8d42c4777e17081e24dbe62dbd/desktop-src/WES/eventmanifestschema-leveltype-complextype.md#L70-L76): " "it held 3 (Warning) on every row on the registered images. Path is the " @@ -237,8 +236,7 @@ "rows on af_case2_win10). User SID is the SID in the record's Security " "element and Process ID the process ID in its Execution element, both " "from the first part; User SID held one value on every row of " - "af_case2_win10 and pc_mus_001_win11. On pc_mus_001_win11 the Script " - "Block Text of the 3 rows is the same text under three Script Block IDs, " + "af_case2_win10 and pc_mus_001_win11. On pc_mus_001_win11 the Script Block Text of the 4 rows is the same text under four Script Block IDs, " "and Parts held 1 of 1 on every row. Record IDs are the EventRecordIDs of " "the parts. Computer is the machine name the record stores and held one " "value on every row of af_case2_win10 and pc_mus_001_win11. A record " @@ -252,7 +250,7 @@ "artifact_icon": "file-text", "sample_data": { "af_case2_win10": "Windows 10 1809 build 17763 | 47 rows", - "pc_mus_001_win11": "Windows 11 22H2 build 22621 | 3 rows", + "pc_mus_001_win11": "Windows 11 22H2 build 22621 | 4 rows", "lonewolf_win10": "Windows 10 Education build 16299 | 1 row", }, }, diff --git a/scripts/artifacts/windowsRdpSessions.py b/scripts/artifacts/windowsRdpSessions.py index af6eb9fe..a3ecb5be 100644 --- a/scripts/artifacts/windowsRdpSessions.py +++ b/scripts/artifacts/windowsRdpSessions.py @@ -17,6 +17,7 @@ evtx = None from scripts.ilapfuncs import artifact_processor, logfunc +from scripts.windows_evtx import log_records # The TerminalServices-LocalSessionManager Operational log records interactive # session activity for both local console and Remote Desktop sessions: a logon, @@ -160,7 +161,7 @@ def rdpSessions(context): rows_here = 0 try: with evtx.Evtx(source) as log: - for record in log.records(): + for record in log_records(log, 'Windows Terminal Services Sessions', relative_source): try: row = _session_row(record.xml()) except ElementTree.ParseError: diff --git a/scripts/artifacts/windowsSecurityLogons.py b/scripts/artifacts/windowsSecurityLogons.py index f3e6edf8..4c71baf5 100644 --- a/scripts/artifacts/windowsSecurityLogons.py +++ b/scripts/artifacts/windowsSecurityLogons.py @@ -18,6 +18,7 @@ evtx = None from scripts.ilapfuncs import artifact_processor, logfunc +from scripts.windows_evtx import log_records # The Security event log (Security.evtx) records logon activity. This reads the # logon family: a successful or failed logon, a logoff, and a logon attempted @@ -50,7 +51,7 @@ "network address and workstation as recorded.", "author": "@AlexisBrignoni, Claude", "creation_date": "2026-09-15", - "last_update_date": "2026-09-24", + "last_update_date": "2026-09-27", "requirements": "python-evtx", "category": "Windows", "notes": "Read from Security.evtx, named in the report's located-at line. Only the logon " @@ -100,7 +101,7 @@ "sample_data": { "pc_mus_001_win11": "Windows 11 22H2 build 22621 | 1576 rows", "af_case2_win10": "Windows 10 1809 build 17763 | 1103 rows", - "lonewolf_win10": "Windows 10 Education build 16299 | 306 rows", + "lonewolf_win10": "Windows 10 Education build 16299 | 914 rows", }, }, } @@ -187,7 +188,7 @@ def securityLogons(context): rows_here = 0 try: with evtx.Evtx(source) as log: - for record in log.records(): + for record in log_records(log, 'Windows Security Logons', relative_source): try: row = _event_rows(record.xml()) except ElementTree.ParseError: diff --git a/scripts/artifacts/windowsServiceInstalls.py b/scripts/artifacts/windowsServiceInstalls.py index 6bf158ca..5d9771ba 100644 --- a/scripts/artifacts/windowsServiceInstalls.py +++ b/scripts/artifacts/windowsServiceInstalls.py @@ -17,6 +17,7 @@ evtx = None from scripts.ilapfuncs import artifact_processor, logfunc +from scripts.windows_evtx import log_records # The System event log records a Service Control Manager event 7045, "A service # was installed in the system", whenever a new service is registered. It carries @@ -36,7 +37,7 @@ "account the service runs under.", "author": "@AlexisBrignoni, Claude", "creation_date": "2026-09-15", - "last_update_date": "2026-09-24", + "last_update_date": "2026-09-27", "requirements": "python-evtx", "category": "Windows", "notes": "Read from System.evtx, named in the report's located-at line. Each row is a " @@ -71,9 +72,9 @@ "output_types": ["standard"], "artifact_icon": "settings", "sample_data": { - "pc_mus_001_win11": "Windows 11 22H2 build 22621 | 44 rows", + "pc_mus_001_win11": "Windows 11 22H2 build 22621 | 51 rows", "af_case2_win10": "Windows 10 1809 build 17763 | 28 rows", - "lonewolf_win10": "Windows 10 Education build 16299 | 38 rows", + "lonewolf_win10": "Windows 10 Education build 16299 | 57 rows", }, }, } @@ -149,7 +150,7 @@ def serviceInstalls(context): rows_here = 0 try: with evtx.Evtx(source) as log: - for record in log.records(): + for record in log_records(log, 'Windows Service Installations', relative_source): try: row = _service_row(record.xml()) except ElementTree.ParseError: diff --git a/scripts/artifacts/windowsShellCoreRunKeys.py b/scripts/artifacts/windowsShellCoreRunKeys.py index 3d01e300..4cfa0377 100644 --- a/scripts/artifacts/windowsShellCoreRunKeys.py +++ b/scripts/artifacts/windowsShellCoreRunKeys.py @@ -30,7 +30,7 @@ "stores.", "author": "@AlexisBrignoni, Claude", "creation_date": "2026-09-23", - "last_update_date": "2026-09-24", + "last_update_date": "2026-09-27", "requirements": "python-evtx", "category": "Windows", "notes": "Read from Microsoft-Windows-Shell-Core%4Operational.evtx, named in the " @@ -75,8 +75,8 @@ "artifact_icon": "play", "sample_data": { "af_case2_win10": "Windows 10 1809 build 17763 | 232 rows", - "pc_mus_001_win11": "Windows 11 22H2 build 22621 | 206 rows", - "lonewolf_win10": "Windows 10 Education build 16299 | 60 rows", + "pc_mus_001_win11": "Windows 11 22H2 build 22621 | 228 rows", + "lonewolf_win10": "Windows 10 Education build 16299 | 66 rows", }, }, } diff --git a/scripts/artifacts/windowsStorageDeviceEvents.py b/scripts/artifacts/windowsStorageDeviceEvents.py index 38b49e99..01da788c 100644 --- a/scripts/artifacts/windowsStorageDeviceEvents.py +++ b/scripts/artifacts/windowsStorageDeviceEvents.py @@ -92,7 +92,7 @@ "record stores.", "author": "@AlexisBrignoni, Claude", "creation_date": "2026-09-23", - "last_update_date": "2026-09-24", + "last_update_date": "2026-09-27", "requirements": "python-evtx", "category": "Windows", "notes": "Read from Microsoft-Windows-Kernel-PnP%4Configuration.evtx, named in the " @@ -134,7 +134,7 @@ "sample_data": { "pc_mus_001_win11": "Windows 11 22H2 build 22621 | 345 rows", "af_case2_win10": "Windows 10 1809 build 17763 | 205 rows", - "lonewolf_win10": "Windows 10 Education build 16299 | 250 rows", + "lonewolf_win10": "Windows 10 Education build 16299 | 260 rows", }, }, } diff --git a/scripts/artifacts/windowsSystemPowerEvents.py b/scripts/artifacts/windowsSystemPowerEvents.py index 33bb2322..fd9870a8 100644 --- a/scripts/artifacts/windowsSystemPowerEvents.py +++ b/scripts/artifacts/windowsSystemPowerEvents.py @@ -21,7 +21,7 @@ from scripts import windows_messages from scripts.ilapfuncs import artifact_processor, logfunc -from scripts.windows_evtx import utc_from_system_time +from scripts.windows_evtx import log_records, utc_from_system_time # The System event log records the machine's power timeline from several # providers: the event log service starting and stopping (a proxy for boot and @@ -90,7 +90,7 @@ "initiating a shutdown or restart.", "author": "@AlexisBrignoni, Claude", "creation_date": "2026-09-15", - "last_update_date": "2026-09-24", + "last_update_date": "2026-09-27", "requirements": "python-evtx; pefile to give stored numbers their names", "category": "Windows", "notes": "Read from System.evtx, named in the report's located-at line. Each row is one " @@ -107,24 +107,26 @@ "low power state); and User32 1074 (a process initiated a shutdown or " "restart). The Event column is the description for that " "provider and Event ID. Event Time (UTC) is the record's " - "TimeCreated SystemTime, stored in UTC. On the registered images the time " - "of a 107 record was not the time the system resumed: each of the 54 was " - "logged 0.4 to 221 seconds after the 42 before it, and the earliest " + "TimeCreated SystemTime, stored in UTC. On af_case2_win10, lonewolf_win10 and " + "pc_mus_001_win11 the time of a 107 record was not the time the system resumed: each of " + "the 95 was logged 0.4 to " + "221 seconds after the 42 before it, and the earliest " "Power-Troubleshooter Wake Time after that 42 came 4 seconds to 9.7 days " "after the 107 was logged. Sleep Time (UTC) and Wake Time (UTC) are the " "SleepTime and WakeTime fields of the Power-Troubleshooter 1 event, which " "its message shows as Sleep Time and Wake Time; they are blank on the " "other rows. " - "Sleep Time and Wake Time were the same, or 0.008 second apart, on 18 " - "rows: the 17 whose Wake Source Type is 3 (2 on lonewolf_win10 and 15 on " - "pc_mus_001_win11) and one lonewolf_win10 row whose Wake Source Type is 6. " + "Sleep Time and Wake Time were the same, or 0.008 second apart, on 28 rows: the 26 " + "whose Wake Source Type is 3 (9 on lonewolf_win10 and 17 on pc_mus_001_win11) and two " + "lonewolf_win10 rows whose Wake Source Type is 6 and 0. " "The last 42 before each of those Wake Times was 3.0 hours earlier, so " - "equal times there do not mean the system slept for no time. Both fields " - "are UTC: on the registered images, whose SYSTEM hives name the Pacific " - "(af_case2_win10) and Eastern time zones, every Sleep Time was within 2 " - "seconds of the time of a 42 record, and every Power-Troubleshooter event " - "but that lonewolf_win10 row whose Wake Source Type is 6 was logged within " - "2 seconds after its Wake Time (that row 87 minutes after). Detail holds the process, " + "equal times there do not mean the system slept for no time. Both fields are UTC: on " + "af_case2_win10, lonewolf_win10 and pc_mus_001_win11, whose SYSTEM hives name the " + "Pacific (af_case2_win10) and Eastern time zones, every Sleep Time was within 14.4 " + "seconds of the time of a 42 record, and every Power-Troubleshooter event but those two " + "lonewolf_win10 rows was logged within 3.3 seconds after its Wake Time (the rows whose " + "Wake Source Type is 6 and 0 were logged 87 minutes and 12.8 hours after). Detail holds " + "the process, " "action, reason and user a 1074 event stores in its parameters; for a 42 event its " "Reason, which the 42 message shows as Sleep Reason; for a 109 event its Shutdown " "Action Type, Shutdown Event Code and Shutdown Reason, the ShutdownActionType, " @@ -154,21 +156,26 @@ "registered image every boot record in the System log names one build (17763 on " "af_case2_win10, 16299 on lonewolf_win10, 22621 on pc_mus_001_win11) and none of " "these events comes before the first boot record, so every one of these fields was " - "named. Sleep Reason was System Idle (7) on both af_case2_win10 rows; System Idle (7) " - "on six and Button or Lid (0) on one lonewolf_win10 row; and System Idle (7) on 22, " - "Hibernate from Sleep - Fixed Timeout (6) on 14 and Button or Lid (0) on nine " - "pc_mus_001_win11 rows. Shutdown Reason was Kernel API (5) and Shutdown Event Code 0 " + "named. Sleep Reason was System Idle (7) on both af_case2_win10 rows; System Idle (7) on " + "18, Hibernate from Sleep - Fixed Timeout (6) on eight, Button or Lid (0) on six and " + "Application API (4) on one lonewolf_win10 row; and System Idle (7) on 29, Hibernate " + "from Sleep - Fixed Timeout (6) on 16, Button or Lid (0) on 13, and Application API (4) " + "and Battery (2) on one pc_mus_001_win11 row each. Shutdown Reason was Kernel API (5) " + "and Shutdown Event Code 0 " "on every 109 row; Shutdown Action Type was Power Action Shutdown Reset (5) on 12, " "Power Action Shutdown Off (6) on six and Power Action Shutdown (4) on three " "af_case2_win10 rows; Power Action Shutdown Reset (5) on all three lonewolf_win10 " "rows; and Power Action Reboot (5) on eight and Power Action Shutdown (4) on one " "pc_mus_001_win11 row. Wake Source Type was Power Button (1) on both af_case2_win10 " - "rows; Timer - (6) on five and S4 Doze to Hibernate (3) on two lonewolf_win10 rows; " - "and Power Button (1) on 15, S4 Doze to Hibernate (3) on 15, Unknown (0) on 14 and " + "rows; Unknown (0) on ten, S4 Doze to Hibernate (3) on nine, Power Button (1) on eight, " + "Timer - (6) on five and Device - (5) on one lonewolf_win10 row; and Power Button (1) on " + "24, Unknown (0) on 18, S4 Doze to Hibernate (3) on 17 and " "'Unknown, but possibily due to timer - (8)' (spelled so in the map) on one " - "pc_mus_001_win11 row. Wake Source Text, Wake Timer Owner and Wake Timer Context were " - "filled only on the six rows whose Wake Source Type is 6 or 8, where the text names a " - "scheduled task that requested waking the computer. The other fields of the 42, 107 " + "pc_mus_001_win11 row. Wake Source Text was filled only on the seven rows whose Wake " + "Source Type is 5, 6 or 8, and Wake Timer Owner and Wake Timer Context only on the six " + "whose type is 6 or 8, where the text names a scheduled task that requested waking the " + "computer; on the lonewolf_win10 row whose type is 5 the text reads ACPI Lid. The other " + "fields of the 42, 107 " "and Power-Troubleshooter 1 events are not reported, among them the target, effective " "and wake-from states, the flags, the programmed wake times, the durations and the " "hibernation counters: none of the three messages shows them, and the providers' DLLs " @@ -225,9 +232,9 @@ "output_types": ["standard"], "artifact_icon": "power", "sample_data": { - "pc_mus_001_win11": "Windows 11 22H2 build 22621 | 197 rows", + "pc_mus_001_win11": "Windows 11 22H2 build 22621 | 243 rows", "af_case2_win10": "Windows 10 1809 build 17763 | 121 rows", - "lonewolf_win10": "Windows 10 Education build 16299 | 41 rows", + "lonewolf_win10": "Windows 10 Education build 16299 | 119 rows", }, }, } @@ -443,7 +450,7 @@ def systemPowerEvents(context): boots = [] try: with evtx.Evtx(source) as log: - for record in log.records(): + for record in log_records(log, 'Windows System Power Events', relative_source): try: found = _power_record(record.xml()) except ElementTree.ParseError: diff --git a/scripts/artifacts/windowsTaskSchedulerEvents.py b/scripts/artifacts/windowsTaskSchedulerEvents.py index b094c0b6..b6e8feaf 100644 --- a/scripts/artifacts/windowsTaskSchedulerEvents.py +++ b/scripts/artifacts/windowsTaskSchedulerEvents.py @@ -52,9 +52,9 @@ "stores. None of af_case2_win10, lonewolf_win10 and pc_mus_001_win11 carries " "this log file, so the artifact reports nothing on them; it was exercised on " "szechuan_win10, the public " - "DFIR Madness Szechuan Sauce desktop image, where it reported 144 rows " - "(106: 11, 140: 127, 141: 6), Account held a SID on 60 of them, a domain and " - "account name on 82 and an account name with no domain on 2, and every record " + "DFIR Madness Szechuan Sauce desktop image, where it reported 262 rows (106: " + "15, 140: 239, 141: 8), Account held a SID on 111 of them, a domain and " + "account name on 149 and an account name with no domain on 2, and every record " "rendered. A record " "python-evtx cannot render, or whose XML does not parse, is counted in " "the run log and not reported. The task definitions themselves are " @@ -67,7 +67,7 @@ "pc_mus_001_win11": "Windows 11 22H2 build 22621 | 0 rows (no Task Scheduler Operational log on the image)", "af_case2_win10": "Windows 10 1809 build 17763 | 0 rows (no Task Scheduler Operational log on the image)", "lonewolf_win10": "Windows 10 Education build 16299 | 0 rows (no Task Scheduler Operational log on the image)", - "szechuan_win10": "Windows 10 2004 build 19041 | 144 rows", + "szechuan_win10": "Windows 10 2004 build 19041 | 262 rows", }, }, "taskSchedulerActions": { @@ -101,9 +101,9 @@ "stores. None of af_case2_win10, lonewolf_win10 and pc_mus_001_win11 carries " "this log file, so the artifact reports nothing on them; it was exercised on " "szechuan_win10, the public " - "DFIR Madness Szechuan Sauce desktop image, where it reported 201 rows " - "(200: 109, 201: 92), Action was blank on 6 of the 109 200 rows and 5 of " - "the 92 201 rows as stored, and every record rendered. A record " + "DFIR Madness Szechuan Sauce desktop image, where it reported 315 rows (200: " + "170, 201: 145), Action was blank on 8 of the 170 200 rows and 7 of the 145 201 " + "rows as stored, and every record rendered. A record " "python-evtx cannot render, or whose XML does not parse, is counted in " "the run log and not reported. The log's other events (for example 100, " "102, 129) are not reported. Reading needs the python-evtx package (pip " @@ -115,7 +115,7 @@ "pc_mus_001_win11": "Windows 11 22H2 build 22621 | 0 rows (no Task Scheduler Operational log on the image)", "af_case2_win10": "Windows 10 1809 build 17763 | 0 rows (no Task Scheduler Operational log on the image)", "lonewolf_win10": "Windows 10 Education build 16299 | 0 rows (no Task Scheduler Operational log on the image)", - "szechuan_win10": "Windows 10 2004 build 19041 | 201 rows", + "szechuan_win10": "Windows 10 2004 build 19041 | 315 rows", }, }, } diff --git a/scripts/artifacts/windowsVhdmpEvents.py b/scripts/artifacts/windowsVhdmpEvents.py index 814baa65..4a8b25b1 100644 --- a/scripts/artifacts/windowsVhdmpEvents.py +++ b/scripts/artifacts/windowsVhdmpEvents.py @@ -40,7 +40,7 @@ "with the account SID each record stores.", "author": "@AlexisBrignoni, Claude", "creation_date": "2026-09-23", - "last_update_date": "2026-09-23", + "last_update_date": "2026-09-27", "requirements": "python-evtx", "category": "Windows", "notes": "Read from Microsoft-Windows-VHDMP-Operational.evtx, named in the " @@ -88,7 +88,7 @@ "artifact_icon": "hard-drive", "sample_data": { "af_case2_win10": "Windows 10 1809 build 17763 | 14 rows", - "pc_mus_001_win11": "Windows 11 22H2 build 22621 | 245 rows", + "pc_mus_001_win11": "Windows 11 22H2 build 22621 | 260 rows", "lonewolf_win10": "Windows 10 Education build 16299 | 0 rows (no VHDMP Operational log on the image)", }, }, diff --git a/scripts/artifacts/windowsWlanEvents.py b/scripts/artifacts/windowsWlanEvents.py index 5ae72349..c46bfc4a 100644 --- a/scripts/artifacts/windowsWlanEvents.py +++ b/scripts/artifacts/windowsWlanEvents.py @@ -29,7 +29,7 @@ "profile, authentication and encryption each record stores.", "author": "@AlexisBrignoni, Claude", "creation_date": "2026-09-23", - "last_update_date": "2026-09-24", + "last_update_date": "2026-09-27", "requirements": "python-evtx", "category": "Windows", "notes": "Read from Microsoft-Windows-WLAN-AutoConfig%4Operational.evtx, named in " @@ -52,8 +52,7 @@ "of these events carries a BSSID field in the manifest, so no access " "point hardware address is reported. SSID, BSS Type, Adapter and " "Interface GUID each held one value on every row of pc_mus_001_win11 and " - "lonewolf_win10. Disconnect Reason is empty on lonewolf_win10, which " - "carries no 8003 records. Event Time (UTC) is the record's TimeCreated " + "lonewolf_win10. Event Time (UTC) is the record's TimeCreated " "SystemTime, which python-evtx renders from the FILETIME the record " "stores, counted in UTC (python-evtx 0.8.1, " "https://github.com/williballenthin/python-evtx/blob/cab997af04b6caae68b306e5c2c40b3aa751454e/Evtx/BinaryParser.py#L105-L113). " @@ -73,8 +72,8 @@ "output_types": ["standard"], "artifact_icon": "wifi", "sample_data": { - "pc_mus_001_win11": "Windows 11 22H2 build 22621 | 45 rows", - "lonewolf_win10": "Windows 10 Education build 16299 | 9 rows", + "pc_mus_001_win11": "Windows 11 22H2 build 22621 | 46 rows", + "lonewolf_win10": "Windows 10 Education build 16299 | 13 rows", "af_case2_win10": "Windows 10 1809 build 17763 | 0 rows (no WLAN-AutoConfig Operational log on the image)", }, }, diff --git a/scripts/artifacts/windowsWmiActivity.py b/scripts/artifacts/windowsWmiActivity.py index 211c5c44..2ab14876 100644 --- a/scripts/artifacts/windowsWmiActivity.py +++ b/scripts/artifacts/windowsWmiActivity.py @@ -30,7 +30,7 @@ "process ID each record stores.", "author": "@AlexisBrignoni, Claude", "creation_date": "2026-09-23", - "last_update_date": "2026-09-24", + "last_update_date": "2026-09-27", "requirements": "python-evtx", "category": "Windows", "notes": "Read from Microsoft-Windows-WMI-Activity%4Operational.evtx, named in the " @@ -56,9 +56,9 @@ "them processid, MachineName and providerName, and both spellings are " "read; Possible Cause is PossibleCause. Every value is reported as stored, with " "any leading or trailing whitespace removed. Client Machine held one value on " - "every 5860 row of each " - "registered image (12, 16 and 3 rows), the same text as Computer on 12 of " - "12, 16 of 16 and 2 of 3 of those rows. On each registered image the 5861 " + "every 5860 row of af_case2_win10, pc_mus_001_win11 and lonewolf_win10 (12, 29 " + "and 3 rows), the same text as Computer on 12 of 12, 29 " + "of 29 and 2 of 3 of those rows. On each registered image the 5861 " "rows named one Event Filter and Consumer pair (12 rows on " "af_case2_win10, 10 on pc_mus_001_win11 and 3 on lonewolf_win10). Event " "Time (UTC) is the record's TimeCreated SystemTime, which python-evtx " @@ -79,7 +79,7 @@ "artifact_icon": "zap", "sample_data": { "af_case2_win10": "Windows 10 1809 build 17763 | 36 rows", - "pc_mus_001_win11": "Windows 11 22H2 build 22621 | 36 rows", + "pc_mus_001_win11": "Windows 11 22H2 build 22621 | 49 rows", "lonewolf_win10": "Windows 10 Education build 16299 | 9 rows", }, }, @@ -91,7 +91,7 @@ "result code).", "author": "@AlexisBrignoni, Claude", "creation_date": "2026-09-23", - "last_update_date": "2026-09-24", + "last_update_date": "2026-09-27", "requirements": "python-evtx", "category": "Windows", "notes": "Read from Microsoft-Windows-WMI-Activity%4Operational.evtx, named in the " @@ -113,9 +113,9 @@ "Every value is reported as stored, with any leading or trailing whitespace " "removed; the manifest formats both result " "codes as hexadecimal. Result Code held one value on every 5857 row of " - "each registered image. Component and Possible Cause held the same text " - "as each other on every 5858 row of lonewolf_win10 (156 rows), on 173 of " - "174 rows on af_case2_win10 and on 235 of 246 on pc_mus_001_win11. Event " + "each registered image. Component and Possible Cause held the same text as " + "each other on 278 of the 280 5858 rows on lonewolf_win10, 173 of 174 on " + "af_case2_win10 and 305 of 338 on pc_mus_001_win11. Event " "Time (UTC) is the record's TimeCreated SystemTime, which python-evtx " "renders from the FILETIME the record stores, counted in UTC (python-evtx " "0.8.1, " @@ -134,8 +134,8 @@ "artifact_icon": "activity", "sample_data": { "af_case2_win10": "Windows 10 1809 build 17763 | 287 rows", - "pc_mus_001_win11": "Windows 11 22H2 build 22621 | 506 rows", - "lonewolf_win10": "Windows 10 Education build 16299 | 225 rows", + "pc_mus_001_win11": "Windows 11 22H2 build 22621 | 665 rows", + "lonewolf_win10": "Windows 10 Education build 16299 | 592 rows", }, }, } diff --git a/scripts/windows_evtx.py b/scripts/windows_evtx.py index 6f5c695b..1dee9185 100644 --- a/scripts/windows_evtx.py +++ b/scripts/windows_evtx.py @@ -2,11 +2,8 @@ Author: @AlexisBrignoni, Claude. -Shared by the event log artifacts for PowerShell, storage devices, Task -Scheduler, BITS, the Remote Desktop client, WLAN, Windows Installer, Shell-Core -Run key processing, WMI activity, virtual disks, network profiles and Microsoft -Defender. Each record is rendered to XML by python-evtx and read with -ElementTree. +Shared by the event log artifacts. Each record is rendered to XML by +python-evtx and read with ElementTree. `read_event_records(context, file_name, label, ...)` reads every matched copy of one log and returns the parsed records it kept, each with the staged path it @@ -15,6 +12,21 @@ is counted and skipped instead of ending the read of the rest of the file, and the count is written to the run log for each file. +`log_records(log, label, relative_source)` yields the records of an open +python-evtx log, and every artifact that reads a log reads it through this. +python-evtx's Evtx.records() reads only as many chunks as the file header's +chunk count (FileHeader.chunks, python-evtx v0.8.1, +https://github.com/williballenthin/python-evtx/blob/cab997af04b6caae68b306e5c2c40b3aa751454e/Evtx/Evtx.py#L223-L242). +libyal's description of the format says that the header of a log marked dirty +(flag 0x0001) can count fewer chunks than the file holds, that Event Viewer +seems to correct such a file, and that libevtx keeps scanning for chunks after +the last one the header indicates ('Dirty file with invalid number of chunks', +https://github.com/libyal/libevtx/blob/53ff3377d1360a9a3a428e7190c289757ccbf82b/documentation/Windows%20XML%20Event%20Log%20(EVTX).asciidoc?plain=1#L1817-L1842). +So for a log marked dirty this also reads each chunk after the counted ones +that carries the chunk signature and whose header and data checksums match, +and skips a record there whose number was already read. A log not marked dirty +is read exactly as python-evtx reads it. + `utc_from_system_time(value)` parses TimeCreated SystemTime. python-evtx 0.8.x renders it as '2018-03-27 09:35:33.595600+00:00' and 0.7.x as '2018-03-27 09:35:33.595600'; both come from the record's FILETIME converted in @@ -150,6 +162,68 @@ def _text(parent, name): return element.text.strip() if element is not None and element.text else '' +def _intact(chunk): + """Whether a chunk carries the chunk signature and both of its checksums match.""" + try: + return bool(chunk.check_magic() + and chunk.calculate_header_checksum() == chunk.header_checksum() + and chunk.calculate_data_checksum() == chunk.data_checksum()) + except Exception: # pylint: disable=broad-exception-caught + return False + + +def log_records(log, label='', relative_source=''): + """Every record of an open python-evtx log, with the chunks a dirty header does not count. + + The counted chunks are read as python-evtx reads them. For a log marked dirty, + each later chunk is read when it is intact (_intact), a record whose number was + already read is skipped, and a chunk whose records stop parsing part way through + keeps the records read before that point. The run log names the log and says how + many records came from chunks the header does not count. + """ + header = log.get_file_header() + counted = header.chunk_count() + dirty = header.is_dirty() + seen = set() + added = read_chunks = failed_checksum = stopped = 0 + for index, chunk in enumerate(header.chunks(include_inactive=dirty)): + if index < counted: + for record in chunk.records(): + if dirty: + seen.add(record.record_num()) + yield record + continue + if not chunk.check_magic(): + continue + if not _intact(chunk): + failed_checksum += 1 + continue + read_chunks += 1 + records = chunk.records() + while True: + try: + record = next(records) + number = record.record_num() + except StopIteration: + break + except Exception: # pylint: disable=broad-exception-caught + stopped += 1 + break + if number in seen: + continue + seen.add(number) + added += 1 + yield record + if read_chunks or failed_checksum: + message = (f'{label}: {relative_source} is marked dirty; {added} record(s) were read ' + f'from {read_chunks} chunk(s) after the {counted} its header counts') + if failed_checksum: + message += f', and {failed_checksum} chunk(s) after them failed their checksums and were not read' + if stopped: + message += f'; {stopped} of those chunk(s) stopped parsing part way through' + logfunc(message) + + def read_event_records(context, file_name, label, event_ids=None, provider=None): """Read every copy of one event log in files_found. @@ -172,7 +246,7 @@ def read_event_records(context, file_name, label, event_ids=None, provider=None) try: with evtx.Evtx(source) as log: sources.append(source) - for record in log.records(): + for record in log_records(log, label, relative_source): read += 1 try: xml_text = record.xml() From b083eb6976e22dff0654ae745df81d123b82d9db Mon Sep 17 00:00:00 2001 From: Brigs Date: Sun, 27 Sep 2026 17:13:09 -0400 Subject: [PATCH 2/2] Look up the record number field by name in the reader test python-evtx declares record_num on each Record instance at run time, so pylint, which can see the class when python-evtx is installed, reports it as a missing member. Co-Authored-By: Claude Opus 5.5 --- admin/test/scripts/test_windows_evtx_uncounted_chunks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/admin/test/scripts/test_windows_evtx_uncounted_chunks.py b/admin/test/scripts/test_windows_evtx_uncounted_chunks.py index d4603a08..3417702b 100644 --- a/admin/test/scripts/test_windows_evtx_uncounted_chunks.py +++ b/admin/test/scripts/test_windows_evtx_uncounted_chunks.py @@ -198,7 +198,7 @@ def test_python_evtx_can_list_the_chunks_its_header_does_not_count(self): for name in ('check_magic', 'header_checksum', 'calculate_header_checksum', 'data_checksum', 'calculate_data_checksum', 'records'): self.assertTrue(callable(getattr(chunk, name))) - self.assertTrue(callable(windows_evtx.evtx.Record(bytearray(0x100), 0, chunk).record_num)) + self.assertTrue(callable(getattr(windows_evtx.evtx.Record(bytearray(0x100), 0, chunk), 'record_num'))) if __name__ == '__main__':