Skip to content

Commit 9755cef

Browse files
abrignoniclaude
andcommitted
Read newer iOS (26) Call History columns
Adds four ZCALLRECORD columns that newer iOS (26) stores carry, selected only when present so older CallHistory.storedata schemas keep parsing: Auto-Answered Reason and Communication Trust Score (both as stored), and Originating Device Name and Blocked By Extension Name. The old call_history.db path reports them as NULL. The existing twelve columns are unchanged. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 8215332 commit 9755cef

1 file changed

Lines changed: 35 additions & 10 deletions

File tree

‎scripts/artifacts/callHistory.py‎

Lines changed: 35 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,10 @@
44
"description": "Extract Call History",
55
"author": "@AlexisBrignoni - @JohnHyla",
66
"creation_date": "2020-04-30",
7-
"last_update_date": "2026-07-31",
7+
"last_update_date": "2026-09-23",
88
"requirements": "none",
99
"category": "Call History",
10-
"notes": 'Call type and disconnected-cause value mappings are community-established decodes reported as observed; unrecognized values are reported as stored. When both CallHistory.storedata and CallHistoryTemp.storedata hold records, rows from both are reported with a Source File path column and the report names both stores. On none of the nine registered corpora run did both stores hold records, so that path was exercised on a constructed tree with the same records under both names.',
10+
"notes": 'Call type and disconnected-cause value mappings are community-established decodes reported as observed; unrecognized values are reported as stored. When both CallHistory.storedata and CallHistoryTemp.storedata hold records, rows from both are reported with a Source File path column and the report names both stores. On none of the nine registered corpora run did both stores hold records, so that path was exercised on a constructed tree with the same records under both names. Newer iOS (26) adds columns to ZCALLRECORD that are read here when present and left blank on older stores that lack them: Auto-Answered Reason and Communication Trust Score are reported as stored, and Originating Device Name and Blocked By Extension Name are reported as stored. iOS 26 introduced call screening and hold assist, which auto-answer calls; the meaning of the Auto-Answered Reason integer is reported as stored and not otherwise interpreted. On a tested iOS 26.5 sample the auto-answered reason and communication trust score were set on every call, with one call carrying a non-zero auto-answered reason, while the originating device name and blocked-by-extension name columns were present in the schema but empty, so those two are code-present and unexercised on that sample.',
1111
"paths": (
1212
'*/mobile/Library/CallHistoryDB/CallHistory*',
1313
'*/mobile/Library/CallHistoryDB/call_history.db*'),
@@ -38,7 +38,12 @@
3838
# The Call Ending Timestamp provides an "at-a-glance" review of call lengths during analysis and review
3939
# Additional details published within "Maximizing iOS Call Log Timestamps and Call Duration Effectiveness: Will You Answer the Call?" at https://sqlmcgee.wordpress.com/2022/11/30/maximizing-ios-call-log-timestamps-and-call-duration-effectiveness-will-you-answer-the-call/
4040

41-
from scripts.ilapfuncs import artifact_processor, get_file_path, get_sqlite_db_records, convert_bytes_to_unit, convert_cocoa_core_data_ts_to_utc
41+
from scripts.ilapfuncs import artifact_processor, get_file_path, get_sqlite_db_records, convert_bytes_to_unit, convert_cocoa_core_data_ts_to_utc, does_column_exist_in_db
42+
43+
# Columns added to ZCALLRECORD in newer iOS (26). Selected only when present so
44+
# older CallHistory.storedata schemas keep parsing.
45+
_IOS26_CALL_COLUMNS = ('ZAUTOANSWEREDREASON', 'ZCOMMUNICATIONTRUSTSCORE',
46+
'ZORIGINATINGDEVICENAME', 'ZBLOCKEDBYEXTENSIONNAME')
4247

4348
@artifact_processor
4449
def callHistory(context):
@@ -89,6 +94,7 @@ def callHistory(context):
8994
END ZDISCONNECTED_CAUSE,
9095
upper(ZISO_COUNTRY_CODE),
9196
ZLOCATION
97+
__IOS26_COLUMNS__
9298
from ZCALLRECORD
9399
'''
94100

@@ -117,10 +123,24 @@ def callHistory(context):
117123
face_time_data,
118124
'N/A' as ZDISCONNECTED_CAUSE,
119125
country_code,
120-
'N/A' as ZLOCATION
126+
'N/A' as ZLOCATION,
127+
NULL as ZAUTOANSWEREDREASON,
128+
NULL as ZCOMMUNICATIONTRUSTSCORE,
129+
NULL as ZORIGINATINGDEVICENAME,
130+
NULL as ZBLOCKEDBYEXTENSIONNAME
121131
from call
122132
'''
123133

134+
# Build the newer-iOS column list, using NULL for any column a given schema
135+
# does not have, so the same query runs against old and new stores alike.
136+
schema_ref = db_path or temp_db_path
137+
extra_columns = ''
138+
if schema_ref:
139+
parts = [col if does_column_exist_in_db(schema_ref, 'ZCALLRECORD', col)
140+
else f'NULL as {col}' for col in _IOS26_CALL_COLUMNS]
141+
extra_columns = ', ' + ', '.join(parts)
142+
query = query.replace('__IOS26_COLUMNS__', extra_columns)
143+
124144
# NOTE: I think it could be changed for a better pattern, for the moment
125145
# I'm just list()-ing everything to keep the logic working.
126146
# maybe later on we can review it and improve the code quality.
@@ -151,10 +171,11 @@ def callHistory(context):
151171

152172
facetime_data = convert_bytes_to_unit(record[8])
153173

154-
record_data = [starting_time, ending_time, record[2], record[3], record[4], an, record[6],
155-
record[7], facetime_data, record[9], record[10], record[11]]
174+
record_data = [starting_time, ending_time, record[2], record[3], record[4], an, record[6],
175+
record[7], facetime_data, record[9], record[10], record[11],
176+
record[12], record[13], record[14], record[15]]
156177
if records_in_both_db:
157-
record_data.append(record[12])
178+
record_data.append(record[16])
158179
data_list.append(tuple(record_data))
159180

160181
headers = [
@@ -167,9 +188,13 @@ def callHistory(context):
167188
'Answered',
168189
'Call Duration',
169190
'FaceTime Data',
170-
'Disconnected Cause',
171-
'ISO Country Code',
172-
'Location'
191+
'Disconnected Cause',
192+
'ISO Country Code',
193+
'Location',
194+
'Auto-Answered Reason (as stored)',
195+
'Communication Trust Score (as stored)',
196+
'Originating Device Name',
197+
'Blocked By Extension Name'
173198
]
174199
if records_in_both_db:
175200
headers.append('Source File path')

0 commit comments

Comments
 (0)