cocoapods-entitlements-statistics 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,185 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'forwardable'
4
+ require 'cfpropertylist'
5
+
6
+ module AppEntitlementsStatistics
7
+
8
+ # iOS app.entitlements parser
9
+ class EntitlementsPlist
10
+ extend Forwardable
11
+
12
+ def initialize(file)
13
+ @file = file
14
+ end
15
+
16
+ #
17
+ # Extract the capabilities
18
+ # https://developer.apple.com/help/account/reference/supported-capabilities-ios
19
+ # https://developer.apple.com/documentation/bundleresources/entitlements
20
+ # https://developer.apple.com/library/archive/documentation/Miscellaneous/Reference/EntitlementKeyReference/Chapters/AboutEntitlements.html
21
+ # a few entitlements are inherited from the iOS provisioning profile used to run the app.
22
+ #
23
+ def enabled_capabilities
24
+ capabilities = Hash.new
25
+ info.each do |key, value|
26
+ case key
27
+ when 'com.apple.developer.game-center'
28
+ capabilities['Game Center'] = {
29
+ key => value
30
+ }
31
+ when 'keychain-access-groups'
32
+ capabilities['Keychain sharing'] = {
33
+ key => value
34
+ }
35
+ # when 'aps-environment'
36
+ # capabilities['Push Notifications'] = {
37
+ # key => value
38
+ # }
39
+ when 'com.apple.developer.applesignin'
40
+ capabilities['Sign In with Apple'] = {
41
+ key => value
42
+ }
43
+ when 'com.apple.developer.siri'
44
+ capabilities['SiriKit'] = {
45
+ key => value
46
+ }
47
+ when 'com.apple.security.application-groups'
48
+ capabilities['App Groups'] = {
49
+ key => value
50
+ }
51
+ when 'com.apple.developer.associated-domains'
52
+ capabilities['Associated Domains'] = {
53
+ key => value
54
+ }
55
+ when 'com.apple.developer.default-data-protection'
56
+ capabilities['Data Protection'] = {
57
+ key => value
58
+ }
59
+ when 'com.apple.developer.networking.networkextension'
60
+ capabilities ['Network Extensions'] = {
61
+ key => value
62
+ }
63
+ when 'com.apple.developer.networking.vpn.api'
64
+ capabilities ['Personal VPN'] = {
65
+ key => value
66
+ }
67
+ when 'com.apple.developer.healthkit',
68
+ 'com.apple.developer.healthkit.access'
69
+ capabilities['HealthKit'] = {
70
+ 'com.apple.developer.healthkit' => info['com.apple.developer.healthkit'],
71
+ 'com.apple.developer.healthkit.access' => info['com.apple.developer.healthkit.access'],
72
+ } unless capabilities.include?('HealthKit')
73
+ when 'com.apple.developer.icloud-services',
74
+ 'com.apple.developer.icloud-container-identifiers'
75
+ capabilities['iCloud'] = {
76
+ 'com.apple.developer.icloud-services' => info['com.apple.developer.icloud-services'],
77
+ 'com.apple.developer.icloud-container-identifiers' => info['com.apple.developer.icloud-container-identifiers'],
78
+ } unless capabilities.include?('iCloud')
79
+ when 'com.apple.developer.in-app-payments'
80
+ capabilities['Apple Pay'] = {
81
+ key => value
82
+ }
83
+ when 'com.apple.developer.homekit'
84
+ capabilities['HomeKit'] = {
85
+ key => value
86
+ }
87
+ when 'com.apple.developer.user-fonts'
88
+ capabilities['Fonts'] = {
89
+ key => value
90
+ }
91
+ when 'com.apple.developer.pass-type-identifiers'
92
+ capabilities['Wallet'] = {
93
+ key => value
94
+ }
95
+ when 'inter-app-audio'
96
+ capabilities['Inter-App Audio'] = {
97
+ key => value
98
+ }
99
+ when 'com.apple.developer.networking.multipath'
100
+ capabilities['Multipath'] = {
101
+ key => value
102
+ }
103
+ when 'com.apple.developer.authentication-services.autofill-credential-provider'
104
+ capabilities['AutoFill Credential Provider'] = {
105
+ key => value
106
+ }
107
+ when 'com.apple.developer.networking.wifi-info'
108
+ capabilities['Access WiFi Information'] = {
109
+ key => value
110
+ }
111
+ when 'com.apple.external-accessory.wireless-configuration'
112
+ capabilities['Wireless Accessory Configuration'] = {
113
+ key => value
114
+ }
115
+ when 'com.apple.developer.kernel.extended-virtual-addressing'
116
+ capabilities['Extended Virtual Address Space'] = {
117
+ key => value
118
+ }
119
+ when 'com.apple.developer.nfc.readersession.formats'
120
+ capabilities['NFC Tag Reading'] = {
121
+ key => value
122
+ }
123
+ when 'com.apple.developer.ClassKit-environment'
124
+ capabilities['ClassKit'] = {
125
+ key => value
126
+ }
127
+ when 'com.apple.developer.networking.HotspotConfiguration'
128
+ capabilities['Hotspot'] = {
129
+ key => value
130
+ }
131
+ when 'com.apple.developer.devicecheck.appattest-environment'
132
+ capabilities['App Attest'] = {
133
+ key => value
134
+ }
135
+ when 'com.apple.developer.coremedia.hls.low-latency'
136
+ capabilities['Low Latency HLS'] = {
137
+ key => value
138
+ }
139
+ when 'com.apple.developer.associated-domains.mdm-managed'
140
+ capabilities['MDM Managed Associated Domains'] = {
141
+ key => value
142
+ }
143
+ end
144
+ end
145
+ capabilities
146
+ end
147
+
148
+
149
+ def game_center
150
+ info.try(:[], 'com.apple.developer.game-center').nil?
151
+ end
152
+
153
+ def keychain_access_groups
154
+ info.try(:[], 'keychain-access-groups').nil?
155
+ end
156
+
157
+
158
+ def [](key)
159
+ info.try(:[], key.to_s)
160
+ end
161
+
162
+ def_delegators :info, :to_h
163
+
164
+ def method_missing(method_name, *args, &block)
165
+ info.try(:[], method_name.to_s.ai_camelcase) ||
166
+ info.send(method_name) ||
167
+ super
168
+ end
169
+
170
+ def respond_to_missing?(method_name, *args)
171
+ info.key?(method_name.to_s.ai_camelcase) ||
172
+ info.respond_to?(method_name) ||
173
+ super
174
+ end
175
+
176
+ private
177
+
178
+ def info
179
+ return unless File.file?(@file)
180
+
181
+ @info ||= CFPropertyList.native_types(CFPropertyList::List.new(file: @file).value)
182
+ end
183
+
184
+ end
185
+ end
@@ -0,0 +1,112 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "mobile_provision"
4
+ require_relative "info_plist"
5
+ require_relative "entitlements"
6
+ require_relative "helper"
7
+
8
+ require 'fileutils'
9
+ require 'forwardable'
10
+
11
+ module AppEntitlementsStatistics
12
+
13
+ class Extracter
14
+ include Helper::Sigflat
15
+ include Helper::Utils
16
+ extend Forwardable
17
+
18
+ attr_reader :system_capabilities
19
+ attr_reader :info_plist_path
20
+ attr_reader :entitlements_path
21
+ attr_reader :identifier
22
+ attr_reader :cur_version
23
+
24
+ def initialize(args)
25
+ args.each do |key ,value|
26
+ case key
27
+ when "cur_version"
28
+ @cur_version = value
29
+ when "product_bundle_identifier"
30
+ @identifier = value
31
+ when "info_plist_path"
32
+ @info_plist_path = value
33
+ when "entitlements_path"
34
+ @entitlements_path = value
35
+ when "system_capabilities"
36
+ @system_capabilities = value
37
+ end
38
+ end
39
+ end
40
+
41
+ def extract_update
42
+ capabilities_summary = Hash.new
43
+ capabilities = extract_capabilities_info
44
+ capabilities.each do |key,value|
45
+ capabilities_summary[key] = createsig(value)
46
+ end
47
+
48
+ usage_desc_summary = Hash.new
49
+ usage_desc = plistInfo.permis_usagedescription
50
+ usage_desc.each do |key,value|
51
+ usage_desc_summary[key] = createsig(value)
52
+ end
53
+
54
+ yaml_content = {
55
+ 'Capabilities_Summary' => capabilities_summary,
56
+ 'Capabilities' => capabilities,
57
+ 'PermissionsUsageDescription_Summary' => usage_desc_summary,
58
+ 'PermissionsUsageDescription' => usage_desc
59
+ }
60
+ yaml_name = entitlements_yaml_name(@cur_version, @identifier , path: @store_path)
61
+
62
+ File.open(yaml_name, "w") { |file| file.write(yaml_content.to_yaml) }
63
+ update_versions
64
+ yaml_content
65
+ end
66
+
67
+
68
+ def update_versions
69
+ yaml_name = versions_yaml_name(@identifier , path: @store_path)
70
+ yaml_content = [ ]
71
+ if File.exist?(yaml_name)
72
+ yaml_content = YAML.load_file(yaml_name)
73
+ end
74
+ version = @cur_version
75
+ if !yaml_content.include?(version)
76
+ yaml_content.unshift(version)
77
+ File.open(yaml_name, "w") { |file| file.write(yaml_content.to_yaml) }
78
+ end
79
+ end
80
+
81
+ def extract_capabilities_info
82
+ #{"com.apple.BackgroundModes"=>{"enabled"=>"1"}, "com.apple.InAppPurchase"=>{"enabled"=>"1"}, "com.apple.Push"=>{"enabled"=>"1"}}
83
+ capabilities_info = {}
84
+
85
+ @system_capabilities.each do |key,value|
86
+ case key
87
+ when "com.apple.InAppPurchase"
88
+ capabilities_info["In-App Purchase"] = { key => value }
89
+ when "com.apple.BackgroundModes"
90
+ h1 = {key => value}
91
+ h2 = plistInfo.backgroundModes["Background Modes"]
92
+ capabilities_info["Background Modes"] = h1.merge(h2)
93
+ when "com.apple.Push"
94
+ capabilities_info["Push Notifications"] = { key => value }
95
+ end
96
+ end
97
+ capabilities_info = capabilities_info.merge(entitlementsInfo.enabled_capabilities)
98
+ capabilities_info = capabilities_info.merge(plistInfo.enabled_capabilities)
99
+ capabilities_info
100
+ end
101
+
102
+ def plistInfo
103
+ @plistInfo ||= InfoPlist.new(@info_plist_path)
104
+ end
105
+
106
+ def entitlementsInfo
107
+ @entitlementsInfo ||= EntitlementsPlist.new(@entitlements_path)
108
+ end
109
+
110
+ end
111
+
112
+ end
@@ -0,0 +1,90 @@
1
+ require 'tmpdir'
2
+ require 'crimp'
3
+ require 'cocoapods'
4
+
5
+
6
+ module AppEntitlementsStatistics
7
+
8
+ module Helper
9
+
10
+ module Sigflat
11
+ def createsig(body)
12
+ return Crimp.signature(body)
13
+ end
14
+ end
15
+
16
+ module Utils
17
+
18
+ def defalut_store_path(app_identifier)
19
+ root_path = "#{Dir.home}/appInfo-#{app_identifier}"
20
+ FileUtils.mkdir_p(root_path) unless File.exists?(root_path)
21
+ root_path
22
+ end
23
+
24
+ def entitlements_yaml_name(v,app_identifier,path: nil)
25
+ dir_path = path ? "#{path}-#{app_identifier}" : defalut_store_path(app_identifier)
26
+ yaml_name = "#{dir_path}/entitlements_#{v}.yml";
27
+ yaml_name
28
+ end
29
+
30
+ def versions_yaml_name(app_identifier,path: nil)
31
+ dir_path = path ? "#{path}-#{app_identifier}" : defalut_store_path(app_identifier)
32
+ yaml_name = "#{dir_path}/entitlements_versions.yml";
33
+ yaml_name
34
+ end
35
+
36
+ def report_file_name(path: nil)
37
+ path = path ? "#{path}/entitlements_statistics" : "entitlements_statistics"
38
+ FileUtils.mkdir_p(path) unless File.exists?(path)
39
+ file_name = "#{path}/analyze_report"
40
+ file_name
41
+ end
42
+
43
+ end
44
+
45
+ module Archive
46
+ require 'zip'
47
+ require 'fileutils'
48
+ require 'securerandom'
49
+
50
+ # Unarchive zip file
51
+ #
52
+ # source: https://github.com/soffes/lagunitas/blob/master/lib/lagunitas/ipa.rb
53
+ def unarchive(file, path: nil)
54
+ path = path ? "#{path}-" : ''
55
+ root_path = "#{Dir.mktmpdir}/AppInfo-#{path}#{SecureRandom.hex}"
56
+ # puts root_path
57
+ Zip::File.open(file) do |zip_file|
58
+ if block_given?
59
+ yield root_path, zip_file
60
+ else
61
+ zip_file.each do |f|
62
+ f_path = File.join(root_path, f.name)
63
+ FileUtils.mkdir_p(File.dirname(f_path))
64
+ zip_file.extract(f, f_path) unless File.exist?(f_path)
65
+ end
66
+ end
67
+ end
68
+
69
+ root_path
70
+ end
71
+
72
+
73
+ def tempdir(file, prefix:, system: false)
74
+ dest_path = if system
75
+ Dir.mktmpdir("appinfo-#{prefix}-#{File.basename(file, '.*')}-", '/tmp')
76
+ else
77
+ File.join(File.dirname(file), prefix)
78
+ end
79
+
80
+ dest_file = File.join(dest_path, File.basename(file))
81
+ FileUtils.mkdir_p(dest_path, mode: 0_700) unless system
82
+ dest_file
83
+ end
84
+
85
+ end
86
+
87
+
88
+ end
89
+
90
+ end
@@ -0,0 +1,202 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'forwardable'
4
+ require 'cfpropertylist'
5
+
6
+ module AppEntitlementsStatistics
7
+
8
+ # Apple Device Type
9
+ module Device
10
+ MACOS = 'macOS'
11
+ IPHONE = 'iPhone'
12
+ IPAD = 'iPad'
13
+ UNIVERSAL = 'Universal'
14
+ end
15
+
16
+ # iOS Info.plist parser
17
+ class InfoPlist
18
+ extend Forwardable
19
+
20
+ def initialize(file)
21
+ @file = file
22
+ end
23
+
24
+ #
25
+ # Extract the permissions UsageDescription from the Info.plist
26
+ # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CocoaKeys.html
27
+ #
28
+ def permis_usagedescription
29
+ desc = Hash.new
30
+ info.each do |key, value|
31
+ if key.include?("UsageDescription")
32
+ desc[key] = value
33
+ end
34
+ end
35
+ desc
36
+ end
37
+
38
+ def backgroundModes
39
+ value = info.try(:[], 'UIBackgroundModes')
40
+ h = Hash.new
41
+ if !value.nil?
42
+ h["Background Modes"] = {
43
+ "UIBackgroundModes" => value
44
+ }
45
+ end
46
+ h
47
+ end
48
+
49
+ #
50
+ # Extract the capabilities from the Info.plist
51
+ # https://mercury.tencent.com/introduction/show?item=ios
52
+ #
53
+ def enabled_capabilities
54
+ capabilities = Hash.new
55
+ info.each do |key, value|
56
+ case key
57
+ when 'UIRequiredDeviceCapabilities'
58
+ capabilities['RequiredDeviceCapabilities'] = {
59
+ key => value
60
+ }
61
+ when 'UIFileSharingEnabled'
62
+ capabilities['FileSharingEnabled' ] = {
63
+ key => value
64
+ }
65
+ when 'CADisableMinimumFrameDurationOnPhone'
66
+ capabilities['DisableMinimumFrameDurationOnPhone'] = {
67
+ key => value
68
+ }
69
+ when 'GCSupportedGameControllers'
70
+ capabilities['GameControllers'] = {
71
+ key => value
72
+ }
73
+ when 'GCSupportsControllerUserInteraction'
74
+ capabilities['SupportsControllerUserInteraction' ] = {
75
+ key => value
76
+ }
77
+ when 'MKDirectionsApplicationSupportedModes'
78
+ capabilities['Maps'] = {
79
+ key => value
80
+ }
81
+ when 'NSAppTransportSecurity'
82
+ capabilities['AppTransportSecurity'] = {
83
+ key => value
84
+ }
85
+ when 'CFBundleDocumentTypes'
86
+ capabilities['BundleDocumentTypes'] = {
87
+ key => value
88
+ }
89
+ end
90
+ end
91
+ capabilities
92
+ end
93
+
94
+
95
+ def device_type
96
+ device_family = info.try(:[], 'UIDeviceFamily')
97
+ if device_family == [1]
98
+ Device::IPHONE
99
+ elsif device_family == [2]
100
+ Device::IPAD
101
+ elsif device_family == [1, 2]
102
+ Device::UNIVERSAL
103
+ elsif !info.try(:[], 'DTSDKName').nil? || !info.try(:[], 'DTPlatformName').nil?
104
+ Device::MACOS
105
+ end
106
+ end
107
+
108
+ def version
109
+ release_version || build_version
110
+ end
111
+
112
+ def build_version
113
+ info.try(:[], 'CFBundleVersion')
114
+ end
115
+
116
+ def release_version
117
+ info.try(:[], 'CFBundleShortVersionString')
118
+ end
119
+
120
+ def identifier
121
+ info.try(:[], 'CFBundleIdentifier')
122
+ end
123
+ alias bundle_id identifier
124
+
125
+ def name
126
+ display_name || bundle_name
127
+ end
128
+
129
+ def display_name
130
+ info.try(:[], 'CFBundleDisplayName')
131
+ end
132
+
133
+ def bundle_name
134
+ info.try(:[], 'CFBundleName')
135
+ end
136
+
137
+ def min_os_version
138
+ min_sdk_version || min_system_version
139
+ end
140
+
141
+ #
142
+ # Extract the Minimum OS Version from the Info.plist (iOS Only)
143
+ #
144
+ def min_sdk_version
145
+ info.try(:[], 'MinimumOSVersion')
146
+ end
147
+
148
+ #
149
+ # Extract the Minimum OS Version from the Info.plist (macOS Only)
150
+ #
151
+ def min_system_version
152
+ info.try(:[], 'LSMinimumSystemVersion')
153
+ end
154
+
155
+ def iphone?
156
+ device_type == Device::IPHONE
157
+ end
158
+
159
+ def ipad?
160
+ device_type == Device::IPAD
161
+ end
162
+
163
+ def universal?
164
+ device_type == Device::UNIVERSAL
165
+ end
166
+
167
+ def macos?
168
+ device_type == Device::MACOS
169
+ end
170
+
171
+ def device_family
172
+ info.try(:[], 'UIDeviceFamily') || []
173
+ end
174
+
175
+ def [](key)
176
+ info.try(:[], key.to_s)
177
+ end
178
+
179
+ def_delegators :info, :to_h
180
+
181
+ def method_missing(method_name, *args, &block)
182
+ info.try(:[], method_name.to_s.ai_camelcase) ||
183
+ info.send(method_name) ||
184
+ super
185
+ end
186
+
187
+ def respond_to_missing?(method_name, *args)
188
+ info.key?(method_name.to_s.ai_camelcase) ||
189
+ info.respond_to?(method_name) ||
190
+ super
191
+ end
192
+
193
+ private
194
+
195
+ def info
196
+ return unless File.file?(@file)
197
+
198
+ @info ||= CFPropertyList.native_types(CFPropertyList::List.new(file: @file).value)
199
+ end
200
+
201
+ end
202
+ end