diff --git a/admin/test/cases/data/teams/testdata.teams.teamsCalls.josh_ios15_ffs.zip b/admin/test/cases/data/teams/testdata.teams.teamsCalls.josh_ios15_ffs.zip
new file mode 100644
index 00000000..3638dc4d
Binary files /dev/null and b/admin/test/cases/data/teams/testdata.teams.teamsCalls.josh_ios15_ffs.zip differ
diff --git a/admin/test/cases/data/teams/testdata.teams.teamsContacts.josh_ios15_ffs.zip b/admin/test/cases/data/teams/testdata.teams.teamsContacts.josh_ios15_ffs.zip
new file mode 100644
index 00000000..58b1471a
Binary files /dev/null and b/admin/test/cases/data/teams/testdata.teams.teamsContacts.josh_ios15_ffs.zip differ
diff --git a/admin/test/cases/data/teams/testdata.teams.teamsLocations.josh_ios15_ffs.zip b/admin/test/cases/data/teams/testdata.teams.teamsLocations.josh_ios15_ffs.zip
new file mode 100644
index 00000000..5ff61af2
Binary files /dev/null and b/admin/test/cases/data/teams/testdata.teams.teamsLocations.josh_ios15_ffs.zip differ
diff --git a/admin/test/cases/data/teams/testdata.teams.teamsMessages.josh_ios15_ffs.zip b/admin/test/cases/data/teams/testdata.teams.teamsMessages.josh_ios15_ffs.zip
new file mode 100644
index 00000000..a6eb957b
Binary files /dev/null and b/admin/test/cases/data/teams/testdata.teams.teamsMessages.josh_ios15_ffs.zip differ
diff --git a/admin/test/cases/data/teams/testdata.teams.teamsUser.josh_ios15_ffs.zip b/admin/test/cases/data/teams/testdata.teams.teamsUser.josh_ios15_ffs.zip
new file mode 100644
index 00000000..91bdc86e
Binary files /dev/null and b/admin/test/cases/data/teams/testdata.teams.teamsUser.josh_ios15_ffs.zip differ
diff --git a/admin/test/cases/testdata.teams.json b/admin/test/cases/testdata.teams.json
new file mode 100644
index 00000000..44289763
--- /dev/null
+++ b/admin/test/cases/testdata.teams.json
@@ -0,0 +1,72 @@
+{
+ "josh_ios15_ffs": {
+ "description": "",
+ "maker": "",
+ "make_data": {
+ "input_data_path": "/Users/jameshabben/Documents/phone-images/Josh/iOS_15_Public_Image.tar.gz",
+ "os": "macOS-15.0-x86_64-i386-64bit",
+ "timestamp": "2024-11-04T13:38:37.697695",
+ "last_commit": {
+ "hash": "de6fdbe64b57ef6f08cdcc5947c2bd89a6eb677c",
+ "author_name": "abrignoni",
+ "author_email": "abrignoni@gmail.com",
+ "date": "2023-10-10T21:56:23-04:00",
+ "message": "Timezone offset main code"
+ }
+ },
+ "artifacts": {
+ "teamsMessages": {
+ "search_patterns": [
+ "*/mobile/Containers/Shared/AppGroup/*/SkypeSpacesDogfood/*/Skype*.sqlite*",
+ "*/mobile/Containers/Shared/AppGroup/*/SkypeSpacesDogfood/Downloads/*/Images/*"
+ ],
+ "file_count": 34,
+ "expected_output": {
+ "headers": [],
+ "data": []
+ }
+ },
+ "teamsContacts": {
+ "search_patterns": [
+ "*/mobile/Containers/Shared/AppGroup/*/SkypeSpacesDogfood/*/Skype*.sqlite*"
+ ],
+ "file_count": 1,
+ "expected_output": {
+ "headers": [],
+ "data": []
+ }
+ },
+ "teamsUser": {
+ "search_patterns": [
+ "*/mobile/Containers/Shared/AppGroup/*/SkypeSpacesDogfood/*/Skype*.sqlite*"
+ ],
+ "file_count": 1,
+ "expected_output": {
+ "headers": [],
+ "data": []
+ }
+ },
+ "teamsCalls": {
+ "search_patterns": [
+ "*/mobile/Containers/Shared/AppGroup/*/SkypeSpacesDogfood/*/Skype*.sqlite*"
+ ],
+ "file_count": 1,
+ "expected_output": {
+ "headers": [],
+ "data": []
+ }
+ },
+ "teamsLocations": {
+ "search_patterns": [
+ "*/mobile/Containers/Shared/AppGroup/*/SkypeSpacesDogfood/*/Skype*.sqlite*"
+ ],
+ "file_count": 1,
+ "expected_output": {
+ "headers": [],
+ "data": []
+ }
+ }
+ },
+ "image_name": "josh_ios15_ffs"
+ }
+}
\ No newline at end of file
diff --git a/admin/test/results/teams/teams.teamsCalls.josh_ios15_ffs.20241104220314.json b/admin/test/results/teams/teams.teamsCalls.josh_ios15_ffs.20241104220314.json
new file mode 100644
index 00000000..69dea678
--- /dev/null
+++ b/admin/test/results/teams/teams.teamsCalls.josh_ios15_ffs.20241104220314.json
@@ -0,0 +1,120 @@
+{
+ "metadata": {
+ "module_name": "teams",
+ "artifact_name": "Teams Call Logs",
+ "function_name": "teamsCalls",
+ "case_number": "josh_ios15_ffs",
+ "number_of_columns": 14,
+ "number_of_rows": 5,
+ "total_data_size_bytes": 1076,
+ "input_zip_path": "admin/test/cases/data/teams/testdata.teams.teamsCalls.josh_ios15_ffs.zip",
+ "start_time": "2024-11-04T22:03:14.018178+00:00",
+ "end_time": "2024-11-04T22:03:14.110998+00:00",
+ "run_time_seconds": 0.014806032180786133,
+ "last_commit": {
+ "hash": "de6fdbe64b57ef6f08cdcc5947c2bd89a6eb677c",
+ "author_name": "abrignoni",
+ "author_email": "abrignoni@gmail.com",
+ "date": "2023-10-10T21:56:23-04:00",
+ "message": "Timezone offset main code"
+ }
+ },
+ "headers": [
+ "Compose Timestamp",
+ "From",
+ "Display Name",
+ "Content",
+ "Call Start",
+ "Call Connect",
+ "Call End",
+ "Call Direction",
+ "Call Type",
+ "Call State",
+ "Call Originator",
+ "Call Target",
+ "Call Originator Name",
+ "Call Participant Name"
+ ],
+ "data": [
+ [
+ 1682691710,
+ "8:live:.cid.832804f1252ed9b2",
+ "",
+ "Call Logs for Call 6d8fa7a2-9a2e-4cf2-8c87-cc946fbe5663",
+ 1682691683,
+ 1682691687,
+ 1682691710,
+ "outgoing",
+ "twoParty",
+ "accepted",
+ "8:live:.cid.832804f1252ed9b2",
+ "8:live:.cid.5638f9306b7b8594",
+ "ThisIs DFIR",
+ ""
+ ],
+ [
+ 1682691942,
+ "8:live:.cid.832804f1252ed9b2",
+ "",
+ "Call Logs for Call 1019490c-938a-45be-a3ec-b10069e7c2fc",
+ 1682691917,
+ 1682691921,
+ 1682691942,
+ "outgoing",
+ "twoParty",
+ "accepted",
+ "8:live:.cid.832804f1252ed9b2",
+ "8:live:.cid.5638f9306b7b8594",
+ "ThisIs DFIR",
+ ""
+ ],
+ [
+ 1682692211,
+ "8:live:.cid.832804f1252ed9b2",
+ "",
+ "Call Logs for Call 7f99d235-02b8-4c1c-adb7-f94038019166",
+ 1682692116,
+ 1682692120,
+ 1682692211,
+ "incoming",
+ "twoParty",
+ "accepted",
+ "8:live:.cid.5638f9306b7b8594",
+ "8:live:.cid.832804f1252ed9b2",
+ "ThisIs DfirTwo",
+ ""
+ ],
+ [
+ 1682692733,
+ "8:live:.cid.832804f1252ed9b2",
+ "",
+ "Call Logs for Call c808711b-e466-4eaf-86c2-2eae9b1cc39b",
+ 1682692633,
+ 1682692636,
+ 1682692732,
+ "outgoing",
+ "twoParty",
+ "accepted",
+ "8:live:.cid.832804f1252ed9b2",
+ "8:live:.cid.5638f9306b7b8594",
+ "ThisIs DFIR",
+ ""
+ ],
+ [
+ 1682693144,
+ "8:live:.cid.832804f1252ed9b2",
+ "",
+ "Call Logs for Call 43f78ea2-b67f-4e73-af63-eeeabab8ed68",
+ 1682693035,
+ 1682693039,
+ 1682693144,
+ "incoming",
+ "twoParty",
+ "accepted",
+ "8:live:.cid.5638f9306b7b8594",
+ "8:live:.cid.832804f1252ed9b2",
+ "ThisIs DfirTwo",
+ ""
+ ]
+ ]
+}
\ No newline at end of file
diff --git a/admin/test/results/teams/teams.teamsContacts.josh_ios15_ffs.20241104220314.json b/admin/test/results/teams/teams.teamsContacts.josh_ios15_ffs.20241104220314.json
new file mode 100644
index 00000000..5d2f7e60
--- /dev/null
+++ b/admin/test/results/teams/teams.teamsContacts.josh_ios15_ffs.20241104220314.json
@@ -0,0 +1,74 @@
+{
+ "metadata": {
+ "module_name": "teams",
+ "artifact_name": "Teams Contacts",
+ "function_name": "teamsContacts",
+ "case_number": "josh_ios15_ffs",
+ "number_of_columns": 3,
+ "number_of_rows": 9,
+ "total_data_size_bytes": 244,
+ "input_zip_path": "admin/test/cases/data/teams/testdata.teams.teamsContacts.josh_ios15_ffs.zip",
+ "start_time": "2024-11-04T22:03:14.111761+00:00",
+ "end_time": "2024-11-04T22:03:14.186558+00:00",
+ "run_time_seconds": 0.0014848709106445312,
+ "last_commit": {
+ "hash": "de6fdbe64b57ef6f08cdcc5947c2bd89a6eb677c",
+ "author_name": "abrignoni",
+ "author_email": "abrignoni@gmail.com",
+ "date": "2023-10-10T21:56:23-04:00",
+ "message": "Timezone offset main code"
+ }
+ },
+ "headers": [
+ "Display Name",
+ "Email",
+ "Phone Number"
+ ],
+ "data": [
+ [
+ "ThisIs DFIRThree",
+ "thisisdfirthree@gmail.com",
+ ""
+ ],
+ [
+ "This Is DFIR Two",
+ "",
+ "9198887386"
+ ],
+ [
+ "This Is DFIR Two",
+ "thisisdfirtwo@gmail.com",
+ ""
+ ],
+ [
+ "Josh Hickman",
+ "",
+ "9195790479"
+ ],
+ [
+ "Josh Hickman",
+ "",
+ "9193912507"
+ ],
+ [
+ "Josh Hickman",
+ "joshuahickman957@gmail.com",
+ ""
+ ],
+ [
+ "Liz",
+ "",
+ "19192084530"
+ ],
+ [
+ "Thom De'Fer",
+ "",
+ "9198027080"
+ ],
+ [
+ "Apple Inc.",
+ "",
+ "1800MYAPPLE"
+ ]
+ ]
+}
\ No newline at end of file
diff --git a/admin/test/results/teams/teams.teamsLocations.josh_ios15_ffs.20241104220313.json b/admin/test/results/teams/teams.teamsLocations.josh_ios15_ffs.20241104220313.json
new file mode 100644
index 00000000..9c32872c
--- /dev/null
+++ b/admin/test/results/teams/teams.teamsLocations.josh_ios15_ffs.20241104220313.json
@@ -0,0 +1,206 @@
+{
+ "metadata": {
+ "module_name": "teams",
+ "artifact_name": "Teams Shared Locations",
+ "function_name": "teamsLocations",
+ "case_number": "josh_ios15_ffs",
+ "number_of_columns": 12,
+ "number_of_rows": 12,
+ "total_data_size_bytes": 6888,
+ "input_zip_path": "admin/test/cases/data/teams/testdata.teams.teamsLocations.josh_ios15_ffs.zip",
+ "start_time": "2024-11-04T22:03:13.913213+00:00",
+ "end_time": "2024-11-04T22:03:14.017468+00:00",
+ "run_time_seconds": 0.017634153366088867,
+ "last_commit": {
+ "hash": "de6fdbe64b57ef6f08cdcc5947c2bd89a6eb677c",
+ "author_name": "abrignoni",
+ "author_email": "abrignoni@gmail.com",
+ "date": "2023-10-10T21:56:23-04:00",
+ "message": "Timezone offset main code"
+ }
+ },
+ "headers": [
+ "Timestamp",
+ "From",
+ "Display Name",
+ "Content",
+ "Card URL",
+ "Card Title",
+ "Card Text",
+ "Card URL2",
+ "Latitude",
+ "Longitude",
+ "Card Expires",
+ "Device ID"
+ ],
+ "data": [
+ [
+ 1638647000,
+ "8:live:.cid.832804f1252ed9b2",
+ "ThisIs DFIR",
+ "
",
+ "https://www.bing.com/maps?rtp=adr.~pos.35.6602339_-78.8112703_301%20Crossway%20Ln%2C%20301%20Crossway%20Ln",
+ "Maps",
+ "301 Crossway Ln",
+ "https://urlp.asm.skype.com/v1/url/content?url=https%3a%2f%2fmaps.googleapis.com%2fmaps%2fapi%2fstaticmap%3fcenter%3d35.6602339%2c-78.8112703%26zoom%3d15%26size%3d400x200%26markers%3dcolor%3ared%257C35.6602339%2c-78.8112703%26key%3dAIzaSyCkvtEv7EA_JdZaOCEWrc7Wm6N7M0nLvco",
+ null,
+ null,
+ null,
+ null
+ ],
+ [
+ 1638647113,
+ "8:live:.cid.5638f9306b7b8594",
+ "ThisIs DfirTwo",
+ "
",
+ "https://www.bing.com/maps?rtp=adr.~pos.35.6580764_-78.8103737_106%20Fountain%20Ridge%20Pl%2C%20106%20Fountain%20Ridge%20Pl",
+ "Maps",
+ "106 Fountain Ridge Pl",
+ "https://urlp.asm.skype.com/v1/url/content?url=https%3a%2f%2fmaps.googleapis.com%2fmaps%2fapi%2fstaticmap%3fcenter%3d35.6580764%2c-78.8103737%26zoom%3d15%26size%3d400x200%26markers%3dcolor%3ared%257C35.6580764%2c-78.8103737%26key%3dAIzaSyCkvtEv7EA_JdZaOCEWrc7Wm6N7M0nLvco",
+ null,
+ null,
+ null,
+ null
+ ],
+ [
+ 1638647177,
+ "8:live:.cid.5638f9306b7b8594",
+ "ThisIs DfirTwo",
+ "
",
+ "https://www.bing.com/maps?rtp=adr.~pos.35.6629958_-78.8167595_My%20current%20location%2C%20120%20Flint%20Point%20Ln",
+ "Maps",
+ "My current location",
+ "https://urlp.asm.skype.com/v1/url/content?url=https%3a%2f%2fmaps.googleapis.com%2fmaps%2fapi%2fstaticmap%3fcenter%3d35.6629958%2c-78.8167595%26zoom%3d15%26size%3d400x200%26markers%3dcolor%3ared%257C35.6629958%2c-78.8167595%26key%3dAIzaSyCkvtEv7EA_JdZaOCEWrc7Wm6N7M0nLvco",
+ "35.6629958",
+ "-78.8167595",
+ 1638620176,
+ "43c48367-3021-4aa7-862a-3b76d16bf44f"
+ ],
+ [
+ 1638647537,
+ "8:live:.cid.832804f1252ed9b2",
+ "ThisIs DFIR",
+ "
",
+ "https://www.bing.com/maps?rtp=adr.~pos.35.6607699_-78.8310529_My%20current%20location%2C%20501%20St%20Croix%20Dr",
+ "Maps",
+ "My current location",
+ "https://urlp.asm.skype.com/v1/url/content?url=https%3a%2f%2fmaps.googleapis.com%2fmaps%2fapi%2fstaticmap%3fcenter%3d35.6607699%2c-78.8310529%26zoom%3d15%26size%3d400x200%26markers%3dcolor%3ared%257C35.6607699%2c-78.8310529%26key%3dAIzaSyCkvtEv7EA_JdZaOCEWrc7Wm6N7M0nLvco",
+ "35.6607699",
+ "-78.8310529",
+ 1638620537,
+ "afb3c199-fa72-44b1-add4-9274ec608049"
+ ],
+ [
+ 1670248018,
+ "8:live:.cid.832804f1252ed9b2",
+ "ThisIs DFIR",
+ "
",
+ "https://www.bing.com/maps?rtp=adr.~pos.35.6551144_-78.8538701_137%20Thomas%20Mill%20Rd%2C%20137%20Thomas%20Mill%20Rd",
+ "Maps",
+ "137 Thomas Mill Rd",
+ "https://us-prod.asyncgw.teams.microsoft.com/urlp/v1/url/content?url=https%3a%2f%2fmaps.googleapis.com%2fmaps%2fapi%2fstaticmap%3fcenter%3d35.6551144%2c-78.8538701%26zoom%3d15%26size%3d400x200%26markers%3dcolor%3ared%257C35.6551144%2c-78.8538701%26key%3dAIzaSyCkvtEv7EA_JdZaOCEWrc7Wm6N7M0nLvco",
+ null,
+ null,
+ null,
+ null
+ ],
+ [
+ 1670248078,
+ "8:live:.cid.832804f1252ed9b2",
+ "ThisIs DFIR",
+ "
",
+ "https://www.bing.com/maps?rtp=adr.~pos.35.6557741_-78.853738_My%20current%20location%2C%20M44W%2BCCF",
+ "Maps",
+ "My current location",
+ "https://us-prod.asyncgw.teams.microsoft.com/urlp/v1/url/content?url=https%3a%2f%2fmaps.googleapis.com%2fmaps%2fapi%2fstaticmap%3fcenter%3d35.6557741%2c-78.853738%26zoom%3d15%26size%3d400x200%26markers%3dcolor%3ared%257C35.6557741%2c-78.853738%26key%3dAIzaSyCkvtEv7EA_JdZaOCEWrc7Wm6N7M0nLvco",
+ "35.6557741",
+ "-78.853738",
+ 1670221078,
+ "baf69df3-2b20-49f2-84b5-3f7022809765"
+ ],
+ [
+ 1670248450,
+ "8:live:.cid.5638f9306b7b8594",
+ "ThisIs DfirTwo",
+ "
",
+ "https://www.bing.com/maps?rtp=adr.~pos.35.667286_-78.8750147_105%20Lively%20Oaks%20Way%2C%20105%20Lively%20Oaks%20Way",
+ "Maps",
+ "105 Lively Oaks Way",
+ "https://us-prod.asyncgw.teams.microsoft.com/urlp/v1/url/content?url=https%3a%2f%2fmaps.googleapis.com%2fmaps%2fapi%2fstaticmap%3fcenter%3d35.667286%2c-78.8750147%26zoom%3d15%26size%3d400x200%26markers%3dcolor%3ared%257C35.667286%2c-78.8750147%26key%3dAIzaSyCkvtEv7EA_JdZaOCEWrc7Wm6N7M0nLvco",
+ null,
+ null,
+ null,
+ null
+ ],
+ [
+ 1670248567,
+ "8:live:.cid.5638f9306b7b8594",
+ "ThisIs DfirTwo",
+ "
",
+ "https://www.bing.com/maps?rtp=adr.~pos.35.6681226_-78.8778239_My%20current%20location%2C%201701%20Green%20Oaks%20Pkwy",
+ "Maps",
+ "My current location",
+ "https://us-prod.asyncgw.teams.microsoft.com/urlp/v1/url/content?url=https%3a%2f%2fmaps.googleapis.com%2fmaps%2fapi%2fstaticmap%3fcenter%3d35.6681226%2c-78.8778239%26zoom%3d15%26size%3d400x200%26markers%3dcolor%3ared%257C35.6681226%2c-78.8778239%26key%3dAIzaSyCkvtEv7EA_JdZaOCEWrc7Wm6N7M0nLvco",
+ "35.6681226",
+ "-78.8778239",
+ 1670221568,
+ "c7b923c2-8add-4a37-9fb3-05793018dc20"
+ ],
+ [
+ 1684329012,
+ "8:live:.cid.832804f1252ed9b2",
+ "ThisIs DFIR",
+ "",
+ "https://www.bing.com/maps?rtp=adr.~pos.35.6291711725048_-78.79738913609707",
+ "Directions",
+ "116 Moss Creek Pl,\u00a0Fuquay-Varina,\u00a0NC",
+ "https://us-api.asm.skype.com/v1/objects/0-eus-d20-1308f0f6962b7b2bee476737cfccaa51/views/imgo",
+ "35.6291711725048",
+ "-78.79738913609707",
+ 1684305609,
+ "C85A6FB0-7F91-4170-800F-1AB5CFA82903"
+ ],
+ [
+ 1684329467,
+ "8:live:.cid.5638f9306b7b8594",
+ "ThisIs DfirTwo",
+ "",
+ "https://www.bing.com/maps?rtp=adr.~pos.35.64467656406671_-78.81252548233516",
+ "Directions",
+ "100 Aldborough Way,\u00a0Holly Springs,\u00a0NC",
+ "https://us-api.asm.skype.com/v1/objects/0-eus-d17-718b896f15a188f1b73393e2c75b7936/views/imgo",
+ "35.64467656406671",
+ "-78.81252548233516",
+ 1684306065,
+ "83A2ADBB-F6A4-42D4-A10B-9F3D59754653"
+ ],
+ [
+ 1684330048,
+ "8:live:.cid.5638f9306b7b8594",
+ "ThisIs DfirTwo",
+ "",
+ "https://www.bing.com/maps?rtp=adr.~pos.35.65531608947976_-78.83036282408914",
+ "Directions",
+ "309 Cayman Ave",
+ "https://us-api.asm.skype.com/v1/objects/0-eus-d4-f393ddcdfef3665aafc6f2242c23327d/views/imgo",
+ null,
+ null,
+ null,
+ null
+ ],
+ [
+ 1684330223,
+ "8:live:.cid.832804f1252ed9b2",
+ "ThisIs DFIR",
+ "",
+ "https://www.bing.com/maps?rtp=adr.~pos.35.66164532015291_-78.83040382854551",
+ "Directions",
+ "508 Sandy Point Way",
+ "https://us-api.asm.skype.com/v1/objects/0-eus-d18-9342a8f893cdbb5481a9ae0285d93de5/views/imgo",
+ null,
+ null,
+ null,
+ null
+ ]
+ ]
+}
\ No newline at end of file
diff --git a/admin/test/results/teams/teams.teamsMessages.josh_ios15_ffs.20241104220314.json b/admin/test/results/teams/teams.teamsMessages.josh_ios15_ffs.20241104220314.json
new file mode 100644
index 00000000..568667ec
--- /dev/null
+++ b/admin/test/results/teams/teams.teamsMessages.josh_ios15_ffs.20241104220314.json
@@ -0,0 +1,598 @@
+{
+ "metadata": {
+ "module_name": "teams",
+ "artifact_name": "Teams Messages",
+ "function_name": "teamsMessages",
+ "case_number": "josh_ios15_ffs",
+ "number_of_columns": 5,
+ "number_of_rows": 81,
+ "total_data_size_bytes": 11309,
+ "input_zip_path": "admin/test/cases/data/teams/testdata.teams.teamsMessages.josh_ios15_ffs.zip",
+ "start_time": "2024-11-04T22:03:14.263204+00:00",
+ "end_time": "2024-11-04T22:03:14.356771+00:00",
+ "run_time_seconds": 0.004088640213012695,
+ "last_commit": {
+ "hash": "de6fdbe64b57ef6f08cdcc5947c2bd89a6eb677c",
+ "author_name": "abrignoni",
+ "author_email": "abrignoni@gmail.com",
+ "date": "2023-10-10T21:56:23-04:00",
+ "message": "Timezone offset main code"
+ }
+ },
+ "headers": [
+ "Timestamp",
+ "Display Name",
+ "From",
+ "Message",
+ "Shared Media"
+ ],
+ "data": [
+ [
+ 1637783068,
+ "ThisIs DFIR",
+ "8:live:.cid.832804f1252ed9b2",
+ "\n
\n
\n
\r\n\n
\n
",
+ ""
+ ],
+ [
+ 1637783107,
+ "ThisIs DfirTwo",
+ "8:live:.cid.5638f9306b7b8594",
+ "Poor Pete. ",
+ ""
+ ],
+ [
+ 1637783162,
+ "ThisIs DfirTwo",
+ "8:live:.cid.5638f9306b7b8594",
+ "I'll send a picture.
",
+ ""
+ ],
+ [
+ 1637783225,
+ "ThisIs DfirTwo",
+ "8:live:.cid.5638f9306b7b8594",
+ "\n
\n
\n
\r\n\n
\n
",
+ ""
+ ],
+ [
+ 1637783354,
+ "ThisIs DFIR",
+ "8:live:.cid.832804f1252ed9b2",
+ "The logic is...interesting. ",
+ ""
+ ],
+ [
+ 1637783420,
+ "ThisIs DfirTwo",
+ "8:live:.cid.5638f9306b7b8594",
+ "I'll audio call you.
",
+ ""
+ ],
+ [
+ 1637783673,
+ "ThisIs DfirTwo",
+ "8:live:.cid.5638f9306b7b8594",
+ "Looks like the call failed. I'll try it again.
",
+ ""
+ ],
+ [
+ 1637783830,
+ "ThisIs DFIR",
+ "8:live:.cid.832804f1252ed9b2",
+ "That one failed too, maybe...?",
+ ""
+ ],
+ [
+ 1637783914,
+ "ThisIs DfirTwo",
+ "8:live:.cid.5638f9306b7b8594",
+ "\n
\n
\n
\n
",
+ ""
+ ],
+ [
+ 1637783974,
+ "ThisIs DFIR",
+ "8:live:.cid.832804f1252ed9b2",
+ "I will try to audio call you. ",
+ ""
+ ],
+ [
+ 1637785340,
+ "ThisIs DfirTwo",
+ "8:live:.cid.5638f9306b7b8594",
+ "Try your audio call again. ",
+ ""
+ ],
+ [
+ 1637786909,
+ "ThisIs DfirTwo",
+ "8:live:.cid.5638f9306b7b8594",
+ "Oh well. Nothing works. ",
+ ""
+ ],
+ [
+ 1637786946,
+ "ThisIs DFIR",
+ "8:live:.cid.832804f1252ed9b2",
+ "Send a bad message and I will delete it. ",
+ ""
+ ],
+ [
+ 1637787008,
+ "ThisIs DfirTwo",
+ "8:live:.cid.5638f9306b7b8594",
+ "Here's your bad message.
",
+ ""
+ ],
+ [
+ 1637787099,
+ "ThisIs DFIR",
+ "8:live:.cid.832804f1252ed9b2",
+ "Looks like only the sender can delete. ",
+ ""
+ ],
+ [
+ 1637787129,
+ "ThisIs DFIR",
+ "8:live:.cid.832804f1252ed9b2",
+ "",
+ ""
+ ],
+ [
+ 1637787239,
+ "ThisIs DFIR",
+ "8:live:.cid.832804f1252ed9b2",
+ "Deleted. ",
+ ""
+ ],
+ [
+ 1638647000,
+ "ThisIs DFIR",
+ "8:live:.cid.832804f1252ed9b2",
+ "
",
+ ""
+ ],
+ [
+ 1638647113,
+ "ThisIs DfirTwo",
+ "8:live:.cid.5638f9306b7b8594",
+ "
",
+ ""
+ ],
+ [
+ 1638647177,
+ "ThisIs DfirTwo",
+ "8:live:.cid.5638f9306b7b8594",
+ "
",
+ ""
+ ],
+ [
+ 1638647466,
+ "",
+ "8:live:.cid.5638f9306b7b8594",
+ "Location sharing ended",
+ ""
+ ],
+ [
+ 1638647537,
+ "ThisIs DFIR",
+ "8:live:.cid.832804f1252ed9b2",
+ "
",
+ ""
+ ],
+ [
+ 1638647821,
+ "",
+ "8:live:.cid.832804f1252ed9b2",
+ "Location sharing ended",
+ ""
+ ],
+ [
+ 1660692600,
+ "",
+ "19:204b46fc734948a796f40213b2e59e21@thread.v2",
+ "16606926006148:live:.cid.832804f1252ed9b216606926002308:live:.cid.832804f1252ed9b28:live:.cid.832804f1252ed9b2",
+ ""
+ ],
+ [
+ 1660692600,
+ "",
+ "19:204b46fc734948a796f40213b2e59e21@thread.v2",
+ "16606926009468:live:.cid.832804f1252ed9b2True",
+ ""
+ ],
+ [
+ 1660692600,
+ "",
+ "19:204b46fc734948a796f40213b2e59e21@thread.v2",
+ "16606926007118:live:.cid.832804f1252ed9b2DFIR family",
+ ""
+ ],
+ [
+ 1660692602,
+ "",
+ "19:204b46fc734948a796f40213b2e59e21@thread.v2",
+ "16606926021568:live:.cid.832804f1252ed9b2URL@https://us-api.asm.skype.com/v1/objects/0-wus-d3-4caefafab6664c06dfa331c304479adf/views/avatar_fullsize",
+ ""
+ ],
+ [
+ 1668540913,
+ "ThisIs DfirTwo",
+ "8:live:.cid.5638f9306b7b8594",
+ "You there?",
+ ""
+ ],
+ [
+ 1668540965,
+ "ThisIs DFIR",
+ "8:live:.cid.832804f1252ed9b2",
+ "Yes sir, I am. ",
+ ""
+ ],
+ [
+ 1668541028,
+ "ThisIs DfirTwo",
+ "8:live:.cid.5638f9306b7b8594",
+ "Awesome. We can make this quick then. ",
+ ""
+ ],
+ [
+ 1668541104,
+ "ThisIs DFIR",
+ "8:live:.cid.832804f1252ed9b2",
+ "Agreed. Let me find a picture. Hang on. ",
+ ""
+ ],
+ [
+ 1668541174,
+ "ThisIs DFIR",
+ "8:live:.cid.832804f1252ed9b2",
+ "\n
\n
\n
\n
",
+ ""
+ ],
+ [
+ 1668541229,
+ "ThisIs DfirTwo",
+ "8:live:.cid.5638f9306b7b8594",
+ "Is that accurate?",
+ ""
+ ],
+ [
+ 1668541329,
+ "ThisIs DFIR",
+ "8:live:.cid.832804f1252ed9b2",
+ "I have no idea. It's got to be generally correct though.
",
+ ""
+ ],
+ [
+ 1668541420,
+ "ThisIs DfirTwo",
+ "8:live:.cid.5638f9306b7b8594",
+ "Certainly looks that way. Lol. ",
+ ""
+ ],
+ [
+ 1668541538,
+ "ThisIs DFIR",
+ "8:live:.cid.832804f1252ed9b2",
+ "\r\n
\r\nThisIs DfirTwo\r\nCertainly looks that way. Lol.
\r\n
\r\nThe numbers may not be, but the trend probably is.
",
+ ""
+ ],
+ [
+ 1668541600,
+ "ThisIs DfirTwo",
+ "8:live:.cid.5638f9306b7b8594",
+ "I'll send a picture. Standby.
",
+ ""
+ ],
+ [
+ 1668541688,
+ "ThisIs DfirTwo",
+ "8:live:.cid.5638f9306b7b8594",
+ "\n
\n
\n
\n
",
+ ""
+ ],
+ [
+ 1668541862,
+ "ThisIs DFIR",
+ "8:live:.cid.832804f1252ed9b2",
+ "Still accurate in 2022. ",
+ ""
+ ],
+ [
+ 1668541968,
+ "ThisIs DfirTwo",
+ "8:live:.cid.5638f9306b7b8594",
+ "I'd say. Especially the part about the Christmas music.
",
+ ""
+ ],
+ [
+ 1668542061,
+ "ThisIs DFIR",
+ "8:live:.cid.832804f1252ed9b2",
+ "Yep. I'll audio call you. Hang on.
",
+ ""
+ ],
+ [
+ 1668542371,
+ "ThisIs DfirTwo",
+ "8:live:.cid.5638f9306b7b8594",
+ "My turn. ",
+ ""
+ ],
+ [
+ 1668542608,
+ "ThisIs DFIR",
+ "8:live:.cid.832804f1252ed9b2",
+ "Video call time. ",
+ ""
+ ],
+ [
+ 1668542834,
+ "ThisIs DfirTwo",
+ "8:live:.cid.5638f9306b7b8594",
+ "I'll video call you.
",
+ ""
+ ],
+ [
+ 1668543051,
+ "ThisIs DFIR",
+ "8:live:.cid.832804f1252ed9b2",
+ "",
+ ""
+ ],
+ [
+ 1668543082,
+ "ThisIs DfirTwo",
+ "8:live:.cid.5638f9306b7b8594",
+ "Then delete the one you just sent. ",
+ ""
+ ],
+ [
+ 1668543198,
+ "ThisIs DFIR",
+ "8:live:.cid.832804f1252ed9b2",
+ "Done. ",
+ ""
+ ],
+ [
+ 1670248018,
+ "ThisIs DFIR",
+ "8:live:.cid.832804f1252ed9b2",
+ "
",
+ ""
+ ],
+ [
+ 1670248078,
+ "ThisIs DFIR",
+ "8:live:.cid.832804f1252ed9b2",
+ "
",
+ ""
+ ],
+ [
+ 1670248307,
+ "",
+ "8:live:.cid.832804f1252ed9b2",
+ "Location sharing ended",
+ ""
+ ],
+ [
+ 1670248450,
+ "ThisIs DfirTwo",
+ "8:live:.cid.5638f9306b7b8594",
+ "
",
+ ""
+ ],
+ [
+ 1670248567,
+ "ThisIs DfirTwo",
+ "8:live:.cid.5638f9306b7b8594",
+ "
",
+ ""
+ ],
+ [
+ 1670248836,
+ "",
+ "8:live:.cid.5638f9306b7b8594",
+ "Location sharing ended",
+ ""
+ ],
+ [
+ 1682689489,
+ "ThisIs DfirTwo",
+ "8:live:.cid.5638f9306b7b8594",
+ "You here yet?",
+ ""
+ ],
+ [
+ 1682689653,
+ "ThisIs DFIR",
+ "8:live:.cid.832804f1252ed9b2",
+ "I am. Battery and all.",
+ ""
+ ],
+ [
+ 1682689697,
+ "ThisIs DfirTwo",
+ "8:live:.cid.5638f9306b7b8594",
+ "Yeah, how is it doing this morning?",
+ ""
+ ],
+ [
+ 1682689776,
+ "ThisIs DFIR",
+ "8:live:.cid.832804f1252ed9b2",
+ "Still horrible. It\u2019s drained like 30 percent in the last hour or so.",
+ ""
+ ],
+ [
+ 1682689829,
+ "ThisIs DfirTwo",
+ "8:live:.cid.5638f9306b7b8594",
+ "Yikes! \u00a0That is bad. When we do location stuff you\u2019ll need to be plugged in.",
+ ""
+ ],
+ [
+ 1682690018,
+ "ThisIs DFIR",
+ "8:live:.cid.832804f1252ed9b2",
+ "This phone is retiring after this image.",
+ ""
+ ],
+ [
+ 1682690055,
+ "ThisIs DfirTwo",
+ "8:live:.cid.5638f9306b7b8594",
+ "Definitely. I\u2019ll send you a picture. Hang on.",
+ ""
+ ],
+ [
+ 1682690228,
+ "ThisIs DfirTwo",
+ "8:live:.cid.5638f9306b7b8594",
+ "",
+ ""
+ ],
+ [
+ 1682691522,
+ "ThisIs DFIR",
+ "8:live:.cid.832804f1252ed9b2",
+ "That picture is truth.",
+ ""
+ ],
+ [
+ 1682691569,
+ "ThisIs DfirTwo",
+ "8:live:.cid.5638f9306b7b8594",
+ "Thought you might like it.",
+ ""
+ ],
+ [
+ 1682691618,
+ "ThisIs DFIR",
+ "8:live:.cid.832804f1252ed9b2",
+ "I will audio call you now.",
+ ""
+ ],
+ [
+ 1682691710,
+ "",
+ "8:live:.cid.832804f1252ed9b2",
+ "Call Logs for Call 6d8fa7a2-9a2e-4cf2-8c87-cc946fbe5663",
+ ""
+ ],
+ [
+ 1682691783,
+ "ThisIs DfirTwo",
+ "8:live:.cid.5638f9306b7b8594",
+ "What happened?",
+ ""
+ ],
+ [
+ 1682691853,
+ "ThisIs DFIR",
+ "8:live:.cid.832804f1252ed9b2",
+ "No clue. I\u2019ll try it again.",
+ ""
+ ],
+ [
+ 1682691942,
+ "",
+ "8:live:.cid.832804f1252ed9b2",
+ "Call Logs for Call 1019490c-938a-45be-a3ec-b10069e7c2fc",
+ ""
+ ],
+ [
+ 1682692056,
+ "ThisIs DFIR",
+ "8:live:.cid.832804f1252ed9b2",
+ "Well, it happened again. You try audio calling and see what happens.",
+ ""
+ ],
+ [
+ 1682692211,
+ "",
+ "8:live:.cid.832804f1252ed9b2",
+ "Call Logs for Call 7f99d235-02b8-4c1c-adb7-f94038019166",
+ ""
+ ],
+ [
+ 1682692488,
+ "ThisIs DfirTwo",
+ "8:live:.cid.5638f9306b7b8594",
+ "Well, that worked. You can try video calling me now.",
+ ""
+ ],
+ [
+ 1682692733,
+ "",
+ "8:live:.cid.832804f1252ed9b2",
+ "Call Logs for Call c808711b-e466-4eaf-86c2-2eae9b1cc39b",
+ ""
+ ],
+ [
+ 1682692945,
+ "ThisIs DfirTwo",
+ "8:live:.cid.5638f9306b7b8594",
+ "I\u2019ll video call. Standby.",
+ ""
+ ],
+ [
+ 1682693144,
+ "",
+ "8:live:.cid.832804f1252ed9b2",
+ "Call Logs for Call 43f78ea2-b67f-4e73-af63-eeeabab8ed68",
+ ""
+ ],
+ [
+ 1682693224,
+ "ThisIs DFIR",
+ "8:live:.cid.832804f1252ed9b2",
+ "I am sending you this message. I will delete it in a minute.",
+ ""
+ ],
+ [
+ 1684329012,
+ "ThisIs DFIR",
+ "8:live:.cid.832804f1252ed9b2",
+ "",
+ ""
+ ],
+ [
+ 1684329357,
+ "",
+ "8:live:.cid.832804f1252ed9b2",
+ "Location sharing ended",
+ ""
+ ],
+ [
+ 1684329467,
+ "ThisIs DfirTwo",
+ "8:live:.cid.5638f9306b7b8594",
+ "",
+ ""
+ ],
+ [
+ 1684329757,
+ "",
+ "8:live:.cid.5638f9306b7b8594",
+ "Location sharing ended",
+ ""
+ ],
+ [
+ 1684330048,
+ "ThisIs DfirTwo",
+ "8:live:.cid.5638f9306b7b8594",
+ "",
+ ""
+ ],
+ [
+ 1684330223,
+ "ThisIs DFIR",
+ "8:live:.cid.832804f1252ed9b2",
+ "",
+ ""
+ ]
+ ]
+}
\ No newline at end of file
diff --git a/admin/test/results/teams/teams.teamsUser.josh_ios15_ffs.20241104220314.json b/admin/test/results/teams/teams.teamsUser.josh_ios15_ffs.20241104220314.json
new file mode 100644
index 00000000..a429db1f
--- /dev/null
+++ b/admin/test/results/teams/teams.teamsUser.josh_ios15_ffs.20241104220314.json
@@ -0,0 +1,39 @@
+{
+ "metadata": {
+ "module_name": "teams",
+ "artifact_name": "Teams User Information",
+ "function_name": "teamsUser",
+ "case_number": "josh_ios15_ffs",
+ "number_of_columns": 3,
+ "number_of_rows": 2,
+ "total_data_size_bytes": 56,
+ "input_zip_path": "admin/test/cases/data/teams/testdata.teams.teamsUser.josh_ios15_ffs.zip",
+ "start_time": "2024-11-04T22:03:14.187212+00:00",
+ "end_time": "2024-11-04T22:03:14.262576+00:00",
+ "run_time_seconds": 0.0019540786743164062,
+ "last_commit": {
+ "hash": "de6fdbe64b57ef6f08cdcc5947c2bd89a6eb677c",
+ "author_name": "abrignoni",
+ "author_email": "abrignoni@gmail.com",
+ "date": "2023-10-10T21:56:23-04:00",
+ "message": "Timezone offset main code"
+ }
+ },
+ "headers": [
+ "Timestamp Last Sync",
+ "Display Name",
+ "Phone Number"
+ ],
+ "data": [
+ [
+ 1684415520,
+ "ThisIs DFIR",
+ "19195794674"
+ ],
+ [
+ 1684328990,
+ "ThisIs DfirTwo",
+ ""
+ ]
+ ]
+}
\ No newline at end of file
diff --git a/scripts/artifacts/teams.py b/scripts/artifacts/teams.py
index 3506db25..1c41a582 100644
--- a/scripts/artifacts/teams.py
+++ b/scripts/artifacts/teams.py
@@ -1,3 +1,59 @@
+__artifacts_v2__ = {
+ "teamsMessages": {
+ "name": "Teams Messages",
+ "description": "Microsoft Teams messages and shared media",
+ "author": "",
+ "version": "1.0",
+ "category": "Microsoft Teams",
+ "paths": ('*/mobile/Containers/Shared/AppGroup/*/SkypeSpacesDogfood/*/Skype*.sqlite*',
+ '*/mobile/Containers/Shared/AppGroup/*/SkypeSpacesDogfood/Downloads/*/Images/*'),
+ "output_types": "standard",
+ "chatParams": {
+ "threadDiscriminatorColumn": "Display Name",
+ "textColumn": "Message",
+ "timeColumn": "Timestamp",
+ "senderColumn": "From",
+ "mediaColumn": "Shared Media"
+ }
+ },
+ "teamsContacts": {
+ "name": "Teams Contacts",
+ "description": "Microsoft Teams contact list",
+ "author": "",
+ "version": "1.0",
+ "category": "Microsoft Teams",
+ "paths": ('*/mobile/Containers/Shared/AppGroup/*/SkypeSpacesDogfood/*/Skype*.sqlite*',),
+ "output_types": "standard"
+ },
+ "teamsUser": {
+ "name": "Teams User Information",
+ "description": "Microsoft Teams user profile and sync data",
+ "author": "",
+ "version": "1.0",
+ "category": "Microsoft Teams",
+ "paths": ('*/mobile/Containers/Shared/AppGroup/*/SkypeSpacesDogfood/*/Skype*.sqlite*',),
+ "output_types": "standard"
+ },
+ "teamsCalls": {
+ "name": "Teams Call Logs",
+ "description": "Microsoft Teams call history",
+ "author": "",
+ "version": "1.0",
+ "category": "Microsoft Teams",
+ "paths": ('*/mobile/Containers/Shared/AppGroup/*/SkypeSpacesDogfood/*/Skype*.sqlite*',),
+ "output_types": "standard"
+ },
+ "teamsLocations": {
+ "name": "Teams Shared Locations",
+ "description": "Microsoft Teams shared location data",
+ "author": "",
+ "version": "1.0",
+ "category": "Microsoft Teams",
+ "paths": ('*/mobile/Containers/Shared/AppGroup/*/SkypeSpacesDogfood/*/Skype*.sqlite*',),
+ "output_types": "all"
+ }
+}
+
import sqlite3
import io
import json
@@ -5,250 +61,407 @@
import re
import shutil
import datetime
+from scripts.ilapfuncs import logfunc, convert_ts_human_to_timezone_offset, artifact_processor, open_sqlite_db_readonly
import nska_deserialize as nd
-import scripts.artifacts.artGlobals
-
-from packaging import version
-from scripts.artifact_report import ArtifactHtmlReport
-from scripts.ilapfuncs import logfunc, logdevinfo, timeline, kmlgen, tsv, is_platform_windows, open_sqlite_db_readonly
-
-def get_teams(files_found, report_folder, seeker, wrap_text, timezone_offset):
- CacheFile = 0
+@artifact_processor
+def teamsMessages(files_found, report_folder, seeker, wrap_text, timezone_offset):
+ if not files_found:
+ return
+
+ data_list = []
+ cache_file = None
+ db_file = None
+
+ # Find required files
for file_found in files_found:
file_found = str(file_found)
-
if file_found.endswith('.sqlite'):
- databasedata = file_found
- if file_found.endswith('CacheFile'):
- CacheFile = file_found
+ db_file = file_found
+ elif file_found.endswith('CacheFile'):
+ cache_file = file_found
+
+ if not db_file:
+ logfunc('No Teams database found')
+ return
- if CacheFile != 0:
- with open(CacheFile ,'rb') as nsfile:
- nsplist = nd.deserialize_plist(nsfile)
+ # Initialize cache dictionary
+ nsplist = {}
+ if cache_file:
+ try:
+ with open(cache_file, 'rb') as nsfile:
+ nsplist = nd.deserialize_plist(nsfile)
+ except Exception as e:
+ logfunc(f'Error parsing CacheFile: {str(e)}')
+ nsplist = {}
- db = open_sqlite_db_readonly(databasedata)
+ db = open_sqlite_db_readonly(db_file)
cursor = db.cursor()
+
cursor.execute('''
- SELECT
- datetime('2001-01-01', "ZARRIVALTIME" || ' seconds'),
- ZIMDISPLAYNAME,
- ZCONTENT
- from ZSMESSAGE
+ SELECT
+ datetime('2001-01-01', "ZARRIVALTIME" || ' seconds') as timestamp,
+ ZIMDISPLAYNAME,
+ ZCONTENT,
+ ZFROM
+ FROM ZSMESSAGE
+ ORDER BY timestamp
''')
- all_rows = cursor.fetchall()
- usageentries = len(all_rows)
- data_list = []
-
- if usageentries > 0:
- for row in all_rows:
- thumb =''
+ for row in cursor.fetchall():
+ timestamp = convert_ts_human_to_timezone_offset(row[0], timezone_offset)
+ display_name = row[1] if row[1] else ''
+ content = row[2] if row[2] else ''
+ sender = row[3] if row[3] else ''
+ thumb = ''
+
+ # Process media content
+ if content and ''
- except:
- logfunc(f'Error on block lines 47 - 57, content on {row[0]}')
- data_list.append((row[0], row[1], row[2], thumb))
-
- description = 'Teams Messages'
- report = ArtifactHtmlReport('Teams Messages')
- report.start_artifact_report(report_folder, 'Teams Messages', description)
- report.add_script()
- data_headers = ('Timestamp', 'Name', 'Message', 'Shared Media')
- report.write_artifact_data_table(data_headers, data_list, file_found, html_no_escape=['Shared Media']
-)
- report.end_artifact_report()
+ try:
+ shutil.copy2(match, report_folder)
+ data_file_name = os.path.basename(match)
+ thumb = f'
'
+ except Exception as e:
+ logfunc(f'Error copying media file: {str(e)}')
+ except Exception as e:
+ logfunc(f'Error processing image for message at {timestamp}: {str(e)}')
- tsvname = 'Microsoft Teams Messages'
- tsv(report_folder, data_headers, data_list, tsvname)
-
- tlactivity = 'Microsoft Teams Messages'
- timeline(report_folder, tlactivity, data_list, data_headers)
- else:
- logfunc('No Microsoft Teams Messages data available')
+ data_list.append((timestamp, display_name, sender, content, thumb))
+
+ db.close()
+
+ data_headers = (
+ ('Timestamp', 'datetime'),
+ 'Display Name',
+ 'From',
+ 'Message',
+ 'Shared Media'
+ )
+
+ return data_headers, data_list, db_file
+
+@artifact_processor
+def teamsContacts(files_found, report_folder, seeker, wrap_text, timezone_offset):
+ if not files_found:
+ return
+
+ data_list = []
+ db_file = None
+
+ for file_found in files_found:
+ if file_found.endswith('.sqlite'):
+ db_file = file_found
+ break
+
+ if not db_file:
+ logfunc('No Teams database found')
+ return
+
+ db = open_sqlite_db_readonly(db_file)
+ cursor = db.cursor()
cursor.execute('''
- SELECT
- ZDISPLAYNAME,
- zemail,
- ZPHONENUMBER
- from
- ZDEVICECONTACTHASH
+ SELECT
+ ZDISPLAYNAME,
+ ZEMAIL,
+ ZPHONENUMBER
+ FROM ZDEVICECONTACTHASH
''')
- all_rows = cursor.fetchall()
- usageentries = len(all_rows)
+ for row in cursor.fetchall():
+ display_name = row[0] if row[0] else ''
+ email = row[1] if row[1] else ''
+
+ # Clean phone number
+ phone = row[2] if row[2] else ''
+ if phone:
+ # Remove common formatting characters
+ phone = phone.replace('(', '').replace(')', '').replace('-', '')
+ phone = phone.replace(' ', '').replace('.', '').replace('+', '')
+
+ data_list.append((display_name, email, phone))
+
+ db.close()
+
+ data_headers = (
+ 'Display Name',
+ ('Email', 'phonenumber'),
+ ('Phone Number', 'phonenumber')
+ )
+
+ return data_headers, data_list, db_file
+
+@artifact_processor
+def teamsUser(files_found, report_folder, seeker, wrap_text, timezone_offset):
+ if not files_found:
+ return
+
data_list = []
+ db_file = None
- if usageentries > 0:
- for row in all_rows:
- data_list.append((row[0], row[1], row[2]))
-
- description = 'Teams Contact'
- report = ArtifactHtmlReport('Teams Contact')
- report.start_artifact_report(report_folder, 'Teams Contact', description)
- report.add_script()
- data_headers = ('Display Name', 'Email', 'Phone Number')
- report.write_artifact_data_table(data_headers, data_list, file_found)
- report.end_artifact_report()
-
- tsvname = 'Teams Contact'
- tsv(report_folder, data_headers, data_list, tsvname)
-
- else:
- logfunc('No Teams Contact data available')
+ for file_found in files_found:
+ if file_found.endswith('.sqlite'):
+ db_file = file_found
+ break
+
+ if not db_file:
+ logfunc('No Teams database found')
+ return
+
+ db = open_sqlite_db_readonly(db_file)
+ cursor = db.cursor()
cursor.execute('''
- SELECT
- datetime('2001-01-01', "ZTS_LASTSYNCEDAT" || ' seconds'),
- ZDISPLAYNAME,
- ZTELEPHONENUMBER
- from zuser
+ SELECT
+ datetime('2001-01-01', "ZTS_LASTSYNCEDAT" || ' seconds'),
+ ZDISPLAYNAME,
+ ZTELEPHONENUMBER
+ FROM ZUSER
''')
- all_rows = cursor.fetchall()
- usageentries = len(all_rows)
+ for row in cursor.fetchall():
+ timestamp = convert_ts_human_to_timezone_offset(row[0], timezone_offset)
+ display_name = row[1] if row[1] else ''
+ phone = row[2] if row[2] else ''
+ data_list.append((timestamp, display_name, phone))
+
+ db.close()
+
+ data_headers = (
+ ('Timestamp Last Sync', 'datetime'),
+ 'Display Name',
+ ('Phone Number', 'phonenumber')
+ )
+
+ return data_headers, data_list, db_file
+
+@artifact_processor
+def teamsCalls(files_found, report_folder, seeker, wrap_text, timezone_offset):
+ if not files_found:
+ return
+
data_list = []
+ db_file = None
+
+ for file_found in files_found:
+ if file_found.endswith('.sqlite'):
+ db_file = file_found
+ break
+
+ if not db_file:
+ logfunc('No Teams database found')
+ return
+
+ db = open_sqlite_db_readonly(db_file)
+ cursor = db.cursor()
- if usageentries > 0:
- for row in all_rows:
- data_list.append((row[0], row[1], row[2]))
-
- description = 'Teams User'
- report = ArtifactHtmlReport('Teams User')
- report.start_artifact_report(report_folder, 'Teams User', description)
- report.add_script()
- data_headers = ('Timestamp Last Sync', 'Display Name', 'Phone Number')
- report.write_artifact_data_table(data_headers, data_list, file_found)
- report.end_artifact_report()
-
- tsvname = 'Microsoft Teams User'
- tsv(report_folder, data_headers, data_list, tsvname)
-
- tlactivity = 'Microsoft Teams User'
- timeline(report_folder, tlactivity, data_list, data_headers)
-
- else:
- logfunc('No Teams User data available')
-
cursor.execute('''
- SELECT
- ZCOMPOSETIME,
- zfrom,
- ZIMDISPLAYNAME,
- zcontent,
- ZPROPERTIES
- from ZSMESSAGE, ZMESSAGEPROPERTIES
- where ZSMESSAGE.ZTSID = ZMESSAGEPROPERTIES.ZTSID
- order by ZCOMPOSETIME
+ SELECT
+ ZCOMPOSETIME,
+ ZFROM,
+ ZIMDISPLAYNAME,
+ ZCONTENT,
+ ZPROPERTIES
+ FROM ZSMESSAGE
+ JOIN ZMESSAGEPROPERTIES ON ZSMESSAGE.ZTSID = ZMESSAGEPROPERTIES.ZTSID
+ WHERE ZPROPERTIES IS NOT NULL
+ ORDER BY ZCOMPOSETIME
''')
- all_rows = cursor.fetchall()
- usageentries = len(all_rows)
- data_list_calls = []
- data_list_cards = []
- data_list_unparsed = []
+ for row in cursor.fetchall():
+ try:
+ compose_time = convert_ts_human_to_timezone_offset(row[0].replace('T', ' '), timezone_offset)
+ plist_file_object = io.BytesIO(row[4])
+
+ try:
+ plist = nd.deserialize_plist(plist_file_object)
+ except Exception as e:
+ logfunc(f'Failed to read plist: {str(e)}')
+ continue
+
+ if 'call-log' in plist:
+ try:
+ if isinstance(plist['call-log'], dict):
+ datacalls = plist['call-log']
+ else:
+ datacalls = json.loads(plist['call-log'])
+
+ call_start = convert_ts_human_to_timezone_offset(
+ datacalls.get('startTime', '').replace('T', ' '),
+ timezone_offset
+ ) if datacalls.get('startTime') else ''
+
+ call_connect = convert_ts_human_to_timezone_offset(
+ datacalls.get('connectTime', '').replace('T', ' '),
+ timezone_offset
+ ) if datacalls.get('connectTime') else ''
+
+ call_end = convert_ts_human_to_timezone_offset(
+ datacalls.get('endTime', '').replace('T', ' '),
+ timezone_offset
+ ) if datacalls.get('endTime') else ''
+
+ data_list.append((
+ compose_time,
+ row[1] if row[1] else '', # from
+ row[2] if row[2] else '', # display_name
+ row[3] if row[3] else '', # content
+ call_start,
+ call_connect,
+ call_end,
+ datacalls.get('callDirection', ''),
+ datacalls.get('callType', ''),
+ datacalls.get('callState', ''),
+ datacalls.get('originator', ''),
+ datacalls.get('target', ''),
+ datacalls.get('originatorParticipant', {}).get('displayName', ''),
+ datacalls.get('targetParticipant', {}).get('displayName', '')
+ ))
+ except Exception as e:
+ logfunc(f'Error processing call log data: {str(e)} - Data: {plist["call-log"]}')
+
+ except Exception as e:
+ logfunc(f'Error processing row: {str(e)}')
+
+ db.close()
- if usageentries > 0:
- for row in all_rows:
- plist = ''
- composetime = row[0].replace('T',' ')
+ data_headers = (
+ ('Compose Timestamp', 'datetime'),
+ 'From',
+ 'Display Name',
+ 'Content',
+ ('Call Start', 'datetime'),
+ ('Call Connect', 'datetime'),
+ ('Call End', 'datetime'),
+ 'Call Direction',
+ 'Call Type',
+ 'Call State',
+ 'Call Originator',
+ 'Call Target',
+ 'Call Originator Name',
+ 'Call Participant Name'
+ )
+
+ return data_headers, data_list, db_file
+
+@artifact_processor
+def teamsLocations(files_found, report_folder, seeker, wrap_text, timezone_offset):
+ if not files_found:
+ return
+
+ data_list = []
+ db_file = None
+
+ for file_found in files_found:
+ if file_found.endswith('.sqlite'):
+ db_file = file_found
+ break
+
+ if not db_file:
+ logfunc('No Teams database found')
+ return
+
+ db = open_sqlite_db_readonly(db_file)
+ cursor = db.cursor()
+
+ cursor.execute('''
+ SELECT
+ ZCOMPOSETIME,
+ ZFROM,
+ ZIMDISPLAYNAME,
+ ZCONTENT,
+ ZPROPERTIES
+ FROM ZSMESSAGE
+ JOIN ZMESSAGEPROPERTIES ON ZSMESSAGE.ZTSID = ZMESSAGEPROPERTIES.ZTSID
+ WHERE ZPROPERTIES IS NOT NULL
+ ORDER BY ZCOMPOSETIME
+ ''')
+
+ for row in cursor.fetchall():
+ try:
+ compose_time = convert_ts_human_to_timezone_offset(row[0].replace('T', ' '), timezone_offset)
plist_file_object = io.BytesIO(row[4])
- if row[4].find(b'NSKeyedArchiver') == -1:
- if sys.version_info >= (3, 9):
- plist = plistlib.load(plist_file_object)
- else:
- plist = biplist.readPlist(plist_file_object)
- else:
+
+ try:
+ plist = nd.deserialize_plist(plist_file_object)
+ except Exception as e:
+ logfunc(f'Failed to read plist: {str(e)}')
+ continue
+
+ if 'cards' in plist:
try:
- plist = nd.deserialize_plist(plist_file_object)
- except (nd.DeserializeError, nd.biplist.NotBinaryPlistException, nd.biplist.InvalidPlistException,
- nd.plistlib.InvalidFileException, nd.ccl_bplist.BplistError, ValueError, TypeError, OSError, OverflowError) as ex:
- logfunc(f'Failed to read plist for {row[4]}, error was:' + str(ex))
- if 'call-log' in plist:
- datacalls = json.loads(plist['call-log'])
- callstart = (datacalls.get('startTime'))
- callstart = callstart.replace('T',' ')
- callconnect = (datacalls.get('connectTime'))
- callconnect = callconnect.replace('T',' ')
- callend = (datacalls['endTime'])
- callend = callend.replace('T',' ')
- calldirection = (datacalls['callDirection'])
- calltype = (datacalls['callType'])
- callstate = (datacalls['callState'])
- calloriginator = (datacalls['originator'])
- calltarget = (datacalls['target'])
- calloriginatordname = (datacalls['originatorParticipant']['displayName'])
- callparticipantdname = (datacalls['targetParticipant']['displayName'])
- data_list_calls.append((composetime, row[1], row[2], row[3], callstart, callconnect, callend, calldirection, calltype, callstate, calloriginator, calltarget, calloriginatordname, callparticipantdname))
- elif 'cards' in plist:
- cards = json.loads(plist['cards'])
- cardurl = (cards[0]['content']['body'][0]['selectAction']['url'])
- cardtitle = (cards[0]['content']['body'][0]['selectAction']['title'])
- cardtext = (cards[0]['content']['body'][1]['text'])
- cardurl2 = (cards[0]['content']['body'][0]['url'])
- if (cards[0]['content']['body'][0].get('id')) is not None:
- idcontent = json.loads(cards[0]['content']['body'][0]['id'])
- cardlat = (idcontent.get('latitude'))
- cardlong = (idcontent.get('longitude'))
- cardexpires = (idcontent.get('expiresAt'))
- cardexpires = datetime.datetime.fromtimestamp(cardexpires / 1000)
- carddevid = (idcontent.get('deviceId'))
- data_list_cards.append((composetime, row[1], row[2], row[3], cardurl, cardtitle, cardtext, cardurl2, cardlat, cardlong, cardexpires, carddevid))
- else:
- data_list_unparsed.append((composetime, row[1], row[2], row[3], plist))
-
- description = 'Microsoft Teams Call Logs'
- report = ArtifactHtmlReport('Microsoft Teams Call Logs')
- report.start_artifact_report(report_folder, 'Teams Call Logs', description)
- report.add_script()
- data_headers = ('Compose Timestamp', 'From', 'Display Name', 'Content',' Call Start', 'Call Connect', 'Call End', 'Call Direction', 'Call Type', 'Call State', 'Call Originator', 'Call Target', 'Call Originator Name', 'Call Participant Name')
- report.write_artifact_data_table(data_headers, data_list_calls, file_found)
- report.end_artifact_report()
-
- tsvname = 'Microsoft Teams Call Logs'
- tsv(report_folder, data_headers, data_list_calls, tsvname)
-
- tlactivity = 'Microsoft Teams Call Logs'
- timeline(report_folder, tlactivity, data_list_calls, data_headers)
-
- description = 'Microsoft Teams Shared Locations'
- report = ArtifactHtmlReport('Microsoft Teams Shared Locations')
- report.start_artifact_report(report_folder, 'Teams Shared Locations', description)
- report.add_script()
- data_headers = ('Timestamp', 'From', 'Display Name', 'Content','Card URL', 'Card Title', 'Card Text', 'Card URL2', 'Latitude', 'Longitude', 'Card Expires', 'Device ID')
- report.write_artifact_data_table(data_headers, data_list_cards, file_found)
- report.end_artifact_report()
-
- tsvname = 'Microsoft Teams Shared Locations'
- tsv(report_folder, data_headers, data_list_cards, tsvname)
-
- tlactivity = 'Microsoft Teams Shared Locations'
- timeline(report_folder, tlactivity, data_list_cards, data_headers)
-
- kmlactivity = 'Microsoft Teams Shared Locations'
- kmlgen(report_folder, kmlactivity, data_list_cards, data_headers)
-
- else:
- logfunc('No Microsoft Teams Call Logs & Cards data available')
-
+ cards = json.loads(plist['cards'])
+ card = cards[0]['content']['body'][0]
+
+ card_url = card.get('selectAction', {}).get('url', '')
+ card_title = card.get('selectAction', {}).get('title', '')
+ card_text = cards[0]['content']['body'][1].get('text', '')
+ card_url2 = card.get('url', '')
+
+ card_lat = None
+ card_long = None
+ card_expires = None
+ card_devid = None
+
+ if card.get('id'):
+ try:
+ id_content = json.loads(card['id'])
+ card_lat = id_content.get('latitude')
+ card_long = id_content.get('longitude')
+ expires_ms = id_content.get('expiresAt')
+ if expires_ms:
+ card_expires = convert_ts_human_to_timezone_offset(
+ datetime.datetime.fromtimestamp(expires_ms / 1000).strftime('%Y-%m-%d %H:%M:%S'),
+ timezone_offset
+ )
+ card_devid = id_content.get('deviceId')
+ except Exception as e:
+ logfunc(f'Error processing card ID data: {str(e)}')
+
+ data_list.append((
+ compose_time,
+ row[1] if row[1] else '', # from
+ row[2] if row[2] else '', # display_name
+ row[3] if row[3] else '', # content
+ card_url,
+ card_title,
+ card_text,
+ card_url2,
+ card_lat,
+ card_long,
+ card_expires,
+ card_devid
+ ))
+ except Exception as e:
+ logfunc(f'Error processing card data: {str(e)}')
+
+ except Exception as e:
+ logfunc(f'Error processing row: {str(e)}')
db.close()
-__artifacts__ = {
- "teams": (
- "Microsoft Teams",
- ('*/mobile/Containers/Shared/AppGroup/*/SkypeSpacesDogfood/*/Skype*.sqlite*','*/mobile/Containers/Shared/AppGroup/*/SkypeSpacesDogfood/Downloads/*/Images/*'),
- get_teams)
-}
-
-
\ No newline at end of file
+ data_headers = (
+ ('Timestamp', 'datetime'),
+ 'From',
+ 'Display Name',
+ 'Content',
+ 'Card URL',
+ 'Card Title',
+ 'Card Text',
+ 'Card URL2',
+ 'Latitude',
+ 'Longitude',
+ ('Card Expires', 'datetime'),
+ 'Device ID'
+ )
+
+ return data_headers, data_list, db_file
+