-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathfixer.sh
executable file
·503 lines (411 loc) · 18.2 KB
/
fixer.sh
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
#!/bin/bash
# Copyright (c) 2024, crasowas.
#
# Use of this source code is governed by a MIT-style license
# that can be found in the LICENSE file or at
# https://opensource.org/licenses/MIT.
# Force replace the existing privacy manifest when the `-f` option is enabled
force=false
# Enable silent mode to disable build output when the `-s` option is enabled
silent=false
# Parse command-line options
while getopts ":fs" opt; do
case $opt in
f)
force=true
;;
s)
silent=true
;;
\?)
echo "Invalid option: -$OPTARG" >&2
exit 1
;;
esac
done
shift $((OPTIND - 1))
# Directory of the app generated after the build
app_dir="${TARGET_BUILD_DIR}/${WRAPPER_NAME}"
frameworks_dir="$app_dir/Frameworks"
# Check if the app exists
if [ ! -d "$app_dir" ] || [[ "$app_dir" != *.app ]]; then
echo "Unable to find the app: $app_dir"
exit 1
fi
# Absolute path of the script and the fixer root directory
script_path="$(realpath "$0")"
fixer_root_dir="$(dirname "$script_path")"
# Default template paths
templates_dir="$fixer_root_dir/Templates"
user_templates_dir="$fixer_root_dir/Templates/UserTemplates"
# Common privacy manifest template file names
readonly APP_TEMPLATE_FILE_NAME="AppTemplate.xcprivacy"
readonly FRAMEWORK_TEMPLATE_FILE_NAME="FrameworkTemplate.xcprivacy"
# Use user-defined app privacy manifest template if it exists, otherwise fallback to default
app_template_file="$user_templates_dir/$APP_TEMPLATE_FILE_NAME"
if [ ! -f "$app_template_file" ]; then
app_template_file="$templates_dir/$APP_TEMPLATE_FILE_NAME"
fi
# Use user-defined framework privacy manifest template if it exists, otherwise fallback to default
framework_template_file="$user_templates_dir/$FRAMEWORK_TEMPLATE_FILE_NAME"
if [ ! -f "$framework_template_file" ]; then
framework_template_file="$templates_dir/$FRAMEWORK_TEMPLATE_FILE_NAME"
fi
# Disable build output in silent mode
if [ "$silent" == false ]; then
# Script used to generate the privacy access report
report_script="$fixer_root_dir/Report/report.sh"
# An array to record template usage for generating the privacy access report
template_usage_records=()
# Build output directory
build_dir="$fixer_root_dir/Build/${PRODUCT_NAME}-${CONFIGURATION}_${MARKETING_VERSION}_${CURRENT_PROJECT_VERSION}_$(date +%Y%m%d%H%M%S)"
# Ensure the build directory exists
mkdir -p "$build_dir"
# Redirect both stdout and stderr to a log file while keeping console output
exec > >(tee "$build_dir/fix.log") 2>&1
fi
# File name of the privacy manifest
readonly PRIVACY_MANIFEST_FILE_NAME="PrivacyInfo.xcprivacy"
# Universal delimiter
readonly DELIMITER=":"
# Space escape symbol for handling space in path
readonly SPACE_ESCAPE="\u0020"
# Categories of required reason APIs
readonly API_CATEGORIES=(
"NSPrivacyAccessedAPICategoryFileTimestamp"
"NSPrivacyAccessedAPICategorySystemBootTime"
"NSPrivacyAccessedAPICategoryDiskSpace"
"NSPrivacyAccessedAPICategoryActiveKeyboards"
"NSPrivacyAccessedAPICategoryUserDefaults"
)
# Symbol of the required reason APIs and their categories
#
# See also:
# * https://developer.apple.com/documentation/bundleresources/privacy_manifest_files/describing_use_of_required_reason_api
# * https://github.com/Wooder/ios_17_required_reason_api_scanner/blob/main/required_reason_api_binary_scanner.sh
readonly API_SYMBOLS=(
# NSPrivacyAccessedAPICategoryFileTimestamp
"NSPrivacyAccessedAPICategoryFileTimestamp${DELIMITER}getattrlist"
"NSPrivacyAccessedAPICategoryFileTimestamp${DELIMITER}getattrlistbulk"
"NSPrivacyAccessedAPICategoryFileTimestamp${DELIMITER}fgetattrlist"
"NSPrivacyAccessedAPICategoryFileTimestamp${DELIMITER}stat"
"NSPrivacyAccessedAPICategoryFileTimestamp${DELIMITER}fstat"
"NSPrivacyAccessedAPICategoryFileTimestamp${DELIMITER}fstatat"
"NSPrivacyAccessedAPICategoryFileTimestamp${DELIMITER}lstat"
"NSPrivacyAccessedAPICategoryFileTimestamp${DELIMITER}getattrlistat"
"NSPrivacyAccessedAPICategoryFileTimestamp${DELIMITER}NSFileCreationDate"
"NSPrivacyAccessedAPICategoryFileTimestamp${DELIMITER}NSFileModificationDate"
"NSPrivacyAccessedAPICategoryFileTimestamp${DELIMITER}NSURLContentModificationDateKey"
"NSPrivacyAccessedAPICategoryFileTimestamp${DELIMITER}NSURLCreationDateKey"
# NSPrivacyAccessedAPICategorySystemBootTime
"NSPrivacyAccessedAPICategorySystemBootTime${DELIMITER}systemUptime"
"NSPrivacyAccessedAPICategorySystemBootTime${DELIMITER}mach_absolute_time"
# NSPrivacyAccessedAPICategoryDiskSpace
"NSPrivacyAccessedAPICategoryDiskSpace${DELIMITER}statfs"
"NSPrivacyAccessedAPICategoryDiskSpace${DELIMITER}statvfs"
"NSPrivacyAccessedAPICategoryDiskSpace${DELIMITER}fstatfs"
"NSPrivacyAccessedAPICategoryDiskSpace${DELIMITER}fstatvfs"
"NSPrivacyAccessedAPICategoryDiskSpace${DELIMITER}NSFileSystemFreeSize"
"NSPrivacyAccessedAPICategoryDiskSpace${DELIMITER}NSFileSystemSize"
"NSPrivacyAccessedAPICategoryDiskSpace${DELIMITER}NSURLVolumeAvailableCapacityKey"
"NSPrivacyAccessedAPICategoryDiskSpace${DELIMITER}NSURLVolumeAvailableCapacityForImportantUsageKey"
"NSPrivacyAccessedAPICategoryDiskSpace${DELIMITER}NSURLVolumeAvailableCapacityForOpportunisticUsageKey"
"NSPrivacyAccessedAPICategoryDiskSpace${DELIMITER}NSURLVolumeTotalCapacityKey"
# NSPrivacyAccessedAPICategoryActiveKeyboards
"NSPrivacyAccessedAPICategoryActiveKeyboards${DELIMITER}activeInputModes"
# NSPrivacyAccessedAPICategoryUserDefaults
"NSPrivacyAccessedAPICategoryUserDefaults${DELIMITER}NSUserDefaults"
)
# Print the elements of an array along with their indices
function print_array() {
local -a array=("$@")
for ((i=0; i<${#array[@]}; i++)); do
echo "[$i] $(decode_path "${array[i]}")"
done
}
# Split a string into substrings using a specified delimiter
function split_string_by_delimiter() {
local string="$1"
local -a substrings=()
IFS="$DELIMITER" read -ra substrings <<< "$string"
echo "${substrings[@]}"
}
# Encode a path string by replacing space with an escape character
function encode_path() {
echo "$1" | sed "s/ /$SPACE_ESCAPE/g"
}
# Decode a path string by replacing encoded character with space
function decode_path() {
echo "$1" | sed "s/$SPACE_ESCAPE/ /g"
}
function get_dependency_name() {
local dep_path="$1"
local dir_name="$(basename "$dep_path")"
# Remove `.app`, `.framework`, and `.xcframework` suffixes
local dep_name="${dir_name%.*}"
echo "$dep_name"
}
# Analyze the specified binary file for API symbols and their categories
function analyze_binary_file() {
local file_path="$1"
local -a results=()
# Check if the API symbol exists in the binary file using `nm` and `strings`
local nm_output=$(nm "$file_path" 2>/dev/null | xcrun swift-demangle)
local strings_output=$(strings "$file_path")
local combined_output="$nm_output"$'\n'"$strings_output"
for api_symbol in "${API_SYMBOLS[@]}"; do
local substrings=($(split_string_by_delimiter "$api_symbol"))
local category=${substrings[0]}
local api=${substrings[1]}
if echo "$combined_output" | grep -E "$api\$" >/dev/null; then
local index=-1
for ((i=0; i < ${#results[@]}; i++)); do
local result="${results[i]}"
local result_substrings=($(split_string_by_delimiter "$result"))
# If the category matches an existing result, update it
if [ "$category" == "${result_substrings[0]}" ]; then
index=i
results[i]="${result_substrings[0]}$DELIMITER${result_substrings[1]},$api$DELIMITER${result_substrings[2]}"
break
fi
done
# If no matching category found, add a new result
if [[ $index -eq -1 ]]; then
results+=("$category$DELIMITER$api$DELIMITER$(encode_path "$file_path")")
fi
fi
done
echo "${results[@]}"
}
# Analyze API usage in a binary file
function analyze_api_usage() {
local dir_path="$1"
local -a results=()
local dir_name="$(basename "$dir_path")"
local binary_name="${dir_name%.*}"
local binary_file="$dir_path/$binary_name"
if [ -f "$binary_file" ]; then
results+=($(analyze_binary_file "$binary_file"))
fi
echo "${results[@]}"
}
# Search for privacy manifest files in a directory
function search_privacy_manifest_files() {
local dir_path="$1"
local -a privacy_manifest_files=()
# Create a temporary file to store search results
local temp_file="$(mktemp)"
# Ensure the temporary file is deleted on script exit
trap "rm -f $temp_file" EXIT
# Find privacy manifest files within the specified directory and store the results in the temporary file
find "$dir_path" -type f -name "$PRIVACY_MANIFEST_FILE_NAME" -print0 2>/dev/null > "$temp_file"
while IFS= read -r -d '' file_path; do
privacy_manifest_files+=($(encode_path "$file_path"))
done < "$temp_file"
echo "${privacy_manifest_files[@]}"
}
function get_privacy_manifest_file() {
# If there are multiple privacy manifest files, return the one with the shortest path
local privacy_manifest_file="$(printf "%s\n" "$@" | awk '{print length, $0}' | sort -n | head -n1 | cut -d ' ' -f2-)"
echo "$(decode_path "$privacy_manifest_file")"
}
# Get unique categories from analysis results
function get_categories() {
local results=("$@")
local -a categories=()
for result in "${results[@]}"; do
local substrings=($(split_string_by_delimiter "$result"))
local category=${substrings[0]}
if [[ ! "${categories[@]}" =~ "$category" ]]; then
categories+=("$category")
fi
done
echo "${categories[@]}"
}
# Get template file for the specified app or framework
function get_template_file() {
local dir_path="$1"
local template_file=""
if [[ "$dir_path" == *.app ]]; then
template_file="$app_template_file"
else
# Give priority to the user-defined framework privacy manifest template
local dep_name="$(get_dependency_name "$dir_path")"
local dep_template_file="$user_templates_dir/${dep_name}.xcprivacy"
if [ -f "$dep_template_file" ]; then
template_file="$dep_template_file"
else
template_file="$framework_template_file"
fi
fi
echo "$template_file"
}
# Copy the template file to the privacy manifest location, overwriting if it exists
# If the privacy manifest file doesn't exist, copy it to the root directory of the app or framework by default
function copy_template_file() {
local dir_path="$1"
local template_file="$2"
local privacy_manifest_file="$3"
if [ -z "$privacy_manifest_file" ]; then
privacy_manifest_file="$dir_path/$PRIVACY_MANIFEST_FILE_NAME"
fi
cp "$template_file" "$privacy_manifest_file"
echo "$privacy_manifest_file"
}
# Check if the specified template file should be modified
#
# The following template files will be modified based on analysis:
# * Templates/AppTemplate.xcprivacy
# * Templates/FrameworkTemplate.xcprivacy
# * Templates/UserTemplates/FrameworkTemplate.xcprivacy
function is_template_modifiable() {
local template_file="$1"
local template_file_name="$(basename "$template_file")"
if [[ "$template_file" != "$user_templates_dir"* ]] || [ "$template_file_name" == "$FRAMEWORK_TEMPLATE_FILE_NAME" ]; then
return 0
else
return 1
fi
}
# Re-sign the target app or framework if code signing is enabled
function resign() {
if [ -n "${EXPANDED_CODE_SIGN_IDENTITY:-}" ] && [ "${CODE_SIGNING_REQUIRED:-}" != "NO" ] && [ "${CODE_SIGNING_ALLOWED}" != "NO" ]; then
echo "Re-signing $1 with Identity ${EXPANDED_CODE_SIGN_IDENTITY_NAME}"
/usr/bin/codesign --force --sign "${EXPANDED_CODE_SIGN_IDENTITY}" ${OTHER_CODE_SIGN_FLAGS:-} --preserve-metadata=identifier,entitlements "$1"
fi
}
# Fix the privacy manifest for the app or specified framework
# To accelerate the build, existing privacy manifests will be left unchanged unless the `-f` option is enabled
# After fixing, the app or framework will be automatically re-signed
function fix() {
local dir_path="$1"
local force_resign="$2"
local privacy_manifest_file=""
if [[ "$dir_path" == *.app ]]; then
# Per the documentation, the privacy manifest should be placed at the root of the app’s bundle
# Reference: https://developer.apple.com/documentation/bundleresources/adding-a-privacy-manifest-to-your-app-or-third-party-sdk#Add-a-privacy-manifest-to-your-app
privacy_manifest_file="$dir_path/$PRIVACY_MANIFEST_FILE_NAME"
else
# Per the documentation, the privacy manifest should be placed at the root of the framework’s bundle
# Some SDKs don’t follow the guideline, so we use a search-based approach for now
# Reference: https://developer.apple.com/documentation/bundleresources/adding-a-privacy-manifest-to-your-app-or-third-party-sdk#Add-a-privacy-manifest-to-your-framework
local privacy_manifest_files=($(search_privacy_manifest_files "$dir_path"))
privacy_manifest_file="$(get_privacy_manifest_file "${privacy_manifest_files[@]}")"
fi
# Check if the privacy manifest file exists
if [ -f "$privacy_manifest_file" ]; then
echo "💡 Found privacy manifest file: $privacy_manifest_file"
if [ "$force" == false ]; then
if [ "$force_resign" == true ]; then
resign "$dir_path"
fi
echo "✅ Privacy manifest file already exists, skipping fix."
return
fi
else
echo "⚠️ Missing privacy manifest file!"
fi
local results=($(analyze_api_usage "$dir_path"))
echo "API usage analysis result(s): ${#results[@]}"
print_array "${results[@]}"
local template_file="$(get_template_file "$dir_path")"
template_usage_records+=("$(basename "$dir_path")$DELIMITER$template_file")
privacy_manifest_file="$(copy_template_file "$dir_path" "$template_file" "$privacy_manifest_file")"
if is_template_modifiable "$template_file"; then
local categories=($(get_categories "${results[@]}"))
local remove_categories=()
# Check if categories is non-empty
if [[ ${#categories[@]} -gt 0 ]]; then
# Convert categories to a single space-separated string for easy matching
local categories_set=" ${categories[*]} "
# Iterate through each element in `API_CATEGORIES`
for element in "${API_CATEGORIES[@]}"; do
# If element is not found in `categories_set`, add it to `remove_categories`
if [[ ! $categories_set =~ " $element " ]]; then
remove_categories+=("$element")
fi
done
else
# If categories is empty, add all of `API_CATEGORIES` to `remove_categories`
remove_categories=("${API_CATEGORIES[@]}")
fi
# Remove extra spaces in the XML file to simplify node removal
xmllint --noblanks "$privacy_manifest_file" -o "$privacy_manifest_file"
# Build a sed command to remove all matching nodes at once
local sed_pattern=""
for category in "${remove_categories[@]}"; do
# Find the node for the current category
local remove_node="$(xmllint --xpath "//dict[string='$category']" "$privacy_manifest_file" 2>/dev/null || true)"
# If the node is found, escape special characters and append it to the sed pattern
if [[ -n "$remove_node" ]]; then
local escaped_node=$(echo "$remove_node" | sed 's/[\/&]/\\&/g')
sed_pattern+="s/$escaped_node//g;"
fi
done
# Apply the combined sed pattern to the file if it's not empty
if [[ -n "$sed_pattern" ]]; then
sed -i "" "$sed_pattern" "$privacy_manifest_file"
fi
# Reformat the XML file to restore spaces for readability
xmllint --format "$privacy_manifest_file" -o "$privacy_manifest_file"
fi
resign "$dir_path"
echo "✅ Privacy manifest file fixed: $privacy_manifest_file."
}
# Fix privacy manifests for all frameworks
function fix_frameworks() {
if ! [ -d "$frameworks_dir" ]; then
return
fi
echo "🛠️ Fixing Frameworks..."
for path in "$frameworks_dir"/*; do
if [ -d "$path" ]; then
local dep_name="$(get_dependency_name "$path")"
echo "Analyzing $dep_name ..."
fix "$path" false
echo ""
fi
done
}
# Fix the privacy manifest for the app
function fix_app() {
echo "🛠️ Fixing $(basename "$app_dir" .app) App..."
# Since the framework may have undergone fixes, the app must be forcefully re-signed
fix "$app_dir" true
echo ""
}
# Generate the privacy access report for the app
function generate_report() {
local original="$1"
if [ "$silent" == true ]; then
return
fi
local dir_name="$(basename "$app_dir")"
local app_name="${dir_name%.*}"
local report_name=""
# Adjust output names if the app is flagged as original
if [ "$original" == true ]; then
dir_name="${app_name}-original.app"
report_name="report-original.html"
else
dir_name="$app_name.app"
report_name="report.html"
fi
local target_app_dir="$build_dir/$dir_name"
local report_path="$build_dir/$report_name"
echo "Copy app to $target_app_dir"
rsync -a "$app_dir/" "$target_app_dir/"
# Generate the privacy access report using the script
sh "$report_script" "$target_app_dir" "$report_path" "${template_usage_records[@]}"
echo ""
}
start_time=$(date +%s)
generate_report true
fix_frameworks
fix_app
generate_report false
end_time=$(date +%s)
echo "🎉 All fixed! ⏰ $((end_time - start_time)) seconds"
echo "🌟 If you found this script helpful, please consider giving it a star on GitHub. Your support is appreciated. Thank you!"
echo "🔗 Homepage: https://github.com/crasowas/app_privacy_manifest_fixer"
echo "🐛 Report issues: https://github.com/crasowas/app_privacy_manifest_fixer/issues"