Commit f5a086e0 by pidan

提交隐私策略脚本

parent 1cc26998
...@@ -191,7 +191,7 @@ ...@@ -191,7 +191,7 @@
E4508BFDD31A448835C4240A /* [CP] Embed Pods Frameworks */, E4508BFDD31A448835C4240A /* [CP] Embed Pods Frameworks */,
FB49A0F918199836D41DAF4D /* [CP] Copy Pods Resources */, FB49A0F918199836D41DAF4D /* [CP] Copy Pods Resources */,
BED3D2C32C58DC7A0028E28F /* Embed PlugIns */, BED3D2C32C58DC7A0028E28F /* Embed PlugIns */,
D22E79862D9A863C00A3E6FC /* ShellScript */, D22E79862D9A863C00A3E6FC /* Fix Privacy Manifest */,
); );
buildRules = ( buildRules = (
); );
...@@ -277,8 +277,9 @@ ...@@ -277,8 +277,9 @@
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0; showEnvVarsInLog = 0;
}; };
D22E79862D9A863C00A3E6FC /* ShellScript */ = { D22E79862D9A863C00A3E6FC /* Fix Privacy Manifest */ = {
isa = PBXShellScriptBuildPhase; isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 12; buildActionMask = 12;
files = ( files = (
); );
...@@ -286,14 +287,15 @@ ...@@ -286,14 +287,15 @@
); );
inputPaths = ( inputPaths = (
); );
name = "Fix Privacy Manifest";
outputFileListPaths = ( outputFileListPaths = (
); );
outputPaths = ( outputPaths = (
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh; shellPath = /bin/sh;
shellScript = " shellScript = "\"${PROJECT_DIR}/app_privacy_manifest_fixer/fixer.sh\"\n";
"; showEnvVarsInLog = 0;
}; };
E4508BFDD31A448835C4240A /* [CP] Embed Pods Frameworks */ = { E4508BFDD31A448835C4240A /* [CP] Embed Pods Frameworks */ = {
isa = PBXShellScriptBuildPhase; isa = PBXShellScriptBuildPhase;
......
# macOS
.DS_Store
# Build
/Build/
## 1.4.1
- Fix macOS app re-signing issue.
- Automatically enable Hardened Runtime in macOS codesign.
- Add clean script.
## 1.4.0
- Support for macOS app ([#9](https://github.com/crasowas/app_privacy_manifest_fixer/issues/9)).
## 1.3.11
- Fix install issue by skipping `PBXAggregateTarget` ([#4](https://github.com/crasowas/app_privacy_manifest_fixer/issues/4)).
## 1.3.10
- Fix app re-signing issue.
- Enhance Build Phases script robustness.
## 1.3.9
- Add log file output.
## 1.3.8
- Add version info to privacy access report.
- Remove empty tables from privacy access report.
## 1.3.7
- Enhance API symbols analysis with strings tool.
- Improve performance of API usage analysis.
## 1.3.5
- Fix issue with inaccurate privacy manifest search.
- Disable dependency analysis to force the script to run on every build.
- Add placeholder for privacy access report.
- Update build output directory naming convention.
- Add examples for privacy access report.
## 1.3.0
- Add privacy access report generation.
## 1.2.3
- Fix issue with relative path parameter.
- Add support for all application targets.
## 1.2.1
- Fix backup issue with empty user templates directory.
## 1.2.0
- Add uninstall script.
## 1.1.2
- Remove `Templates/.gitignore` to track `UserTemplates`.
- Fix incorrect use of `App.xcprivacy` template in `App.framework`.
## 1.1.0
- Add logs for latest release fetch failure.
- Fix issue with converting published time to local time.
- Disable showing environment variables in the build log.
- Add `--install-builds-only` command line option.
## 1.0.0
- Initial version.
\ No newline at end of file
#!/bin/bash
# Copyright (c) 2025, 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.
set -e
# Prevent duplicate loading
if [ -n "$CONSTANTS_SH_LOADED" ]; then
return
fi
readonly CONSTANTS_SH_LOADED=1
# File name of the privacy manifest
readonly PRIVACY_MANIFEST_FILE_NAME="PrivacyInfo.xcprivacy"
# Common privacy manifest template file names
readonly APP_TEMPLATE_FILE_NAME="AppTemplate.xcprivacy"
readonly FRAMEWORK_TEMPLATE_FILE_NAME="FrameworkTemplate.xcprivacy"
# Universal delimiter
readonly DELIMITER=":"
# Space escape symbol for handling space in path
readonly SPACE_ESCAPE="\u0020"
# Default value when the version cannot be retrieved
readonly UNKNOWN_VERSION="unknown"
# 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/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"
)
#!/bin/bash
# Copyright (c) 2025, 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.
set -e
# Prevent duplicate loading
if [ -n "$UTILS_SH_LOADED" ]; then
return
fi
readonly UTILS_SH_LOADED=1
# Absolute path of the script and the tool's root directory
script_path="$(realpath "${BASH_SOURCE[0]}")"
tool_root_path="$(dirname "$(dirname "$script_path")")"
# Load common constants
source "$tool_root_path/Common/constants.sh"
# 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"
}
# Get the dependency name by removing common suffixes
function get_dependency_name() {
local path="$1"
local dir_name="$(basename "$path")"
# Remove `.app`, `.framework`, and `.xcframework` suffixes
local dep_name="${dir_name%.*}"
echo "$dep_name"
}
# Get the executable name from the specified `Info.plist` file
function get_plist_executable() {
local plist_file="$1"
if [ ! -f "$plist_file" ]; then
echo ""
else
/usr/libexec/PlistBuddy -c "Print :CFBundleExecutable" "$plist_file" 2>/dev/null || echo ""
fi
}
# Get the version from the specified `Info.plist` file
function get_plist_version() {
local plist_file="$1"
if [ ! -f "$plist_file" ]; then
echo "$UNKNOWN_VERSION"
else
/usr/libexec/PlistBuddy -c "Print :CFBundleShortVersionString" "$plist_file" 2>/dev/null || echo "$UNKNOWN_VERSION"
fi
}
# Get the path of the specified framework version
function get_framework_path() {
local path="$1"
local version_path="$2"
if [ -z "$version_path" ]; then
echo "$path"
else
echo "$path/$version_path"
fi
}
# Search for privacy manifest files in the specified directory
function search_privacy_manifest_files() {
local 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 "$path" -type f -name "$PRIVACY_MANIFEST_FILE_NAME" -print0 2>/dev/null > "$temp_file"
while IFS= read -r -d '' file; do
privacy_manifest_files+=($(encode_path "$file"))
done < "$temp_file"
echo "${privacy_manifest_files[@]}"
}
# Get the privacy manifest file with the shortest path
function get_privacy_manifest_file() {
local privacy_manifest_file="$(printf "%s\n" "$@" | awk '{print length, $0}' | sort -n | head -n1 | cut -d ' ' -f2-)"
echo "$(decode_path "$privacy_manifest_file")"
}
# 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.
require 'xcodeproj'
RUN_SCRIPT_PHASE_NAME = 'Fix Privacy Manifest'
if ARGV.length < 2
puts "Usage: ruby xcode_install_helper.rb <project_path> <script_content> [install_builds_only (true|false)]"
exit 1
end
project_path = ARGV[0]
run_script_content = ARGV[1]
install_builds_only = ARGV[2] == 'true'
# Find the first .xcodeproj file in the project directory
xcodeproj_path = Dir.glob(File.join(project_path, "*.xcodeproj")).first
# Validate the .xcodeproj file existence
unless xcodeproj_path
puts "Error: No .xcodeproj file found in the specified directory."
exit 1
end
# Open the Xcode project file
begin
project = Xcodeproj::Project.open(xcodeproj_path)
rescue StandardError => e
puts "Error: Unable to open the project file - #{e.message}"
exit 1
end
# Process all targets in the project
project.targets.each do |target|
# Skip PBXAggregateTarget
if target.is_a?(Xcodeproj::Project::Object::PBXAggregateTarget)
puts "Skipping aggregate target: #{target.name}."
next
end
# Check if the target is a native application target
if target.product_type == 'com.apple.product-type.application'
puts "Processing target: #{target.name}..."
# Check for an existing Run Script phase with the specified name
existing_phase = target.shell_script_build_phases.find { |phase| phase.name == RUN_SCRIPT_PHASE_NAME }
# Remove the existing Run Script phase if found
if existing_phase
puts " - Removing existing Run Script."
target.build_phases.delete(existing_phase)
end
# Add the new Run Script phase at the end
puts " - Adding new Run Script."
new_phase = target.new_shell_script_build_phase(RUN_SCRIPT_PHASE_NAME)
new_phase.shell_script = run_script_content
# Disable showing environment variables in the build log
new_phase.show_env_vars_in_log = '0'
# Run only for deployment post-processing if install_builds_only is true
new_phase.run_only_for_deployment_postprocessing = install_builds_only ? '1' : '0'
# Disable dependency analysis to force the script to run on every build, unless restricted to deployment builds by post-processing setting
new_phase.always_out_of_date = '1'
else
puts "Skipping non-application target: #{target.name}."
end
end
# Save the project file
begin
project.save
puts "Successfully added the Run Script phase: '#{RUN_SCRIPT_PHASE_NAME}'."
rescue StandardError => e
puts "Error: Unable to save the project file - #{e.message}"
exit 1
end
# 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.
require 'xcodeproj'
RUN_SCRIPT_PHASE_NAME = 'Fix Privacy Manifest'
if ARGV.length < 1
puts "Usage: ruby xcode_uninstall_helper.rb <project_path>"
exit 1
end
project_path = ARGV[0]
# Find the first .xcodeproj file in the project directory
xcodeproj_path = Dir.glob(File.join(project_path, "*.xcodeproj")).first
# Validate the .xcodeproj file existence
unless xcodeproj_path
puts "Error: No .xcodeproj file found in the specified directory."
exit 1
end
# Open the Xcode project file
begin
project = Xcodeproj::Project.open(xcodeproj_path)
rescue StandardError => e
puts "Error: Unable to open the project file - #{e.message}"
exit 1
end
# Process all targets in the project
project.targets.each do |target|
# Check if the target is an application target
if target.product_type == 'com.apple.product-type.application'
puts "Processing target: #{target.name}..."
# Check for an existing Run Script phase with the specified name
existing_phase = target.shell_script_build_phases.find { |phase| phase.name == RUN_SCRIPT_PHASE_NAME }
# Remove the existing Run Script phase if found
if existing_phase
puts " - Removing existing Run Script."
target.build_phases.delete(existing_phase)
else
puts " - No existing Run Script found."
end
else
puts "Skipping non-application target: #{target.name}."
end
end
# Save the project file
begin
project.save
puts "Successfully removed the Run Script phase: '#{RUN_SCRIPT_PHASE_NAME}'."
rescue StandardError => e
puts "Error: Unable to save the project file - #{e.message}"
exit 1
end
MIT License
Copyright (c) 2024 crasowas
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
<!--
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.
-->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Privacy Access Report</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 20px;
color: #333;
background-color: #f9f9f9;
line-height: 1.6;
}
.card {
background-color: #fff;
border-radius: 10px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
margin-bottom: 20px;
padding: 20px;
min-width: 735px;
}
h2 {
font-size: 1.2em;
margin: 0 0 15px;
padding: 12px 20px;
color: #fff;
background-color: #5a9e6d;
border-radius: 8px;
display: flex;
justify-content: space-between;
align-items: center;
}
h2 .version {
font-size: 0.7em;
color: #5a9e6d;
background: #f1f1f1;
padding: 2px 6px;
border-radius: 6px;
}
a {
text-decoration: none;
color: #5a9e6d;
background-color: #fcfcfc;
padding: 8px 16px;
border: 1px solid #5a9e6d;
border-radius: 5px;
font-size: 0.9em;
margin-right: 16px;
transition: background-color 0.3s ease, color 0.3s ease;
}
a:hover {
color: #fff;
background-color: #5a9e6d;
}
a.warning {
color: #e0b73c;
background-color: #fcfcfc;
border: 1px solid #e0b73c;
}
a.warning:hover {
color: #fff;
background-color: #e0b73c;
}
table {
width: 100%;
border-collapse: collapse;
background-color: #fff;
border-radius: 8px;
overflow: hidden;
margin-top: 20px;
}
th,
td {
border: 1px solid #ddd;
padding: 12px 20px;
text-align: left;
}
th {
color: #fff;
background-color: #b0b8b1;
font-weight: bold;
}
tbody tr:nth-child(odd) {
background-color: #f9f9f9;
}
tbody tr:hover {
background-color: #f0f0f0;
}
</style>
</head>
<body>
<div class="card" style="display: flex; justify-content: space-between; align-items: center;">
<span>
This report was generated using version <strong>{{TOOL_VERSION}}</strong>.
</span>
<a href="https://github.com/crasowas/app_privacy_manifest_fixer" target="_blank">Like this
project? 🌟Star it on GitHub!</a>
</div>
{{REPORT_CONTENT}}
</body>
</html>
\ No newline at end of file
#!/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.
set -e
# Absolute path of the script and the tool's root directory
script_path="$(realpath "$0")"
tool_root_path="$(dirname "$(dirname "$script_path")")"
# Load common constants and utils
source "$tool_root_path/Common/constants.sh"
source "$tool_root_path/Common/utils.sh"
# Path to the app
app_path="$1"
# Check if the app exists
if [ ! -d "$app_path" ] || [[ "$app_path" != *.app ]]; then
echo "Unable to find the app: $app_path"
exit 1
fi
# Check if the app is iOS or macOS
is_ios_app=true
frameworks_dir="$app_path/Frameworks"
if [ -d "$app_path/Contents/MacOS" ]; then
is_ios_app=false
frameworks_dir="$app_path/Contents/Frameworks"
fi
report_output_file="$2"
# Additional arguments as template usage records
template_usage_records=("${@:2}")
# Copy report template to output file
report_template_file="$tool_root_path/Report/report-template.html"
if ! rsync -a "$report_template_file" "$report_output_file"; then
echo "Failed to copy the report template to $report_output_file"
exit 1
fi
# Read the current tool's version from the VERSION file
tool_version_file="$tool_root_path/VERSION"
tool_version="N/A"
if [ -f "$tool_version_file" ]; then
tool_version="$(cat "$tool_version_file")"
fi
# Initialize report content
report_content=""
# Get the template file used for fixing based on the app or framework name
function get_used_template_file() {
local name="$1"
for template_usage_record in "${template_usage_records[@]}"; do
if [[ "$template_usage_record" == "$name$DELIMITER"* ]]; then
echo "${template_usage_record#*$DELIMITER}"
return
fi
done
echo ""
}
# Analyze accessed API types and their corresponding reasons
function analyze_privacy_accessed_api() {
local privacy_manifest_file="$1"
local -a results=()
if [ -f "$privacy_manifest_file" ]; then
local api_count=$(xmllint --xpath 'count(//dict/key[text()="NSPrivacyAccessedAPIType"])' "$privacy_manifest_file")
for ((i=1; i<=api_count; i++)); do
local api_type=$(xmllint --xpath "(//dict/key[text()='NSPrivacyAccessedAPIType']/following-sibling::string[1])[$i]/text()" "$privacy_manifest_file" 2>/dev/null)
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 "/" -)
if [ -z "$api_type" ]; then
api_type="N/A"
fi
if [ -z "$api_reasons" ]; then
api_reasons="N/A"
fi
results+=("$api_type$DELIMITER$api_reasons")
done
fi
echo "${results[@]}"
}
# Get the path to the `Info.plist` file for the specified app or framework
function get_plist_file() {
local path="$1"
local version_path="$2"
local plist_file=""
if [[ "$path" == *.app ]]; then
if [ "$is_ios_app" == true ]; then
plist_file="$path/Info.plist"
else
plist_file="$path/Contents/Info.plist"
fi
elif [[ "$path" == *.framework ]]; then
local framework_path="$(get_framework_path "$path" "$version_path")"
if [ "$is_ios_app" == true ]; then
plist_file="$framework_path/Info.plist"
else
plist_file="$framework_path/Resources/Info.plist"
fi
fi
echo "$plist_file"
}
# Add an HTML <div> element with the `card` class
function add_html_card_container() {
local card="$1"
report_content="$report_content<div class=\"card\">$card</div>"
}
# Generate an HTML <h2> element
function generate_html_header() {
local title="$1"
local version="$2"
echo "<h2>$title<span class=\"version\">Version $version</span></h2>"
}
# Generate an HTML <a> element with optional `warning` class
function generate_html_anchor() {
local text="$1"
local href="$2"
local warning="$3"
if [ "$warning" == true ]; then
echo "<a class=\"warning\" href=\"$href\">$text</a>"
else
echo "<a href=\"$href\">$text</a>"
fi
}
# Generate an HTML <table> element
function generate_html_table() {
local thead="$1"
local tbody="$2"
echo "<table>$thead$tbody</table>"
}
# Generate an HTML <thead> element
function generate_html_thead() {
local ths=("$@")
local tr=""
for th in "${ths[@]}"; do
tr="$tr<th>$th</th>"
done
echo "<thead><tr>$tr</tr></thead>"
}
# Generate an HTML <tbody> element
function generate_html_tbody() {
local trs=("$@")
local tbody=""
for tr in "${trs[@]}"; do
tbody="$tbody<tr>"
local tds=($(split_string_by_delimiter "$tr"))
for td in "${tds[@]}"; do
tbody="$tbody<td>$td</td>"
done
tbody="$tbody</tr>"
done
echo "<tbody>$tbody</tbody>"
}
# Generate the report content for the specified directory
function generate_report_content() {
local path="$1"
local version_path="$2"
local privacy_manifest_file=""
if [[ "$path" == *.app ]]; then
# 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
# 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
if [ "$is_ios_app" == true ]; then
privacy_manifest_file="$path/$PRIVACY_MANIFEST_FILE_NAME"
else
privacy_manifest_file="$path/Contents/Resources/$PRIVACY_MANIFEST_FILE_NAME"
fi
else
# 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
# 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 framework_path="$(get_framework_path "$path" "$version_path")"
local privacy_manifest_files=($(search_privacy_manifest_files "$framework_path"))
privacy_manifest_file="$(get_privacy_manifest_file "${privacy_manifest_files[@]}")"
fi
local name="$(basename "$path")"
local title="$name"
if [ -n "$version_path" ]; then
title="$name ($version_path)"
fi
local plist_file="$(get_plist_file "$path" "$version_path")"
local version="$(get_plist_version "$plist_file")"
local card="$(generate_html_header "$title" "$version")"
if [ -f "$privacy_manifest_file" ]; then
card="$card$(generate_html_anchor "$PRIVACY_MANIFEST_FILE_NAME" "$privacy_manifest_file" false)"
local used_template_file="$(get_used_template_file "$name$version_path")"
if [ -f "$used_template_file" ]; then
card="$card$(generate_html_anchor "Template Used: $(basename "$used_template_file")" "$used_template_file" false)"
fi
local trs=($(analyze_privacy_accessed_api "$privacy_manifest_file"))
# Generate table only if the accessed privacy API types array is not empty
if [[ ${#trs[@]} -gt 0 ]]; then
local thead="$(generate_html_thead "NSPrivacyAccessedAPIType" "NSPrivacyAccessedAPITypeReasons")"
local tbody="$(generate_html_tbody "${trs[@]}")"
card="$card$(generate_html_table "$thead" "$tbody")"
fi
else
card="$card$(generate_html_anchor "Missing Privacy Manifest" "$path" true)"
fi
add_html_card_container "$card"
}
# Generate the report content for app
function generate_app_report_content() {
generate_report_content "$app_path" ""
}
# Generate the report content for frameworks
function generate_frameworks_report_content() {
if ! [ -d "$frameworks_dir" ]; then
return
fi
for path in "$frameworks_dir"/*; do
if [ -d "$path" ]; then
local versions_dir="$path/Versions"
if [ -d "$versions_dir" ]; then
for version in $(ls -1 "$versions_dir" | grep -vE '^Current$'); do
local version_path="Versions/$version"
generate_report_content "$path" "$version_path"
done
else
generate_report_content "$path" ""
fi
fi
done
}
# Generate the final report with all content
function generate_final_report() {
# Replace placeholders in the template with the tool's version and report content
sed -i "" -e "s|{{TOOL_VERSION}}|$tool_version|g" -e "s|{{REPORT_CONTENT}}|${report_content}|g" "$report_output_file"
echo "Privacy Access Report has been generated: $report_output_file"
}
generate_app_report_content
generate_frameworks_report_content
generate_final_report
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSPrivacyTracking</key>
<false/>
<key>NSPrivacyTrackingDomains</key>
<array/>
<key>NSPrivacyCollectedDataTypes</key>
<array/>
<key>NSPrivacyAccessedAPITypes</key>
<array>
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategoryFileTimestamp</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>C617.1</string>
</array>
</dict>
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategorySystemBootTime</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>35F9.1</string>
</array>
</dict>
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategoryDiskSpace</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>E174.1</string>
</array>
</dict>
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategoryActiveKeyboards</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>54BD.1</string>
</array>
</dict>
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategoryUserDefaults</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>CA92.1</string>
</array>
</dict>
</array>
</dict>
</plist>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSPrivacyTracking</key>
<false/>
<key>NSPrivacyTrackingDomains</key>
<array/>
<key>NSPrivacyCollectedDataTypes</key>
<array/>
<key>NSPrivacyAccessedAPITypes</key>
<array>
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategoryFileTimestamp</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>0A2A.1</string>
</array>
</dict>
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategorySystemBootTime</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>35F9.1</string>
</array>
</dict>
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategoryDiskSpace</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>E174.1</string>
</array>
</dict>
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategoryActiveKeyboards</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>54BD.1</string>
</array>
</dict>
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategoryUserDefaults</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>C56D.1</string>
</array>
</dict>
</array>
</dict>
</plist>
v1.4.1
\ No newline at end of file
#!/bin/bash
# Copyright (c) 2025, 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.
set -e
target_paths=("Build")
echo "Cleaning..."
deleted_anything=false
for path in "${target_paths[@]}"; do
if [ -e "$path" ]; then
echo "Removing $path..."
rm -rf "./$path"
deleted_anything=true
fi
done
if [ "$deleted_anything" == true ]; then
echo "Cleanup completed."
else
echo "Nothing to clean."
fi
#!/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.
set -e
# Check if at least one argument (project_path) is provided
if [[ "$#" -lt 1 ]]; then
echo "Usage: $0 <project_path> [options...]"
exit 1
fi
project_path="$1"
shift
options=()
install_builds_only=false
# Check if the `--install-builds-only` option is provided and separate it from other options
for arg in "$@"; do
if [ "$arg" == "--install-builds-only" ]; then
install_builds_only=true
else
options+=("$arg")
fi
done
# Verify Ruby installation
if ! command -v ruby &>/dev/null; then
echo "Ruby is not installed. Please install Ruby and try again."
exit 1
fi
# Check if xcodeproj gem is installed
if ! gem list -i xcodeproj &>/dev/null; then
echo "The 'xcodeproj' gem is not installed."
read -p "Would you like to install it now? [Y/n] " response
if [[ "$response" =~ ^[Nn]$ ]]; then
echo "Please install 'xcodeproj' manually and re-run the script."
exit 1
fi
gem install xcodeproj || { echo "Failed to install 'xcodeproj'."; exit 1; }
fi
# Convert project path to an absolute path if it is relative
if [[ ! "$project_path" = /* ]]; then
project_path="$(realpath "$project_path")"
fi
# Absolute path of the script and the tool's root directory
script_path="$(realpath "$0")"
tool_root_path="$(dirname "$script_path")"
tool_portable_path="$tool_root_path"
# If the tool's root directory is inside the project path, make the path portable
if [[ "$tool_root_path" == "$project_path"* ]]; then
# Extract the path of the tool's root directory relative to the project path
tool_relative_path="${tool_root_path#$project_path}"
# Formulate a portable path using the `PROJECT_DIR` environment variable provided by Xcode
tool_portable_path="\${PROJECT_DIR}${tool_relative_path}"
fi
run_script_content="\"$tool_portable_path/fixer.sh\" ${options[@]}"
# Execute the Ruby helper script
ruby "$tool_root_path/Helper/xcode_install_helper.rb" "$project_path" "$run_script_content" "$install_builds_only"
#!/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.
set -e
# Check if the project path is provided
if [[ $# -eq 0 ]]; then
echo "Usage: $0 <project_path>"
exit 1
fi
project_path="$1"
# Verify Ruby installation
if ! command -v ruby &>/dev/null; then
echo "Ruby is not installed. Please install Ruby and try again."
exit 1
fi
# Check if xcodeproj gem is installed
if ! gem list -i xcodeproj &>/dev/null; then
echo "The 'xcodeproj' gem is not installed."
read -p "Would you like to install it now? [Y/n] " response
if [[ "$response" =~ ^[Nn]$ ]]; then
echo "Please install 'xcodeproj' manually and re-run the script."
exit 1
fi
gem install xcodeproj || { echo "Failed to install 'xcodeproj'."; exit 1; }
fi
# Convert project path to an absolute path if it is relative
if [[ ! "$project_path" = /* ]]; then
project_path="$(realpath "$project_path")"
fi
# Absolute path of the script and the tool's root directory
script_path="$(realpath "$0")"
tool_root_path="$(dirname "$script_path")"
# Execute the Ruby helper script
ruby "$tool_root_path/Helper/xcode_uninstall_helper.rb" "$project_path"
#!/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.
set -e
# Absolute path of the script and the tool's root directory
script_path="$(realpath "$0")"
tool_root_path="$(dirname "$script_path")"
# Repository details
readonly REPO_OWNER="crasowas"
readonly REPO_NAME="app_privacy_manifest_fixer"
# URL to fetch the latest release information
readonly LATEST_RELEASE_URL="https://api.github.com/repos/$REPO_OWNER/$REPO_NAME/releases/latest"
# Fetch the release information from GitHub API
release_info=$(curl -s "$LATEST_RELEASE_URL")
# Extract the latest release version, download URL, and published time
latest_version=$(echo "$release_info" | grep -o '"tag_name": "[^"]*' | sed 's/"tag_name": "//')
download_url=$(echo "$release_info" | grep -o '"zipball_url": "[^"]*' | sed 's/"zipball_url": "//')
published_time=$(echo "$release_info" | grep -o '"published_at": "[^"]*' | sed 's/"published_at": "//')
# Ensure the latest version, download URL, and published time are successfully retrieved
if [ -z "$latest_version" ] || [ -z "$download_url" ] || [ -z "$published_time" ]; then
echo "Unable to fetch the latest release information."
echo "Request URL: $LATEST_RELEASE_URL"
echo "Response Data: $release_info"
exit 1
fi
# Convert UTC time to local time
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")
# Read the current tool's version from the VERSION file
tool_version_file="$tool_root_path/VERSION"
if [ ! -f "$tool_version_file" ]; then
echo "VERSION file not found."
exit 1
fi
local_version="$(cat "$tool_version_file")"
# Skip upgrade if the current version is already the latest
if [ "$local_version" == "$latest_version" ]; then
echo "Version $latest_version$published_time"
echo "Already up-to-date."
exit 0
fi
# Create a temporary directory for downloading the release
temp_dir=$(mktemp -d)
trap "rm -rf $temp_dir" EXIT
download_file_name="latest-release.tar.gz"
# Download the latest release archive
echo "Downloading version $latest_version..."
curl -L "$download_url" -o "$temp_dir/$download_file_name"
# Check if the download was successful
if [ $? -ne 0 ]; then
echo "Download failed, please check your network connection and try again."
exit 1
fi
# Extract the downloaded release archive
echo "Extracting files..."
tar -xzf "$temp_dir/$download_file_name" -C "$temp_dir"
# Find the extracted release
extracted_release_path=$(find "$temp_dir" -mindepth 1 -maxdepth 1 -type d -name "*$REPO_NAME*" | head -n 1)
# Verify that an extracted release was found
if [ -z "$extracted_release_path" ]; then
echo "No extracted release found for the latest version."
exit 1
fi
user_templates_dir="$tool_root_path/Templates/UserTemplates"
user_templates_backup_dir="$temp_dir/Templates/UserTemplates"
# Backup the user templates directory if it exists
if [ -d "$user_templates_dir" ]; then
echo "Backing up user templates..."
mkdir -p "$user_templates_backup_dir"
rsync -a --exclude='.*' "$user_templates_dir/" "$user_templates_backup_dir/"
fi
# Replace old version files with the new version files
echo "Replacing old version files..."
rsync -a --delete "$extracted_release_path/" "$tool_root_path/"
# Restore the user templates from the backup
if [ -d "$user_templates_backup_dir" ]; then
echo "Restoring user templates..."
rsync -a --exclude='.*' "$user_templates_backup_dir/" "$user_templates_dir/"
fi
# Upgrade complete
echo "Version $latest_version$published_time"
echo "Upgrade completed successfully!"
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment