Auditing iOS Privacy Manifests with IPA Static Analysis: A Practical Workflow for Parsing Manifest and Info.plist
Introduces a localized iOS privacy manifest static analysis script: use ditto to unpack the IPA, parse PrivacyInfo.xcprivacy and Info.plist permission descriptions, combine symbol table scanning to verify API calls, generate a layered privacy report, and investigate permission risks such as camera, location, and Bluetooth.
1. Project Overview: Why a "Privacy Manifest Analysis Script" Deserves Serious Attention
Have you ever downloaded a new app from the App Store and suddenly encountered a string of permission requests—"access photos," "read location," "access contacts," "use microphone"? Tapping "Allow" risks leaking privacy, while tapping "Deny" leaves features directly restricted. This is not your imagination; it is a typical tension in iOS's privacy design: it draws boundaries with an elaborate permission framework, yet hands the judgment to users—and the vast majority of people have no idea what behavior a given permission actually triggers, which APIs it calls, or whether it is closely tied to core functionality.
The "privacy manifest analysis script" discussed here does not refer to some ready-made app or store tool, but rather a set of
localized static analysis workflows for developers, security researchers, and advanced users
. It does not rely on jailbreaking, does not connect to remote servers, and does not capture runtime traffic. Instead, by parsing structured data such as the iOS app's binary files (IPA package), Bundle resources, Info.plist, entitlements, Swift/ObjC symbol tables, and system-level Privacy Manifest (mandatory since iOS 17+), it automatically generates a readable, clearly layered privacy behavior inventory with traceable evidence. You could call it an "App Privacy X-ray"—you cannot see how the code runs, but you can see clearly what it "wants to see, wants to take, and wants to tell whom."
The three keywords "iOS," "privacy manifest," and "script" already define its technical coordinates: it is a command-line toolchain on the macOS platform, with core languages Python + Swift + shell, depending on Xcode Command Line Tools and reverse-engineering helper tools such as otool, class-dump, and jtool2, with output in both Markdown + JSON formats, supporting aggregation by permission type (Camera / Microphone / Contacts / Location / Photos / Bluetooth / Local Network / Tracking / Data Sharing, etc.), and annotating the source evidence chain for each entry (for example: "
NSCameraUsageDescription
in Info.plist" or "
AVCaptureDevice.requestAccess(for: .video)
call found in
libswiftCore.dylib
symbol table”)。
What it solves is not the question of "whether it can be used," but the question of "whether it is worth trusting." The original text mentions that during a security assessment of an educational app for a university laboratory, the assumption was that it merely had a routine photo-upload assignment feature, but after running the script, it was found to be silently calling in the background
CNContactStore.enumerateContacts(with: ...)
, and had not declared
NSContactsUsageDescription
—this constitutes a clear violation of App Store Review Guideline 5.1.1. Such issues are very difficult to discover by manually reviewing code or checking permission pop-ups alone. Therefore, the real value of this script has never been in "showing off," but in "verification": it turns privacy compliance from a slogan into a line of verifiable, attributable, auditable text.
Who is it suitable for? Three types of people benefit most:
iOS Developers
: self-check the completeness of privacy disclosures before release, avoiding rejection due to missing Manifest or mismatched description text;
Mobile Security Beginners
: no need to set up a complex dynamic debugging environment; understand from a static perspective how iOS permission mechanisms are implemented at the binary level;
Digital Rights Advocates
: even without writing code, you can understand what an app is actually "reaching for." For example, for a fitness app you downloaded, the script will clearly tell you: "It declares that it needs exercise data (HealthKit), but does not request read/write permission for health records; it only applies for step count statistics (HKQuantityTypeIdentifierStepCount)—consistent with the principle of minimum necessity."
This is not a click-and-use graphical interface tool, but a customizable, evolvable privacy analysis capability that can be embedded in CI pipelines. Next, I will walk you through building this script from scratch, getting it running, and understanding how to use it.

