Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions ACKNOWLEDGEMENTS
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,11 @@ Copied with permission from [Digital Corpora](https://digitalcorpora.org/):
* ios/com.apple.commcenter.data.plist
* ios/com.apple.MobileBackup.plist
* ios/healthdb_secure_iOS_13_4_1.sqlite
* ios/healthdb_secure_iOS_13_3_1.sqlite
* ios/healthdb_secure_iOS_13_4_1.sqlite
* ios/healthdb_secure_iOS_15.sqlite
* ios/healthdb_secure_iOS_16.sqlite
* ios/healthdb_secure_iOS_17.sqlite
* ios/IMODb2.sqlite
* ios/NoteStore.sqlite
* viber_data
196 changes: 196 additions & 0 deletions plaso/data/formatters/ios.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,26 @@ short_source: 'SQLITE'
source: 'iOS Health database'
---
type: 'conditional'
data_type: 'ios:health:all_watch_sleep_ios17'
enumeration_helpers:
- input_attribute: 'sleep_state_code'
output_attribute: 'sleep_state_label'
default_value: 'UNKNOWN'
values:
1: 'In Bed'
2: 'Awake'
3: 'REM'
4: 'Core'
5: 'Deep'
message:
- 'Sleep State: {sleep_state_label}'
- 'Duration: {sleep_state_hms}'
short_message:
- '{sleep_state_label} for {sleep_state_hms}'
short_source: 'SQLITE'
source: 'iOS Health database'
---
type: 'conditional'
data_type: 'ios:health:headphone_audio_levels'
message:
- 'Sound Level: {decibels} dB'
Expand Down Expand Up @@ -167,6 +187,23 @@ short_source: 'SQLITE'
source: 'iOS Health database'
---
type: 'conditional'
data_type: 'ios:health:source_devices_latest'
message:
- 'Device Name: {device_name}'
- 'Manufacturer: {manufacturer}'
- 'Model: {model}'
- 'Hardware Version: {hardware}'
- 'Software/Firmware: {software} / {firmware}'
- 'Local Identifier: {device_local_identifier}'
- 'Sync Provenance: {sync_provenance}'
- 'Sync Identity: {sync_identity}'
- 'Creation Date: {creation_date_str}'
short_message:
- 'Source Device: {device_name} ({model})'
short_source: 'SQLITE'
source: 'iOS Health database'
---
type: 'conditional'
data_type: 'ios:health:source_devices'
message:
- 'Device Name: {device_name}'
Expand Down Expand Up @@ -194,6 +231,48 @@ short_source: 'SQLITE'
source: 'iOS Health database'
---
type: 'conditional'
data_type: 'ios:health:watch_by_sleep_period_latest'
message:
- 'Total Sleep Duration: {total_duration} minutes'
- 'Deep Sleep: {deep_duration_hms} ({deep_percent}%)'
- 'REM Sleep: {rem_duration_hms} ({rem_percent}%)'
- 'Core Sleep: {core_duration_hms} ({core_percent}%)'
- 'Awake: {awake_duration_hms} ({awake_percent}%)'
- 'Start Time: {start_date_str}'
- 'End Time: {end_date_str}'
short_message:
- 'Sleep Period: {total_duration} min (Deep: {deep_percent}%, REM: {rem_percent}%)'
short_source: 'SQLITE'
source: 'iOS Health database'
---
type: 'conditional'
data_type: 'ios:health:watch_by_sleep_period'
message:
- 'Total Time in Bed: {time_in_bed_hms}'
- 'Actual Sleep Percent: {asleep_percent}%'
- 'In Bed Duration: {in_bed_duration_hms} ({in_bed_percent}%)'
- 'Device: {device_name}'
- 'Start: {start_date_str}'
- 'End: {end_date_str}'
- 'Data Type ID: {data_type_id}'
short_message:
- 'Sleep Period: {time_in_bed_hms} (Asleep: {asleep_percent}%)'
short_source: 'SQLITE'
source: 'iOS Health database'
---
type: 'conditional'
data_type: 'ios:health:watch_worn'
message:
- 'Duration Worn: {hours_worn} hours'
- 'Time Off After This Period: {hours_off_before_next} hours'
- 'Start Wearing: {start_time_str}'
- 'Last Worn Time: {last_worn_time_str}'
short_message:
- 'Worn for {hours_worn}h (Then off for {hours_off_before_next}h)'
short_source: 'SQLITE'
source: 'iOS Health database'
---
type: 'conditional'
data_type: 'ios:health:weight'
message:
- 'Weight: {weight:.2f} kg'
Expand Down Expand Up @@ -309,6 +388,123 @@ short_source: 'SQLITE'
source: 'iOS Health database'
---
type: 'conditional'
data_type: 'ios:health:workouts_ios16'
enumeration_helpers:
- input_attribute: 'activity_type'
output_attribute: 'activity_label'
default_value: 'OTHER'
values:
1: "AMERICAN FOOTBALL"
2: "ARCHERY"
3: "AUSTRALIAN FOOTBALL"
4: "BADMINTON"
5: "BASEBALL"
6: "BASKETBALL"
7: "BOWLING"
8: "BOXING"
9: "CLIMBING"
10: "CRICKET"
11: "CROSS TRAINING"
12: "CURLING"
13: "CYCLING"
16: "ELLIPTICAL"
17: "EQUESTRIAN SPORTS"
18: "FENCING"
19: "FISHING"
20: "FUNCTION STRENGTH TRAINING"
21: "GOLF"
22: "GYMNASTICS"
23: "HANDBALL"
24: "HIKING"
25: "HOCKEY"
26: "HUNTING"
27: "LACROSS"
28: "MARTIAL ARTS"
29: "MIND AND BODY"
31: "PADDLE SPORTS"
32: "PLAY"
33: "PREPARATION AND RECOVERY"
34: "RACQUETBALL"
35: "ROWING"
36: "RUGBY"
37: "RUNNING"
38: "SAILING"
39: "SKATING SPORTS"
40: "SNOW SPORTS"
41: "SOCCER"
42: "SOFTBALL"
43: "SQUASH"
44: "STAIRSTEPPER"
45: "SURFING SPORTS"
46: "SWIMMING"
47: "TABLE TENNIS"
48: "TENNIS"
49: "TRACK AND FIELD"
50: "TRADITIONAL STRENGTH TRAINING"
51: "VOLLEYBALL"
52: "WALKING"
53: "WATER FITNESS"
54: "WATER POLO"
55: "WATER SPORTS"
56: "WRESTLING"
57: "YOGA"
58: "BARRE"
59: "CORE TRAINING"
60: "CROSS COUNTRY SKIING"
61: "DOWNHILL SKIING"
62: "FLEXIBILITY"
63: "HIGH INTENSITY INTERVAL TRAINING (HIIT)"
64: "JUMP ROPE"
65: "KICKBOXING"
66: "PILATES"
67: "SNOWBOARDING"
68: "STAIRS"
69: "STEP TRAINING"
70: "WHEELCHAIR WALK PACE"
71: "WHEELCHAIR RUN PACE"
72: "TAI CHI"
73: "MIXED CARDIO"
74: "HAND CYCLING"
75: "DISC SPORTS"
76: "FITNESS GAMING"
77: "DANCE"
78: "SOCIAL DANCE"
79: "PICKLEBALL"
80: "COOLDOWN"
3000: "OTHER"
- input_attribute: 'goal_type'
output_attribute: 'goal_label'
default_value: 'Unknown'
values:
0: "Open"
1: "Distance in meters"
2: "Time in seconds"
3: "Kilocalories"
- input_attribute: 'location_type'
output_attribute: 'location_label'
default_value: 'Unknown'
values:
1: 'Indoor'
2: 'Outdoor'
message:
- 'Activity: {activity_label} ({location_label})'
- 'Workout Goal: {goal_label} ({goal})'
- 'Duration: {workout_duration}'
- 'Distance: {total_distance} km'
- 'Energy: {total_active_energy_kcal} kcal Active / {total_resting_energy_kcal} kcal Resting'
- 'Heart Rate: Avg {avg_heart_rate_bpm} BPM (Min: {min_heart_rate_bpm}, Max: {max_heart_rate_bpm})'
- 'Environment: {temperature_c} °C, {humidity_percent}% Humidity'
- 'Elevation: Min {min_ground_elevation_m}m, Max {max_ground_elevation_m}m'
- 'Location: Lat {latitude}, Lon {longitude}'
- 'Device: {hardware} (Source: {source})'
- 'Timezone: {timezone}'
- 'Added to Health: {added_timestamp_str}'
short_message:
- '{activity_label}: {total_distance} km in {workout_duration}'
short_source: 'SQLITE'
source: 'iOS Health database'
---
type: 'conditional'
data_type: 'ios:health:wrist_temperature'
message:
- 'Wrist temperature: {wrist_temperature} °C'
Expand Down
6 changes: 6 additions & 0 deletions plaso/parsers/sqlite_plugins/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,12 @@
from plaso.parsers.sqlite_plugins import ios_accounts
from plaso.parsers.sqlite_plugins import ios_datausage
from plaso.parsers.sqlite_plugins import ios_health
from plaso.parsers.sqlite_plugins import ios_health_all_watch_sleep_latest
from plaso.parsers.sqlite_plugins import ios_health_source_devices_latest
from plaso.parsers.sqlite_plugins import ios_health_watch_by_sleep_period
Comment thread
joachimmetz marked this conversation as resolved.
from plaso.parsers.sqlite_plugins import ios_health_watch_by_sleep_period_latest
from plaso.parsers.sqlite_plugins import ios_health_watch_worn_data
from plaso.parsers.sqlite_plugins import ios_health_workouts_latest
from plaso.parsers.sqlite_plugins import ios_imohdchat
from plaso.parsers.sqlite_plugins import ios_instagram
from plaso.parsers.sqlite_plugins import ios_kik
Expand Down
129 changes: 129 additions & 0 deletions plaso/parsers/sqlite_plugins/ios_health_all_watch_sleep_latest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
"""SQLite parser plugin for iOS Health - All Watch Sleep Data (iOS 17)."""

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note to self, this is basically the same as ios_health._ParseAllWatchSleepSample with the exception of filtering on several category values


from dfdatetime import cocoa_time as dfdatetime_cocoa_time

from plaso.containers import events
from plaso.parsers import sqlite
from plaso.parsers.sqlite_plugins import interface


class IOSHealthAllWatchSleepLatestEventData(events.EventData):
"""iOS Health - All Watch Sleep (stages) event data.

Attributes:
end_time (dfdatetime.DateTimeValues): date and time the sleep ended.
sleep_state_code (int): sleep state code (stages 2-5).
sleep_state_hms (str): duration of sleep formatted as 'HH:MM:SS'.
start_time (dfdatetime.DateTimeValues): date and time the sleep started.
"""

DATA_TYPE = "ios:health:all_watch_sleep_ios17"

def __init__(self):
"""Initializes event data."""
super().__init__(data_type=self.DATA_TYPE)
self.end_time = None
self.sleep_state_code = None
self.sleep_state_hms = None
self.start_time = None


class IOSHealthAllWatchSleepLatestPlugin(interface.SQLitePlugin):
"""SQLite parser plugin for iOS Health Sleep Stages (iOS 17+)."""

NAME = "ios_health_all_watch_sleep_ios17"
DATA_FORMAT = "iOS Health Sleep Stages from healthdb_secure.sqlite (iOS 17+)"

REQUIRED_STRUCTURE = {
"samples": frozenset(["data_id", "start_date", "end_date"]),
"category_samples": frozenset(["data_id", "value"]),
}

QUERIES = [
(
(
"SELECT s.start_date AS start_date, "
"s.end_date AS end_date, "
"cs.value AS category_value "
"FROM samples s "
"JOIN category_samples cs ON s.data_id = cs.data_id"
),
"ParseSleepRow",
)
]

def _GetCocoaDateTime(self, qh, row, name):
"""Retrieves a Cocoa Time value from the row.

Args:
qh (int): hash of the query.
row (sqlite3.Row): row.
name (str): name of the value.

Returns:
dfdatetime.CocoaTime: Date and time value or None.
"""
ts = self._GetRowValue(qh, row, name)
if ts is None:
return None
return dfdatetime_cocoa_time.CocoaTime(timestamp=ts)

@staticmethod
def _SecondsToHMS(seconds_value):
"""Converts seconds to HH:MM:SS format.

Args:
seconds_value (int|float): total seconds.

Returns:
str: formatted string or None.
"""
try:
total = int(float(seconds_value))
except (TypeError, ValueError):
return None
hh = total // 3600
mm = (total % 3600) // 60
ss = total % 60
return f"{hh:02d}:{mm:02d}:{ss:02d}"

def ParseSleepRow(self, parser_mediator, query, row, **unused_kwargs):
"""Parses a sleep data row.

Args:
parser_mediator (ParserMediator): mediates interactions.
query (str): query that created the row.
row (sqlite3.Row): row.
"""
query_hash = hash(query)

raw_code = self._GetRowValue(query_hash, row, "category_value")
try:
code = int(raw_code) if raw_code is not None else None
except (TypeError, ValueError):
code = None

if code not in (2, 3, 4, 5):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is not transparent to the user or the reviewer. What is the reason for skipping these codes?

return

event_data = IOSHealthAllWatchSleepLatestEventData()

event_data.start_time = self._GetCocoaDateTime(query_hash, row, "start_date")
event_data.end_time = self._GetCocoaDateTime(query_hash, row, "end_date")
event_data.sleep_state_code = code

duration = None
if (
event_data.start_time
and event_data.end_time
and event_data.start_time.timestamp is not None
and event_data.end_time.timestamp is not None
):
duration = event_data.end_time.timestamp - event_data.start_time.timestamp

event_data.sleep_state_hms = self._SecondsToHMS(duration)

parser_mediator.ProduceEventData(event_data)


sqlite.SQLiteParser.RegisterPlugin(IOSHealthAllWatchSleepLatestPlugin)
Loading
Loading