diff --git a/privacycommand/Sources/privacycommand/Views/StaticAnalysisView.swift b/privacycommand/Sources/privacycommand/Views/StaticAnalysisView.swift index 6c11360..5715b3d 100644 --- a/privacycommand/Sources/privacycommand/Views/StaticAnalysisView.swift +++ b/privacycommand/Sources/privacycommand/Views/StaticAnalysisView.swift @@ -98,7 +98,8 @@ struct StaticAnalysisView: View { crossCheck: report.privacyManifest.map { PrivacyManifestReader.crossCheck( manifest: $0, - scan: BinaryStringScanner.scan(executable: report.bundle.executableURL)) + importedSymbols: Set(MachOInspector.importedSymbols( + of: report.bundle.executableURL))) }) .id(SectionAnchor.privacyManifest.rawValue) SandboxContainerView(info: sandboxContainer) diff --git a/privacycommand/Sources/privacycommandCore/Analysis/MachOInspector.swift b/privacycommand/Sources/privacycommandCore/Analysis/MachOInspector.swift index 083928f..bed3501 100644 --- a/privacycommand/Sources/privacycommandCore/Analysis/MachOInspector.swift +++ b/privacycommand/Sources/privacycommandCore/Analysis/MachOInspector.swift @@ -39,10 +39,9 @@ public enum MachOInspector { buildPlatform: nil, sliceCount: 0) } - /// Best-effort parse of the first thin slice (or first arch of a fat - /// binary) for the load-command details we care about. Returns - /// `.empty` for unrecognised formats rather than throwing — this is a - /// soft analysis, not a load-bearing parser. + /// Best-effort parse of every arch slice for the load-command details we + /// care about. Returns `.empty` for unrecognised formats rather than + /// throwing — this is a soft analysis, not a load-bearing parser. public static func loadCommands(of url: URL) -> LoadCommandsSummary { guard let data = try? Data(contentsOf: url, options: [.mappedIfSafe]), data.count >= 32 else { return .empty } @@ -78,8 +77,7 @@ public enum MachOInspector { } /// Enumerate the (offset, is64) of every Mach-O slice -- one entry for a - /// thin file, every fat arch for a universal binary. Generalises - /// `firstThinSlice` (which the symbol/arch readers still use). + /// thin file, every fat arch for a universal binary. private static func enumerateSlices(in data: Data) -> [(Int, Bool)] { let magic = data.withUnsafeBytes { $0.loadUnaligned(as: UInt32.self) } if magic == mh_magic || magic == mh_cigam { return [(0, false)] } @@ -132,16 +130,27 @@ public enum MachOInspector { /// disassembly, and readable even on encrypted App Store binaries (the /// `__LINKEDIT` symbol table is not encrypted). /// - /// Parses the first thin slice's `LC_SYMTAB` directly — no `nm`/`objdump` - /// dependency, so it works even without Xcode Command Line Tools. + /// Parses `LC_SYMTAB` in EVERY arch slice directly — no `nm`/`objdump` + /// dependency, so it works even without Xcode Command Line Tools. Slices + /// of a universal binary can import different symbols (and x86_64 slices + /// spell some libc imports differently, e.g. `_stat$INODE64`), so the + /// result is the de-duplicated union across all slices: a symbol imported + /// by only one slice is still reported. /// Leading underscores are preserved (Mach-O convention). Returns at most /// `limit` symbols. Returns `[]` (never throws) for unparseable inputs. public static func importedSymbols(of url: URL, limit: Int = 8000) -> [String] { guard let data = try? Data(contentsOf: url, options: [.mappedIfSafe]), data.count >= 32 else { return [] } - let (sliceOffset, is64) = firstThinSlice(in: data) - guard let sliceOffset else { return [] } - return parseImportedSymbols(in: data, at: sliceOffset, is64: is64, limit: limit) + var out: [String] = [] + var seen = Set() + for (sliceOffset, is64) in enumerateSlices(in: data) { + guard out.count < limit else { break } + for name in parseImportedSymbols(in: data, at: sliceOffset, is64: is64, limit: limit) { + guard out.count < limit else { break } + if seen.insert(name).inserted { out.append(name) } + } + } + return out } public static func architectures(of url: URL) throws -> [String] { @@ -209,8 +218,6 @@ public enum MachOInspector { // MARK: - Load command parsing - /// Locate the start of the first thin Mach-O slice. For a thin binary - /// the offset is 0; for a fat binary we read the first arch's offset. /// Decode a Mach-O nibble-packed version (X.Y.Z in bits xxxx.yy.zz). static func decodeVersion(_ v: UInt32) -> String { let x = (v >> 16) & 0xFFFF, y = (v >> 8) & 0xFF, z = v & 0xFF @@ -234,41 +241,6 @@ public enum MachOInspector { } } - private static func firstThinSlice(in data: Data) -> (Int?, Bool) { - let magic = data.withUnsafeBytes { $0.loadUnaligned(as: UInt32.self) } - if magic == mh_magic || magic == mh_cigam { return (0, false) } - if magic == mh_magic_64 || magic == mh_cigam_64 { return (0, true) } - let isFat = magic == fatMagic || magic == fatMagicSwapped - || magic == fat64Magic || magic == fat64MagicSwapped - guard isFat, data.count >= 16 else { return (nil, false) } - let swapped = magic == fatMagicSwapped || magic == fat64MagicSwapped - let is64 = magic == fat64Magic || magic == fat64MagicSwapped - // First arch starts at 8; offset field is at +8 (32-bit) or +8 (64-bit). - // 32-bit arch struct: cputype(4) cpusubtype(4) offset(4) size(4) align(4) - // 64-bit arch struct: cputype(4) cpusubtype(4) offset(8) size(8) align(4) reserved(4) - let offsetField = 8 + 8 - let offset: UInt64 - if is64 { - var raw: UInt64 = 0 - data.withUnsafeBytes { - raw = $0.baseAddress!.advanced(by: offsetField).loadUnaligned(as: UInt64.self) - } - offset = swapped ? raw.byteSwapped : raw - } else { - var raw: UInt32 = 0 - data.withUnsafeBytes { - raw = $0.baseAddress!.advanced(by: offsetField).loadUnaligned(as: UInt32.self) - } - offset = UInt64(swapped ? raw.byteSwapped : raw) - } - guard offset < UInt64(data.count) else { return (nil, false) } - let sliceMagic = data.withUnsafeBytes { - $0.baseAddress!.advanced(by: Int(offset)).loadUnaligned(as: UInt32.self) - } - let sliceIs64 = sliceMagic == mh_magic_64 || sliceMagic == mh_cigam_64 - return (Int(offset), sliceIs64) - } - private static func parseThinLoadCommands(in data: Data, at sliceOffset: Int, is64: Bool) -> LoadCommandsSummary { // mach_header(_64): magic(4) cputype(4) cpusubtype(4) filetype(4) // ncmds(4) sizeofcmds(4) flags(4) [reserved(4) for 64] diff --git a/privacycommand/Sources/privacycommandCore/Analysis/PrivacyManifestReader.swift b/privacycommand/Sources/privacycommandCore/Analysis/PrivacyManifestReader.swift index 8c67197..16654df 100644 --- a/privacycommand/Sources/privacycommandCore/Analysis/PrivacyManifestReader.swift +++ b/privacycommand/Sources/privacycommandCore/Analysis/PrivacyManifestReader.swift @@ -102,37 +102,31 @@ public enum PrivacyManifestReader { // MARK: - Cross-check - /// Compare the manifest's `accessedAPITypes` against the binary scan's - /// observed symbol references. Returns mismatches in both directions. + /// Compare the manifest's `accessedAPITypes` against the binary's + /// undefined-external symbols (`MachOInspector.importedSymbols(of:)`), + /// using the `RequiredReasonAPIs` vocabulary. Returns mismatches in both + /// directions. + /// + /// Matching is exact on nlist spellings — `_stat`, `_stat$INODE64`, + /// `_OBJC_CLASS_$_NSUserDefaults` — never substring-based. A symbol that + /// belongs to more than one category (the `getattrlist` family sits in + /// both FileTimestamp and DiskSpace) counts as evidence for each. + /// + /// Categories the manifest declares that Apple doesn't document (they + /// collapse to `.other`) are excluded from `declaredButUnused`: no symbol + /// evidence can exist for them, and flagging the unknown value itself is + /// a validity check, not a cross-check. public static func crossCheck(manifest: PrivacyManifest, - scan: BinaryStringScanner.Result) -> PrivacyManifestCrossCheck { - let symbolToCategory: [String: PrivacyManifest.AccessedAPI.Category] = [ - // File timestamps - "NSFileSystemNumber": .fileTimestamp, - "creationDate": .fileTimestamp, - "fileModificationDate": .fileTimestamp, - // Disk space - "volumeAvailableCapacityKey": .diskSpace, - "systemFreeSize": .diskSpace, - "NSURLVolumeAvailableCapacityKey": .diskSpace, - // System boot time - "kern.boottime": .systemBootTime, - "mach_absolute_time": .systemBootTime, - // User defaults - "NSUserDefaults": .userDefaults, - // Active keyboards - "TIInputSource": .activeKeyboards, - // CoreMotion / pedometer - "CMPedometer": .userDefaults, // close enough for cross-check - ] - - var symbolEvidenceByCategory: [PrivacyManifest.AccessedAPI.Category: [String]] = [:] - for (symbol, cat) in symbolToCategory where scan.foundFrameworkSymbols.contains(symbol) { - symbolEvidenceByCategory[cat, default: []].append(symbol) + importedSymbols: Set) -> PrivacyManifestCrossCheck { + var evidenceByCategory: [PrivacyManifest.AccessedAPI.Category: Set] = [:] + for symbol in importedSymbols { + for category in RequiredReasonAPIs.categories(forNlistSpelling: symbol) { + evidenceByCategory[category, default: []].insert(symbol) + } } let declaredCategories = Set(manifest.accessedAPITypes.map(\.category)) - let observedCategories = Set(symbolEvidenceByCategory.keys) + let observedCategories = Set(evidenceByCategory.keys) let declaredButUnused = declaredCategories.subtracting(observedCategories) .filter { $0 != .other } @@ -142,7 +136,7 @@ public enum PrivacyManifestReader { declaredButUnused: declaredButUnused.sorted(by: { $0.rawValue < $1.rawValue }), usedButUndeclared: usedButUndeclared.sorted(by: { $0.rawValue < $1.rawValue }) .map { PrivacyManifestCrossCheck.Mismatch( - category: $0, evidence: symbolEvidenceByCategory[$0] ?? []) } + category: $0, evidence: (evidenceByCategory[$0] ?? []).sorted()) } ) } diff --git a/privacycommand/Sources/privacycommandCore/Analysis/RequiredReasonAPIs.swift b/privacycommand/Sources/privacycommandCore/Analysis/RequiredReasonAPIs.swift new file mode 100644 index 0000000..f96495b --- /dev/null +++ b/privacycommand/Sources/privacycommandCore/Analysis/RequiredReasonAPIs.swift @@ -0,0 +1,229 @@ +import Foundation + +/// Apple's "required reason API" vocabulary as data: the five +/// `NSPrivacyAccessedAPICategory*` values, the symbols in each, and the +/// approved reason codes with their restrictions. +/// +/// Required-reason declarations apply to iOS, iPadOS, tvOS, visionOS and +/// watchOS only — Apple explicitly excludes macOS, even for Mac App Store +/// apps. The vocabulary is still useful when auditing macOS binaries (the +/// same symbols reveal the same behaviour); it just carries no App Review +/// obligation there. +public enum RequiredReasonAPIs { + + public typealias Category = PrivacyManifest.AccessedAPI.Category + + /// One approved reason code and the restriction Apple attaches to it. + /// Codes are category-scoped: a syntactically real code declared under a + /// different category is invalid. + public struct ReasonCode: Sendable, Hashable { + /// The code as it appears in a manifest, e.g. "C617.1". + public let code: String + /// Apple's stated permitted use and off-device restrictions. + public let note: String + + public init(code: String, note: String) { + self.code = code + self.note = note + } + } + + /// One API from Apple's per-category symbol list, with the exact + /// spellings it produces in a Mach-O symbol table. + public struct SymbolEntry: Sendable, Hashable { + /// The API as Apple's documentation names it, e.g. "fstat(_:_:)". + public let apiName: String + /// Exact undefined-external (nlist) spellings this API produces: + /// C functions and exported constants keep their leading underscore + /// (`_stat`, `_NSFileCreationDate`); x86_64 slices spell part of the + /// stat family with a `$INODE64` suffix (`_stat$INODE64`); Objective-C + /// class references appear as `_OBJC_CLASS_$_NSUserDefaults`. APIs + /// reached only through `objc_msgSend` (properties like + /// `ProcessInfo.systemUptime`) leave no per-selector import — the + /// class reference is the only nlist-visible evidence, so matches on + /// those spellings are class-level, not member-level. Swift-mangled + /// accessor symbols are not enumerated here. + public let nlistSpellings: Set + + public init(apiName: String, nlistSpellings: Set) { + self.apiName = apiName + self.nlistSpellings = nlistSpellings + } + } + + /// One documented category: its reason codes and its symbol list. + public struct CategoryEntry: Sendable { + public let category: Category + public let reasonCodes: [ReasonCode] + public let symbols: [SymbolEntry] + + public init(category: Category, reasonCodes: [ReasonCode], symbols: [SymbolEntry]) { + self.category = category + self.reasonCodes = reasonCodes + self.symbols = symbols + } + } + + // https://developer.apple.com/documentation/bundleresources/describing-use-of-required-reason-api + public static let categories: [CategoryEntry] = [ + CategoryEntry( + category: .fileTimestamp, + reasonCodes: [ + ReasonCode(code: "DDA9.1", note: "Display file timestamps to the person using the device. Information accessed for this reason, or any derived information, may not be sent off-device."), + ReasonCode(code: "C617.1", note: "Access timestamps, size, or other metadata of files inside the app container, app group container, or the app's CloudKit container."), + ReasonCode(code: "3B52.1", note: "Access timestamps, size, or other metadata of files or directories the user specifically granted access to, e.g. via a document picker."), + ReasonCode(code: "0A2A.1", note: "Third-party SDKs only: a wrapper function around file timestamp APIs called only when the app calls the wrapper. May not be declared by an SDK that exists primarily to wrap required reason APIs; data may not be used for the SDK's own purposes or sent off-device by the SDK."), + ], + symbols: [ + SymbolEntry(apiName: "FileAttributeKey.creationDate", + nlistSpellings: ["_NSFileCreationDate"]), + SymbolEntry(apiName: "FileAttributeKey.modificationDate", + nlistSpellings: ["_NSFileModificationDate"]), + SymbolEntry(apiName: "UIDocument.fileModificationDate", + nlistSpellings: ["_OBJC_CLASS_$_UIDocument"]), + SymbolEntry(apiName: "URLResourceKey.contentModificationDateKey", + nlistSpellings: ["_NSURLContentModificationDateKey"]), + SymbolEntry(apiName: "URLResourceKey.creationDateKey", + nlistSpellings: ["_NSURLCreationDateKey"]), + SymbolEntry(apiName: "getattrlist(_:_:_:_:_:)", + nlistSpellings: ["_getattrlist"]), + SymbolEntry(apiName: "getattrlistbulk(_:_:_:_:_:)", + nlistSpellings: ["_getattrlistbulk"]), + SymbolEntry(apiName: "fgetattrlist(_:_:_:_:_:)", + nlistSpellings: ["_fgetattrlist"]), + SymbolEntry(apiName: "stat", + nlistSpellings: ["_stat", "_stat$INODE64"]), + SymbolEntry(apiName: "fstat(_:_:)", + nlistSpellings: ["_fstat", "_fstat$INODE64"]), + SymbolEntry(apiName: "fstatat(_:_:_:_:)", + nlistSpellings: ["_fstatat", "_fstatat$INODE64"]), + SymbolEntry(apiName: "lstat(_:_:)", + nlistSpellings: ["_lstat", "_lstat$INODE64"]), + SymbolEntry(apiName: "getattrlistat(_:_:_:_:_:_:)", + nlistSpellings: ["_getattrlistat"]), + ] + ), + CategoryEntry( + category: .systemBootTime, + reasonCodes: [ + ReasonCode(code: "35F9.1", note: "Measure elapsed time between in-app events or enable timers. Data may not be sent off-device, except the amount of time elapsed between in-app events."), + ReasonCode(code: "8FFB.1", note: "Calculate absolute timestamps for in-app events. The absolute timestamps may be sent off-device; the system boot time itself (or anything derived from it) may not."), + ReasonCode(code: "3D61.1", note: "Include system boot time in an optional bug report the person chooses to submit; it must be prominently displayed as part of the report."), + ], + symbols: [ + SymbolEntry(apiName: "ProcessInfo.systemUptime", + nlistSpellings: ["_OBJC_CLASS_$_NSProcessInfo"]), + SymbolEntry(apiName: "mach_absolute_time()", + nlistSpellings: ["_mach_absolute_time"]), + ] + ), + CategoryEntry( + category: .diskSpace, + reasonCodes: [ + ReasonCode(code: "85F4.1", note: "Display disk space information to the person. May not be sent off-device, except over the local network to another device operated by the same person purely to display it there, with explicit permission, never over the Internet."), + ReasonCode(code: "E174.1", note: "Check whether there is sufficient disk space to write files, or whether space is low so the app can delete files; the app must behave differently in a user-observable way. May not be sent off-device, except to avoid server downloads when space is insufficient."), + ReasonCode(code: "7D9E.1", note: "Include disk space information in an optional bug report the person chooses to submit; it must be prominently displayed. May be sent off-device only after the user affirmatively submits that specific report."), + ReasonCode(code: "B728.1", note: "Health research apps only: detect and inform research participants about low disk space impacting research data collection, per the Health and Health Research review guidelines."), + ], + symbols: [ + SymbolEntry(apiName: "URLResourceKey.volumeAvailableCapacityKey", + nlistSpellings: ["_NSURLVolumeAvailableCapacityKey"]), + SymbolEntry(apiName: "URLResourceKey.volumeAvailableCapacityForImportantUsageKey", + nlistSpellings: ["_NSURLVolumeAvailableCapacityForImportantUsageKey"]), + SymbolEntry(apiName: "URLResourceKey.volumeAvailableCapacityForOpportunisticUsageKey", + nlistSpellings: ["_NSURLVolumeAvailableCapacityForOpportunisticUsageKey"]), + SymbolEntry(apiName: "URLResourceKey.volumeTotalCapacityKey", + nlistSpellings: ["_NSURLVolumeTotalCapacityKey"]), + SymbolEntry(apiName: "FileAttributeKey.systemFreeSize", + nlistSpellings: ["_NSFileSystemFreeSize"]), + SymbolEntry(apiName: "FileAttributeKey.systemSize", + nlistSpellings: ["_NSFileSystemSize"]), + SymbolEntry(apiName: "statfs(_:_:)", + nlistSpellings: ["_statfs", "_statfs$INODE64"]), + SymbolEntry(apiName: "statvfs(_:_:)", + nlistSpellings: ["_statvfs"]), + SymbolEntry(apiName: "fstatfs(_:_:)", + nlistSpellings: ["_fstatfs", "_fstatfs$INODE64"]), + SymbolEntry(apiName: "fstatvfs(_:_:)", + nlistSpellings: ["_fstatvfs"]), + // The getattrlist family sits in BOTH the FileTimestamp and + // DiskSpace symbol lists; one import is evidence for each. + SymbolEntry(apiName: "getattrlist(_:_:_:_:_:)", + nlistSpellings: ["_getattrlist"]), + SymbolEntry(apiName: "fgetattrlist(_:_:_:_:_:)", + nlistSpellings: ["_fgetattrlist"]), + SymbolEntry(apiName: "getattrlistat(_:_:_:_:_:_:)", + nlistSpellings: ["_getattrlistat"]), + ] + ), + CategoryEntry( + category: .activeKeyboards, + reasonCodes: [ + ReasonCode(code: "3EC4.1", note: "Custom keyboard apps only: determine which keyboards are active on the device. Providing a systemwide custom keyboard must be the app's primary functionality. May not be sent off-device."), + ReasonCode(code: "54BD.1", note: "Present the correct customized UI. The app must have text fields for entering or editing text and must behave differently based on active keyboards in a user-observable way. May not be sent off-device."), + ], + symbols: [ + SymbolEntry(apiName: "UITextInputMode.activeInputModes", + nlistSpellings: ["_OBJC_CLASS_$_UITextInputMode"]), + ] + ), + CategoryEntry( + category: .userDefaults, + reasonCodes: [ + ReasonCode(code: "CA92.1", note: "Read and write information accessible only to the app itself. Does not permit reading information written by other apps or the system, nor writing information other apps can access."), + ReasonCode(code: "1C8F.1", note: "Read and write information accessible only to apps, app extensions and App Clips in the same App Group. Does not permit reading or writing across the App Group boundary, or reading system-written data."), + ReasonCode(code: "C56D.1", note: "Third-party SDKs only: a wrapper function around user defaults APIs called only when the app calls the wrapper. May not be declared by an SDK that exists primarily to wrap required reason APIs; data may not be used for the SDK's own purposes or sent off-device by the SDK."), + ReasonCode(code: "AC6B.1", note: "Read the com.apple.configuration.managed key for MDM-set managed app configuration, or set the com.apple.feedback.managed key to store feedback queryable over MDM."), + ], + symbols: [ + SymbolEntry(apiName: "UserDefaults", + nlistSpellings: ["_OBJC_CLASS_$_NSUserDefaults"]), + ] + ), + ] + + // MARK: - Lookups + + /// Exact-match lookup from an nlist spelling to every category whose + /// symbol list contains it. Exact means exact: `stat` (no underscore) + /// matches nothing, and no substring or prefix matching happens. + public static let categoriesByNlistSpelling: [String: Set] = { + var out: [String: Set] = [:] + for entry in categories { + for symbol in entry.symbols { + for spelling in symbol.nlistSpellings { + out[spelling, default: []].insert(entry.category) + } + } + } + return out + }() + + /// Categories a single undefined-external symbol is evidence for + /// (empty for symbols outside the vocabulary). + public static func categories(forNlistSpelling spelling: String) -> Set { + categoriesByNlistSpelling[spelling] ?? [] + } + + /// Reason code → the one category it belongs to. Codes never repeat + /// across categories. + public static let categoryByReasonCode: [String: Category] = { + var out: [String: Category] = [:] + for entry in categories { + for reason in entry.reasonCodes { + out[reason.code] = entry.category + } + } + return out + }() + + /// Whether `code` is an approved reason code for `category`. False for + /// unknown codes and for real codes declared under the wrong category. + public static func isValid(reasonCode code: String, in category: Category) -> Bool { + categoryByReasonCode[code] == category + } + + /// Every approved reason code across all five categories. + public static let allReasonCodes: [ReasonCode] = + categories.flatMap(\.reasonCodes) +} diff --git a/privacycommand/Sources/privacycommandCore/Analysis/StaticAnalyzer.swift b/privacycommand/Sources/privacycommandCore/Analysis/StaticAnalyzer.swift index b139189..a43a7cf 100644 --- a/privacycommand/Sources/privacycommandCore/Analysis/StaticAnalyzer.swift +++ b/privacycommand/Sources/privacycommandCore/Analysis/StaticAnalyzer.swift @@ -358,9 +358,13 @@ public struct StaticAnalyzer { kbArticleID: "app-runtime")) } - // Privacy-manifest cross-check. + // Privacy-manifest cross-check: the binary's undefined-external + // symbols (exact nlist spellings) vs the manifest's declared + // required-reason categories. if let manifest = privacyManifest { - let xc = PrivacyManifestReader.crossCheck(manifest: manifest, scan: scan) + let xc = PrivacyManifestReader.crossCheck( + manifest: manifest, + importedSymbols: Set(MachOInspector.importedSymbols(of: bundle.executableURL))) if !xc.declaredButUnused.isEmpty { enrichedWarnings.append(Finding( severity: .info, diff --git a/privacycommand/privacycommand.xcodeproj/project.pbxproj b/privacycommand/privacycommand.xcodeproj/project.pbxproj index fd39b2b..f3717ec 100644 --- a/privacycommand/privacycommand.xcodeproj/project.pbxproj +++ b/privacycommand/privacycommand.xcodeproj/project.pbxproj @@ -98,6 +98,7 @@ 0BBBBBBB000000000000C113 /* EmbeddedAssetScanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FFFFFFF000000000000C113 /* EmbeddedAssetScanner.swift */; }; 0BBBBBBB000000000000C114 /* BehaviorAnalyzer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FFFFFFF000000000000C114 /* BehaviorAnalyzer.swift */; }; 0BBBBBBB000000000000C115 /* PrivacyManifestReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FFFFFFF000000000000C115 /* PrivacyManifestReader.swift */; }; + 0BBBBBBB000000000000C153 /* RequiredReasonAPIs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FFFFFFF000000000000C153 /* RequiredReasonAPIs.swift */; }; 0BBBBBBB000000000000C152 /* AppExtensionScanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FFFFFFF000000000000C152 /* AppExtensionScanner.swift */; }; 0BBBBBBB000000000000C151 /* AppRuntimeDetector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FFFFFFF000000000000C151 /* AppRuntimeDetector.swift */; }; 0BBBBBBB000000000000C150 /* EmbeddedResourceScanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FFFFFFF000000000000C150 /* EmbeddedResourceScanner.swift */; }; @@ -331,6 +332,7 @@ 0FFFFFFF000000000000C113 /* EmbeddedAssetScanner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmbeddedAssetScanner.swift; sourceTree = ""; }; 0FFFFFFF000000000000C114 /* BehaviorAnalyzer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BehaviorAnalyzer.swift; sourceTree = ""; }; 0FFFFFFF000000000000C115 /* PrivacyManifestReader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacyManifestReader.swift; sourceTree = ""; }; + 0FFFFFFF000000000000C153 /* RequiredReasonAPIs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequiredReasonAPIs.swift; sourceTree = ""; }; 0FFFFFFF000000000000C152 /* AppExtensionScanner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppExtensionScanner.swift; sourceTree = ""; }; 0FFFFFFF000000000000C151 /* AppRuntimeDetector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppRuntimeDetector.swift; sourceTree = ""; }; 0FFFFFFF000000000000C150 /* EmbeddedResourceScanner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmbeddedResourceScanner.swift; sourceTree = ""; }; @@ -637,6 +639,7 @@ 0FFFFFFF000000000000C113 /* EmbeddedAssetScanner.swift */, 0FFFFFFF000000000000C114 /* BehaviorAnalyzer.swift */, 0FFFFFFF000000000000C115 /* PrivacyManifestReader.swift */, + 0FFFFFFF000000000000C153 /* RequiredReasonAPIs.swift */, 0FFFFFFF000000000000C152 /* AppExtensionScanner.swift */, 0FFFFFFF000000000000C151 /* AppRuntimeDetector.swift */, 0FFFFFFF000000000000C150 /* EmbeddedResourceScanner.swift */, @@ -1003,6 +1006,7 @@ 0BBBBBBB000000000000A11D /* PrivacyClaimsView.swift in Sources */, 0BBBBBBB000000000000A11E /* AnomaliesView.swift in Sources */, 0BBBBBBB000000000000C115 /* PrivacyManifestReader.swift in Sources */, + 0BBBBBBB000000000000C153 /* RequiredReasonAPIs.swift in Sources */, 0BBBBBBB000000000000C152 /* AppExtensionScanner.swift in Sources */, 0BBBBBBB000000000000C151 /* AppRuntimeDetector.swift in Sources */, 0BBBBBBB000000000000C150 /* EmbeddedResourceScanner.swift in Sources */,