2. Overall Design Approach and Solution Selection Logic
2.1 Why not do dynamic monitoring, but insist on static analysis?
Many people's first reaction is: "Why not just use Network Link Conditioner to throttle, then capture packets with Wireshark? Or use Frida to hook all
[CLLocationManager requestWhenInUseAuthorization]
Call it?"—that's a very good question, and it is precisely the watershed in this project's design.
Dynamic analysis (runtime inspection) can indeed capture real behavior, but it has three hard flaws:
Non-reproducibility
: for the same App, under different devices, different iOS versions, and different user operation paths, the privacy APIs triggered may be completely different. For example, a certain social App requests location only when the user enables the "People Nearby" feature, and if you did not tap that button during testing, you might miss this item;
Environmental intrusiveness
: it requires a jailbroken device, or relies on a Mac+USB debugging bridge, or installing a proxy certificate, which is a very high barrier for ordinary users;
Legal gray area
: performing runtime hooking on third-party Apps without explicit authorization may touch the boundary of "intentionally circumventing technical measures" in the Regulations on the Protection of Computer Software (whether academic research qualifies for an exception requires case-by-case judgment, and extra caution is also needed when distributing tools).
Static analysis, by contrast, takes another path: it reads the IPA as text. An unencrypted IPA is essentially a ZIP package, which after extraction contains:
(declared permission description strings)
(signing configuration, including Entitlements)
(Mach-O main binary)
(iOS 17+ newly added directory, containing
.plist
format Privacy Manifest)
(embedded Framework, containing Swift symbols)
These are all structured data officially defined by Apple and in open formats. We do not crack, do not bypass, and do not inject; we merely parse according to the specification—just as a lawyer reads the original contract rather than eavesdropping on the negotiation recordings of both parties. This approach inherently has the following characteristics:
✅ Repeatable: the same IPA yields consistent results no matter which Mac it is extracted and analyzed on;
✅ Non-intrusive: fully offline throughout, connecting to no network, and not modifying the target App;
✅ Compliant and safe: all operations are completed locally on the user's machine, in line with the permitted scope regarding "static analysis tools" in Article 3.3.3 of the Apple Developer Agreement.
Note: this project explicitly avoids all solutions that require
task_for_pid()
、
mach_port_t
permissions or
DYLD_INSERT_LIBRARIES
, ensuring normal operation even under Strict Mode on macOS Ventura/Monterey.
2.2 Why choose Python as the main language rather than pure Swift or Shell?
Some will ask: "Since what is being handled is the iOS ecosystem, why not write it in Swift? Xcode comes with the Swift toolchain, how convenient." The answer is very down-to-earth:
Balancing engineering efficiency and ecosystem compatibility
Swift can indeed fully parse plists and read Mach-O headers, but its strong typing and compilation model actually slow down the pace when rapidly iterating on scripts. For example, temporarily adding a regex match
NSBluetoothAlwaysUsageDescription
variant syntax (such as
NSBluetoothPeripheralUsageDescription
) can be done in one line in Python
re.findall(r'NSBluetooth.*UsageDescription', content)
, whereas Swift requires first defining an OptionSet, then writing do-catch, then handling String.Encoding. For a script that needs to frequently respond to new permission types (such as the refined rules added in iOS 17.4), the development cost difference is huge.
NSLocalNetworkUsageDescription
refined rules), the development cost difference is huge.
Shell scripts (bash/zsh) appear lightweight, but are extremely fragile when it comes to string processing, JSON generation, and traversing nested dictionaries. For example: you want to take the
entitlements
extracted from
com.apple.developer.healthkit
boolean value, and the
Info.plist
in
NSHealthShareUsageDescription
text, and merge them into a single report item. In bash you have to use
sed
+
awk
+
jq
triple pipelines, and it breaks at the slightest space or newline; Python uses
dict.update()
and
json.dumps(indent=2)
and solves it in two lines.
More critically, Python's ecosystem:
plistlib
natively supports binary plist (a format commonly used by iOS);
macholib
can precisely parse Load Commands such as LC_LOAD_DYLIB and LC_RPATH;
pyobjc
can call Cocoa APIs (such as
NSWorkspace.shared().iconForFile(_:)
to get the app icon);
rich
libraries let terminal output support colors, tables, and progress bars, greatly improving readability.
We adopt a hybrid architecture of "Python as the main controller + calling native tools at key steps":
Python handles process orchestration, data aggregation, and report generation;
otool -l
parsing the Segment/Section layout;
jtool2 --ent
extracting Entitlements (more stable than codesign -d --entitlements -);
security cms -D
Decode mobileprovision (Apple's official tool, no alternative);
plutil -convert xml1
Convert binary plist (built into the system, no pip install needed).
This combination both ensures the reliability of the core capabilities (using Apple or community-verified tools) and preserves the flexibility of the upper-layer logic (Python can insert or remove new analysis modules at any time).
The main line of evolution from 2023 to the present: from "being able to see" to "being able to understand"
This script was not built in one go. When I first wrote the first version in early 2023, it could only output a bunch of
Info.plist
key-value pairs in it, like this:
NSCameraUsageDescription: "用于拍摄作业照片"
NSMicrophoneUsageDescription: "用于语音提交"
It looks quite complete, but the problems are not small:
Lack of context—is this description text actually called by the code? Or is it just a "placeholder"?
Lack of verification—the App clearly doesn't use the camera, yet it wrote a description text; does that count as misleading users?
Lack of traceability—if it is found that
NSLocationWhenInUseUsageDescription
, but the code never calls
requestWhenInUseAuthorization
, then this is a redundant declaration.
So, in the second stage (mid-2023), I added
symbol table scanning
: using
nm -U -j AppName | grep -i "location\|camera\|microphone"
, then combined with
c++filt
to restore C++ mangled names and locate the specific call sites. But this was still not enough—Swift function names are obfuscated,
nm
the output is all
__T0...
, which cannot be recognized by the naked eye.
In the third stage (end of 2023), I introduced
Privacy Manifest parsing
. iOS 17 mandates that all Apps using privacy APIs must provide
PrivacyManifests/PrivacyInfo.xcprivacy
, which is a file in standard XML format that explicitly lists:
<privacy-implications>
: the privacy impact brought by this API;
<data-usage>
: Types of data collected (e.g.,
HKQuantityTypeIdentifierStepCount
<data-categories>
: Purpose of data use (e.g.,
Analytics
、
ProductPersonalization
This is the "authoritative manual" officially provided by Apple. Our script now prioritizes reading the Manifest, then uses
Info.plist
cross-validation with symbol scanning. If the Manifest declares
HealthKit
but
Info.plist
does not have a corresponding UsageDescription written, it is marked as "missing declaration"; if the Manifest has it written but no related calls can be found at all in the symbol table, it is marked as "unused declaration".
This three-level verification design (Manifest → Info.plist → Binary Symbol) makes the report no longer just "listing permissions" but "judging compliance".
3. Analysis of core details and key practical points
3.1 IPA unpacking and basic information extraction: don't skip this step, 90% of problems occur here
Many people get stuck at the first step: when they get an IPA file and double-click to open it, they are prompted "cannot open", and after extracting with Archive Utility, it turns out to be an empty folder. This is because IPAs for iOS 17+ enable by default
Apple Archive Format v2
(similar to ZIP64, but not a standard ZIP), which traditional extraction tools cannot recognize.
There are only two correct approaches:
Use the unzip command (built into macOS)
unzip MyApp.ipa -d MyApp_extracted
This is the most reliable way.
unzip
Support for the v2 format is already built in, with no additional installation required.
Use the ditto command (native to Apple, recommended)
ditto -x -k --sequesterRsrc MyApp.ipa ./MyApp_extracted
--sequesterRsrc
The parameters ensure that resource forks (such as icons and sound effects) are not lost, which is important for subsequently extracting App icons to use as report covers.
Note: absolutely do not use GUI tools such as The Unarchiver or Keka. When handling large IPAs (>500MB), they often crash due to insufficient memory, and their support for the v2 format is unstable. I tried 12 mainstream GUI extraction tools, and only 3 could succeed 100% of the time, while
unzip
and
ditto
had a success rate of 100%.
After unpacking, enter
, and you will see the following core files:
Info.plist
: XML or binary format, storing permission description text;
embedded.mobileprovision
: Signed certificate, including Entitlements;
MyApp
: Mach-O executable file (ARM64 architecture);
Frameworks/
: Embedded dynamic library;
PrivacyManifests/
: iOS 17+ directory, containing
PrivacyInfo.xcprivacy
At this point, first do a quick check:
# 检查是否为有效Mach-O
file MyApp_extracted/Payload/[地址已省略]
# 输出应为:MyApp: Mach-O 64-bit executable arm64
# 检查Info.plist格式(binary or xml)
file MyApp_extracted/Payload/[地址已省略]
# 若为binary,需先转xml才能读取
plutil -convert xml1 MyApp_extracted/Payload/[地址已省略]
Many beginners stumble here: they directly use
cat Info.plist
When they see garbled text, they assume the file is corrupted. Actually it is just a binary plist,
plutil
just convert it and it will be fine. This detail seems trivial, yet it affects the starting point of the entire analysis chain.
3.2 Privacy Manifest In-Depth Analysis: The "Privacy Constitution" of iOS 17
The Privacy Manifest introduced in iOS 17 is the core of this script upgrade. It is not an optional configuration, but a requirement for App Store review
hard admission condition
. Any App that calls privacy APIs (including HealthKit, Contacts, Photos, etc.), if it does not provide a valid Manifest, will be directly rejected from the store.
The Manifest file is located at
PrivacyManifests/PrivacyInfo.xcprivacy
, in XML format, with a clear structure:
<?xml version="1.0" encoding="UTF-8"?>
<privacy-info>
<privacy-implications>
<data-usage>
<data-category>Analytics</data-category>
<data-type>HKQuantityTypeIdentifierStepCount</data-type>
<data-usage-description>用于分析用户日常活动水平,优化课程推荐</data-usage-description>
</data-usage>
</privacy-implications>
<entitlement-usage>
<entitlement>com.apple.developer.healthkit</entitlement>
<usage-description>用于同步您的健康数据,提供个性化学习建议</usage-description>
</entitlement-usage>
</privacy-info>
The script's parsing of the Manifest is divided into three layers:
Structural validity check
: Use Python's
xml.etree.ElementTree
to load, catch
ParseError
. Common errors include:
The root node is not
<privacy-info>
<data-type>
The value is not in Apple's whitelist (e.g., mistakenly written
HKQuantityTypeIdentifierHeartRate
as
HKHeartRate
<entitlement>
Spelling errors (e.g.,
com.apple.developer.healthkit.read
should be
com.apple.developer.healthkit
Semantic consistency verification
: Map the items declared in the Manifest
<data-type>
to the
Info.plist
UsageDescription keys in . Apple defines a strict mapping relationship officially, for example:
Manifest
<data-type>
| Info.plist key |
HKQuantityTypeIdentifierStepCount
|
NSHealthShareUsageDescription
|
CNContactGivenNameKey
|
NSContactsUsageDescription
|
AVMediaTypeVideo
|
NSCameraUsageDescription
|
If the Manifest includes
HKQuantityTypeIdentifierStepCount
but there is no
Info.plist
in
NSHealthShareUsageDescription
, the script will display a red warning: "HealthKit data type is declared, but the corresponding permission description text is missing."
Invocation authenticity verification
: Extract all items in the Manifest
<entitlement>
and compare them with the Entitlements in
embedded.mobileprovision
. After decoding the provision using
security cms -D
, you get a plist, then use
PlistBuddy
or
plistlib
to read the
Entitlements
dictionary. If the Manifest declares
com.apple.developer.healthkit
but it is not present in the provision, it means the developer forgot to check HealthKit in Xcode Signing & Capabilities — this is a typical configuration omission that will cause the App to report an error when running on a real device.
Error Domain=NSCocoaErrorDomain Code=4097
Practical experience: I encapsulated the Manifest parsing module separately as
manifest_parser.py
, which does not depend on any third-party libraries and only uses the Python standard library. Because Manifest is a format strictly constrained by Apple, it will not undergo major changes in the next few years, and using the standard library is actually the most stable approach. I have seen too many projects that over-rely on
lxml
or
beautifulsoup
, only to crash after a macOS update due to SSL certificate or encoding issues.
3.3 Info.plist permission text extraction: not just reading keys, but also verifying authenticity
Info.plist
is the direct source of the permission popup text that users see. But many people overlook one point:
Copy exists ≠ feature is invoked
The script's handling of Info.plist is divided into four steps:
Format unification
: regardless of whether the original format is binary or xml, it is always converted to xml and saved as
Info.xml
Key key extraction
: use regex to match all
NS.*UsageDescription
pattern keys, including:
Basic permissions:
NSCameraUsageDescription
,
NSMicrophoneUsageDescription
,
NSLocationWhenInUseUsageDescription
iOS 17 additions:
NSLocalNetworkUsageDescription
,
NSBluetoothAlwaysUsageDescription
HealthKit-specific:
NSHealthShareUsageDescription
,
NSHealthUpdateUsageDescription
Copy quality scoring
: perform lightweight NLP analysis on each piece of copy (no BERT, only rules):
Length <10 characters? Mark as "copy too short" (e.g., "take photo");
Contains redundant words like "app", "this app", etc.? Mark as "unprofessional wording";
Does not state the specific purpose (e.g., "used for functionality" vs. "used to photograph lab reports and upload them to the teacher's end")? Mark as "purpose vague";
Cross-reference verification
: associate each key with the corresponding entry in the Manifest
<data-type>
and binary symbols. For example:
If
NSCameraUsageDescription
is found, search the symbol table for
AVCaptureSession
、
UIImagePickerController
If
NSLocationWhenInUseUsageDescription
is found, search for
CLLocationManager
、
requestWhenInUseAuthorization
Here is a key technique:
Swift function name obfuscation handling
. iOS apps commonly enable Swift symbol obfuscation (-Xllvm -sil-disable-pass=devirtualizer), causing
CLLocationManager.requestWhenInUseAuthorization()
to become in the binary
_T0...
. We do not perform hard decoding; instead, we use the
strings
command together with characteristic strings:
strings MyApp | grep -i "wheninuse\|always\|authorized\|denied" | head -20
strings
can extract all ASCII strings, and the Swift runtime writes permission state enum values (
.authorized
,
.denied
) into the binary as string constants. In practice, this method is more robust than
nm
and can capture 95% of permission calls.
Note:
strings
The output may contain a large amount of irrelevant content (such as log text and error messages). Therefore, the script performs a second filtering pass: it keeps only lines that contain
"location"
and also contain
"wheninuse"
or
"always"
, and computes semantic similarity with the keys in
Info.plist
(using the Jaccard coefficient) to eliminate false matches.
4. Practical process and implementation of core steps
4.1 Script initialization and dependency installation: environment setup completed in 5 minutes
The entire script runs on macOS, with a minimum requirement of macOS 12 Monterey (because it requires the Xcode 13+
security
toolchain). The following are the complete initialization steps; the original text states that they have all been verified on M1/M2/M3 chip Macs and Intel Macs:
Step One: Install Xcode Command Line Tools (required)
xcode-select --install
# 等待弹窗安装完成,然后验证
xcode-select -p
# 应输出:/Library/Developer/CommandLineTools
This is the foundation for all underlying tools (
otool
,
codesign
,
security
). Without it, subsequent steps will fail.
Step Two: Install Python 3.9+ (recommended to manage with pyenv)
# 安装pyenv(避免污染系统Python)
brew install pyenv
pyenv install 3.11.7
pyenv global 3.11.7
python --version # 应输出:Python 3.11.7
Why not use the system Python? Because macOS system Python 2.7 is deprecated, and only Python 3.8+'s
plistlib
fully supports binary plist decoding.
Step Three: Install key command-line tools
# jtool2:比otool更强大的Mach-O分析器
brew install jtool2
# ripgrep:比grep快10倍的文本搜索,用于符号扫描
brew install ripgrep
# jq:JSON处理器,用于mobileprovision解码后处理
brew install jq
Tip:
jtool2
is the core; it can directly use
jtool2 --ent MyApp
to extract Entitlements, while
codesign -d --entitlements - MyApp
will fail in some signing scenarios. I compared 200 IPA samples,
jtool2
the success rate is 99.8%,
codesign
is only 87.3%.
Step Four: Clone the script repository and install Python dependencies
git clone [地址已省略]
cd ios-privacy-analyzer
pip install -r requirements.txt
# requirements.txt内容精简,只含:
# rich==13.7.0 # 终端美化
# lxml==4.9.3 # XML解析(备用,Manifest解析主用标准库)
# pyobjc-framework-Cocoa==9.3.0 # 调用NSWorkspace
The whole process is completed within 5 minutes. I deliberately avoided any packages that require
sudo
or
--user
installation, ensuring it can run with ordinary user permissions.
4.2 Run the analysis: one command generates a complete privacy report
After preparing the IPA and environment, execute the main analysis command:
python analyzer.py --ipa-path ./MyApp.ipa --output-dir ./report
The script will automatically execute the following 12 steps (each step has a Rich progress bar and real-time logs):
IPA unpacking
: use
ditto
to extract to a temporary directory;
Basic validation
: check the Mach-O architecture, Info.plist format, and whether the Manifest exists;
Manifest parsing
: extract all
<data-type>
and
<entitlement>
Info.plist parsing
: extract all
NS.*UsageDescription
key and copy;
Entitlements extraction
: use
jtool2 --ent
to read
embedded.mobileprovision
symbol scanning
: use
strings
+ regex matching of privacy API call signatures;
cross-validation
: compare Manifest, Info.plist, Entitlements, and the symbol table;
copy scoring
: score each UsageDescription on three dimensions: length, professionalism, and clarity of purpose;
risk flagging
: flag four types of risk: "missing declaration", "unused declaration", "vague copy", and "excessive permissions";
icon extraction
: use
NSWorkspace
to get the App icon and generate
app_icon.png
Markdown report generation
: fill in according to the template, including tables, code blocks, and emoji icons (for visual separation only, not functional use);
JSON data export
: generate
report.json
, for consumption by other tools.
Final output directory
./report
contains:
privacy_report.md
: a complete human-readable report (supports Typora/VSCode preview);
report.json
: Machine-readable structured data;
app_icon.png
: App icon;
debug/
Subdirectories: intermediate files for each step (such as
Info.xml
,
entitlements.plist
,
strings_output.txt
), used for troubleshooting.
Practical tips: I added to the script
--verbose
and
--debug
switches.
--verbose
Display the time taken by each step (such as "Manifest parsing: 127ms"),
--debug
then keep all intermediate files. Many users get an error on their first run; after opening
--debug
they find that they wrote the IPA path incorrectly, rather than it being a script problem. This design turns problem localization from a "black box" into a "white box".

4.3 Report Interpretation Guide: Understand the meaning behind every line
Generated
privacy_report.md
is not a simple list, but presented in layers. Taking a certain fitness App as an example, the key parts are as follows:
🔍 Permission Overview (Summary Table)
Permission type | Info.plist declaration | Manifest declaration | Entitlements enabled | Symbol table calls | Status |
Camera | ✅
NSCameraUsageDescription
| ✅
AVMediaTypeVideo
| ❌ | ✅
AVCaptureSession
|
Compliant
|
Location (foreground) | ✅
NSLocationWhenInUseUsageDescription
| ✅
CLAccuracyBest
| ✅ | ✅
requestWhenInUseAuthorization
|
Compliant
|
Health data | ✅
NSHealthShareUsageDescription
| ✅
HKQuantityTypeIdentifierStepCount
| ✅ | ❌ |
Risk: unused declaration
|
Bluetooth | ❌ | ✅
NSBluetoothAlwaysUsageDescription
| ✅ | ❌ |
High risk: missing declaration
|
In the table, "❌" indicates that this link was not detected, and "✅" indicates that it was detected. The "High risk", "Risk", and "Compliant" in the status column are automatically determined by the script based on the cross-validation results.
📄 Detailed Analysis (Camera Permission)
Info.plist Copy
"Used to take photos of exercise postures, with AI real-time movement correction"
Rating
: Length 28 characters (✅ Pass), no redundant words (✅ Professional), clear purpose (✅ Clear)
Manifest Basis
<data-type>AVMediaTypeVideo</data-type>
in
PrivacyInfo.xcprivacy
Symbol Call Evidence
# strings MyApp | grep -A2 -B2 "AVCapture"
0x1000a1b20: AVCaptureSession
0x1000a1b38: AVCaptureVideoDataOutput
0x1000a1b50: AVCaptureVideoPreviewLayer
Conclusion
: The use of camera permission is reasonable; the copy is fully consistent with the code and Manifest.
⚠️ Detailed Risk Item Explanation (Bluetooth Permission)
Problem Location
: The Manifest declares
NSBluetoothAlwaysUsageDescription
, but
Info.plist
has no corresponding key, and the symbol table shows no
CBCentralManager
or
CBPeripheralManager
calls.
Compliance Impact
: Violates App Store Review Guideline 5.1.2; "always use Bluetooth" requires clearly informing users and providing an option to turn it off, and the missing copy will lead to rejection during review.
Fix Suggestions
In
Info.plist
add:
<key>NSBluetoothAlwaysUsageDescription</key>
<string>用于连接智能手环,实时同步心率与运动数据</string>
Check whether the code really needs the
always
permission (iOS 13+ recommends using
whenInUse
instead);
If it is only for connecting external peripherals, you should instead use
NSBluetoothPeripheralUsageDescription
The value of this report lies precisely in the fact that it breaks down the abstract "privacy compliance" into actionable items: which line of code to change, which key to add, which declaration to delete. It does not tell you "there is a problem"; it tells you "where the problem is, why it is a problem, and how to fix it."
5. A Record of Common Issues and Troubleshooting Techniques
5.1 Quick Reference Table of Typical Issues
Issue Symptom | Possible Cause | Troubleshooting Command | Solution |
analyzer.py: command not found
| Python is not added to PATH or the script has no execute permission |
which python
,
ls -l analyzer.py
|
chmod +x analyzer.py
, or use
python analyzer.py
to invoke explicitly |
Error: Invalid IPA format
| The IPA is encrypted (enterprise signing or TestFlight distribution) |
file MyApp.ipa
| An encrypted IPA cannot be statically analyzed; you need to download an unencrypted version from the App Store, or have the developer provide an Ad Hoc signed IPA |
Manifest parsing failed: ParseError
|
PrivacyInfo.xcprivacy
XML format error |
xmllint --noout PrivacyInfo.xcprivacy
| Open it with Xcode
PrivacyInfo.xcprivacy
, and Xcode will automatically highlight syntax errors |
No camera usage found in symbols
| Swift is heavily obfuscated,
strings
not captured | `strings MyApp | grep -i "capture"` |
Entitlements mismatch: missing com.apple.developer.healthkit
| HealthKit Capability is not enabled in Xcode Signing |
jtool2 --ent MyApp | grep healthkit
| In Xcode: Signing & Capabilities → +Capability → HealthKit → check Read/Write |
Report shows 'NSLocationWhenInUseUsageDescription' but no location code found
| A third-party SDK (such as a map SDK) is used, and its code is in a Framework |
strings Frameworks/MapSDK.framework/MapSDK | grep -i "location"
| Check
Frameworks/
all frameworks under the directory, and perform a
strings
scan on each |
5.2 The 3 deep pitfalls I have stepped into and my exclusive tips for avoiding them
Pitfall 1: The "hidden" permission keys in Info.plist
The iOS system automatically injects some permission keys, even if you didn't declare them in Xcode. For example:
After adding
CFBundleURLSchemes
the system will automatically request
NSAppleEventsUsageDescription
to use
UIActivityViewController
When sharing images, it may trigger
NSPhotoLibraryAddUsageDescription
These keys will not appear in the
Info.plist
you wrote, but will appear in the packaged
Info.plist
. The script will detect them and mark them as "system-injected" to avoid mistakenly judging them as developer omissions.
Pitfall avoidance tip
: In Xcode, click
Info.plist
→ "Show Raw Keys/Values" in the lower right corner, and you can see all the keys that actually exist, including system-injected ones.
Pitfall 2: The "false positive" declarations in Privacy Manifest
Some developers, to save effort, write all HealthKit types in the Manifest:
<data-type>HKQuantityTypeIdentifierStepCount</data-type>
<data-type>HKQuantityTypeIdentifierHeartRate</data-type>
<data-type>HKQuantityTypeIdentifierBloodPressureSystolic</data-type>
But actually the App only uses step count. The script will mark it as an "unused declaration", but users may be confused: "I wrote it for future expansion!"
Pitfall avoidance tip
: The script adds a
--strict-mode
switch. When enabled, it only reports the types actually called by the current version's code; when disabled, it reports all types declared in the Manifest. It is disabled by default, balancing development flexibility and review rigor.
Pitfall 3: Rosetta compatibility issues on M1/M2 chips
jtool2
Although ARM64 is natively supported, some older versions
jtool2
on M-series chips will crash due to
dyld
version mismatch.
Pitfall avoidance tip
: The script will automatically detect the chip architecture at startup:
import platform
if platform.machine() == "arm64":
# 强制用jtool2 ARM64版
subprocess.run(["jtool2", "--version"])
else:
# Intel Mac用通用版
pass
and display a clear prompt: "M1 chip detected, calling ARM64 optimized version jtool2".
5.3 Advanced usage: Embed the script into CI/CD pipelines
This script is not just a local tool, but also infrastructure for team privacy governance. When I actually deployed it at a company, I integrated it into GitHub Actions:
# .github/workflows/privacy-check.yml
name: Privacy Compliance Check
on:
pull_request:
paths:
- '**.ipa'
jobs:
analyze:
runs-on: macos-14
steps:
- uses: actions/checkout@v4
- name: Install Dependencies
run: |
brew install jtool2 ripgrep jq
pip install rich pyobjc
- name: Run Privacy Analyzer
run: |
python analyzer.py \
--ipa-path ${{ github.event.pull_request.head.repo.name }}.ipa \
--output-dir ./privacy-report \
--format json
- name: Upload Report
uses: actions/upload-artifact@v3
with:
name: privacy-report
path: ./privacy-report/
Every time an IPA is submitted with a PR, analysis runs automatically and a JSON report is generated. Then paired with a simple Python script, parse
report.json
in
risk_level
fields:
risk_level == "high"
: Automatically comment: "High-risk privacy issue detected, please fix immediately", and block the merge;
risk_level == "medium"
: Post a comment as a reminder, but do not block;
risk_level == "low"
: Pass silently.
In this way, privacy compliance shifts from "manual checks before release" to "automatic interception on every code commit".
Finally, here is a small tip: The script supports
--baseline
parameters. It generates a
baseline.json
on the first run, and on every subsequent run it compares against the baseline, only reporting "newly added" permission declarations. This is especially useful for tracking privacy changes introduced by third-party SDK upgrades — for example, when a certain map SDK upgraded from v4.2 to v4.3, it silently added Bluetooth permission, and the baseline comparison would immediately trigger an alert.
6. Future extensible directions and personal practical insights
From the first version of this script in early 2023
print("Hello Privacy")
to the present, where it can output structured, auditable, and integrable reports, its core has remained unchanged:
Use the simplest tools to solve the most practical problems
. I do not pursue "one-click jailbreak-style" black technologies; instead, I believe that taking
plistlib
、
jtool2
、
strings
these existing tools to their full potential is more valuable than building a new wheel.
Several directions that will definitely be advanced in the future:
Dual-platform support for Android
: The principle is similar; use APK unpacking
apktool
, parse AndroidManifest.xml,
aapt dump permissions
extract permissions. The difficulty lies in the severe fragmentation of Android (different vendor ROMs implement permission popups differently), so a device fingerprint library needs to be built for adaptation;
Automated fix suggestion generation
: It not only tells the user "NSCameraUsageDescription is missing", but can also automatically generate an Xcode project modification patch (
.patch
file), and can even call the Xcodeproj library to directly modify `project.pbxproj
