diff --git a/admin/test/cases/data/safaribrowsing/testdata.safaribrowsing.safariBookmarks.case1.zip b/admin/test/cases/data/safaribrowsing/testdata.safaribrowsing.safariBookmarks.case1.zip new file mode 100644 index 0000000..10ebb11 Binary files /dev/null and b/admin/test/cases/data/safaribrowsing/testdata.safaribrowsing.safariBookmarks.case1.zip differ diff --git a/admin/test/cases/data/safaribrowsing/testdata.safaribrowsing.safariCloudTabs.case1.zip b/admin/test/cases/data/safaribrowsing/testdata.safaribrowsing.safariCloudTabs.case1.zip new file mode 100644 index 0000000..68ff4ad Binary files /dev/null and b/admin/test/cases/data/safaribrowsing/testdata.safaribrowsing.safariCloudTabs.case1.zip differ diff --git a/admin/test/cases/data/safaribrowsing/testdata.safaribrowsing.safariHistory.case1.zip b/admin/test/cases/data/safaribrowsing/testdata.safaribrowsing.safariHistory.case1.zip new file mode 100644 index 0000000..b71111d Binary files /dev/null and b/admin/test/cases/data/safaribrowsing/testdata.safaribrowsing.safariHistory.case1.zip differ diff --git a/admin/test/cases/data/safaribrowsing/testdata.safaribrowsing.safariLastSession.case1.zip b/admin/test/cases/data/safaribrowsing/testdata.safaribrowsing.safariLastSession.case1.zip new file mode 100644 index 0000000..c0b6bbd Binary files /dev/null and b/admin/test/cases/data/safaribrowsing/testdata.safaribrowsing.safariLastSession.case1.zip differ diff --git a/admin/test/cases/data/safaribrowsing/testdata.safaribrowsing.safariRecentlyClosedTabs.case1.zip b/admin/test/cases/data/safaribrowsing/testdata.safaribrowsing.safariRecentlyClosedTabs.case1.zip new file mode 100644 index 0000000..82cc79f Binary files /dev/null and b/admin/test/cases/data/safaribrowsing/testdata.safaribrowsing.safariRecentlyClosedTabs.case1.zip differ diff --git a/admin/test/cases/data/safaribrowsing/testdata.safaribrowsing.safariTopSites.case1.zip b/admin/test/cases/data/safaribrowsing/testdata.safaribrowsing.safariTopSites.case1.zip new file mode 100644 index 0000000..b462a79 Binary files /dev/null and b/admin/test/cases/data/safaribrowsing/testdata.safaribrowsing.safariTopSites.case1.zip differ diff --git a/admin/test/cases/testdata.safaribrowsing.json b/admin/test/cases/testdata.safaribrowsing.json new file mode 100644 index 0000000..c7928ed --- /dev/null +++ b/admin/test/cases/testdata.safaribrowsing.json @@ -0,0 +1,80 @@ +{ + "case1": { + "description": "Real macOS 11.2.1 (Big Sur) device data from Josh Hickman's public forensic training image (thisisdfir account), same source as the Keychains and iMessage case1 fixtures. Validates all six Safari artifacts: History (27 visits), Bookmarks (7 bookmarks), Top Sites (12 entries), Recently Closed Tabs (2 tabs), iCloud Tabs/CloudTabs.db (2 synced tabs), and Last Session/Open Tabs (2 tabs). All six artifacts returned real, non-empty data — no known gaps for this case.", + "maker": "@Gear-I", + "make_data": { + "input_data_path": "macOS-BigSur-full.zip", + "os": "macOS-26.5.2-arm64-arm-64bit-Mach-O", + "timestamp": "2026-08-30T10:35:10.712953", + "last_commit": { + "hash": "3a6007b031dd3b7a749e75cb161e658d24260306", + "author_name": "Dielle De Noon", + "author_email": "N/A", + "date": "2026-08-30T10:30:56-04:00", + "message": "Corrected results for Recently Closed Tabs for Safari" + } + }, + "artifacts": { + "safariHistory": { + "search_patterns": [ + "*/Library/Safari/History.db*" + ], + "file_count": 2, + "expected_output": { + "headers": [], + "data": [] + } + }, + "safariBookmarks": { + "search_patterns": [ + "*/Library/Safari/Bookmarks.plist" + ], + "file_count": 1, + "expected_output": { + "headers": [], + "data": [] + } + }, + "safariTopSites": { + "search_patterns": [ + "*/Library/Safari/TopSites.plist" + ], + "file_count": 1, + "expected_output": { + "headers": [], + "data": [] + } + }, + "safariRecentlyClosedTabs": { + "search_patterns": [ + "*/Library/Safari/RecentlyClosedTabs.plist" + ], + "file_count": 1, + "expected_output": { + "headers": [], + "data": [] + } + }, + "safariCloudTabs": { + "search_patterns": [ + "*/Library/Safari/CloudTabs.db*" + ], + "file_count": 2, + "expected_output": { + "headers": [], + "data": [] + } + }, + "safariLastSession": { + "search_patterns": [ + "*/Library/Safari/LastSession.plist" + ], + "file_count": 1, + "expected_output": { + "headers": [], + "data": [] + } + } + } + } +} \ No newline at end of file diff --git a/admin/test/results/safaribrowsing/safaribrowsing.safariBookmarks.case1.20260830143822.json b/admin/test/results/safaribrowsing/safaribrowsing.safariBookmarks.case1.20260830143822.json new file mode 100644 index 0000000..712ac65 --- /dev/null +++ b/admin/test/results/safaribrowsing/safaribrowsing.safariBookmarks.case1.20260830143822.json @@ -0,0 +1,82 @@ +{ + "metadata": { + "module_name": "safaribrowsing", + "artifact_name": "Safari Bookmarks", + "function_name": "safariBookmarks", + "case_number": "case1", + "number_of_columns": 5, + "number_of_rows": 7, + "total_data_size_bytes": 1057, + "media_checkins": 0, + "media_embedded_checkins": 0, + "input_zip_path": "admin/test/cases/data/safaribrowsing/testdata.safaribrowsing.safariBookmarks.case1.zip", + "start_time": "2026-08-30T14:38:22.422183+00:00", + "end_time": "2026-08-30T14:38:22.431801+00:00", + "run_time_seconds": 0.00018095970153808594, + "last_commit": { + "hash": "3a6007b031dd3b7a749e75cb161e658d24260306", + "author_name": "Dielle De Noon", + "author_email": "dielleaiko@hotmail.com", + "date": "2026-08-30T10:30:56-04:00", + "message": "Corrected results for Recently Closed Tabs for Safari" + } + }, + "headers": [ + "Folder Path", + "Title", + "URL", + "Bookmark UUID", + "Source File" + ], + "data": [ + [ + "BookmarksBar", + "Apple", + "https://www.apple.com/", + "4D98B1AE-8E3C-47E3-B6C1-C2FB7865F045", + "tsk-data-volume-full/Users/thisisdfir/Library/Safari/Bookmarks.plist" + ], + [ + "BookmarksBar", + "Bing", + "https://www.bing.com/", + "C99275EB-0F49-4F80-8823-3564FF998F56", + "tsk-data-volume-full/Users/thisisdfir/Library/Safari/Bookmarks.plist" + ], + [ + "BookmarksBar", + "Google", + "https://www.google.com/?client=safari&channel=iphone_bm", + "5B538E3C-D440-444A-A41E-B05BBFE4F5C5", + "tsk-data-volume-full/Users/thisisdfir/Library/Safari/Bookmarks.plist" + ], + [ + "BookmarksBar", + "Yahoo", + "https://yahoo.com/", + "BC105CB7-4BA4-4112-BE3C-334AA82F6FBD", + "tsk-data-volume-full/Users/thisisdfir/Library/Safari/Bookmarks.plist" + ], + [ + "BookmarksBar", + "Mac Rumors", + "https://www.macrumors.com/", + "BDE4ABCD-0A5C-4453-9D66-8C69CDBA5840", + "tsk-data-volume-full/Users/thisisdfir/Library/Safari/Bookmarks.plist" + ], + [ + "BookmarksBar", + "Ars Technica", + "https://arstechnica.com/", + "252D3E5D-2B36-42F2-A655-8A1590662A19", + "tsk-data-volume-full/Users/thisisdfir/Library/Safari/Bookmarks.plist" + ], + [ + "BookmarksBar", + "Cult of Mac", + "https://www.cultofmac.com/", + "A27FDC44-2A21-4448-B07A-190599424CCE", + "tsk-data-volume-full/Users/thisisdfir/Library/Safari/Bookmarks.plist" + ] + ] +} \ No newline at end of file diff --git a/admin/test/results/safaribrowsing/safaribrowsing.safariCloudTabs.case1.20260830143822.json b/admin/test/results/safaribrowsing/safaribrowsing.safariCloudTabs.case1.20260830143822.json new file mode 100644 index 0000000..f808ce6 --- /dev/null +++ b/admin/test/results/safaribrowsing/safaribrowsing.safariCloudTabs.case1.20260830143822.json @@ -0,0 +1,65 @@ +{ + "metadata": { + "module_name": "safaribrowsing", + "artifact_name": "Safari iCloud Tabs (CloudTabs.db)", + "function_name": "safariCloudTabs", + "case_number": "case1", + "number_of_columns": 11, + "number_of_rows": 2, + "total_data_size_bytes": 444, + "media_checkins": 0, + "media_embedded_checkins": 0, + "input_zip_path": "admin/test/cases/data/safaribrowsing/testdata.safaribrowsing.safariCloudTabs.case1.zip", + "start_time": "2026-08-30T14:38:22.466191+00:00", + "end_time": "2026-08-30T14:38:22.476040+00:00", + "run_time_seconds": 0.00041222572326660156, + "last_commit": { + "hash": "3a6007b031dd3b7a749e75cb161e658d24260306", + "author_name": "Dielle De Noon", + "author_email": "dielleaiko@hotmail.com", + "date": "2026-08-30T10:30:56-04:00", + "message": "Corrected results for Recently Closed Tabs for Safari" + } + }, + "headers": [ + "Tab Title", + "URL", + "Device Name", + "Device UUID", + "Device Last Modified", + "Ephemeral Device", + "Pinned", + "Showing Reader", + "Reader Scroll Page", + "Tab UUID", + "Source File" + ], + "data": [ + [ + "/r/Memes the original since 2008", + "https://www.reddit.com/r/memes/", + "This\u2019s Mac", + "80B6064F-5AF9-4D65-BE9C-13ECFDD5DC24", + 1613595906, + "", + "", + "", + 0, + "5E01C263-426D-4605-A95E-7126DA274557", + "tsk-data-volume-full/Users/thisisdfir/Library/Safari/CloudTabs.db" + ], + [ + "reddit: the front page of the internet", + "https://www.reddit.com/", + "This\u2019s Mac", + "80B6064F-5AF9-4D65-BE9C-13ECFDD5DC24", + 1613595906, + "", + "", + "", + 0, + "56E957DF-77B6-45A8-BE84-AA584AA4ADC8", + "tsk-data-volume-full/Users/thisisdfir/Library/Safari/CloudTabs.db" + ] + ] +} \ No newline at end of file diff --git a/admin/test/results/safaribrowsing/safaribrowsing.safariHistory.case1.20260830143822.json b/admin/test/results/safaribrowsing/safaribrowsing.safariHistory.case1.20260830143822.json new file mode 100644 index 0000000..c2bedd6 --- /dev/null +++ b/admin/test/results/safaribrowsing/safaribrowsing.safariHistory.case1.20260830143822.json @@ -0,0 +1,362 @@ +{ + "metadata": { + "module_name": "safaribrowsing", + "artifact_name": "Safari History", + "function_name": "safariHistory", + "case_number": "case1", + "number_of_columns": 10, + "number_of_rows": 27, + "total_data_size_bytes": 4665, + "media_checkins": 0, + "media_embedded_checkins": 0, + "input_zip_path": "admin/test/cases/data/safaribrowsing/testdata.safaribrowsing.safariHistory.case1.zip", + "start_time": "2026-08-30T14:38:22.432076+00:00", + "end_time": "2026-08-30T14:38:22.444135+00:00", + "run_time_seconds": 0.002719879150390625, + "last_commit": { + "hash": "3a6007b031dd3b7a749e75cb161e658d24260306", + "author_name": "Dielle De Noon", + "author_email": "dielleaiko@hotmail.com", + "date": "2026-08-30T10:30:56-04:00", + "message": "Corrected results for Recently Closed Tabs for Safari" + } + }, + "headers": [ + "Visit Time", + "URL", + "Domain", + "Visit Title", + "Item Visit Count (lifetime)", + "Load Successful", + "HTTP Non-GET", + "Synthesized", + "Origin (raw)", + "Source File" + ], + "data": [ + [ + 1613595429, + "https://www.reddit.com/r/memes/", + "reddit", + "/r/Memes the original since 2008", + 1, + "Yes", + "", + "", + 0, + "tsk-data-volume-full/Users/thisisdfir/Library/Safari/History.db" + ], + [ + 1613595425, + "https://www.reddit.com/r/memes", + "reddit", + "Internet for the Spirit", + 1, + "Yes", + "", + "", + 0, + "tsk-data-volume-full/Users/thisisdfir/Library/Safari/History.db" + ], + [ + 1613595161, + "https://www.reddit.com/r/wholesomememes/", + "reddit", + "Internet for the Spirit", + 3, + "Yes", + "", + "", + 0, + "tsk-data-volume-full/Users/thisisdfir/Library/Safari/History.db" + ], + [ + 1613595086, + "https://www.reddit.com/r/wholesomememes/comments/lm39sb/a_typical_day_with_a_five_year_old_using_an_80s/", + "reddit", + "A typical day with a five year old using an 80's cartoon as an illustration : wholesomememes", + 1, + "Yes", + "", + "", + 0, + "tsk-data-volume-full/Users/thisisdfir/Library/Safari/History.db" + ], + [ + 1613594925, + "https://www.reddit.com/r/wholesomememes/", + "reddit", + "Internet for the Spirit", + 3, + "Yes", + "", + "", + 0, + "tsk-data-volume-full/Users/thisisdfir/Library/Safari/History.db" + ], + [ + 1613594924, + "https://www.reddit.com/r/wholesomememes/", + "reddit", + "Internet for the Spirit", + 3, + "Yes", + "", + "", + 0, + "tsk-data-volume-full/Users/thisisdfir/Library/Safari/History.db" + ], + [ + 1613594902, + "https://www.reddit.com/", + "reddit", + "reddit: the front page of the internet", + 2, + "Yes", + "", + "", + 0, + "tsk-data-volume-full/Users/thisisdfir/Library/Safari/History.db" + ], + [ + 1613594900, + "https://www.reddit.com/", + "reddit", + "reddit: the front page of the internet", + 2, + "Yes", + "", + "", + 0, + "tsk-data-volume-full/Users/thisisdfir/Library/Safari/History.db" + ], + [ + 1613594900, + "https://reddit.com/", + "reddit", + "", + 1, + "Yes", + "", + "", + 0, + "tsk-data-volume-full/Users/thisisdfir/Library/Safari/History.db" + ], + [ + 1613594835, + "https://www.apple.com/", + "apple", + "", + 1, + "Yes", + "", + "", + 0, + "tsk-data-volume-full/Users/thisisdfir/Library/Safari/History.db" + ], + [ + 1613594205, + "https://www.newsobserver.com/", + "newsobserver", + "", + 1, + "Yes", + "", + "", + 0, + "tsk-data-volume-full/Users/thisisdfir/Library/Safari/History.db" + ], + [ + 1613594063, + "https://www.nhl.com/capitals/schedule/2021-02-01/ET", + "nhl", + "Washington Capitals Schedule | Washington Capitals", + 2, + "Yes", + "", + "", + 0, + "tsk-data-volume-full/Users/thisisdfir/Library/Safari/History.db" + ], + [ + 1613594062, + "https://www.nhl.com/capitals/schedule/2021-02-01/ET", + "nhl", + "Washington Capitals Schedule | Washington Capitals", + 2, + "Yes", + "", + "", + 0, + "tsk-data-volume-full/Users/thisisdfir/Library/Safari/History.db" + ], + [ + 1613594061, + "https://www.nhl.com/capitals/schedule", + "nhl", + "", + 1, + "Yes", + "", + "", + 0, + "tsk-data-volume-full/Users/thisisdfir/Library/Safari/History.db" + ], + [ + 1613594014, + "https://www.google.com/search?client=safari&rls=en&q=Washington+Capital+2021+schedule&ie=UTF-8&oe=UTF-8", + "google", + "Washington Capital 2021 schedule - Google Search", + 2, + "Yes", + "", + "", + 0, + "tsk-data-volume-full/Users/thisisdfir/Library/Safari/History.db" + ], + [ + 1613594013, + "https://www.google.com/search?client=safari&rls=en&q=Washington+Capital+2021+schedule&ie=UTF-8&oe=UTF-8", + "google", + "Washington Capital 2021 schedule - Google Search", + 2, + "Yes", + "", + "", + 0, + "tsk-data-volume-full/Users/thisisdfir/Library/Safari/History.db" + ], + [ + 1613593870, + "https://www.cultofmac.com/735417/apple-pays-out-163-million-in-unmatched-royalties-for-apple-music/", + "cultofmac", + "", + 1, + "Yes", + "", + "", + 0, + "tsk-data-volume-full/Users/thisisdfir/Library/Safari/History.db" + ], + [ + 1613593840, + "https://www.cultofmac.com/", + "cultofmac", + "", + 1, + "Yes", + "", + "", + 0, + "tsk-data-volume-full/Users/thisisdfir/Library/Safari/History.db" + ], + [ + 1613593628, + "https://arstechnica.com/gadgets/2021/02/galaxy-s21-ultra-200-cheaper-but-still-not-competitive/", + "arstechnica", + "", + 1, + "Yes", + "", + "", + 0, + "tsk-data-volume-full/Users/thisisdfir/Library/Safari/History.db" + ], + [ + 1613593424, + "https://arstechnica.com/", + "arstechnica", + "", + 1, + "Yes", + "", + "", + 0, + "tsk-data-volume-full/Users/thisisdfir/Library/Safari/History.db" + ], + [ + 1613422100, + "https://www.apple.com/icloud/", + "apple", + "iCloud - Apple", + 1, + "Yes", + "", + "", + 0, + "tsk-data-volume-full/Users/thisisdfir/Library/Safari/History.db" + ], + [ + 1610914608, + "https://help.apple.com/macos/big-sur/mac-mini/?lang=en&cases=kIFAGHvXTAepRptQXiXe5w,N0bfKOvERSiAp06mHOcgOQ¬ification=1", + "help.apple", + "Welcome to Mac mini", + 1, + "Yes", + "", + "", + 0, + "tsk-data-volume-full/Users/thisisdfir/Library/Safari/History.db" + ], + [ + 1610914608, + "https://help.apple.com/macos/big-sur/mac-mini?lang=en&cases=kIFAGHvXTAepRptQXiXe5w,N0bfKOvERSiAp06mHOcgOQ¬ification=1", + "help.apple", + "", + 1, + "Yes", + "", + "", + 0, + "tsk-data-volume-full/Users/thisisdfir/Library/Safari/History.db" + ], + [ + 1607810675, + "https://help.apple.com/macos/big-sur/macbook-air/?lang=en&cases=kIFAGHvXTAepRptQXiXe5w,N0bfKOvERSiAp06mHOcgOQ¬ification=1#big-sur", + "help.apple", + "Welcome to MacBook Air", + 1, + "Yes", + "", + "", + 0, + "tsk-data-volume-full/Users/thisisdfir/Library/Safari/History.db" + ], + [ + 1607810665, + "https://help.apple.com/macos/big-sur/macbook-air/?lang=en&cases=kIFAGHvXTAepRptQXiXe5w,N0bfKOvERSiAp06mHOcgOQ¬ification=1#performance", + "help.apple", + "Welcome to MacBook Air", + 1, + "Yes", + "", + "", + 0, + "tsk-data-volume-full/Users/thisisdfir/Library/Safari/History.db" + ], + [ + 1607810659, + "https://help.apple.com/macos/big-sur/macbook-air/?lang=en&cases=kIFAGHvXTAepRptQXiXe5w,N0bfKOvERSiAp06mHOcgOQ¬ification=1", + "help.apple", + "Welcome to MacBook Air", + 1, + "Yes", + "", + "", + 0, + "tsk-data-volume-full/Users/thisisdfir/Library/Safari/History.db" + ], + [ + 1607810659, + "https://help.apple.com/macos/big-sur/macbook-air?lang=en&cases=kIFAGHvXTAepRptQXiXe5w,N0bfKOvERSiAp06mHOcgOQ¬ification=1", + "help.apple", + "", + 1, + "Yes", + "", + "", + 0, + "tsk-data-volume-full/Users/thisisdfir/Library/Safari/History.db" + ] + ] +} \ No newline at end of file diff --git a/admin/test/results/safaribrowsing/safaribrowsing.safariLastSession.case1.20260830143822.json b/admin/test/results/safaribrowsing/safaribrowsing.safariLastSession.case1.20260830143822.json new file mode 100644 index 0000000..9c29e17 --- /dev/null +++ b/admin/test/results/safaribrowsing/safaribrowsing.safariLastSession.case1.20260830143822.json @@ -0,0 +1,62 @@ +{ + "metadata": { + "module_name": "safaribrowsing", + "artifact_name": "Safari Last Session (Open Tabs)", + "function_name": "safariLastSession", + "case_number": "case1", + "number_of_columns": 10, + "number_of_rows": 2, + "total_data_size_bytes": 467, + "media_checkins": 0, + "media_embedded_checkins": 0, + "input_zip_path": "admin/test/cases/data/safaribrowsing/testdata.safaribrowsing.safariLastSession.case1.zip", + "start_time": "2026-08-30T14:38:22.444523+00:00", + "end_time": "2026-08-30T14:38:22.456531+00:00", + "run_time_seconds": 0.0004191398620605469, + "last_commit": { + "hash": "3a6007b031dd3b7a749e75cb161e658d24260306", + "author_name": "Dielle De Noon", + "author_email": "dielleaiko@hotmail.com", + "date": "2026-08-30T10:30:56-04:00", + "message": "Corrected results for Recently Closed Tabs for Safari" + } + }, + "headers": [ + "Tab Title", + "URL", + "Window Closed", + "Last Visit Time", + "Window UUID", + "Tab UUID", + "Tab Index", + "Private Window", + "Session State Size (bytes)", + "Source File" + ], + "data": [ + [ + "reddit: the front page of the internet", + "https://www.reddit.com/", + 1613613906, + 1613594902, + "ABE5FF4F-9597-402C-BE92-C99E53EAEB95", + "56E957DF-77B6-45A8-BE84-AA584AA4ADC8", + 0, + "No", + 1516728, + "tsk-data-volume-full/Users/thisisdfir/Library/Safari/LastSession.plist" + ], + [ + "/r/Memes the original since 2008", + "https://www.reddit.com/r/memes/", + 1613613906, + 1613595429, + "ABE5FF4F-9597-402C-BE92-C99E53EAEB95", + "5E01C263-426D-4605-A95E-7126DA274557", + 1, + "No", + 244757, + "tsk-data-volume-full/Users/thisisdfir/Library/Safari/LastSession.plist" + ] + ] +} \ No newline at end of file diff --git a/admin/test/results/safaribrowsing/safaribrowsing.safariRecentlyClosedTabs.case1.20260830143822.json b/admin/test/results/safaribrowsing/safaribrowsing.safariRecentlyClosedTabs.case1.20260830143822.json new file mode 100644 index 0000000..4b47910 --- /dev/null +++ b/admin/test/results/safaribrowsing/safaribrowsing.safariRecentlyClosedTabs.case1.20260830143822.json @@ -0,0 +1,62 @@ +{ + "metadata": { + "module_name": "safaribrowsing", + "artifact_name": "Safari Recently Closed Tabs", + "function_name": "safariRecentlyClosedTabs", + "case_number": "case1", + "number_of_columns": 10, + "number_of_rows": 2, + "total_data_size_bytes": 397, + "media_checkins": 0, + "media_embedded_checkins": 0, + "input_zip_path": "admin/test/cases/data/safaribrowsing/testdata.safaribrowsing.safariRecentlyClosedTabs.case1.zip", + "start_time": "2026-08-30T14:38:22.406200+00:00", + "end_time": "2026-08-30T14:38:22.421909+00:00", + "run_time_seconds": 0.00013208389282226562, + "last_commit": { + "hash": "3a6007b031dd3b7a749e75cb161e658d24260306", + "author_name": "Dielle De Noon", + "author_email": "dielleaiko@hotmail.com", + "date": "2026-08-30T10:30:56-04:00", + "message": "Corrected results for Recently Closed Tabs for Safari" + } + }, + "headers": [ + "Tab Title", + "URL", + "Closed", + "Last Visit Time", + "Window UUID", + "Tab UUID", + "Tab Index", + "Private Window", + "Session State Size (bytes)", + "Source File" + ], + "data": [ + [ + "iCloud - Apple", + "https://www.apple.com/icloud/", + 1613440101, + null, + "CDC3DA36-7575-4D74-AF19-52AA59D4575D", + "02BB679F-69A3-4AC6-9F52-80185CE96149", + 0, + "No", + "", + "tsk-data-volume-full/Users/thisisdfir/Library/Safari/RecentlyClosedTabs.plist" + ], + [ + "Start Page", + "favorites://", + 1613611415, + null, + "B8164B46-64C9-4D5A-9F12-F4530B5247D7", + "4F771DE3-460D-4A6D-92D1-F17767CDD437", + 0, + "No", + "", + "tsk-data-volume-full/Users/thisisdfir/Library/Safari/RecentlyClosedTabs.plist" + ] + ] +} \ No newline at end of file diff --git a/admin/test/results/safaribrowsing/safaribrowsing.safariTopSites.case1.20260830143822.json b/admin/test/results/safaribrowsing/safaribrowsing.safariTopSites.case1.20260830143822.json new file mode 100644 index 0000000..1970eac --- /dev/null +++ b/admin/test/results/safaribrowsing/safaribrowsing.safariTopSites.case1.20260830143822.json @@ -0,0 +1,104 @@ +{ + "metadata": { + "module_name": "safaribrowsing", + "artifact_name": "Safari Top Sites", + "function_name": "safariTopSites", + "case_number": "case1", + "number_of_columns": 4, + "number_of_rows": 12, + "total_data_size_bytes": 1246, + "media_checkins": 0, + "media_embedded_checkins": 0, + "input_zip_path": "admin/test/cases/data/safaribrowsing/testdata.safaribrowsing.safariTopSites.case1.zip", + "start_time": "2026-08-30T14:38:22.456855+00:00", + "end_time": "2026-08-30T14:38:22.465914+00:00", + "run_time_seconds": 0.00011706352233886719, + "last_commit": { + "hash": "3a6007b031dd3b7a749e75cb161e658d24260306", + "author_name": "Dielle De Noon", + "author_email": "dielleaiko@hotmail.com", + "date": "2026-08-30T10:30:56-04:00", + "message": "Corrected results for Recently Closed Tabs for Safari" + } + }, + "headers": [ + "Title", + "URL", + "Built-in Default", + "Source File" + ], + "data": [ + [ + "", + "https://www.apple.com/startpage/", + "Yes", + "tsk-data-volume-full/Users/thisisdfir/Library/Safari/TopSites.plist" + ], + [ + "iCloud", + "https://www.icloud.com/", + "Yes", + "tsk-data-volume-full/Users/thisisdfir/Library/Safari/TopSites.plist" + ], + [ + "Yahoo", + "https://www.yahoo.com/", + "Yes", + "tsk-data-volume-full/Users/thisisdfir/Library/Safari/TopSites.plist" + ], + [ + "Bing", + "https://www.bing.com/", + "Yes", + "tsk-data-volume-full/Users/thisisdfir/Library/Safari/TopSites.plist" + ], + [ + "Google", + "https://www.google.com/?client=safari&channel=mac_bm", + "Yes", + "tsk-data-volume-full/Users/thisisdfir/Library/Safari/TopSites.plist" + ], + [ + "Wikipedia", + "https://www.wikipedia.org/", + "Yes", + "tsk-data-volume-full/Users/thisisdfir/Library/Safari/TopSites.plist" + ], + [ + "Facebook", + "https://www.facebook.com/", + "Yes", + "tsk-data-volume-full/Users/thisisdfir/Library/Safari/TopSites.plist" + ], + [ + "Twitter", + "https://twitter.com/", + "Yes", + "tsk-data-volume-full/Users/thisisdfir/Library/Safari/TopSites.plist" + ], + [ + "LinkedIn", + "https://www.linkedin.com/", + "Yes", + "tsk-data-volume-full/Users/thisisdfir/Library/Safari/TopSites.plist" + ], + [ + "The Weather Channel", + "https://www.weather.com/", + "Yes", + "tsk-data-volume-full/Users/thisisdfir/Library/Safari/TopSites.plist" + ], + [ + "Yelp", + "https://www.yelp.com/", + "Yes", + "tsk-data-volume-full/Users/thisisdfir/Library/Safari/TopSites.plist" + ], + [ + "TripAdvisor", + "https://www.tripadvisor.com/", + "Yes", + "tsk-data-volume-full/Users/thisisdfir/Library/Safari/TopSites.plist" + ] + ] +} \ No newline at end of file diff --git a/scripts/artifacts/safaribrowsing.py b/scripts/artifacts/safaribrowsing.py new file mode 100644 index 0000000..7907d87 --- /dev/null +++ b/scripts/artifacts/safaribrowsing.py @@ -0,0 +1,438 @@ +__artifacts_v2__ = { + "safariHistory": { + "name": "Safari History", + "description": "Each row in History.db's history_visits table, " + "joined back to history_items for the URL, domain " + "and lifetime visit count.", + "author": "Gear-I & Claude", + "creation_date": "2026-08-23", + "last_update_date": "2026-08-23", + "requirements": "none", + "category": "Safari (macOS)", + "notes": "visit_time confirmed as Mac Absolute Time in SECONDS " + "since 2001-01-01 against real data -- see module " + "docstring.", + "paths": ( + "*/Library/Safari/History.db*", + ), + "output_types": ["standard"], + "artifact_icon": "clock", + "sample_data": { + "macos_bigsur_thisisdfir": "macOS Big Sur (Josh Hickman / " + "thisisdfir public test image, acquired 2021-02-20), " + "History.db | 22 history items, 27 visits", + }, + }, + "safariBookmarks": { + "name": "Safari Bookmarks", + "description": "Each leaf bookmark in Bookmarks.plist -- " + "Bookmarks Bar, Bookmarks Menu and Reading List -- " + "with its folder path, title and URL.", + "author": "Gear-I & Claude", + "creation_date": "2026-08-23", + "last_update_date": "2026-08-23", + "requirements": "none", + "category": "Safari (macOS)", + "notes": "Leaf title is read from URIDictionary.title, not the " + "top-level Title key (that key only exists on folder " + "nodes) -- confirmed against real bookmarks-bar entries, " + "see module docstring.", + "paths": ( + "*/Library/Safari/Bookmarks.plist", + ), + "output_types": ["standard"], + "artifact_icon": "bookmark", + "sample_data": { + "macos_bigsur_thisisdfir": "macOS Big Sur (Josh Hickman / " + "thisisdfir public test image, acquired 2021-02-20), " + "Bookmarks.plist | 7 bookmarks-bar entries (Bookmarks Menu " + "and Reading List empty on this image)", + }, + }, + "safariTopSites": { + "name": "Safari Top Sites", + "description": "Each entry in TopSites.plist's TopSites list, " + "flagging which are Apple's shipped built-in " + "defaults versus frecency-derived from real " + "browsing.", + "author": "Gear-I & Claude", + "creation_date": "2026-08-23", + "last_update_date": "2026-08-23", + "requirements": "none", + "category": "Safari (macOS)", + "notes": "On the validation image all entries were built-in " + "defaults (TopSiteIsBuiltIn == True) -- that column is " + "included so an analyst can distinguish real earned top " + "sites from defaults on other data.", + "paths": ( + "*/Library/Safari/TopSites.plist", + ), + "output_types": ["standard"], + "artifact_icon": "star", + "sample_data": { + "macos_bigsur_thisisdfir": "macOS Big Sur (Josh Hickman / " + "thisisdfir public test image, acquired 2021-02-20), " + "TopSites.plist | 12 entries, all TopSiteIsBuiltIn=True " + "on this image", + }, + }, + "safariRecentlyClosedTabs": { + "name": "Safari Recently Closed Tabs", + "description": "Each tab in RecentlyClosedTabs.plist's " + "ClosedTabOrWindowPersistentStates -- window and " + "tab UUIDs, close time, title and URL.", + "author": "Gear-I & Claude", + "creation_date": "2026-08-23", + "last_update_date": "2026-08-23", + "requirements": "none", + "category": "Safari (macOS)", + "notes": "Each tab also carries a large SessionState " + "NSKeyedArchiver-style binary blob (per-tab back/forward " + "navigation history) -- not decoded, only its size is " + "reported (see module docstring).", + "paths": ( + "*/Library/Safari/RecentlyClosedTabs.plist", + ), + "output_types": ["standard"], + "artifact_icon": "x-circle", + "sample_data": { + "macos_bigsur_thisisdfir": "macOS Big Sur (Josh Hickman / " + "thisisdfir public test image, acquired 2021-02-20), " + "RecentlyClosedTabs.plist | 2 closed windows, 1 tab each " + "(start page favorites://, apple.com/icloud)", + }, + }, + "safariCloudTabs": { + "name": "Safari iCloud Tabs (CloudTabs.db)", + "description": "Tabs synced to this Mac from other Apple devices " + "via iCloud Tabs/Handoff, joined to the " + "originating device's name.", + "author": "Gear-I & Claude", + "creation_date": "2026-08-23", + "last_update_date": "2026-08-23", + "requirements": "none", + "category": "Safari (macOS)", + "notes": "system_fields and position are opaque NSKeyedArchiver/" + "zlib CloudKit-metadata blobs and are not decoded. " + "cloud_tab_close_requests exists in the schema but was " + "not inspected for this pass.", + "paths": ( + "*/Library/Safari/CloudTabs.db*", + ), + "output_types": ["standard"], + "artifact_icon": "cloud", + "sample_data": { + "macos_bigsur_thisisdfir": "macOS Big Sur (Josh Hickman / " + "thisisdfir public test image, acquired 2021-02-20), " + "CloudTabs.db | 2 synced tabs from 1 device ('This's " + "Mac')", + }, + }, + "safariLastSession": { + "name": "Safari Last Session (Open Tabs)", + "description": "Windows and tabs that were open the last time " + "Safari quit, from LastSession.plist -- title, " + "URL, last-visit time and whether the window was " + "private.", + "author": "Gear-I & Claude", + "creation_date": "2026-08-23", + "last_update_date": "2026-08-23", + "requirements": "none", + "category": "Safari (macOS)", + "notes": "Same tab shape as RecentlyClosedTabs, see that " + "artifact's notes on the undecoded SessionState blob. " + "LastVisitTime confirmed as Mac Absolute Time in seconds, " + "same unit as History.db.", + "paths": ( + "*/Library/Safari/LastSession.plist", + ), + "output_types": ["standard"], + "artifact_icon": "layout", + "sample_data": { + "macos_bigsur_thisisdfir": "macOS Big Sur (Josh Hickman / " + "thisisdfir public test image, acquired 2021-02-20), " + "LastSession.plist | 1 open window, 2 tabs (reddit.com " + "front page, r/memes)", + }, + }, +} + +import os +import plistlib +from datetime import datetime, timezone + +from scripts.ilapfuncs import artifact_processor, logfunc, open_sqlite_db_readonly + +# Seconds between the Unix epoch (1970-01-01) and the Mac/Cocoa epoch +# (2001-01-01). Confirmed against real data for both History.db's +# visit_time and LastSession/RecentlyClosedTabs' LastVisitTime -- see +# module docstring. +_MAC_EPOCH_OFFSET = 978307200 + + +def _mac_abs_s_to_utc(value): + """Mac/Cocoa Absolute Time in SECONDS since 2001-01-01.""" + if not value: + return None + try: + value = float(value) + except (TypeError, ValueError): + return None + try: + return datetime.fromtimestamp(value + _MAC_EPOCH_OFFSET, tz=timezone.utc) + except (OverflowError, OSError, ValueError): + return None + + +def _find_file(files_found, basename): + for path in files_found: + if os.path.basename(path) == basename: + return path + return None + + +def _load_plist(path): + try: + with open(path, "rb") as handle: + return plistlib.load(handle) + except Exception as ex: # pylint: disable=broad-exception-caught + logfunc(f"Safari: could not parse plist '{path}': {ex}") + return None + + +_HISTORY_QUERY = """ + SELECT + hv.visit_time, hi.url, hi.domain_expansion, hv.title, + hi.visit_count, hv.load_successful, hv.http_non_get, + hv.synthesized, hv.origin + FROM history_visits hv + JOIN history_items hi ON hi.id = hv.history_item + ORDER BY hv.visit_time DESC +""" + + +@artifact_processor +def safariHistory(context): + data_headers = ( + ("Visit Time", "datetime"), "URL", "Domain", "Visit Title", + "Item Visit Count (lifetime)", "Load Successful", "HTTP Non-GET", + "Synthesized", "Origin (raw)", "Source File", + ) + files_found = [str(f) for f in context.get_files_found()] + source = _find_file(files_found, "History.db") + if not source: + return data_headers, [], "" + + database = open_sqlite_db_readonly(source) + if database is None: + return data_headers, [], source + relative_source = context.get_relative_path(source) + + data_list = [] + for row in database.execute(_HISTORY_QUERY): + (visit_time, url, domain, title, visit_count, load_successful, + http_non_get, synthesized, origin) = row + data_list.append(( + _mac_abs_s_to_utc(visit_time), url or "", domain or "", + title or "", visit_count if visit_count is not None else "", + "Yes" if load_successful else "No", + "Yes" if http_non_get else "", + "Yes" if synthesized else "", + origin if origin is not None else "", relative_source, + )) + + database.close() + logfunc(f"Safari History: {len(data_list)} visit(s).") + return data_headers, data_list, source + + +def _walk_bookmarks(node, folder_path, rows): + if not isinstance(node, dict): + return + node_type = node.get("WebBookmarkType") + + if node_type == "WebBookmarkTypeLeaf": + title = (node.get("URIDictionary") or {}).get("title", "") + rows.append(( + folder_path, title, node.get("URLString", ""), + node.get("WebBookmarkUUID", ""), + )) + return + + # Folder/list node (WebBookmarkTypeList) or the unlabeled root dict -- + # both use 'Children'; proxy nodes (e.g. the History shortcut) have no + # useful children and simply fall through with nothing appended. + name = node.get("Title", "") + child_path = f"{folder_path}/{name}" if folder_path and name else (name or folder_path) + for child in node.get("Children", []) or []: + _walk_bookmarks(child, child_path, rows) + + +@artifact_processor +def safariBookmarks(context): + data_headers = ("Folder Path", "Title", "URL", "Bookmark UUID", "Source File") + files_found = [str(f) for f in context.get_files_found()] + source = _find_file(files_found, "Bookmarks.plist") + if not source: + return data_headers, [], "" + + plist = _load_plist(source) + if plist is None: + return data_headers, [], source + relative_source = context.get_relative_path(source) + + rows = [] + _walk_bookmarks(plist, "", rows) + data_list = [row + (relative_source,) for row in rows] + + logfunc(f"Safari Bookmarks: {len(data_list)} bookmark(s).") + return data_headers, data_list, source + + +@artifact_processor +def safariTopSites(context): + data_headers = ("Title", "URL", "Built-in Default", "Source File") + files_found = [str(f) for f in context.get_files_found()] + source = _find_file(files_found, "TopSites.plist") + if not source: + return data_headers, [], "" + + plist = _load_plist(source) + if plist is None: + return data_headers, [], source + relative_source = context.get_relative_path(source) + + data_list = [] + for site in plist.get("TopSites", []) or []: + data_list.append(( + site.get("TopSiteTitle", ""), site.get("TopSiteURLString", ""), + "Yes" if site.get("TopSiteIsBuiltIn") else "No", relative_source, + )) + + logfunc(f"Safari Top Sites: {len(data_list)} entr(ies).") + return data_headers, data_list, source + + +def _tab_rows(window): + window_uuid = window.get("WindowUUID", "") + window_closed = window.get("DateClosed") + is_private = "Yes" if window.get("IsPrivateWindow") else "No" + rows = [] + for tab in window.get("TabStates", []) or []: + session_state = tab.get("SessionState") + state_size = len(session_state) if isinstance(session_state, (bytes, bytearray)) else "" + rows.append(( + tab.get("TabTitle", ""), tab.get("TabURL", ""), + tab.get("DateClosed") or window_closed, + tab.get("LastVisitTime"), + window_uuid, tab.get("TabUUID", ""), + tab.get("TabIndex", ""), is_private, state_size, + )) + return rows + + +@artifact_processor +def safariRecentlyClosedTabs(context): + data_headers = ( + "Tab Title", "URL", ("Closed", "datetime"), ("Last Visit Time", "datetime"), + "Window UUID", "Tab UUID", "Tab Index", "Private Window", + "Session State Size (bytes)", "Source File", + ) + files_found = [str(f) for f in context.get_files_found()] + source = _find_file(files_found, "RecentlyClosedTabs.plist") + if not source: + return data_headers, [], "" + + plist = _load_plist(source) + if plist is None: + return data_headers, [], source + relative_source = context.get_relative_path(source) + + data_list = [] + for entry in plist.get("ClosedTabOrWindowPersistentStates", []) or []: + window = entry.get("PersistentState", {}) or {} + for title, url, closed, last_visit, w_uuid, t_uuid, idx, priv, state_size in _tab_rows(window): + data_list.append(( + title, url, closed, _mac_abs_s_to_utc(last_visit), + w_uuid, t_uuid, idx, priv, state_size, relative_source, + )) + + logfunc(f"Safari Recently Closed Tabs: {len(data_list)} tab(s).") + return data_headers, data_list, source + + +@artifact_processor +def safariLastSession(context): + data_headers = ( + "Tab Title", "URL", ("Window Closed", "datetime"), + ("Last Visit Time", "datetime"), "Window UUID", "Tab UUID", + "Tab Index", "Private Window", "Session State Size (bytes)", + "Source File", + ) + files_found = [str(f) for f in context.get_files_found()] + source = _find_file(files_found, "LastSession.plist") + if not source: + return data_headers, [], "" + + plist = _load_plist(source) + if plist is None: + return data_headers, [], source + relative_source = context.get_relative_path(source) + + data_list = [] + for window in plist.get("SessionWindows", []) or []: + for title, url, closed, last_visit, w_uuid, t_uuid, idx, priv, state_size in _tab_rows(window): + data_list.append(( + title, url, closed, _mac_abs_s_to_utc(last_visit), + w_uuid, t_uuid, idx, priv, state_size, relative_source, + )) + + logfunc(f"Safari Last Session: {len(data_list)} tab(s).") + return data_headers, data_list, source + + +_CLOUDTABS_QUERY = """ + SELECT + ct.tab_uuid, ct.title, ct.url, ct.is_pinned, ct.is_showing_reader, + ct.reader_scroll_position_page_index, ctd.device_name, + ctd.device_uuid, ctd.last_modified, ctd.is_ephemeral_device + FROM cloud_tabs ct + LEFT JOIN cloud_tab_devices ctd ON ctd.device_uuid = ct.device_uuid + ORDER BY ctd.last_modified DESC +""" + + +@artifact_processor +def safariCloudTabs(context): + data_headers = ( + "Tab Title", "URL", "Device Name", "Device UUID", + ("Device Last Modified", "datetime"), "Ephemeral Device", + "Pinned", "Showing Reader", "Reader Scroll Page", "Tab UUID", + "Source File", + ) + files_found = [str(f) for f in context.get_files_found()] + source = _find_file(files_found, "CloudTabs.db") + if not source: + return data_headers, [], "" + + database = open_sqlite_db_readonly(source) + if database is None: + return data_headers, [], source + relative_source = context.get_relative_path(source) + + data_list = [] + for row in database.execute(_CLOUDTABS_QUERY): + (tab_uuid, title, url, is_pinned, is_reader, reader_page, + device_name, device_uuid, last_modified, is_ephemeral) = row + data_list.append(( + title or "", url or "", device_name or "", device_uuid or "", + _mac_abs_s_to_utc(last_modified), + "Yes" if is_ephemeral else "", + "Yes" if is_pinned else "", "Yes" if is_reader else "", + reader_page if reader_page is not None else "", + tab_uuid or "", relative_source, + )) + + database.close() + logfunc(f"Safari iCloud Tabs: {len(data_list)} synced tab(s).") + return data_headers, data_list, source