├── .gitignore ├── CHANGELOG.md ├── Common ├── constants.sh └── utils.sh ├── Helper ├── xcode_install_helper.rb └── xcode_uninstall_helper.rb ├── LICENSE ├── README.md ├── README.zh-CN.md ├── Report ├── report-template.html └── report.sh ├── Templates ├── AppTemplate.xcprivacy ├── FrameworkTemplate.xcprivacy └── UserTemplates │ └── .gitkeep ├── VERSION ├── clean.sh ├── fixer.sh ├── install.sh ├── uninstall.sh └── upgrade.sh /.gitignore: -------------------------------------------------------------------------------- 1 | # macOS 2 | .DS_Store 3 | 4 | # Build 5 | /Build/ 6 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 1.4.1 2 | - Fix macOS app re-signing issue. 3 | - Automatically enable Hardened Runtime in macOS codesign. 4 | - Add clean script. 5 | 6 | ## 1.4.0 7 | - Support for macOS app ([#9](https://github.com/crasowas/app_privacy_manifest_fixer/issues/9)). 8 | 9 | ## 1.3.11 10 | - Fix install issue by skipping `PBXAggregateTarget` ([#4](https://github.com/crasowas/app_privacy_manifest_fixer/issues/4)). 11 | 12 | ## 1.3.10 13 | - Fix app re-signing issue. 14 | - Enhance Build Phases script robustness. 15 | 16 | ## 1.3.9 17 | - Add log file output. 18 | 19 | ## 1.3.8 20 | - Add version info to privacy access report. 21 | - Remove empty tables from privacy access report. 22 | 23 | ## 1.3.7 24 | - Enhance API symbols analysis with strings tool. 25 | - Improve performance of API usage analysis. 26 | 27 | ## 1.3.5 28 | - Fix issue with inaccurate privacy manifest search. 29 | - Disable dependency analysis to force the script to run on every build. 30 | - Add placeholder for privacy access report. 31 | - Update build output directory naming convention. 32 | - Add examples for privacy access report. 33 | 34 | ## 1.3.0 35 | - Add privacy access report generation. 36 | 37 | ## 1.2.3 38 | - Fix issue with relative path parameter. 39 | - Add support for all application targets. 40 | 41 | ## 1.2.1 42 | - Fix backup issue with empty user templates directory. 43 | 44 | ## 1.2.0 45 | - Add uninstall script. 46 | 47 | ## 1.1.2 48 | - Remove `Templates/.gitignore` to track `UserTemplates`. 49 | - Fix incorrect use of `App.xcprivacy` template in `App.framework`. 50 | 51 | ## 1.1.0 52 | - Add logs for latest release fetch failure. 53 | - Fix issue with converting published time to local time. 54 | - Disable showing environment variables in the build log. 55 | - Add `--install-builds-only` command line option. 56 | 57 | ## 1.0.0 58 | - Initial version. -------------------------------------------------------------------------------- /Common/constants.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Copyright (c) 2025, crasowas. 4 | # 5 | # Use of this source code is governed by a MIT-style license 6 | # that can be found in the LICENSE file or at 7 | # https://opensource.org/licenses/MIT. 8 | 9 | set -e 10 | 11 | # Prevent duplicate loading 12 | if [ -n "$CONSTANTS_SH_LOADED" ]; then 13 | return 14 | fi 15 | 16 | readonly CONSTANTS_SH_LOADED=1 17 | 18 | # File name of the privacy manifest 19 | readonly PRIVACY_MANIFEST_FILE_NAME="PrivacyInfo.xcprivacy" 20 | 21 | # Common privacy manifest template file names 22 | readonly APP_TEMPLATE_FILE_NAME="AppTemplate.xcprivacy" 23 | readonly FRAMEWORK_TEMPLATE_FILE_NAME="FrameworkTemplate.xcprivacy" 24 | 25 | # Universal delimiter 26 | readonly DELIMITER=":" 27 | 28 | # Space escape symbol for handling space in path 29 | readonly SPACE_ESCAPE="\u0020" 30 | 31 | # Default value when the version cannot be retrieved 32 | readonly UNKNOWN_VERSION="unknown" 33 | 34 | # Categories of required reason APIs 35 | readonly API_CATEGORIES=( 36 | "NSPrivacyAccessedAPICategoryFileTimestamp" 37 | "NSPrivacyAccessedAPICategorySystemBootTime" 38 | "NSPrivacyAccessedAPICategoryDiskSpace" 39 | "NSPrivacyAccessedAPICategoryActiveKeyboards" 40 | "NSPrivacyAccessedAPICategoryUserDefaults" 41 | ) 42 | 43 | # Symbol of the required reason APIs and their categories 44 | # 45 | # See also: 46 | # * https://developer.apple.com/documentation/bundleresources/describing-use-of-required-reason-api 47 | # * https://github.com/Wooder/ios_17_required_reason_api_scanner/blob/main/required_reason_api_binary_scanner.sh 48 | readonly API_SYMBOLS=( 49 | # NSPrivacyAccessedAPICategoryFileTimestamp 50 | "NSPrivacyAccessedAPICategoryFileTimestamp${DELIMITER}getattrlist" 51 | "NSPrivacyAccessedAPICategoryFileTimestamp${DELIMITER}getattrlistbulk" 52 | "NSPrivacyAccessedAPICategoryFileTimestamp${DELIMITER}fgetattrlist" 53 | "NSPrivacyAccessedAPICategoryFileTimestamp${DELIMITER}stat" 54 | "NSPrivacyAccessedAPICategoryFileTimestamp${DELIMITER}fstat" 55 | "NSPrivacyAccessedAPICategoryFileTimestamp${DELIMITER}fstatat" 56 | "NSPrivacyAccessedAPICategoryFileTimestamp${DELIMITER}lstat" 57 | "NSPrivacyAccessedAPICategoryFileTimestamp${DELIMITER}getattrlistat" 58 | "NSPrivacyAccessedAPICategoryFileTimestamp${DELIMITER}NSFileCreationDate" 59 | "NSPrivacyAccessedAPICategoryFileTimestamp${DELIMITER}NSFileModificationDate" 60 | "NSPrivacyAccessedAPICategoryFileTimestamp${DELIMITER}NSURLContentModificationDateKey" 61 | "NSPrivacyAccessedAPICategoryFileTimestamp${DELIMITER}NSURLCreationDateKey" 62 | # NSPrivacyAccessedAPICategorySystemBootTime 63 | "NSPrivacyAccessedAPICategorySystemBootTime${DELIMITER}systemUptime" 64 | "NSPrivacyAccessedAPICategorySystemBootTime${DELIMITER}mach_absolute_time" 65 | # NSPrivacyAccessedAPICategoryDiskSpace 66 | "NSPrivacyAccessedAPICategoryDiskSpace${DELIMITER}statfs" 67 | "NSPrivacyAccessedAPICategoryDiskSpace${DELIMITER}statvfs" 68 | "NSPrivacyAccessedAPICategoryDiskSpace${DELIMITER}fstatfs" 69 | "NSPrivacyAccessedAPICategoryDiskSpace${DELIMITER}fstatvfs" 70 | "NSPrivacyAccessedAPICategoryDiskSpace${DELIMITER}NSFileSystemFreeSize" 71 | "NSPrivacyAccessedAPICategoryDiskSpace${DELIMITER}NSFileSystemSize" 72 | "NSPrivacyAccessedAPICategoryDiskSpace${DELIMITER}NSURLVolumeAvailableCapacityKey" 73 | "NSPrivacyAccessedAPICategoryDiskSpace${DELIMITER}NSURLVolumeAvailableCapacityForImportantUsageKey" 74 | "NSPrivacyAccessedAPICategoryDiskSpace${DELIMITER}NSURLVolumeAvailableCapacityForOpportunisticUsageKey" 75 | "NSPrivacyAccessedAPICategoryDiskSpace${DELIMITER}NSURLVolumeTotalCapacityKey" 76 | # NSPrivacyAccessedAPICategoryActiveKeyboards 77 | "NSPrivacyAccessedAPICategoryActiveKeyboards${DELIMITER}activeInputModes" 78 | # NSPrivacyAccessedAPICategoryUserDefaults 79 | "NSPrivacyAccessedAPICategoryUserDefaults${DELIMITER}NSUserDefaults" 80 | ) 81 | -------------------------------------------------------------------------------- /Common/utils.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Copyright (c) 2025, crasowas. 4 | # 5 | # Use of this source code is governed by a MIT-style license 6 | # that can be found in the LICENSE file or at 7 | # https://opensource.org/licenses/MIT. 8 | 9 | set -e 10 | 11 | # Prevent duplicate loading 12 | if [ -n "$UTILS_SH_LOADED" ]; then 13 | return 14 | fi 15 | 16 | readonly UTILS_SH_LOADED=1 17 | 18 | # Absolute path of the script and the tool's root directory 19 | script_path="$(realpath "${BASH_SOURCE[0]}")" 20 | tool_root_path="$(dirname "$(dirname "$script_path")")" 21 | 22 | # Load common constants 23 | source "$tool_root_path/Common/constants.sh" 24 | 25 | # Print the elements of an array along with their indices 26 | function print_array() { 27 | local -a array=("$@") 28 | 29 | for ((i=0; i<${#array[@]}; i++)); do 30 | echo "[$i] $(decode_path "${array[i]}")" 31 | done 32 | } 33 | 34 | # Split a string into substrings using a specified delimiter 35 | function split_string_by_delimiter() { 36 | local string="$1" 37 | local -a substrings=() 38 | 39 | IFS="$DELIMITER" read -ra substrings <<< "$string" 40 | 41 | echo "${substrings[@]}" 42 | } 43 | 44 | # Encode a path string by replacing space with an escape character 45 | function encode_path() { 46 | echo "$1" | sed "s/ /$SPACE_ESCAPE/g" 47 | } 48 | 49 | # Decode a path string by replacing encoded character with space 50 | function decode_path() { 51 | echo "$1" | sed "s/$SPACE_ESCAPE/ /g" 52 | } 53 | 54 | # Get the dependency name by removing common suffixes 55 | function get_dependency_name() { 56 | local path="$1" 57 | 58 | local dir_name="$(basename "$path")" 59 | # Remove `.app`, `.framework`, and `.xcframework` suffixes 60 | local dep_name="${dir_name%.*}" 61 | 62 | echo "$dep_name" 63 | } 64 | 65 | # Get the executable name from the specified `Info.plist` file 66 | function get_plist_executable() { 67 | local plist_file="$1" 68 | 69 | if [ ! -f "$plist_file" ]; then 70 | echo "" 71 | else 72 | /usr/libexec/PlistBuddy -c "Print :CFBundleExecutable" "$plist_file" 2>/dev/null || echo "" 73 | fi 74 | } 75 | 76 | # Get the version from the specified `Info.plist` file 77 | function get_plist_version() { 78 | local plist_file="$1" 79 | 80 | if [ ! -f "$plist_file" ]; then 81 | echo "$UNKNOWN_VERSION" 82 | else 83 | /usr/libexec/PlistBuddy -c "Print :CFBundleShortVersionString" "$plist_file" 2>/dev/null || echo "$UNKNOWN_VERSION" 84 | fi 85 | } 86 | 87 | # Get the path of the specified framework version 88 | function get_framework_path() { 89 | local path="$1" 90 | local version_path="$2" 91 | 92 | if [ -z "$version_path" ]; then 93 | echo "$path" 94 | else 95 | echo "$path/$version_path" 96 | fi 97 | } 98 | 99 | # Search for privacy manifest files in the specified directory 100 | function search_privacy_manifest_files() { 101 | local path="$1" 102 | local -a privacy_manifest_files=() 103 | 104 | # Create a temporary file to store search results 105 | local temp_file="$(mktemp)" 106 | 107 | # Ensure the temporary file is deleted on script exit 108 | trap "rm -f $temp_file" EXIT 109 | 110 | # Find privacy manifest files within the specified directory and store the results in the temporary file 111 | find "$path" -type f -name "$PRIVACY_MANIFEST_FILE_NAME" -print0 2>/dev/null > "$temp_file" 112 | 113 | while IFS= read -r -d '' file; do 114 | privacy_manifest_files+=($(encode_path "$file")) 115 | done < "$temp_file" 116 | 117 | echo "${privacy_manifest_files[@]}" 118 | } 119 | 120 | # Get the privacy manifest file with the shortest path 121 | function get_privacy_manifest_file() { 122 | local privacy_manifest_file="$(printf "%s\n" "$@" | awk '{print length, $0}' | sort -n | head -n1 | cut -d ' ' -f2-)" 123 | 124 | echo "$(decode_path "$privacy_manifest_file")" 125 | } 126 | -------------------------------------------------------------------------------- /Helper/xcode_install_helper.rb: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2024, crasowas. 2 | # 3 | # Use of this source code is governed by a MIT-style license 4 | # that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | require 'xcodeproj' 8 | 9 | RUN_SCRIPT_PHASE_NAME = 'Fix Privacy Manifest' 10 | 11 | if ARGV.length < 2 12 | puts "Usage: ruby xcode_install_helper.rb [install_builds_only (true|false)]" 13 | exit 1 14 | end 15 | 16 | project_path = ARGV[0] 17 | run_script_content = ARGV[1] 18 | install_builds_only = ARGV[2] == 'true' 19 | 20 | # Find the first .xcodeproj file in the project directory 21 | xcodeproj_path = Dir.glob(File.join(project_path, "*.xcodeproj")).first 22 | 23 | # Validate the .xcodeproj file existence 24 | unless xcodeproj_path 25 | puts "Error: No .xcodeproj file found in the specified directory." 26 | exit 1 27 | end 28 | 29 | # Open the Xcode project file 30 | begin 31 | project = Xcodeproj::Project.open(xcodeproj_path) 32 | rescue StandardError => e 33 | puts "Error: Unable to open the project file - #{e.message}" 34 | exit 1 35 | end 36 | 37 | # Process all targets in the project 38 | project.targets.each do |target| 39 | # Skip PBXAggregateTarget 40 | if target.is_a?(Xcodeproj::Project::Object::PBXAggregateTarget) 41 | puts "Skipping aggregate target: #{target.name}." 42 | next 43 | end 44 | 45 | # Check if the target is a native application target 46 | if target.product_type == 'com.apple.product-type.application' 47 | puts "Processing target: #{target.name}..." 48 | 49 | # Check for an existing Run Script phase with the specified name 50 | existing_phase = target.shell_script_build_phases.find { |phase| phase.name == RUN_SCRIPT_PHASE_NAME } 51 | 52 | # Remove the existing Run Script phase if found 53 | if existing_phase 54 | puts " - Removing existing Run Script." 55 | target.build_phases.delete(existing_phase) 56 | end 57 | 58 | # Add the new Run Script phase at the end 59 | puts " - Adding new Run Script." 60 | new_phase = target.new_shell_script_build_phase(RUN_SCRIPT_PHASE_NAME) 61 | new_phase.shell_script = run_script_content 62 | # Disable showing environment variables in the build log 63 | new_phase.show_env_vars_in_log = '0' 64 | # Run only for deployment post-processing if install_builds_only is true 65 | new_phase.run_only_for_deployment_postprocessing = install_builds_only ? '1' : '0' 66 | # Disable dependency analysis to force the script to run on every build, unless restricted to deployment builds by post-processing setting 67 | new_phase.always_out_of_date = '1' 68 | else 69 | puts "Skipping non-application target: #{target.name}." 70 | end 71 | end 72 | 73 | # Save the project file 74 | begin 75 | project.save 76 | puts "Successfully added the Run Script phase: '#{RUN_SCRIPT_PHASE_NAME}'." 77 | rescue StandardError => e 78 | puts "Error: Unable to save the project file - #{e.message}" 79 | exit 1 80 | end 81 | -------------------------------------------------------------------------------- /Helper/xcode_uninstall_helper.rb: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2024, crasowas. 2 | # 3 | # Use of this source code is governed by a MIT-style license 4 | # that can be found in the LICENSE file or at 5 | # https://opensource.org/licenses/MIT. 6 | 7 | require 'xcodeproj' 8 | 9 | RUN_SCRIPT_PHASE_NAME = 'Fix Privacy Manifest' 10 | 11 | if ARGV.length < 1 12 | puts "Usage: ruby xcode_uninstall_helper.rb " 13 | exit 1 14 | end 15 | 16 | project_path = ARGV[0] 17 | 18 | # Find the first .xcodeproj file in the project directory 19 | xcodeproj_path = Dir.glob(File.join(project_path, "*.xcodeproj")).first 20 | 21 | # Validate the .xcodeproj file existence 22 | unless xcodeproj_path 23 | puts "Error: No .xcodeproj file found in the specified directory." 24 | exit 1 25 | end 26 | 27 | # Open the Xcode project file 28 | begin 29 | project = Xcodeproj::Project.open(xcodeproj_path) 30 | rescue StandardError => e 31 | puts "Error: Unable to open the project file - #{e.message}" 32 | exit 1 33 | end 34 | 35 | # Process all targets in the project 36 | project.targets.each do |target| 37 | # Check if the target is an application target 38 | if target.product_type == 'com.apple.product-type.application' 39 | puts "Processing target: #{target.name}..." 40 | 41 | # Check for an existing Run Script phase with the specified name 42 | existing_phase = target.shell_script_build_phases.find { |phase| phase.name == RUN_SCRIPT_PHASE_NAME } 43 | 44 | # Remove the existing Run Script phase if found 45 | if existing_phase 46 | puts " - Removing existing Run Script." 47 | target.build_phases.delete(existing_phase) 48 | else 49 | puts " - No existing Run Script found." 50 | end 51 | else 52 | puts "Skipping non-application target: #{target.name}." 53 | end 54 | end 55 | 56 | # Save the project file 57 | begin 58 | project.save 59 | puts "Successfully removed the Run Script phase: '#{RUN_SCRIPT_PHASE_NAME}'." 60 | rescue StandardError => e 61 | puts "Error: Unable to save the project file - #{e.message}" 62 | exit 1 63 | end 64 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 crasowas 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # App Privacy Manifest Fixer 2 | 3 | [![Latest Version](https://img.shields.io/github/v/release/crasowas/app_privacy_manifest_fixer?logo=github)](https://github.com/crasowas/app_privacy_manifest_fixer/releases/latest) 4 | ![Supported Platforms](https://img.shields.io/badge/Supported%20Platforms-iOS%20%7C%20macOS-brightgreen) 5 | [![License](https://img.shields.io/badge/License-MIT-green.svg)](https://opensource.org/licenses/MIT) 6 | 7 | **English | [简体中文](./README.zh-CN.md)** 8 | 9 | This tool is an automation solution based on Shell scripts, designed to analyze and fix the privacy manifest of iOS/macOS apps to ensure compliance with App Store requirements. It leverages the [App Store Privacy Manifest Analyzer](https://github.com/crasowas/app_store_required_privacy_manifest_analyser) to analyze API usage within the app and its dependencies, and generate or fix the `PrivacyInfo.xcprivacy` file. 10 | 11 | ## ✨ Features 12 | 13 | - **Non-Intrusive Integration**: No need to modify the source code or adjust the project structure. 14 | - **Fast Installation and Uninstallation**: Quickly install or uninstall the tool with a single command. 15 | - **Automatic Analysis and Fixes**: Automatically analyzes API usage and fixes privacy manifest issues during the project build. 16 | - **Flexible Template Customization**: Supports custom privacy manifest templates for apps and frameworks, accommodating various usage scenarios. 17 | - **Privacy Access Report**: Automatically generates a report displaying the `NSPrivacyAccessedAPITypes` declarations for the app and SDKs. 18 | - **Effortless Version Upgrades**: Provides an upgrade script for quick updates to the latest version. 19 | 20 | ## 📥 Installation 21 | 22 | ### Download the Tool 23 | 24 | 1. Download the [latest release](https://github.com/crasowas/app_privacy_manifest_fixer/releases/latest). 25 | 2. Extract the downloaded file: 26 | - The extracted directory is usually named `app_privacy_manifest_fixer-xxx` (where `xxx` is the version number). 27 | - It is recommended to rename it to `app_privacy_manifest_fixer` or use the full directory name in subsequent paths. 28 | - **It is advised to move the directory to your iOS/macOS project to avoid path-related issues on different devices, and to easily customize the privacy manifest template for each project**. 29 | 30 | ### ⚡ Automatic Installation (Recommended) 31 | 32 | 1. **Navigate to the tool's directory**: 33 | 34 | ```shell 35 | cd /path/to/app_privacy_manifest_fixer 36 | ``` 37 | 38 | 2. **Run the installation script**: 39 | 40 | ```shell 41 | sh install.sh 42 | ``` 43 | 44 | - For Flutter projects, `project_path` should be the path to the `ios/macos` directory within the Flutter project. 45 | - If the installation command is run again, the tool will first remove any existing installation (if present). To modify command-line options, simply rerun the installation command without the need to uninstall first. 46 | 47 | ### Manual Installation 48 | 49 | If you prefer not to use the installation script, you can manually add the `Fix Privacy Manifest` task to the Xcode **Build Phases**. Follow these steps: 50 | 51 | #### 1. Add the Script in Xcode 52 | 53 | - Open your iOS/macOS project in Xcode, go to the **TARGETS** tab, and select your app target. 54 | - Navigate to **Build Phases**, click the **+** button, and select **New Run Script Phase**. 55 | - Rename the newly created **Run Script** to `Fix Privacy Manifest`. 56 | - In the Shell script box, add the following code: 57 | 58 | ```shell 59 | # Use relative path (recommended): if `app_privacy_manifest_fixer` is within the project directory 60 | "$PROJECT_DIR/path/to/app_privacy_manifest_fixer/fixer.sh" 61 | 62 | # Use absolute path: if `app_privacy_manifest_fixer` is outside the project directory 63 | # "/absolute/path/to/app_privacy_manifest_fixer/fixer.sh" 64 | ``` 65 | 66 | **Modify `path/to` or `absolute/path/to` as needed, and ensure the paths are correct. Remove or comment out the unused lines accordingly.** 67 | 68 | #### 2. Adjust the Script Execution Order 69 | 70 | **Move this script after all other Build Phases to ensure the privacy manifest is fixed after all resource copying and build tasks are completed**. 71 | 72 | ### Build Phases Screenshot 73 | 74 | Below is a screenshot of the Xcode Build Phases configuration after successful automatic/manual installation (with no command-line options enabled): 75 | 76 | ![Build Phases Screenshot](https://img.crasowas.dev/app_privacy_manifest_fixer/20250225011407.png) 77 | 78 | ## 🚀 Getting Started 79 | 80 | After installation, the tool will automatically run with each project build, and the resulting application bundle will include the fixes. 81 | 82 | If the `--install-builds-only` command-line option is enabled during installation, the tool will only run during the installation of the build. 83 | 84 | ### Xcode Build Log Screenshot 85 | 86 | Below is a screenshot of the log output from the tool during the project build (by default, it will be saved to the `app_privacy_manifest_fixer/Build` directory, unless the `-s` command-line option is enabled): 87 | 88 | ![Xcode Build Log Screenshot](https://img.crasowas.dev/app_privacy_manifest_fixer/20250225011551.png) 89 | 90 | ## 📖 Usage 91 | 92 | ### Command Line Options 93 | 94 | - **Force overwrite existing privacy manifest (Not recommended)**: 95 | 96 | ```shell 97 | sh install.sh -f 98 | ``` 99 | 100 | Enabling the `-f` option will force the tool to generate a new privacy manifest based on the API usage analysis and privacy manifest template, overwriting the existing privacy manifest. By default (without `-f`), the tool only fixes missing privacy manifests. 101 | 102 | - **Silent mode**: 103 | 104 | ```shell 105 | sh install.sh -s 106 | ``` 107 | 108 | Enabling the `-s` option disables output during the fix process. The tool will no longer copy the generated `*.app`, automatically generate the privacy access report, or output the fix logs. By default (without `-s`), these outputs are stored in the `app_privacy_manifest_fixer/Build` directory. 109 | 110 | - **Run only during installation builds (Recommended)**: 111 | 112 | ```shell 113 | sh install.sh --install-builds-only 114 | ``` 115 | 116 | Enabling the `--install-builds-only` option makes the tool run only during installation builds (such as the **Archive** operation), optimizing build performance for daily development. If you manually installed, this option is ineffective, and you need to manually check the **"For install builds only"** option. 117 | 118 | **Note**: If the iOS/macOS project is built in a development environment (where the generated app contains `*.debug.dylib` files), the tool's API usage analysis results may be inaccurate. 119 | 120 | ### Upgrade the Tool 121 | 122 | To update to the latest version, run the following command: 123 | 124 | ```shell 125 | sh upgrade.sh 126 | ``` 127 | 128 | ### Uninstall the Tool 129 | 130 | To quickly uninstall the tool, run the following command: 131 | 132 | ```shell 133 | sh uninstall.sh 134 | ``` 135 | 136 | ### Clean the Tool-Generated Files 137 | 138 | To remove files generated by the tool, run the following command: 139 | 140 | ```shell 141 | sh clean.sh 142 | ``` 143 | 144 | ## 🔥 Privacy Manifest Templates 145 | 146 | The privacy manifest templates are stored in the [`Templates`](https://github.com/crasowas/app_privacy_manifest_fixer/tree/main/Templates) directory, with the default templates already included in the root directory. 147 | 148 | **How can you customize the privacy manifests for apps or SDKs? Simply use [custom templates](#custom-templates)!** 149 | 150 | ### Template Types 151 | 152 | The templates are categorized as follows: 153 | - **AppTemplate.xcprivacy**: A privacy manifest template for the app. 154 | - **FrameworkTemplate.xcprivacy**: A generic privacy manifest template for frameworks. 155 | - **FrameworkName.xcprivacy**: A privacy manifest template for a specific framework, available only in the `Templates/UserTemplates` directory. 156 | 157 | ### Template Priority 158 | 159 | For an app, the priority of privacy manifest templates is as follows: 160 | - `Templates/UserTemplates/AppTemplate.xcprivacy` > `Templates/AppTemplate.xcprivacy` 161 | 162 | For a specific framework, the priority of privacy manifest templates is as follows: 163 | - `Templates/UserTemplates/FrameworkName.xcprivacy` > `Templates/UserTemplates/FrameworkTemplate.xcprivacy` > `Templates/FrameworkTemplate.xcprivacy` 164 | 165 | ### Default Templates 166 | 167 | The default templates are located in the `Templates` root directory and currently include the following templates: 168 | - `Templates/AppTemplate.xcprivacy` 169 | - `Templates/FrameworkTemplate.xcprivacy` 170 | 171 | These templates will be modified based on the API usage analysis results, especially the `NSPrivacyAccessedAPIType` entries, to generate new privacy manifests for fixes, ensuring compliance with App Store requirements. 172 | 173 | **If adjustments to the privacy manifest template are needed, such as in the following scenarios, avoid directly modifying the default templates. Instead, use a custom template. If a custom template with the same name exists, it will take precedence over the default template for fixes.** 174 | - Generating a non-compliant privacy manifest due to inaccurate API usage analysis. 175 | - Modifying the reason declared in the template. 176 | - Adding declarations for collected data. 177 | 178 | The privacy access API categories and their associated declared reasons in `AppTemplate.xcprivacy` are listed below: 179 | 180 | | [NSPrivacyAccessedAPIType](https://developer.apple.com/documentation/bundleresources/app-privacy-configuration/nsprivacyaccessedapitypes/nsprivacyaccessedapitype) | [NSPrivacyAccessedAPITypeReasons](https://developer.apple.com/documentation/bundleresources/app-privacy-configuration/nsprivacyaccessedapitypes/nsprivacyaccessedapitypereasons) | 181 | |--------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 182 | | NSPrivacyAccessedAPICategoryFileTimestamp | C617.1: Inside app or group container | 183 | | NSPrivacyAccessedAPICategorySystemBootTime | 35F9.1: Measure time on-device | 184 | | NSPrivacyAccessedAPICategoryDiskSpace | E174.1: Write or delete file on-device | 185 | | NSPrivacyAccessedAPICategoryActiveKeyboards | 54BD.1: Customize UI on-device | 186 | | NSPrivacyAccessedAPICategoryUserDefaults | CA92.1: Access info from same app | 187 | 188 | The privacy access API categories and their associated declared reasons in `FrameworkTemplate.xcprivacy` are listed below: 189 | 190 | | [NSPrivacyAccessedAPIType](https://developer.apple.com/documentation/bundleresources/app-privacy-configuration/nsprivacyaccessedapitypes/nsprivacyaccessedapitype) | [NSPrivacyAccessedAPITypeReasons](https://developer.apple.com/documentation/bundleresources/app-privacy-configuration/nsprivacyaccessedapitypes/nsprivacyaccessedapitypereasons) | 191 | |--------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 192 | | NSPrivacyAccessedAPICategoryFileTimestamp | 0A2A.1: 3rd-party SDK wrapper on-device | 193 | | NSPrivacyAccessedAPICategorySystemBootTime | 35F9.1: Measure time on-device | 194 | | NSPrivacyAccessedAPICategoryDiskSpace | E174.1: Write or delete file on-device | 195 | | NSPrivacyAccessedAPICategoryActiveKeyboards | 54BD.1: Customize UI on-device | 196 | | NSPrivacyAccessedAPICategoryUserDefaults | C56D.1: 3rd-party SDK wrapper on-device | 197 | 198 | ### Custom Templates 199 | 200 | To create custom templates, place them in the `Templates/UserTemplates` directory with the following structure: 201 | - `Templates/UserTemplates/AppTemplate.xcprivacy` 202 | - `Templates/UserTemplates/FrameworkTemplate.xcprivacy` 203 | - `Templates/UserTemplates/FrameworkName.xcprivacy` 204 | 205 | Among these templates, only `FrameworkTemplate.xcprivacy` will be modified based on the API usage analysis results to adjust the `NSPrivacyAccessedAPIType` entries, thereby generating a new privacy manifest for framework fixes. The other templates will remain unchanged and will be directly used for fixes. 206 | 207 | **Important Notes:** 208 | - The template for a specific framework must follow the naming convention `FrameworkName.xcprivacy`, where `FrameworkName` should match the name of the framework. For example, the template for `Flutter.framework` should be named `Flutter.xcprivacy`. 209 | - For macOS frameworks, the naming convention should be `FrameworkName.Version.xcprivacy`, where the version name is added to distinguish different versions. For a single version macOS framework, the `Version` is typically `A`. 210 | - The name of an SDK may not exactly match the name of the framework. To determine the correct framework name, check the `Frameworks` directory in the application bundle after building the project. 211 | 212 | ## 📑 Privacy Access Report 213 | 214 | By default, the tool automatically generates privacy access reports for both the original and fixed versions of the app during each project build, and stores the reports in the `app_privacy_manifest_fixer/Build` directory. 215 | 216 | If you need to manually generate a privacy access report for a specific app, run the following command: 217 | 218 | ```shell 219 | sh Report/report.sh 220 | # : Path to the app (e.g., /path/to/App.app) 221 | # : Path to save the report file (e.g., /path/to/report.html) 222 | ``` 223 | 224 | **Note**: The report generated by the tool currently only includes the privacy access section (`NSPrivacyAccessedAPITypes`). To view the data collection section (`NSPrivacyCollectedDataTypes`), please use Xcode to generate the `PrivacyReport`. 225 | 226 | ### Sample Report Screenshots 227 | 228 | | Original App Report (report-original.html) | Fixed App Report (report.html) | 229 | |------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------| 230 | | ![Original App Report](https://img.crasowas.dev/app_privacy_manifest_fixer/20241218230746.png) | ![Fixed App Report](https://img.crasowas.dev/app_privacy_manifest_fixer/20241218230822.png) | 231 | 232 | ## 💡 Important Considerations 233 | 234 | - If the latest version of the SDK supports a privacy manifest, please upgrade as soon as possible to avoid unnecessary risks. 235 | - This tool is a temporary solution and should not replace proper SDK management practices. 236 | - Before submitting your app for review, ensure that the privacy manifest fix complies with the latest App Store requirements. 237 | 238 | ## 🙌 Contributing 239 | 240 | Contributions in any form are welcome, including code optimizations, bug fixes, documentation improvements, and more. Please follow the project's guidelines and maintain a consistent coding style. Thank you for your support! 241 | -------------------------------------------------------------------------------- /README.zh-CN.md: -------------------------------------------------------------------------------- 1 | # App Privacy Manifest Fixer 2 | 3 | [![Latest Version](https://img.shields.io/github/v/release/crasowas/app_privacy_manifest_fixer?logo=github)](https://github.com/crasowas/app_privacy_manifest_fixer/releases/latest) 4 | ![Supported Platforms](https://img.shields.io/badge/Supported%20Platforms-iOS%20%7C%20macOS-brightgreen) 5 | [![License](https://img.shields.io/badge/License-MIT-green.svg)](https://opensource.org/licenses/MIT) 6 | 7 | **[English](./README.md) | 简体中文** 8 | 9 | 本工具是一个基于 Shell 脚本的自动化解决方案,旨在分析和修复 iOS/macOS App 的隐私清单,确保 App 符合 App Store 的要求。它利用 [App Store Privacy Manifest Analyzer](https://github.com/crasowas/app_store_required_privacy_manifest_analyser) 对 App 及其依赖项进行 API 使用分析,并生成或修复`PrivacyInfo.xcprivacy`文件。 10 | 11 | ## ✨ 功能特点 12 | 13 | - **非侵入式集成**:无需修改源码或调整项目结构。 14 | - **极速安装与卸载**:一行命令即可快速完成工具的安装或卸载。 15 | - **自动分析与修复**:项目构建时自动分析 API 使用情况并修复隐私清单问题。 16 | - **灵活定制模板**:支持自定义 App 和 Framework 的隐私清单模板,满足多种使用场景。 17 | - **隐私访问报告**:自动生成报告用于查看 App 和 SDK 的`NSPrivacyAccessedAPITypes`声明情况。 18 | - **版本轻松升级**:提供升级脚本快速更新至最新版本。 19 | 20 | ## 📥 安装 21 | 22 | ### 下载工具 23 | 24 | 1. 下载[最新发布版本](https://github.com/crasowas/app_privacy_manifest_fixer/releases/latest)。 25 | 2. 解压下载的文件: 26 | - 解压后的目录通常为`app_privacy_manifest_fixer-xxx`(其中`xxx`是版本号)。 27 | - 建议重命名为`app_privacy_manifest_fixer`,或在后续路径中使用完整目录名。 28 | - **建议将该目录移动至 iOS/macOS 项目中,以避免因路径问题在不同设备上运行时出现错误,同时便于为每个项目单独自定义隐私清单模板**。 29 | 30 | ### ⚡ 自动安装(推荐) 31 | 32 | 1. **切换到工具所在目录**: 33 | 34 | ```shell 35 | cd /path/to/app_privacy_manifest_fixer 36 | ``` 37 | 38 | 2. **运行以下安装脚本**: 39 | 40 | ```shell 41 | sh install.sh 42 | ``` 43 | 44 | - 如果是 Flutter 项目,`project_path`应为 Flutter 项目中的`ios/macos`目录路径。 45 | - 重复运行安装命令时,工具会先移除现有安装(如果有)。若需修改命令行选项,只需重新运行安装命令,无需先卸载。 46 | 47 | ### 手动安装 48 | 49 | 如果不使用安装脚本,可以手动添加`Fix Privacy Manifest`任务到 Xcode 的 **Build Phases** 完成安装。安装步骤如下: 50 | 51 | #### 1. 在 Xcode 中添加脚本 52 | 53 | - 用 Xcode 打开你的 iOS/macOS 项目,进入 **TARGETS** 选项卡,选择你的 App 目标。 54 | - 进入 **Build Phases**,点击 **+** 按钮,选择 **New Run Script Phase**。 55 | - 将新建的 **Run Script** 重命名为`Fix Privacy Manifest`。 56 | - 在 Shell 脚本框中添加以下代码: 57 | 58 | ```shell 59 | # 使用相对路径(推荐):如果`app_privacy_manifest_fixer`在项目目录内 60 | "$PROJECT_DIR/path/to/app_privacy_manifest_fixer/fixer.sh" 61 | 62 | # 使用绝对路径:如果`app_privacy_manifest_fixer`不在项目目录内 63 | # "/absolute/path/to/app_privacy_manifest_fixer/fixer.sh" 64 | ``` 65 | 66 | **请根据实际情况修改`path/to`或`absolute/path/to`,并确保路径正确。同时,删除或注释掉不适用的行**。 67 | 68 | #### 2. 调整脚本执行顺序 69 | 70 | **将该脚本移动到所有其他 Build Phases 之后,确保隐私清单在所有资源拷贝和编译任务完成后再进行修复**。 71 | 72 | ### Build Phases 截图 73 | 74 | 下面是自动/手动安装成功后的 Xcode Build Phases 配置截图(未启用任何命令行选项): 75 | 76 | ![Build Phases Screenshot](https://img.crasowas.dev/app_privacy_manifest_fixer/20250225011407.png) 77 | 78 | ## 🚀 快速开始 79 | 80 | 安装后,工具将在每次构建项目时自动运行,构建完成后得到的 App 包已经是修复后的结果。 81 | 82 | 如果启用`--install-builds-only`命令行选项安装,工具将仅在安装构建时运行。 83 | 84 | ### Xcode Build Log 截图 85 | 86 | 下面是项目构建时工具输出的日志截图(默认会存储到`app_privacy_manifest_fixer/Build`目录,除非启用`-s`命令行选项): 87 | 88 | ![Xcode Build Log Screenshot](https://img.crasowas.dev/app_privacy_manifest_fixer/20250225011551.png) 89 | 90 | ## 📖 使用方法 91 | 92 | ### 命令行选项 93 | 94 | - **强制覆盖现有隐私清单(不推荐)**: 95 | 96 | ```shell 97 | sh install.sh -f 98 | ``` 99 | 100 | 启用`-f`选项后,工具会根据 API 使用分析结果和隐私清单模板生成新的隐私清单,并强制覆盖现有隐私清单。默认情况下(未启用`-f`),工具仅修复缺失的隐私清单。 101 | 102 | - **静默模式**: 103 | 104 | ```shell 105 | sh install.sh -s 106 | ``` 107 | 108 | 启用`-s`选项后,工具将禁用修复时的输出,不再复制构建生成的`*.app`、自动生成隐私访问报告或输出修复日志。默认情况下(未启用`-s`),这些输出存储在`app_privacy_manifest_fixer/Build`目录。 109 | 110 | - **仅在安装构建时运行(推荐)**: 111 | 112 | ```shell 113 | sh install.sh --install-builds-only 114 | ``` 115 | 116 | 启用`--install-builds-only`选项后,工具仅在执行安装构建(如 **Archive** 操作)时运行,以优化日常开发时的构建性能。如果你是手动安装的,该命令行选项无效,需要手动勾选 **"For install builds only"** 选项。 117 | 118 | **注意**:如果 iOS/macOS 项目在开发环境构建(生成的 App 包含`*.debug.dylib`文件),工具的 API 使用分析结果可能不准确。 119 | 120 | ### 升级工具 121 | 122 | 要更新至最新版本,请运行以下命令: 123 | 124 | ```shell 125 | sh upgrade.sh 126 | ``` 127 | 128 | ### 卸载工具 129 | 130 | 要快速卸载本工具,请运行以下命令: 131 | 132 | ```shell 133 | sh uninstall.sh 134 | ``` 135 | 136 | ### 清理工具生成的文件 137 | 138 | 要删除工具生成的文件,请运行以下命令: 139 | 140 | ```shell 141 | sh clean.sh 142 | ``` 143 | 144 | ## 🔥 隐私清单模板 145 | 146 | 隐私清单模板存储在[`Templates`](https://github.com/crasowas/app_privacy_manifest_fixer/tree/main/Templates)目录,其中根目录已经包含默认模板。 147 | 148 | **如何为 App 或 SDK 自定义隐私清单?只需使用[自定义模板](#自定义模板)!** 149 | 150 | ### 模板类型 151 | 152 | 模板分为以下几类: 153 | - **AppTemplate.xcprivacy**:App 的隐私清单模板。 154 | - **FrameworkTemplate.xcprivacy**:通用的 Framework 隐私清单模板。 155 | - **FrameworkName.xcprivacy**:特定的 Framework 隐私清单模板,仅在`Templates/UserTemplates`目录有效。 156 | 157 | ### 模板优先级 158 | 159 | 对于 App,隐私清单模板的优先级如下: 160 | - `Templates/UserTemplates/AppTemplate.xcprivacy` > `Templates/AppTemplate.xcprivacy` 161 | 162 | 对于特定的 Framework,隐私清单模板的优先级如下: 163 | - `Templates/UserTemplates/FrameworkName.xcprivacy` > `Templates/UserTemplates/FrameworkTemplate.xcprivacy` > `Templates/FrameworkTemplate.xcprivacy` 164 | 165 | ### 默认模板 166 | 167 | 默认模板位于`Templates`根目录,目前包括以下模板: 168 | - `Templates/AppTemplate.xcprivacy` 169 | - `Templates/FrameworkTemplate.xcprivacy` 170 | 171 | 这些模板将根据 API 使用分析结果进行修改,特别是`NSPrivacyAccessedAPIType`条目将被调整,以生成新的隐私清单用于修复,确保符合 App Store 要求。 172 | 173 | **如果需要调整隐私清单模板,例如以下场景,请避免直接修改默认模板,而是使用自定义模板。如果存在相同名称的自定义模板,它将优先于默认模板用于修复。** 174 | - 由于 API 使用分析结果不准确,生成了不合规的隐私清单。 175 | - 需要修改模板中声明的理由。 176 | - 需要声明收集的数据。 177 | 178 | `AppTemplate.xcprivacy`中隐私访问 API 类别及其对应声明的理由如下: 179 | 180 | | [NSPrivacyAccessedAPIType](https://developer.apple.com/documentation/bundleresources/app-privacy-configuration/nsprivacyaccessedapitypes/nsprivacyaccessedapitype) | [NSPrivacyAccessedAPITypeReasons](https://developer.apple.com/documentation/bundleresources/app-privacy-configuration/nsprivacyaccessedapitypes/nsprivacyaccessedapitypereasons) | 181 | |--------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 182 | | NSPrivacyAccessedAPICategoryFileTimestamp | C617.1: Inside app or group container | 183 | | NSPrivacyAccessedAPICategorySystemBootTime | 35F9.1: Measure time on-device | 184 | | NSPrivacyAccessedAPICategoryDiskSpace | E174.1: Write or delete file on-device | 185 | | NSPrivacyAccessedAPICategoryActiveKeyboards | 54BD.1: Customize UI on-device | 186 | | NSPrivacyAccessedAPICategoryUserDefaults | CA92.1: Access info from same app | 187 | 188 | `FrameworkTemplate.xcprivacy`中隐私访问 API 类别及其对应声明的理由如下: 189 | 190 | | [NSPrivacyAccessedAPIType](https://developer.apple.com/documentation/bundleresources/app-privacy-configuration/nsprivacyaccessedapitypes/nsprivacyaccessedapitype) | [NSPrivacyAccessedAPITypeReasons](https://developer.apple.com/documentation/bundleresources/app-privacy-configuration/nsprivacyaccessedapitypes/nsprivacyaccessedapitypereasons) | 191 | |--------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 192 | | NSPrivacyAccessedAPICategoryFileTimestamp | 0A2A.1: 3rd-party SDK wrapper on-device | 193 | | NSPrivacyAccessedAPICategorySystemBootTime | 35F9.1: Measure time on-device | 194 | | NSPrivacyAccessedAPICategoryDiskSpace | E174.1: Write or delete file on-device | 195 | | NSPrivacyAccessedAPICategoryActiveKeyboards | 54BD.1: Customize UI on-device | 196 | | NSPrivacyAccessedAPICategoryUserDefaults | C56D.1: 3rd-party SDK wrapper on-device | 197 | 198 | ### 自定义模板 199 | 200 | 要创建自定义模板,请将其放在`Templates/UserTemplates`目录,结构如下: 201 | - `Templates/UserTemplates/AppTemplate.xcprivacy` 202 | - `Templates/UserTemplates/FrameworkTemplate.xcprivacy` 203 | - `Templates/UserTemplates/FrameworkName.xcprivacy` 204 | 205 | 在这些模板中,只有`FrameworkTemplate.xcprivacy`会根据 API 使用分析结果对`NSPrivacyAccessedAPIType`条目进行调整,以生成新的隐私清单用于 Framework 修复。其他模板保持不变,将直接用于修复。 206 | 207 | **重要说明:** 208 | - 特定的 Framework 模板必须遵循命名规范`FrameworkName.xcprivacy`,其中`FrameworkName`需与 Framework 的名称匹配。例如`Flutter.framework`的模板应命名为`Flutter.xcprivacy`。 209 | - 对于 macOS Framework,应遵循命名规范`FrameworkName.Version.xcprivacy`,额外增加版本名称用于区分不同的版本。对于单一版本的 macOS Framework,`Version`通常为`A`。 210 | - SDK 的名称可能与 Framework 的名称不完全一致。要确定正确的 Framework 名称,请在构建项目后检查 App 包中的`Frameworks`目录。 211 | 212 | ## 📑 隐私访问报告 213 | 214 | 默认情况下,工具会自动在每次构建时为原始 App 和修复后的 App 生成隐私访问报告,并存储到`app_privacy_manifest_fixer/Build`目录。 215 | 216 | 如果需要手动为特定 App 生成隐私访问报告,请运行以下命令: 217 | 218 | ```shell 219 | sh Report/report.sh 220 | # : App路径(例如:/path/to/App.app) 221 | # : 报告文件保存路径(例如:/path/to/report.html) 222 | ``` 223 | 224 | **注意**:工具生成的报告目前仅包含隐私访问部分(`NSPrivacyAccessedAPITypes`),如果想看数据收集部分(`NSPrivacyCollectedDataTypes`)请使用 Xcode 生成`PrivacyReport`。 225 | 226 | ### 报告示例截图 227 | 228 | | 原始 App 报告(report-original.html) | 修复后 App 报告(report.html) | 229 | |------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------| 230 | | ![Original App Report](https://img.crasowas.dev/app_privacy_manifest_fixer/20241218230746.png) | ![Fixed App Report](https://img.crasowas.dev/app_privacy_manifest_fixer/20241218230822.png) | 231 | 232 | ## 💡 重要考量 233 | 234 | - 如果最新版本的 SDK 支持隐私清单,请尽可能升级,以避免不必要的风险。 235 | - 此工具仅为临时解决方案,不应替代正确的 SDK 管理实践。 236 | - 在提交 App 审核之前,请检查隐私清单修复后是否符合最新的 App Store 要求。 237 | 238 | ## 🙌 贡献 239 | 240 | 欢迎任何形式的贡献,包括代码优化、Bug 修复、文档改进等。请确保遵循项目规范,并保持代码风格一致。感谢你的支持! 241 | -------------------------------------------------------------------------------- /Report/report-template.html: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | Privacy Access Report 16 | 17 | 113 | 114 | 115 |
116 | 117 | This report was generated using version {{TOOL_VERSION}}. 118 | 119 | Like this 120 | project? 🌟Star it on GitHub! 121 |
122 | {{REPORT_CONTENT}} 123 | 124 | -------------------------------------------------------------------------------- /Report/report.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Copyright (c) 2024, crasowas. 4 | # 5 | # Use of this source code is governed by a MIT-style license 6 | # that can be found in the LICENSE file or at 7 | # https://opensource.org/licenses/MIT. 8 | 9 | set -e 10 | 11 | # Absolute path of the script and the tool's root directory 12 | script_path="$(realpath "$0")" 13 | tool_root_path="$(dirname "$(dirname "$script_path")")" 14 | 15 | # Load common constants and utils 16 | source "$tool_root_path/Common/constants.sh" 17 | source "$tool_root_path/Common/utils.sh" 18 | 19 | # Path to the app 20 | app_path="$1" 21 | 22 | # Check if the app exists 23 | if [ ! -d "$app_path" ] || [[ "$app_path" != *.app ]]; then 24 | echo "Unable to find the app: $app_path" 25 | exit 1 26 | fi 27 | 28 | # Check if the app is iOS or macOS 29 | is_ios_app=true 30 | frameworks_dir="$app_path/Frameworks" 31 | if [ -d "$app_path/Contents/MacOS" ]; then 32 | is_ios_app=false 33 | frameworks_dir="$app_path/Contents/Frameworks" 34 | fi 35 | 36 | report_output_file="$2" 37 | # Additional arguments as template usage records 38 | template_usage_records=("${@:2}") 39 | 40 | # Copy report template to output file 41 | report_template_file="$tool_root_path/Report/report-template.html" 42 | if ! rsync -a "$report_template_file" "$report_output_file"; then 43 | echo "Failed to copy the report template to $report_output_file" 44 | exit 1 45 | fi 46 | 47 | # Read the current tool's version from the VERSION file 48 | tool_version_file="$tool_root_path/VERSION" 49 | tool_version="N/A" 50 | if [ -f "$tool_version_file" ]; then 51 | tool_version="$(cat "$tool_version_file")" 52 | fi 53 | 54 | # Initialize report content 55 | report_content="" 56 | 57 | # Get the template file used for fixing based on the app or framework name 58 | function get_used_template_file() { 59 | local name="$1" 60 | 61 | for template_usage_record in "${template_usage_records[@]}"; do 62 | if [[ "$template_usage_record" == "$name$DELIMITER"* ]]; then 63 | echo "${template_usage_record#*$DELIMITER}" 64 | return 65 | fi 66 | done 67 | 68 | echo "" 69 | } 70 | 71 | # Analyze accessed API types and their corresponding reasons 72 | function analyze_privacy_accessed_api() { 73 | local privacy_manifest_file="$1" 74 | local -a results=() 75 | 76 | if [ -f "$privacy_manifest_file" ]; then 77 | local api_count=$(xmllint --xpath 'count(//dict/key[text()="NSPrivacyAccessedAPIType"])' "$privacy_manifest_file") 78 | 79 | for ((i=1; i<=api_count; i++)); do 80 | local api_type=$(xmllint --xpath "(//dict/key[text()='NSPrivacyAccessedAPIType']/following-sibling::string[1])[$i]/text()" "$privacy_manifest_file" 2>/dev/null) 81 | local api_reasons=$(xmllint --xpath "(//dict/key[text()='NSPrivacyAccessedAPITypeReasons']/following-sibling::array[1])[position()=$i]/string/text()" "$privacy_manifest_file" 2>/dev/null | paste -sd "/" -) 82 | 83 | if [ -z "$api_type" ]; then 84 | api_type="N/A" 85 | fi 86 | 87 | if [ -z "$api_reasons" ]; then 88 | api_reasons="N/A" 89 | fi 90 | 91 | results+=("$api_type$DELIMITER$api_reasons") 92 | done 93 | fi 94 | 95 | echo "${results[@]}" 96 | } 97 | 98 | # Get the path to the `Info.plist` file for the specified app or framework 99 | function get_plist_file() { 100 | local path="$1" 101 | local version_path="$2" 102 | local plist_file="" 103 | 104 | if [[ "$path" == *.app ]]; then 105 | if [ "$is_ios_app" == true ]; then 106 | plist_file="$path/Info.plist" 107 | else 108 | plist_file="$path/Contents/Info.plist" 109 | fi 110 | elif [[ "$path" == *.framework ]]; then 111 | local framework_path="$(get_framework_path "$path" "$version_path")" 112 | 113 | if [ "$is_ios_app" == true ]; then 114 | plist_file="$framework_path/Info.plist" 115 | else 116 | plist_file="$framework_path/Resources/Info.plist" 117 | fi 118 | fi 119 | 120 | echo "$plist_file" 121 | } 122 | 123 | # Add an HTML
element with the `card` class 124 | function add_html_card_container() { 125 | local card="$1" 126 | 127 | report_content="$report_content
$card
" 128 | } 129 | 130 | # Generate an HTML

element 131 | function generate_html_header() { 132 | local title="$1" 133 | local version="$2" 134 | 135 | echo "

$titleVersion $version

" 136 | } 137 | 138 | # Generate an HTML element with optional `warning` class 139 | function generate_html_anchor() { 140 | local text="$1" 141 | local href="$2" 142 | local warning="$3" 143 | 144 | if [ "$warning" == true ]; then 145 | echo "$text" 146 | else 147 | echo "$text" 148 | fi 149 | } 150 | 151 | # Generate an HTML element 152 | function generate_html_table() { 153 | local thead="$1" 154 | local tbody="$2" 155 | 156 | echo "
$thead$tbody
" 157 | } 158 | 159 | # Generate an HTML element 160 | function generate_html_thead() { 161 | local ths=("$@") 162 | local tr="" 163 | 164 | for th in "${ths[@]}"; do 165 | tr="$tr$th" 166 | done 167 | 168 | echo "$tr" 169 | } 170 | 171 | # Generate an HTML element 172 | function generate_html_tbody() { 173 | local trs=("$@") 174 | local tbody="" 175 | 176 | for tr in "${trs[@]}"; do 177 | tbody="$tbody" 178 | local tds=($(split_string_by_delimiter "$tr")) 179 | 180 | for td in "${tds[@]}"; do 181 | tbody="$tbody$td" 182 | done 183 | 184 | tbody="$tbody" 185 | done 186 | 187 | echo "$tbody" 188 | } 189 | 190 | # Generate the report content for the specified directory 191 | function generate_report_content() { 192 | local path="$1" 193 | local version_path="$2" 194 | local privacy_manifest_file="" 195 | 196 | if [[ "$path" == *.app ]]; then 197 | # Per the documentation, the privacy manifest should be placed at the root of the app’s bundle for iOS, while for macOS, it should be located in `Contents/Resources/` within the app’s bundle 198 | # 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 199 | if [ "$is_ios_app" == true ]; then 200 | privacy_manifest_file="$path/$PRIVACY_MANIFEST_FILE_NAME" 201 | else 202 | privacy_manifest_file="$path/Contents/Resources/$PRIVACY_MANIFEST_FILE_NAME" 203 | fi 204 | else 205 | # Per the documentation, the privacy manifest should be placed at the root of the iOS framework, while for a macOS framework with multiple versions, it should be located in the `Resources` directory within the corresponding version 206 | # Some SDKs don’t follow the guideline, so we use a search-based approach for now 207 | # 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 208 | local framework_path="$(get_framework_path "$path" "$version_path")" 209 | local privacy_manifest_files=($(search_privacy_manifest_files "$framework_path")) 210 | privacy_manifest_file="$(get_privacy_manifest_file "${privacy_manifest_files[@]}")" 211 | fi 212 | 213 | local name="$(basename "$path")" 214 | local title="$name" 215 | if [ -n "$version_path" ]; then 216 | title="$name ($version_path)" 217 | fi 218 | 219 | local plist_file="$(get_plist_file "$path" "$version_path")" 220 | local version="$(get_plist_version "$plist_file")" 221 | local card="$(generate_html_header "$title" "$version")" 222 | 223 | if [ -f "$privacy_manifest_file" ]; then 224 | card="$card$(generate_html_anchor "$PRIVACY_MANIFEST_FILE_NAME" "$privacy_manifest_file" false)" 225 | 226 | local used_template_file="$(get_used_template_file "$name$version_path")" 227 | 228 | if [ -f "$used_template_file" ]; then 229 | card="$card$(generate_html_anchor "Template Used: $(basename "$used_template_file")" "$used_template_file" false)" 230 | fi 231 | 232 | local trs=($(analyze_privacy_accessed_api "$privacy_manifest_file")) 233 | 234 | # Generate table only if the accessed privacy API types array is not empty 235 | if [[ ${#trs[@]} -gt 0 ]]; then 236 | local thead="$(generate_html_thead "NSPrivacyAccessedAPIType" "NSPrivacyAccessedAPITypeReasons")" 237 | local tbody="$(generate_html_tbody "${trs[@]}")" 238 | 239 | card="$card$(generate_html_table "$thead" "$tbody")" 240 | fi 241 | else 242 | card="$card$(generate_html_anchor "Missing Privacy Manifest" "$path" true)" 243 | fi 244 | 245 | add_html_card_container "$card" 246 | } 247 | 248 | # Generate the report content for app 249 | function generate_app_report_content() { 250 | generate_report_content "$app_path" "" 251 | } 252 | 253 | # Generate the report content for frameworks 254 | function generate_frameworks_report_content() { 255 | if ! [ -d "$frameworks_dir" ]; then 256 | return 257 | fi 258 | 259 | for path in "$frameworks_dir"/*; do 260 | if [ -d "$path" ]; then 261 | local versions_dir="$path/Versions" 262 | 263 | if [ -d "$versions_dir" ]; then 264 | for version in $(ls -1 "$versions_dir" | grep -vE '^Current$'); do 265 | local version_path="Versions/$version" 266 | generate_report_content "$path" "$version_path" 267 | done 268 | else 269 | generate_report_content "$path" "" 270 | fi 271 | fi 272 | done 273 | } 274 | 275 | # Generate the final report with all content 276 | function generate_final_report() { 277 | # Replace placeholders in the template with the tool's version and report content 278 | sed -i "" -e "s|{{TOOL_VERSION}}|$tool_version|g" -e "s|{{REPORT_CONTENT}}|${report_content}|g" "$report_output_file" 279 | 280 | echo "Privacy Access Report has been generated: $report_output_file" 281 | } 282 | 283 | generate_app_report_content 284 | generate_frameworks_report_content 285 | generate_final_report 286 | -------------------------------------------------------------------------------- /Templates/AppTemplate.xcprivacy: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSPrivacyTracking 6 | 7 | NSPrivacyTrackingDomains 8 | 9 | NSPrivacyCollectedDataTypes 10 | 11 | NSPrivacyAccessedAPITypes 12 | 13 | 14 | NSPrivacyAccessedAPIType 15 | NSPrivacyAccessedAPICategoryFileTimestamp 16 | NSPrivacyAccessedAPITypeReasons 17 | 18 | C617.1 19 | 20 | 21 | 22 | NSPrivacyAccessedAPIType 23 | NSPrivacyAccessedAPICategorySystemBootTime 24 | NSPrivacyAccessedAPITypeReasons 25 | 26 | 35F9.1 27 | 28 | 29 | 30 | NSPrivacyAccessedAPIType 31 | NSPrivacyAccessedAPICategoryDiskSpace 32 | NSPrivacyAccessedAPITypeReasons 33 | 34 | E174.1 35 | 36 | 37 | 38 | NSPrivacyAccessedAPIType 39 | NSPrivacyAccessedAPICategoryActiveKeyboards 40 | NSPrivacyAccessedAPITypeReasons 41 | 42 | 54BD.1 43 | 44 | 45 | 46 | NSPrivacyAccessedAPIType 47 | NSPrivacyAccessedAPICategoryUserDefaults 48 | NSPrivacyAccessedAPITypeReasons 49 | 50 | CA92.1 51 | 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /Templates/FrameworkTemplate.xcprivacy: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSPrivacyTracking 6 | 7 | NSPrivacyTrackingDomains 8 | 9 | NSPrivacyCollectedDataTypes 10 | 11 | NSPrivacyAccessedAPITypes 12 | 13 | 14 | NSPrivacyAccessedAPIType 15 | NSPrivacyAccessedAPICategoryFileTimestamp 16 | NSPrivacyAccessedAPITypeReasons 17 | 18 | 0A2A.1 19 | 20 | 21 | 22 | NSPrivacyAccessedAPIType 23 | NSPrivacyAccessedAPICategorySystemBootTime 24 | NSPrivacyAccessedAPITypeReasons 25 | 26 | 35F9.1 27 | 28 | 29 | 30 | NSPrivacyAccessedAPIType 31 | NSPrivacyAccessedAPICategoryDiskSpace 32 | NSPrivacyAccessedAPITypeReasons 33 | 34 | E174.1 35 | 36 | 37 | 38 | NSPrivacyAccessedAPIType 39 | NSPrivacyAccessedAPICategoryActiveKeyboards 40 | NSPrivacyAccessedAPITypeReasons 41 | 42 | 54BD.1 43 | 44 | 45 | 46 | NSPrivacyAccessedAPIType 47 | NSPrivacyAccessedAPICategoryUserDefaults 48 | NSPrivacyAccessedAPITypeReasons 49 | 50 | C56D.1 51 | 52 | 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /Templates/UserTemplates/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crasowas/app_privacy_manifest_fixer/86488da6ccb325307e49913b424754f31156d18a/Templates/UserTemplates/.gitkeep -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | v1.4.1 -------------------------------------------------------------------------------- /clean.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Copyright (c) 2025, crasowas. 4 | # 5 | # Use of this source code is governed by a MIT-style license 6 | # that can be found in the LICENSE file or at 7 | # https://opensource.org/licenses/MIT. 8 | 9 | set -e 10 | 11 | target_paths=("Build") 12 | 13 | echo "Cleaning..." 14 | 15 | deleted_anything=false 16 | 17 | for path in "${target_paths[@]}"; do 18 | if [ -e "$path" ]; then 19 | echo "Removing $path..." 20 | rm -rf "./$path" 21 | deleted_anything=true 22 | fi 23 | done 24 | 25 | if [ "$deleted_anything" == true ]; then 26 | echo "Cleanup completed." 27 | else 28 | echo "Nothing to clean." 29 | fi 30 | -------------------------------------------------------------------------------- /fixer.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Copyright (c) 2024, crasowas. 4 | # 5 | # Use of this source code is governed by a MIT-style license 6 | # that can be found in the LICENSE file or at 7 | # https://opensource.org/licenses/MIT. 8 | 9 | set -e 10 | 11 | # Absolute path of the script and the tool's root directory 12 | script_path="$(realpath "$0")" 13 | tool_root_path="$(dirname "$script_path")" 14 | 15 | # Load common constants and utils 16 | source "$tool_root_path/Common/constants.sh" 17 | source "$tool_root_path/Common/utils.sh" 18 | 19 | # Force replace the existing privacy manifest when the `-f` option is enabled 20 | force=false 21 | 22 | # Enable silent mode to disable build output when the `-s` option is enabled 23 | silent=false 24 | 25 | # Parse command-line options 26 | while getopts ":fs" opt; do 27 | case $opt in 28 | f) 29 | force=true 30 | ;; 31 | s) 32 | silent=true 33 | ;; 34 | \?) 35 | echo "Invalid option: -$OPTARG" >&2 36 | exit 1 37 | ;; 38 | esac 39 | done 40 | 41 | shift $((OPTIND - 1)) 42 | 43 | # Path of the app produced by the build process 44 | app_path="${TARGET_BUILD_DIR}/${WRAPPER_NAME}" 45 | 46 | # Check if the app exists 47 | if [ ! -d "$app_path" ] || [[ "$app_path" != *.app ]]; then 48 | echo "Unable to find the app: $app_path" 49 | exit 1 50 | fi 51 | 52 | # Check if the app is iOS or macOS 53 | is_ios_app=true 54 | frameworks_dir="$app_path/Frameworks" 55 | if [ -d "$app_path/Contents/MacOS" ]; then 56 | is_ios_app=false 57 | frameworks_dir="$app_path/Contents/Frameworks" 58 | fi 59 | 60 | # Default template directories 61 | templates_dir="$tool_root_path/Templates" 62 | user_templates_dir="$tool_root_path/Templates/UserTemplates" 63 | 64 | # Use user-defined app privacy manifest template if it exists, otherwise fallback to default 65 | app_template_file="$user_templates_dir/$APP_TEMPLATE_FILE_NAME" 66 | if [ ! -f "$app_template_file" ]; then 67 | app_template_file="$templates_dir/$APP_TEMPLATE_FILE_NAME" 68 | fi 69 | 70 | # Use user-defined framework privacy manifest template if it exists, otherwise fallback to default 71 | framework_template_file="$user_templates_dir/$FRAMEWORK_TEMPLATE_FILE_NAME" 72 | if [ ! -f "$framework_template_file" ]; then 73 | framework_template_file="$templates_dir/$FRAMEWORK_TEMPLATE_FILE_NAME" 74 | fi 75 | 76 | # Disable build output in silent mode 77 | if [ "$silent" == false ]; then 78 | # Script used to generate the privacy access report 79 | report_script="$tool_root_path/Report/report.sh" 80 | # An array to record template usage for generating the privacy access report 81 | template_usage_records=() 82 | 83 | # Build output directory 84 | build_dir="$tool_root_path/Build/${PRODUCT_NAME}-${CONFIGURATION}_${MARKETING_VERSION}_${CURRENT_PROJECT_VERSION}_$(date +%Y%m%d%H%M%S)" 85 | # Ensure the build directory exists 86 | mkdir -p "$build_dir" 87 | 88 | # Redirect both stdout and stderr to a log file while keeping console output 89 | exec > >(tee "$build_dir/fix.log") 2>&1 90 | fi 91 | 92 | # Get the path to the `Info.plist` file for the specified app or framework 93 | function get_plist_file() { 94 | local path="$1" 95 | local version_path="$2" 96 | local plist_file="" 97 | 98 | if [[ "$path" == *.app ]]; then 99 | if [ "$is_ios_app" == true ]; then 100 | plist_file="$path/Info.plist" 101 | else 102 | plist_file="$path/Contents/Info.plist" 103 | fi 104 | elif [[ "$path" == *.framework ]]; then 105 | local framework_path="$(get_framework_path "$path" "$version_path")" 106 | 107 | if [ "$is_ios_app" == true ]; then 108 | plist_file="$framework_path/Info.plist" 109 | else 110 | plist_file="$framework_path/Resources/Info.plist" 111 | fi 112 | fi 113 | 114 | echo "$plist_file" 115 | } 116 | 117 | # Get the path to the executable for the specified app or framework 118 | function get_executable_path() { 119 | local path="$1" 120 | local version_path="$2" 121 | local executable_path="" 122 | 123 | local plist_file="$(get_plist_file "$path" "$version_path")" 124 | local executable_name="$(get_plist_executable "$plist_file")" 125 | 126 | if [[ "$path" == *.app ]]; then 127 | if [ "$is_ios_app" == true ]; then 128 | executable_path="$path/$executable_name" 129 | else 130 | executable_path="$path/Contents/MacOS/$executable_name" 131 | fi 132 | elif [[ "$path" == *.framework ]]; then 133 | local framework_path="$(get_framework_path "$path" "$version_path")" 134 | executable_path="$framework_path/$executable_name" 135 | fi 136 | 137 | echo "$executable_path" 138 | } 139 | 140 | # Analyze the specified binary file for API symbols and their categories 141 | function analyze_binary_file() { 142 | local path="$1" 143 | local -a results=() 144 | 145 | # Check if the API symbol exists in the binary file using `nm` and `strings` 146 | local nm_output=$(nm "$path" 2>/dev/null | xcrun swift-demangle) 147 | local strings_output=$(strings "$path") 148 | local combined_output="$nm_output"$'\n'"$strings_output" 149 | 150 | for api_symbol in "${API_SYMBOLS[@]}"; do 151 | local substrings=($(split_string_by_delimiter "$api_symbol")) 152 | local category=${substrings[0]} 153 | local api=${substrings[1]} 154 | 155 | if echo "$combined_output" | grep -E "$api\$" >/dev/null; then 156 | local index=-1 157 | for ((i=0; i < ${#results[@]}; i++)); do 158 | local result="${results[i]}" 159 | local result_substrings=($(split_string_by_delimiter "$result")) 160 | # If the category matches an existing result, update it 161 | if [ "$category" == "${result_substrings[0]}" ]; then 162 | index=i 163 | results[i]="${result_substrings[0]}$DELIMITER${result_substrings[1]},$api$DELIMITER${result_substrings[2]}" 164 | break 165 | fi 166 | done 167 | 168 | # If no matching category found, add a new result 169 | if [[ $index -eq -1 ]]; then 170 | results+=("$category$DELIMITER$api$DELIMITER$(encode_path "$path")") 171 | fi 172 | fi 173 | done 174 | 175 | echo "${results[@]}" 176 | } 177 | 178 | # Analyze API usage in a binary file 179 | function analyze_api_usage() { 180 | local path="$1" 181 | local version_path="$2" 182 | local -a results=() 183 | 184 | local binary_file="$(get_executable_path "$path" "$version_path")" 185 | 186 | if [ -f "$binary_file" ]; then 187 | results+=($(analyze_binary_file "$binary_file")) 188 | fi 189 | 190 | echo "${results[@]}" 191 | } 192 | 193 | 194 | 195 | # Get unique categories from analysis results 196 | function get_categories() { 197 | local results=("$@") 198 | local -a categories=() 199 | 200 | for result in "${results[@]}"; do 201 | local substrings=($(split_string_by_delimiter "$result")) 202 | local category=${substrings[0]} 203 | if [[ ! "${categories[@]}" =~ "$category" ]]; then 204 | categories+=("$category") 205 | fi 206 | done 207 | 208 | echo "${categories[@]}" 209 | } 210 | 211 | # Get template file for the specified app or framework 212 | function get_template_file() { 213 | local path="$1" 214 | local version_path="$2" 215 | local template_file="" 216 | 217 | if [[ "$path" == *.app ]]; then 218 | template_file="$app_template_file" 219 | else 220 | # Give priority to the user-defined framework privacy manifest template 221 | local dep_name="$(get_dependency_name "$path")" 222 | if [ -n "$version_path" ]; then 223 | dep_name="$dep_name.$(basename "$version_path")" 224 | fi 225 | 226 | local dep_template_file="$user_templates_dir/${dep_name}.xcprivacy" 227 | if [ -f "$dep_template_file" ]; then 228 | template_file="$dep_template_file" 229 | else 230 | template_file="$framework_template_file" 231 | fi 232 | fi 233 | 234 | echo "$template_file" 235 | } 236 | 237 | # Check if the specified template file should be modified 238 | # 239 | # The following template files will be modified based on analysis: 240 | # * Templates/AppTemplate.xcprivacy 241 | # * Templates/FrameworkTemplate.xcprivacy 242 | # * Templates/UserTemplates/FrameworkTemplate.xcprivacy 243 | function is_template_modifiable() { 244 | local template_file="$1" 245 | 246 | local template_file_name="$(basename "$template_file")" 247 | 248 | if [[ "$template_file" != "$user_templates_dir"* ]] || [ "$template_file_name" == "$FRAMEWORK_TEMPLATE_FILE_NAME" ]; then 249 | return 0 250 | else 251 | return 1 252 | fi 253 | } 254 | 255 | # Check if `Hardened Runtime` is enabled for the specified path 256 | function is_hardened_runtime_enabled() { 257 | local path="$1" 258 | 259 | # Check environment variable first 260 | if [ "${ENABLE_HARDENED_RUNTIME:-}" == "YES" ]; then 261 | return 0 262 | fi 263 | 264 | # Check the code signature flags if environment variable is not set 265 | local flags=$(codesign -dvvv "$path" 2>&1 | grep "flags=") 266 | if echo "$flags" | grep -q "runtime"; then 267 | return 0 268 | else 269 | return 1 270 | fi 271 | } 272 | 273 | # Re-sign the target app or framework if code signing is enabled 274 | function resign() { 275 | local path="$1" 276 | 277 | if [ -n "${EXPANDED_CODE_SIGN_IDENTITY:-}" ] && [ "${CODE_SIGNING_REQUIRED:-}" != "NO" ] && [ "${CODE_SIGNING_ALLOWED:-}" != "NO" ]; then 278 | echo "Re-signing $path with Identity ${EXPANDED_CODE_SIGN_IDENTITY_NAME:-$EXPANDED_CODE_SIGN_IDENTITY}" 279 | 280 | local codesign_cmd="/usr/bin/codesign --force --sign ${EXPANDED_CODE_SIGN_IDENTITY} ${OTHER_CODE_SIGN_FLAGS:-} --preserve-metadata=identifier,entitlements" 281 | 282 | if [ "$is_ios_app" == true ]; then 283 | $codesign_cmd "$path" 284 | else 285 | if is_hardened_runtime_enabled "$path"; then 286 | codesign_cmd="$codesign_cmd -o runtime" 287 | fi 288 | 289 | if [ -d "$path/Contents/MacOS" ]; then 290 | find "$path/Contents/MacOS" -type f -name "*.dylib" | while read -r dylib_file; do 291 | $codesign_cmd "$dylib_file" 292 | done 293 | fi 294 | 295 | $codesign_cmd "$path" 296 | fi 297 | fi 298 | } 299 | 300 | # Fix the privacy manifest for the app or specified framework 301 | # To accelerate the build, existing privacy manifests will be left unchanged unless the `-f` option is enabled 302 | # After fixing, the app or framework will be automatically re-signed 303 | function fix() { 304 | local path="$1" 305 | local version_path="$2" 306 | local force_resign="$3" 307 | local privacy_manifest_file="" 308 | 309 | if [[ "$path" == *.app ]]; then 310 | # Per the documentation, the privacy manifest should be placed at the root of the app’s bundle for iOS, while for macOS, it should be located in `Contents/Resources/` within the app’s bundle 311 | # 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 312 | if [ "$is_ios_app" == true ]; then 313 | privacy_manifest_file="$path/$PRIVACY_MANIFEST_FILE_NAME" 314 | else 315 | privacy_manifest_file="$path/Contents/Resources/$PRIVACY_MANIFEST_FILE_NAME" 316 | fi 317 | else 318 | # Per the documentation, the privacy manifest should be placed at the root of the iOS framework, while for a macOS framework with multiple versions, it should be located in the `Resources` directory within the corresponding version 319 | # Some SDKs don’t follow the guideline, so we use a search-based approach for now 320 | # 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 321 | local framework_path="$(get_framework_path "$path" "$version_path")" 322 | local privacy_manifest_files=($(search_privacy_manifest_files "$framework_path")) 323 | privacy_manifest_file="$(get_privacy_manifest_file "${privacy_manifest_files[@]}")" 324 | 325 | if [ -z "$privacy_manifest_file" ]; then 326 | if [ "$is_ios_app" == true ]; then 327 | privacy_manifest_file="$framework_path/$PRIVACY_MANIFEST_FILE_NAME" 328 | else 329 | privacy_manifest_file="$framework_path/Resources/$PRIVACY_MANIFEST_FILE_NAME" 330 | fi 331 | fi 332 | fi 333 | 334 | # Check if the privacy manifest file exists 335 | if [ -f "$privacy_manifest_file" ]; then 336 | echo "💡 Found privacy manifest file: $privacy_manifest_file" 337 | 338 | if [ "$force" == false ]; then 339 | if [ "$force_resign" == true ]; then 340 | resign "$path" 341 | fi 342 | echo "✅ Privacy manifest file already exists, skipping fix." 343 | return 344 | fi 345 | else 346 | echo "⚠️ Missing privacy manifest file!" 347 | fi 348 | 349 | local results=($(analyze_api_usage "$path" "$version_path")) 350 | echo "API usage analysis result(s): ${#results[@]}" 351 | print_array "${results[@]}" 352 | 353 | local template_file="$(get_template_file "$path" "$version_path")" 354 | template_usage_records+=("$(basename "$path")$version_path$DELIMITER$template_file") 355 | 356 | # Copy the template file to the privacy manifest location, overwriting if it exists 357 | cp "$template_file" "$privacy_manifest_file" 358 | 359 | if is_template_modifiable "$template_file"; then 360 | local categories=($(get_categories "${results[@]}")) 361 | local remove_categories=() 362 | 363 | # Check if categories is non-empty 364 | if [[ ${#categories[@]} -gt 0 ]]; then 365 | # Convert categories to a single space-separated string for easy matching 366 | local categories_set=" ${categories[*]} " 367 | 368 | # Iterate through each element in `API_CATEGORIES` 369 | for element in "${API_CATEGORIES[@]}"; do 370 | # If element is not found in `categories_set`, add it to `remove_categories` 371 | if [[ ! $categories_set =~ " $element " ]]; then 372 | remove_categories+=("$element") 373 | fi 374 | done 375 | else 376 | # If categories is empty, add all of `API_CATEGORIES` to `remove_categories` 377 | remove_categories=("${API_CATEGORIES[@]}") 378 | fi 379 | 380 | # Remove extra spaces in the XML file to simplify node removal 381 | xmllint --noblanks "$privacy_manifest_file" -o "$privacy_manifest_file" 382 | 383 | # Build a sed command to remove all matching nodes at once 384 | local sed_pattern="" 385 | for category in "${remove_categories[@]}"; do 386 | # Find the node for the current category 387 | local remove_node="$(xmllint --xpath "//dict[string='$category']" "$privacy_manifest_file" 2>/dev/null || true)" 388 | 389 | # If the node is found, escape special characters and append it to the sed pattern 390 | if [ -n "$remove_node" ]; then 391 | local escaped_node=$(echo "$remove_node" | sed 's/[\/&]/\\&/g') 392 | sed_pattern+="s/$escaped_node//g;" 393 | fi 394 | done 395 | 396 | # Apply the combined sed pattern to the file if it's not empty 397 | if [ -n "$sed_pattern" ]; then 398 | sed -i "" "$sed_pattern" "$privacy_manifest_file" 399 | fi 400 | 401 | # Reformat the XML file to restore spaces for readability 402 | xmllint --format "$privacy_manifest_file" -o "$privacy_manifest_file" 403 | fi 404 | 405 | resign "$path" 406 | 407 | echo "✅ Privacy manifest file fixed: $privacy_manifest_file." 408 | } 409 | 410 | # Fix privacy manifests for all frameworks 411 | function fix_frameworks() { 412 | if ! [ -d "$frameworks_dir" ]; then 413 | return 414 | fi 415 | 416 | echo "🛠️ Fixing Frameworks..." 417 | for path in "$frameworks_dir"/*; do 418 | if [ -d "$path" ]; then 419 | local dep_name="$(get_dependency_name "$path")" 420 | local versions_dir="$path/Versions" 421 | 422 | if [ -d "$versions_dir" ]; then 423 | for version in $(ls -1 "$versions_dir" | grep -vE '^Current$'); do 424 | local version_path="Versions/$version" 425 | echo "Analyzing $dep_name ($version_path) ..." 426 | fix "$path" "$version_path" false 427 | echo "" 428 | done 429 | else 430 | echo "Analyzing $dep_name ..." 431 | fix "$path" "" false 432 | echo "" 433 | fi 434 | fi 435 | done 436 | } 437 | 438 | # Fix the privacy manifest for the app 439 | function fix_app() { 440 | echo "🛠️ Fixing $(basename "$app_path" .app) App..." 441 | # Since the framework may have undergone fixes, the app must be forcefully re-signed 442 | fix "$app_path" "" true 443 | echo "" 444 | } 445 | 446 | # Generate the privacy access report for the app 447 | function generate_report() { 448 | local original="$1" 449 | 450 | if [ "$silent" == true ]; then 451 | return 452 | fi 453 | 454 | local app_name="$(basename "$app_path")" 455 | local name="${app_name%.*}" 456 | local report_name="" 457 | 458 | # Adjust output names if the app is flagged as original 459 | if [ "$original" == true ]; then 460 | app_name="${name}-original.app" 461 | report_name="report-original.html" 462 | else 463 | app_name="$name.app" 464 | report_name="report.html" 465 | fi 466 | 467 | local target_app_path="$build_dir/$app_name" 468 | local report_path="$build_dir/$report_name" 469 | 470 | echo "Copy app to $target_app_path" 471 | rsync -a "$app_path/" "$target_app_path/" 472 | 473 | # Generate the privacy access report using the script 474 | sh "$report_script" "$target_app_path" "$report_path" "${template_usage_records[@]}" 475 | echo "" 476 | } 477 | 478 | start_time=$(date +%s) 479 | 480 | generate_report true 481 | fix_frameworks 482 | fix_app 483 | generate_report false 484 | 485 | end_time=$(date +%s) 486 | 487 | echo "🎉 All fixed! ⏰ $((end_time - start_time)) seconds" 488 | echo "🌟 If you found this script helpful, please consider giving it a star on GitHub. Your support is appreciated. Thank you!" 489 | echo "🔗 Homepage: https://github.com/crasowas/app_privacy_manifest_fixer" 490 | echo "🐛 Report issues: https://github.com/crasowas/app_privacy_manifest_fixer/issues" 491 | -------------------------------------------------------------------------------- /install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Copyright (c) 2024, crasowas. 4 | # 5 | # Use of this source code is governed by a MIT-style license 6 | # that can be found in the LICENSE file or at 7 | # https://opensource.org/licenses/MIT. 8 | 9 | set -e 10 | 11 | # Check if at least one argument (project_path) is provided 12 | if [[ "$#" -lt 1 ]]; then 13 | echo "Usage: $0 [options...]" 14 | exit 1 15 | fi 16 | 17 | project_path="$1" 18 | 19 | shift 20 | 21 | options=() 22 | install_builds_only=false 23 | 24 | # Check if the `--install-builds-only` option is provided and separate it from other options 25 | for arg in "$@"; do 26 | if [ "$arg" == "--install-builds-only" ]; then 27 | install_builds_only=true 28 | else 29 | options+=("$arg") 30 | fi 31 | done 32 | 33 | # Verify Ruby installation 34 | if ! command -v ruby &>/dev/null; then 35 | echo "Ruby is not installed. Please install Ruby and try again." 36 | exit 1 37 | fi 38 | 39 | # Check if xcodeproj gem is installed 40 | if ! gem list -i xcodeproj &>/dev/null; then 41 | echo "The 'xcodeproj' gem is not installed." 42 | read -p "Would you like to install it now? [Y/n] " response 43 | if [[ "$response" =~ ^[Nn]$ ]]; then 44 | echo "Please install 'xcodeproj' manually and re-run the script." 45 | exit 1 46 | fi 47 | gem install xcodeproj || { echo "Failed to install 'xcodeproj'."; exit 1; } 48 | fi 49 | 50 | # Convert project path to an absolute path if it is relative 51 | if [[ ! "$project_path" = /* ]]; then 52 | project_path="$(realpath "$project_path")" 53 | fi 54 | 55 | # Absolute path of the script and the tool's root directory 56 | script_path="$(realpath "$0")" 57 | tool_root_path="$(dirname "$script_path")" 58 | 59 | tool_portable_path="$tool_root_path" 60 | # If the tool's root directory is inside the project path, make the path portable 61 | if [[ "$tool_root_path" == "$project_path"* ]]; then 62 | # Extract the path of the tool's root directory relative to the project path 63 | tool_relative_path="${tool_root_path#$project_path}" 64 | # Formulate a portable path using the `PROJECT_DIR` environment variable provided by Xcode 65 | tool_portable_path="\${PROJECT_DIR}${tool_relative_path}" 66 | fi 67 | 68 | run_script_content="\"$tool_portable_path/fixer.sh\" ${options[@]}" 69 | 70 | # Execute the Ruby helper script 71 | ruby "$tool_root_path/Helper/xcode_install_helper.rb" "$project_path" "$run_script_content" "$install_builds_only" 72 | -------------------------------------------------------------------------------- /uninstall.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Copyright (c) 2024, crasowas. 4 | # 5 | # Use of this source code is governed by a MIT-style license 6 | # that can be found in the LICENSE file or at 7 | # https://opensource.org/licenses/MIT. 8 | 9 | set -e 10 | 11 | # Check if the project path is provided 12 | if [[ $# -eq 0 ]]; then 13 | echo "Usage: $0 " 14 | exit 1 15 | fi 16 | 17 | project_path="$1" 18 | 19 | # Verify Ruby installation 20 | if ! command -v ruby &>/dev/null; then 21 | echo "Ruby is not installed. Please install Ruby and try again." 22 | exit 1 23 | fi 24 | 25 | # Check if xcodeproj gem is installed 26 | if ! gem list -i xcodeproj &>/dev/null; then 27 | echo "The 'xcodeproj' gem is not installed." 28 | read -p "Would you like to install it now? [Y/n] " response 29 | if [[ "$response" =~ ^[Nn]$ ]]; then 30 | echo "Please install 'xcodeproj' manually and re-run the script." 31 | exit 1 32 | fi 33 | gem install xcodeproj || { echo "Failed to install 'xcodeproj'."; exit 1; } 34 | fi 35 | 36 | # Convert project path to an absolute path if it is relative 37 | if [[ ! "$project_path" = /* ]]; then 38 | project_path="$(realpath "$project_path")" 39 | fi 40 | 41 | # Absolute path of the script and the tool's root directory 42 | script_path="$(realpath "$0")" 43 | tool_root_path="$(dirname "$script_path")" 44 | 45 | # Execute the Ruby helper script 46 | ruby "$tool_root_path/Helper/xcode_uninstall_helper.rb" "$project_path" 47 | -------------------------------------------------------------------------------- /upgrade.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Copyright (c) 2024, crasowas. 4 | # 5 | # Use of this source code is governed by a MIT-style license 6 | # that can be found in the LICENSE file or at 7 | # https://opensource.org/licenses/MIT. 8 | 9 | set -e 10 | 11 | # Absolute path of the script and the tool's root directory 12 | script_path="$(realpath "$0")" 13 | tool_root_path="$(dirname "$script_path")" 14 | 15 | # Repository details 16 | readonly REPO_OWNER="crasowas" 17 | readonly REPO_NAME="app_privacy_manifest_fixer" 18 | 19 | # URL to fetch the latest release information 20 | readonly LATEST_RELEASE_URL="https://api.github.com/repos/$REPO_OWNER/$REPO_NAME/releases/latest" 21 | 22 | # Fetch the release information from GitHub API 23 | release_info=$(curl -s "$LATEST_RELEASE_URL") 24 | 25 | # Extract the latest release version, download URL, and published time 26 | latest_version=$(echo "$release_info" | grep -o '"tag_name": "[^"]*' | sed 's/"tag_name": "//') 27 | download_url=$(echo "$release_info" | grep -o '"zipball_url": "[^"]*' | sed 's/"zipball_url": "//') 28 | published_time=$(echo "$release_info" | grep -o '"published_at": "[^"]*' | sed 's/"published_at": "//') 29 | 30 | # Ensure the latest version, download URL, and published time are successfully retrieved 31 | if [ -z "$latest_version" ] || [ -z "$download_url" ] || [ -z "$published_time" ]; then 32 | echo "Unable to fetch the latest release information." 33 | echo "Request URL: $LATEST_RELEASE_URL" 34 | echo "Response Data: $release_info" 35 | exit 1 36 | fi 37 | 38 | # Convert UTC time to local time 39 | published_time=$(TZ=UTC date -j -f "%Y-%m-%dT%H:%M:%SZ" "$published_time" +"%s" | xargs -I{} date -j -r {} +"%Y-%m-%d %H:%M:%S %z") 40 | 41 | # Read the current tool's version from the VERSION file 42 | tool_version_file="$tool_root_path/VERSION" 43 | if [ ! -f "$tool_version_file" ]; then 44 | echo "VERSION file not found." 45 | exit 1 46 | fi 47 | 48 | local_version="$(cat "$tool_version_file")" 49 | 50 | # Skip upgrade if the current version is already the latest 51 | if [ "$local_version" == "$latest_version" ]; then 52 | echo "Version $latest_version • $published_time" 53 | echo "Already up-to-date." 54 | exit 0 55 | fi 56 | 57 | # Create a temporary directory for downloading the release 58 | temp_dir=$(mktemp -d) 59 | trap "rm -rf $temp_dir" EXIT 60 | 61 | download_file_name="latest-release.tar.gz" 62 | 63 | # Download the latest release archive 64 | echo "Downloading version $latest_version..." 65 | curl -L "$download_url" -o "$temp_dir/$download_file_name" 66 | 67 | # Check if the download was successful 68 | if [ $? -ne 0 ]; then 69 | echo "Download failed, please check your network connection and try again." 70 | exit 1 71 | fi 72 | 73 | # Extract the downloaded release archive 74 | echo "Extracting files..." 75 | tar -xzf "$temp_dir/$download_file_name" -C "$temp_dir" 76 | 77 | # Find the extracted release 78 | extracted_release_path=$(find "$temp_dir" -mindepth 1 -maxdepth 1 -type d -name "*$REPO_NAME*" | head -n 1) 79 | 80 | # Verify that an extracted release was found 81 | if [ -z "$extracted_release_path" ]; then 82 | echo "No extracted release found for the latest version." 83 | exit 1 84 | fi 85 | 86 | user_templates_dir="$tool_root_path/Templates/UserTemplates" 87 | user_templates_backup_dir="$temp_dir/Templates/UserTemplates" 88 | 89 | # Backup the user templates directory if it exists 90 | if [ -d "$user_templates_dir" ]; then 91 | echo "Backing up user templates..." 92 | mkdir -p "$user_templates_backup_dir" 93 | rsync -a --exclude='.*' "$user_templates_dir/" "$user_templates_backup_dir/" 94 | fi 95 | 96 | # Replace old version files with the new version files 97 | echo "Replacing old version files..." 98 | rsync -a --delete "$extracted_release_path/" "$tool_root_path/" 99 | 100 | # Restore the user templates from the backup 101 | if [ -d "$user_templates_backup_dir" ]; then 102 | echo "Restoring user templates..." 103 | rsync -a --exclude='.*' "$user_templates_backup_dir/" "$user_templates_dir/" 104 | fi 105 | 106 | # Upgrade complete 107 | echo "Version $latest_version • $published_time" 108 | echo "Upgrade completed successfully!" 109 | --------------------------------------------------------------------------------