├── README.md ├── iCatch_v1.2.1.pyw ├── license.txt └── requirements.txt /README.md: -------------------------------------------------------------------------------- 1 | 2 | # iCatch![iCatch](https://github.com/user-attachments/assets/38d5845a-e9d0-4bf7-9ce2-7b924dbac34e) 3 | 4 | The iOS Cache Analysis for Tracking Coordinates History (iCatch) is a utility to process the iOS Cache.sqlite database and create a timelined KML map for use in Google Earth. 5 | 6 | 7 | This utility allows you to export GPS data from the iOS Cache.SQLite database, generate CSV and KMZ files, and log details for analysis. 8 | 9 | A processing log, csv file containing records of processed files (within time/date filter considerations), and KMZ file are output to a new directory at the user-selected location. 10 | 11 | 12 | ************* 13 | *All times are processed and output in Coordinated Universal Time (UTC-0)* 14 | *Due to their size, multiple KMZ files may be generated and are limited to 10,000 records each.* 15 | ************* 16 | 17 | 18 | 19 | ## Requirements 20 | 21 | To install the required Python libraries, use the command: pip install -r requirements.txt 22 | 23 | **This tool is intended to be run using Python. A Windows executable with the latest updates will be distributed from time to time.** 24 | 25 | ## Usage 26 | This tool was designed with Python and can be run as a windowless Python file or as a Windows executable file. The executable file will be updated from time to time and may not be the latest release. 27 | 28 | To open GUI, from the CLI run 29 | 30 | py iCatch_v1.X.pyw 31 | or double-click the .pyw file from your file explorer. 32 | 1. Input case information. 33 | 2. Select the path of the exported **Cache.sqlite** database which is found at: *private/var/mobile/Library/Caches/com.apple.routined/Cache.sqlite* (please ensure the shm and wal files are included in the same directory so all records are processed). 34 | 3. Select the desired color for the pin and accuracy ring. 35 | 4. Select a radius filter to limit the maximum radius of horizontal accuracy. 36 | 5. Select the Date/Time filter options. This option is enabled by default. 37 | 38 | 39 | a. It is highly recommended to use this option as the database contains tens of thousands of points, and can have thousands of points in a short timeframe. 40 | 6. Select Include Speed and Direction if these datapoints are of interest. These values include reported device speed, output in MPH, and reported "Course" direction, output with degrees and cardinal direction. Reported values that fall in a negative range are considered invalid and are noted as such. 41 | 42 | *The **Triage Dates** function will quickly display the dates that are stored within the database, along with the number of records for those dates.* 43 | 44 | ### Google Earth Pro Usage: 45 | • The KMZ file should be used with the Google Earth Pro desktop version. The file is loaded into temporary places. Each point has two items: the "Pin" and the horizontal accuracy overlay. Only the pin is loaded automatically to save system resources. 46 | 47 | • The timeline slider should be adjusted to a smaller timeframe before loading (full-checking) the loaded KMZ folder. This will ensure that not all (up to) 10,000 points and overlays are loaded at once. 48 | 49 | • To adjust the visible timeframe, use the slider points on the timeline bar, as well as the settings option. 50 | 51 | • All records are processed in UTC-0 and the settings options will allow for timezone display adjustments. 52 | 53 | ## Acknowledgements 54 | For more information about the Cache.sqlite database, including speed artifacts, check out the amazing work by Scott Koenig at: https://theforensicscooter.com/2021/09/22/iphone-device-speeds-in-cache-sqlite-zrtcllocationmo/ 55 | 56 | ## Issues 57 | I'm still trying to figure out GitHub, so if you come across an issue or have suggestions on a better way to do things, please let me know! 58 | -------------------------------------------------------------------------------- /iCatch_v1.2.1.pyw: -------------------------------------------------------------------------------- 1 | import tkinter as tk 2 | from tkinter import filedialog, messagebox, ttk, Toplevel 3 | import sqlite3 4 | import base64 5 | import pandas as pd 6 | from io import BytesIO 7 | from PIL import Image, ImageTk 8 | from tkcalendar import DateEntry 9 | import simplekml 10 | import zipfile 11 | import math 12 | import hashlib 13 | from datetime import datetime, timedelta 14 | import os 15 | 16 | 17 | #Creates horizontal accuracy radius/circle 18 | def create_circle(lat, lon, radius, num_segments=12): # Default to 12 segments, limited to reduce size of file. 19 | coords = [] 20 | for i in range(num_segments): 21 | angle = math.radians(float(i) / num_segments * 360) 22 | dx = radius * math.cos(angle) 23 | dy = radius * math.sin(angle) 24 | 25 | d_lat = dy / 111320 # Latitude degrees per meter 26 | d_lon = dx / (111320 * math.cos(math.radians(lat))) # Longitude degrees per meter (adjusted by latitude) 27 | 28 | point_lat = lat + d_lat 29 | point_lon = lon + d_lon 30 | 31 | coords.append((point_lon, point_lat)) 32 | 33 | return coords 34 | 35 | image_base64 = """ 36 | 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 37 | """ 38 | 39 | # Function to hash files (md5, sha1) 40 | def hash_file(file_path): 41 | md5_hash = hashlib.md5() 42 | sha1_hash = hashlib.sha1() 43 | 44 | with open(file_path, 'rb') as f: 45 | for byte_block in iter(lambda: f.read(4096), b""): 46 | md5_hash.update(byte_block) 47 | sha1_hash.update(byte_block) 48 | 49 | return md5_hash.hexdigest(), sha1_hash.hexdigest() 50 | 51 | # Main function to query database and generate CSV, KMZ, and Log 52 | def generate_outputs(): 53 | org = org_var.get() 54 | examiner = examiner_var.get() 55 | case_num = case_num_var.get() 56 | device_info = device_var.get() 57 | db_path = db_var.get() 58 | output_folder = output_var.get() 59 | icon_color = cache_color_var.get() 60 | 61 | 62 | if not all([org, examiner, case_num, device_info, db_path, output_folder]): 63 | messagebox.showerror("Error", "Please fill all fields.") 64 | return 65 | 66 | if date_filter_var.get(): 67 | start_dt = start_date_var.get() + " " + start_time_var.get() 68 | end_dt = end_date_var.get() + " " + end_time_var.get() 69 | 70 | # Convert start and end to datetime objects for validation 71 | try: 72 | start_datetime = datetime.strptime(start_dt, '%Y-%m-%d %H:%M:%S') 73 | end_datetime = datetime.strptime(end_dt, '%Y-%m-%d %H:%M:%S') 74 | 75 | if end_datetime < start_datetime: 76 | messagebox.showerror("Error", "End date/time cannot be earlier than start date/time.") 77 | return 78 | except ValueError as e: 79 | messagebox.showerror("Error", f"Invalid date/time format: {e}") 80 | return 81 | 82 | conn = sqlite3.connect(f"file:{db_path}?mode=ro", uri=True) 83 | cursor = conn.cursor() 84 | 85 | timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') 86 | output_dir = os.path.join(output_folder, f'iCatch Results {timestamp}') 87 | os.makedirs(output_dir, exist_ok=True) 88 | 89 | # Base SQLite query 90 | query = """ 91 | SELECT 92 | ZRTCLLOCATIONMO.Z_PK AS 'Record ID', 93 | DATETIME(978307200 + CAST(ZTIMESTAMP AS REAL), 'unixepoch') || '.' || 94 | SUBSTR(CAST((ZTIMESTAMP - CAST(ZTIMESTAMP AS INTEGER)) * 1000 AS INTEGER) + 1000, 2, 3) AS 'Timestamp', 95 | PRINTF('%.6f', ZRTCLLOCATIONMO.ZLATITUDE) AS 'Latitude', 96 | PRINTF('%.6f', ZRTCLLOCATIONMO.ZLONGITUDE) AS 'Longitude', 97 | PRINTF('%.2f', ZRTCLLOCATIONMO.ZHORIZONTALACCURACY) AS 'Horizontal Accuracy (M)' 98 | """ 99 | 100 | # Add Speed and Course columns if checkbox is selected 101 | if speed_course_var.get(): 102 | query += """, 103 | CASE 104 | WHEN ZSPEED < 0 THEN 'Invalid/No Speed' 105 | ELSE PRINTF('%.1f', ZSPEED * 2.23694) 106 | END AS "Speed (MPH)", 107 | CASE 108 | WHEN ZCOURSE < 0 THEN 'Invalid/No Course' 109 | ELSE 110 | CASE 111 | WHEN ZCOURSE >= 348.75 OR ZCOURSE < 11.25 THEN 'N' 112 | WHEN ZCOURSE >= 11.25 AND ZCOURSE < 33.75 THEN 'NNE' 113 | WHEN ZCOURSE >= 33.75 AND ZCOURSE < 56.25 THEN 'NE' 114 | WHEN ZCOURSE >= 56.25 AND ZCOURSE < 78.75 THEN 'ENE' 115 | WHEN ZCOURSE >= 78.75 AND ZCOURSE < 101.25 THEN 'E' 116 | WHEN ZCOURSE >= 101.25 AND ZCOURSE < 123.75 THEN 'ESE' 117 | WHEN ZCOURSE >= 123.75 AND ZCOURSE < 146.25 THEN 'SE' 118 | WHEN ZCOURSE >= 146.25 AND ZCOURSE < 168.75 THEN 'SSE' 119 | WHEN ZCOURSE >= 168.75 AND ZCOURSE < 191.25 THEN 'S' 120 | WHEN ZCOURSE >= 191.25 AND ZCOURSE < 213.75 THEN 'SSW' 121 | WHEN ZCOURSE >= 213.75 AND ZCOURSE < 236.25 THEN 'SW' 122 | WHEN ZCOURSE >= 236.25 AND ZCOURSE < 258.75 THEN 'WSW' 123 | WHEN ZCOURSE >= 258.75 AND ZCOURSE < 281.25 THEN 'W' 124 | WHEN ZCOURSE >= 281.25 AND ZCOURSE < 303.75 THEN 'WNW' 125 | WHEN ZCOURSE >= 303.75 AND ZCOURSE < 326.25 THEN 'NW' 126 | WHEN ZCOURSE >= 326.25 AND ZCOURSE < 348.75 THEN 'NNW' 127 | END || ' - ' || PRINTF('%.1f', ZCOURSE) || ' degrees' 128 | END AS "Course Direction" 129 | """ 130 | 131 | # Add date filter if selected 132 | if date_filter_var.get(): 133 | query += f""" 134 | FROM ZRTCLLOCATIONMO 135 | WHERE ZRTCLLOCATIONMO.ZTIMESTAMP >= strftime('%s', '{start_dt}') - strftime('%s', '2001-01-01 00:00:00') 136 | AND ZRTCLLOCATIONMO.ZTIMESTAMP <= strftime('%s', '{end_dt}') - strftime('%s', '2001-01-01 00:00:00') 137 | """ 138 | else: 139 | query += " FROM ZRTCLLOCATIONMO " 140 | 141 | query += "ORDER BY ZRTCLLOCATIONMO.ZTIMESTAMP ASC;" 142 | 143 | # Run SQLite query and save results as CSV 144 | try: 145 | df = pd.read_sql_query(query, conn) 146 | if speed_course_var.get(): 147 | if "Speed (MPH)" not in df.columns or "Course Direction" not in df.columns: 148 | messagebox.showerror("Error", "Speed and Course columns are missing from the query results.") 149 | return 150 | 151 | # Convert columns to numeric after query 152 | df['Horizontal Accuracy (M)'] = pd.to_numeric(df['Horizontal Accuracy (M)'], errors='coerce') 153 | df['Latitude'] = pd.to_numeric(df['Latitude'], errors='coerce') 154 | df['Longitude'] = pd.to_numeric(df['Longitude'], errors='coerce') 155 | 156 | csv_file = os.path.join(output_dir, f"{case_num}_{datetime.now().strftime('%Y%m%d_%H%M')}_output.csv") 157 | df.to_csv(csv_file, index=False) 158 | finally: 159 | conn.close() 160 | 161 | # Get the selected accuracy limit from the dropdown 162 | accuracy_limit_selection = accuracy_limit_var.get() 163 | if accuracy_limit_selection == "No Limit": 164 | accuracy_limit = float('inf') # No limit 165 | else: 166 | accuracy_limit = float(accuracy_limit_selection) 167 | 168 | # Add a column to indicate whether the record was included in the KMZ 169 | df['Included in KMZ'] = df['Horizontal Accuracy (M)'] <= accuracy_limit 170 | 171 | csv_file = os.path.join(output_dir, f"{case_num}_{datetime.now().strftime('%Y%m%d_%H%M')}_output.csv") 172 | df.to_csv(csv_file, index=False) 173 | 174 | 175 | # Split records into batches of 10,000 for KMZ files to reduce size of final file 176 | num_records = len(df) 177 | batch_size = 10000 178 | for batch_num in range(0, num_records, batch_size): 179 | batch_df = df.iloc[batch_num:batch_num+batch_size] 180 | 181 | # Filter batch based on horizontal accuracy 182 | batch_df_kmz = batch_df[batch_df['Horizontal Accuracy (M)'] <= accuracy_limit] 183 | 184 | kmz_file = os.path.join(output_dir, f"{case_num}_{datetime.now().strftime('%Y%m%d_%H%M')}_part{batch_num // batch_size + 1}.kmz") 185 | 186 | # Create KMZ file for each batch 187 | create_kmz(batch_df_kmz, kmz_file, org, examiner, case_num, device_info) 188 | 189 | # Hash the input (database) file 190 | db_md5, db_sha1 = hash_file(db_path) 191 | 192 | # Generate log and hash values for CSV and KMZ files 193 | log_file = os.path.join(output_dir, f"{case_num}_{datetime.now().strftime('%Y%m%d_%H%M')}_log.txt") 194 | with open(log_file, 'w') as log: 195 | log.write(f"***********************************************************************\n***********************************************************************\n** iCatch - iOS Cache Analysis for Tracking Coordinates History v1.2.1 **\n** Created by: \tAaron Willmarth, CFCE,\t\t\t\t\t\t\t\t **\n**\t\t\t\tAXYS Cyber Solutions\t\t\t\t\t\t\t\t **\n**\t\t\t\thttps://github.com/AXYS-Cyber/iCatch\t\t\t\t **\n***********************************************************************\n***********************************************************************\n\n\n") 196 | 197 | log.write(f"Date and Time: {datetime.now().strftime('%Y-%m-%d %H:%M')}\n\n") 198 | log.write(f"Target File: {db_var.get()}\n") 199 | log.write(f"Target File Hashes: MD5: {db_md5} / SHA1: {db_sha1}\n\n") 200 | log.write(f"Output directory: {output_folder}\n\n") 201 | log.write(f"*****CASE INFORMATION*****\n") 202 | log.write(f"Organization: {org}\nExaminer: {examiner}\nCase #: {case_num}\nDevice Info: {device_info}\n**************************\n\n") 203 | # Log output options 204 | log.write(f"******OUTPUT OPTIONS******\n") 205 | if date_filter_var.get(): 206 | start_dt = start_date_var.get() + " " + start_time_var.get() 207 | end_dt = end_date_var.get() + " " + end_time_var.get() 208 | log.write(f"Date filter options:\n") 209 | log.write(f"\tStart Date/Time: {start_dt}\n") 210 | log.write(f"\tEnd Date/Time: {end_dt}\n") 211 | else: 212 | log.write(f"No date filter applied\n") 213 | 214 | if accuracy_limit_selection == "No Limit": 215 | log.write(f"Accuracy filter selection: No limit selected\n") 216 | else: 217 | log.write(f"Accuracy filter selection: {accuracy_limit_selection} meters or less\n") 218 | log.write(f"Icon Color: {icon_color}\n") 219 | log.write(f"****************************\n\n") 220 | log.write(f"Output Files:\n") 221 | 222 | if speed_course_var.get(): 223 | log.write(f"Speed and course direction included.\n") 224 | else: 225 | log.write(f"Speed and course direction not included.\n") 226 | 227 | # Loop through all files in the output directory, hash, and log them 228 | for root, dirs, files in os.walk(output_dir): 229 | for file_name in files: 230 | file_path = os.path.join(root, file_name) 231 | if file_name != os.path.basename(log_file): # Exclude log file from hashing 232 | log.write(f"File: {file_name}\n") 233 | 234 | # Calculate and log file hashes 235 | file_md5, file_sha1 = hash_file(file_path) 236 | log.write(f"\tFile hashes: MD5: {file_md5} / SHA1: {file_sha1}\n\n") 237 | else: 238 | log.write(f"File: {file_name} (Log file, not hashed)\n\n") 239 | 240 | # After writing the log file and generating the KMZs 241 | if messagebox.askyesno("Success", "Outputs generated successfully. Do you want to open the directory?"): 242 | open_directory(output_dir) 243 | 244 | def open_directory(path): 245 | """Open the directory using the default file explorer.""" 246 | try: 247 | if os.name == 'nt': # For Windows 248 | os.startfile(path) 249 | except Exception as e: 250 | messagebox.showerror("Error", f"Failed to open directory: {e}") 251 | 252 | 253 | # Function to create KMZ files 254 | def create_kmz(df, kmz_file, org, examiner, case_num, device_info): 255 | kml = simplekml.Kml() 256 | 257 | # Get the selected color from the dropdown 258 | selected_color = cache_color_var.get() 259 | 260 | # Define a mapping of colors to KML icon URLs and styles 261 | color_map = { 262 | "Red": ('http://maps.google.com/mapfiles/kml/pushpin/red-pushpin.png', simplekml.Color.red), 263 | "Green": ('http://maps.google.com/mapfiles/kml/pushpin/grn-pushpin.png', simplekml.Color.green), 264 | "Blue": ('http://maps.google.com/mapfiles/kml/pushpin/blue-pushpin.png', simplekml.Color.blue), 265 | "Yellow": ('http://maps.google.com/mapfiles/kml/pushpin/ylw-pushpin.png', simplekml.Color.yellow), 266 | "Purple": ('http://maps.google.com/mapfiles/kml/pushpin/purple-pushpin.png', simplekml.Color.purple), 267 | } 268 | 269 | # Default values if color is not found 270 | icon_href, pin_color = color_map.get(selected_color, 271 | ('http://maps.google.com/mapfiles/kml/pushpin/red-pushpin.png', simplekml.Color.red)) 272 | 273 | 274 | # Create a KML layer with case_num and device_info in the name 275 | folder = kml.newfolder(name=f"{case_num} - {device_info} Points") 276 | folder.visibility = 0 # Set visibility to 0 to hide initially 277 | 278 | 279 | for _, row in df.iterrows(): 280 | record_number = row['Record ID'] 281 | lat = row['Latitude'] 282 | lon = row['Longitude'] 283 | start_time = pd.to_datetime(row['Timestamp']).strftime("%Y-%m-%dT%H:%M:%S.") + f"{pd.to_datetime(row['Timestamp']).microsecond // 1000:03d}Z" 284 | accuracy = row['Horizontal Accuracy (M)'] 285 | 286 | if speed_course_var.get(): 287 | speed = row['Speed (MPH)'] 288 | direction = row['Course Direction'] 289 | 290 | # Create placemark and description 291 | pnt = folder.newpoint(name=f"{record_number}", coords=[(lon, lat)]) # Add to folder, not KML directly 292 | pnt.timestamp.when = start_time 293 | 294 | # Static text (organized in a table for two columns) 295 | static_text = f""" 296 | 297 | 298 | 301 | 307 | 308 |
299 | Logo 300 | 302 | {org},
303 | {examiner},
304 | {case_num},
305 | {device_info} 306 |
309 | """ 310 | 311 | # Full description with side-by-side layout 312 | description = ( 313 | f"{static_text}

" 314 | f"Timestamp (UTC):
{start_time}

" 315 | f"Lat: {lat}, Long: {lon}
" 316 | f"Accuracy: {accuracy} meters
" 317 | ) 318 | 319 | # Conditional part for speed and direction 320 | if speed_course_var.get(): 321 | description += ( 322 | f"Speed: {speed} MPH,
" 323 | f"Direction: {direction}" 324 | ) 325 | 326 | pnt.description = description 327 | 328 | 329 | # Style the pin (transparency and color based on selection) 330 | pnt.style.iconstyle.icon.href = icon_href 331 | pnt.style.iconstyle.color = simplekml.Color.changealpha('96', pin_color) # 60% transparency 332 | pnt.style.iconstyle.scale = 1.0 # Standard size for pin 333 | pnt.style.visibility = 0 334 | 335 | # Add a circular polygon to represent the horizontal accuracy 336 | accuracy_circle = folder.newpolygon(name=f"Accuracy for Record {record_number}") # Add to folder 337 | accuracy_circle.outerboundaryis = create_circle(lat, lon, radius=accuracy) 338 | 339 | # Set the same timestamp for the polygon (to sync with the placemark) 340 | accuracy_circle.timestamp.when = start_time 341 | 342 | # Style the circle (color based on selection) 343 | accuracy_circle.style.polystyle.color = simplekml.Color.changealpha('96', pin_color) # 60% transparency 344 | accuracy_circle.style.polystyle.outline = 1 # Add outline for better visibility 345 | accuracy_circle.style.linestyle.color = pin_color # Set outline color 346 | accuracy_circle.visibility = 0 347 | 348 | # Save KML and KMZ 349 | kml.save("temp.kml") 350 | with zipfile.ZipFile(kmz_file, 'w') as kmz: 351 | kmz.write("temp.kml") 352 | os.remove("temp.kml") 353 | 354 | ############### 355 | ## GUI Setup ## 356 | ############### 357 | window = tk.Tk() 358 | window.title("iCatch - iOS Cache Analysis for Tracking Coordinates History, v1.2.1") 359 | 360 | #Sets the initial size of the window 361 | initial_width = 700 362 | initial_height = 325 363 | window.geometry(f'{initial_width}x{initial_height}') 364 | 365 | # Center the window on the screen 366 | window.update_idletasks() # Ensure window dimensions are calculated 367 | width = window.winfo_width() 368 | height = window.winfo_height() 369 | x = (window.winfo_screenwidth() // 2) - (width // 2) 370 | y = (window.winfo_screenheight() // 2) - (height // 2) 371 | window.geometry('{}x{}+{}+{}'.format(width, height, x, y)) 372 | 373 | 374 | 375 | # Decode the base64 string and create an image 376 | image_data = base64.b64decode(image_base64) 377 | image = Image.open(BytesIO(image_data)) 378 | photo = ImageTk.PhotoImage(image) 379 | 380 | # Decode the Base64 string to bytes 381 | icon_data = base64.b64decode(image_base64) 382 | 383 | # Create an image from the decoded bytes 384 | icon_image = Image.open(BytesIO(icon_data)) 385 | icon_photo = ImageTk.PhotoImage(icon_image) 386 | 387 | # Set the icon using the PhotoImage object 388 | window.iconphoto(False, icon_photo) 389 | 390 | # Create image label 391 | image_label = tk.Label(window, image=photo) 392 | image_label.grid(row=0, column=8, rowspan=16, padx=2, pady=2) 393 | 394 | def show_about(): 395 | # Create a new window for the "About" section 396 | about_window = Toplevel(window) 397 | about_window.title("About") 398 | 399 | # Center the window on the screen 400 | about_window.update_idletasks() # Ensure window dimensions are calculated 401 | width = about_window.winfo_width() 402 | height = about_window.winfo_height() 403 | x = (about_window.winfo_screenwidth() // 2) - (width // 2) 404 | y = (about_window.winfo_screenheight() // 2) - (height // 2) 405 | about_window.geometry('{}x{}+{}+{}'.format(width, height, x, y)) 406 | 407 | # Set size and center the window 408 | about_window.geometry("600x400") 409 | about_window.resizable(False, False) 410 | 411 | # Add text to the "About" window 412 | about_text = """iCatch - iOS Cache Analysis for Tracking Coordinates History, v1.2.1 413 | Created by: Aaron Willmarth, CFCE, 414 | AXYS Cyber Solutions 415 | See https://github.com/AXYS-Cyber/iCatch for license information. 416 | This utility allows you to export GPS data from the iOS Cache.SQLite database, 417 | generate CSV and KMZ files, and log details for analysis. 418 | 419 | How to use: 420 | 1. Input case information. 421 | 2. Select path of Cache.sqlite database which is found at: 422 | private/var/mobile/Library/Caches/com.apple.routined/Cache.sqlite 423 | 3. Select desired color for pin and accuracy ring. 424 | 4. Select radius filter to limit the maximum radius of horizontal accuracy. 425 | 5. Select Date/Time filter options. This option is enabled by default. 426 | a. It is highly recommended to use this option as the database contains tens 427 | of thousands of points, and can have thousands of points in a short timeframe. 428 | 429 | ************* 430 | All times are reported in Coordinated Universal Time (UTC-0) 431 | Due to their size, multiple KMZ files may be generated and are limited to 10,000 records each. 432 | ************* 433 | 434 | For more information about this, including speed artifacts, check out the amazing work by 435 | Scott Koenig at: 436 | https://theforensicscooter.com/2021/09/22/iphone-device-speeds-in-cache-sqlite-zrtcllocationmo/ 437 | """ 438 | 439 | # Add a Label widget with the text 440 | tk.Label(about_window, text=about_text, justify="left", padx=10, pady=10).pack() 441 | 442 | def triage_dates(): 443 | db_path = db_var.get() 444 | 445 | if not db_path: 446 | messagebox.showerror("Error", "Please select Cache.Sqlite database path.") 447 | return 448 | 449 | conn = sqlite3.connect(db_path) 450 | cursor = conn.cursor() 451 | 452 | query = """ 453 | SELECT 454 | date(datetime('2001-01-01', ZRTCLLOCATIONMO.ZTIMESTAMP || ' seconds')) AS 'Date', 455 | COUNT(*) AS 'Count' 456 | FROM ZRTCLLOCATIONMO 457 | GROUP BY Date 458 | ORDER BY Date DESC; 459 | """ 460 | 461 | cursor.execute(query) 462 | results = cursor.fetchall() 463 | 464 | conn.close() 465 | 466 | if not results: 467 | messagebox.showinfo("No Data", "No records found.") 468 | return 469 | 470 | # Create a pop-up window to display the results 471 | result_window = tk.Toplevel() 472 | result_window.title("Counts by Date") 473 | 474 | # Center the window on the screen 475 | result_window.update_idletasks() # Ensure window dimensions are calculated 476 | width = result_window.winfo_width() 477 | height = result_window.winfo_height() 478 | x = (result_window.winfo_screenwidth() // 2) - (width // 2) 479 | y = (result_window.winfo_screenheight() // 2) - (height // 2) 480 | result_window.geometry('{}x{}+{}+{}'.format(width, height, x, y)) 481 | 482 | # Padding at the top before records 483 | top_padding_label = tk.Label(result_window, text="") 484 | top_padding_label.pack(pady=(10, 0)) # 20px padding at the top 485 | 486 | # Display the date counts in the pop-up 487 | for i, (date, count) in enumerate(results): 488 | result_label = tk.Label(result_window, text=f"{date}: {count} records") 489 | result_label.pack() 490 | 491 | # Display the total number of records at the bottom 492 | total_records = sum(count for _, count in results) 493 | total_label = tk.Label(result_window, text=f"Total records: {total_records}") 494 | total_label.pack(pady=(10, 20)) # 10px padding above, 20px padding below (bottom of window) 495 | 496 | # Adjust the height after packing all content 497 | result_window.update_idletasks() 498 | result_window.geometry(f"275x{result_window.winfo_reqheight()+35}") # Adjust the height 499 | 500 | # Button to close the result window 501 | close_button = tk.Button(result_window, text="Close", command=result_window.destroy) 502 | close_button.pack() 503 | 504 | 505 | # Organization, Examiner, Case, and Device Info Inputs 506 | org_var = tk.StringVar() 507 | examiner_var = tk.StringVar() 508 | case_num_var = tk.StringVar() 509 | device_var = tk.StringVar() 510 | db_var = tk.StringVar() 511 | output_var = tk.StringVar() 512 | cache_color_var = tk.StringVar() 513 | accuracy_limit_var = tk.StringVar(value="No Limit") 514 | speed_course_var= tk.BooleanVar() 515 | speed_course_var.set(False) 516 | 517 | tk.Label(window, text="Organization").grid(row=0, column=0) 518 | tk.Entry(window, textvariable=org_var).grid(row=0, column=1) 519 | 520 | tk.Label(window, text="Examiner").grid(row=0, column=3) 521 | tk.Entry(window, textvariable=examiner_var).grid(row=0, column=4) 522 | 523 | tk.Label(window, text="Case #").grid(row=2, column=0) 524 | tk.Entry(window, textvariable=case_num_var).grid(row=2, column=1) 525 | 526 | tk.Label(window, text="Device Info").grid(row=2, column=3) 527 | tk.Entry(window, textvariable=device_var).grid(row=2, column=4) 528 | 529 | tk.Label(window, text="Database Path").grid(row=4, column=0) 530 | db_entry = tk.Entry(window, textvariable=db_var) 531 | db_entry.grid(row=4, column=1) 532 | tk.Button(window, text="Browse", command=lambda: db_var.set(filedialog.askopenfilename(filetypes=[("SQLite Files", "*.sqlite")]))).grid(row=4, column=2) 533 | 534 | 535 | tk.Label(window, text="Output Location").grid(row=4, column=3) 536 | tk.Entry(window, textvariable=output_var).grid(row=4, column=4) 537 | tk.Button(window, text="Browse", command=lambda: output_var.set(filedialog.askdirectory())).grid(row=4, column=5) 538 | 539 | tk.Button(window, text="Triage Dates", command=triage_dates).grid(row=5, column=1)####ADDEDED####### 540 | 541 | # Output options frame with border 542 | output_options_frame = tk.LabelFrame(window, text="Output Options", bd=2, relief="solid") 543 | 544 | output_options_frame.grid(row=6, column=0, columnspan=8, padx=4, pady=4) 545 | 546 | # List of colors for the drop-down menu 547 | color_options = ["Red", "Green", "Blue", "Yellow", "Purple"] 548 | 549 | # Drop-down list (Combobox) for selecting a color for cache icons 550 | tk.Label(output_options_frame, text="Select Icon Color").grid(row=0, column=0) 551 | color_combobox = ttk.Combobox(output_options_frame, textvariable=cache_color_var, values=color_options, state="readonly") 552 | color_combobox.grid(row=0, column=1) 553 | color_combobox.current(0) # Set the default selection (first color in the list) 554 | 555 | tk.Label(output_options_frame, text="Accuracy Limit (M):").grid(row=0, column=3) 556 | accuracy_options = ["No Limit", "10", "25", "50", "100", "200", "500"] 557 | tk.OptionMenu(output_options_frame, accuracy_limit_var, *accuracy_options).grid(row=0, column=4) 558 | 559 | tk.Checkbutton(output_options_frame, text="Include Speed and Direction", variable=speed_course_var).grid(row=1, column=0, columnspan=2) 560 | 561 | # Date Filter Options 562 | date_filter_var = tk.BooleanVar() 563 | date_filter_var.set(True) 564 | start_date_var = tk.StringVar() 565 | start_time_var = tk.StringVar(value="00:00:00") 566 | end_date_var = tk.StringVar() 567 | end_time_var = tk.StringVar(value="23:59:59") 568 | 569 | # Options frame with border 570 | cache_options_frame = tk.LabelFrame(window, text="Date/Time Filter Options (all outputs in UTC-0)", bd=2, relief="solid") 571 | 572 | cache_options_frame.grid(row=8, column=0, columnspan=8, padx=4, pady=4) 573 | 574 | tk.Checkbutton(cache_options_frame, text="Use Date/Time Filter", variable=date_filter_var).grid(row=0, column=0, columnspan=2) 575 | 576 | tk.Label(cache_options_frame, text="Start Date:").grid(row=2, column=0) 577 | start_date_picker = DateEntry(cache_options_frame, textvariable=start_date_var, date_pattern='yyyy-mm-dd') 578 | start_date_picker.grid(row=2, column=1) 579 | 580 | tk.Label(cache_options_frame, text="Start Time (HH:MM:SS)").grid(row=2, column=3) 581 | tk.Entry(cache_options_frame, textvariable=start_time_var).grid(row=2, column=4) 582 | 583 | 584 | tk.Label(cache_options_frame, text="End Date:").grid(row=4, column=0) 585 | end_date_picker = DateEntry(cache_options_frame, textvariable=end_date_var, date_pattern='yyyy-mm-dd') 586 | end_date_picker.grid(row=4, column=1) 587 | 588 | tk.Label(cache_options_frame, text="End Time (HH:MM:SS)").grid(row=4, column=3) 589 | tk.Entry(cache_options_frame, textvariable=end_time_var).grid(row=4, column=4) 590 | 591 | # Button to generate CSV, KMZ, and log 592 | tk.Button(window, text="Generate Outputs", command=generate_outputs).grid(row=21, column=8, columnspan=3, pady=20) 593 | 594 | tk.Button(window, text="About", command=show_about).grid(row=21, column=0, columnspan=3, pady=20) 595 | 596 | 597 | 598 | window.mainloop() -------------------------------------------------------------------------------- /license.txt: -------------------------------------------------------------------------------- 1 | iCATCH - iOS Cache Analysis for Tracking Coordinates History License Disclaimer (C)2024 2 | 3 | This script and executable are provided for open use and is intended for educational and non-commercial purposes only. You may use, modify, and distribute this script freely, provided that it is not included in any commercial or paid-for software without explicit permission from the author. 4 | 5 | The author, makes no representations or warranties about the suitability or accuracy of this script for any purpose. Use it at your own risk. The author shall not be liable for any damages arising from the use or inability to use this script. 6 | 7 | By using this script, you agree to these terms. 8 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pandas 2 | Pillow 3 | tkcalendar 4 | simplekml 5 | --------------------------------------------------------------------------------