app_permission_statistics 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.DS_Store +0 -0
- data/.gitignore +8 -0
- data/Gemfile +10 -0
- data/Gemfile.lock +33 -0
- data/README.md +124 -0
- data/Rakefile +4 -0
- data/analyze_report +481 -0
- data/app_permission_statistics.gemspec +39 -0
- data/bin/app_permission_statistics +41 -0
- data/bin/setup +8 -0
- data/lib/app_permission_statistics/analyze.rb +220 -0
- data/lib/app_permission_statistics/core_ext/inflector.rb +35 -0
- data/lib/app_permission_statistics/core_ext/try.rb +112 -0
- data/lib/app_permission_statistics/entitlements.rb +185 -0
- data/lib/app_permission_statistics/extracter.rb +135 -0
- data/lib/app_permission_statistics/helper.rb +90 -0
- data/lib/app_permission_statistics/info_plist.rb +202 -0
- data/lib/app_permission_statistics/mobile_provision.rb +317 -0
- data/lib/app_permission_statistics/version.rb +5 -0
- data/lib/app_permission_statistics.rb +49 -0
- metadata +127 -0
@@ -0,0 +1,135 @@
|
|
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 AppPermissionStatistics
|
12
|
+
|
13
|
+
class Extracter
|
14
|
+
include Helper::Archive
|
15
|
+
include Helper::Sigflat
|
16
|
+
include Helper::Utils
|
17
|
+
extend Forwardable
|
18
|
+
attr_reader :file
|
19
|
+
attr_reader :store_path
|
20
|
+
|
21
|
+
def initialize(file, store_path = nil)
|
22
|
+
@file = file
|
23
|
+
@store_path = store_path
|
24
|
+
end
|
25
|
+
|
26
|
+
def extract_update
|
27
|
+
capabilities_summary = Hash.new
|
28
|
+
capabilities = extract_capabilities_info
|
29
|
+
capabilities.each do |key,value|
|
30
|
+
capabilities_summary[key] = createsig(value)
|
31
|
+
end
|
32
|
+
|
33
|
+
usage_desc_summary = Hash.new
|
34
|
+
usage_desc = plistInfo.permis_usagedescription
|
35
|
+
usage_desc.each do |key,value|
|
36
|
+
usage_desc_summary[key] = createsig(value)
|
37
|
+
end
|
38
|
+
|
39
|
+
yaml_content = {
|
40
|
+
'Capabilities_Summary' => capabilities_summary,
|
41
|
+
'Capabilities' => capabilities,
|
42
|
+
'PermissionsUsageDescription_Summary' => usage_desc_summary,
|
43
|
+
'PermissionsUsageDescription' => usage_desc
|
44
|
+
}
|
45
|
+
yaml_name = entitlements_yaml_name(plistInfo.version, plistInfo.identifier, path: @store_path)
|
46
|
+
|
47
|
+
File.open(yaml_name, "w") { |file| file.write(yaml_content.to_yaml) }
|
48
|
+
update_versions
|
49
|
+
yaml_content
|
50
|
+
end
|
51
|
+
|
52
|
+
|
53
|
+
def update_versions
|
54
|
+
yaml_name = versions_yaml_name(plistInfo.identifier, path: @store_path)
|
55
|
+
yaml_content = [ ]
|
56
|
+
if File.exist?(yaml_name)
|
57
|
+
yaml_content = YAML.load_file(yaml_name)
|
58
|
+
end
|
59
|
+
version = plistInfo.version
|
60
|
+
if !yaml_content.include?(version)
|
61
|
+
yaml_content.unshift(version)
|
62
|
+
File.open(yaml_name, "w") { |file| file.write(yaml_content.to_yaml) }
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def extract_capabilities_info
|
67
|
+
capabilities_info = {}
|
68
|
+
capabilities_info["In-App Purchase"] = true
|
69
|
+
capabilities_info = capabilities_info.merge(entitlementsInfo.enabled_capabilities)
|
70
|
+
capabilities_info = capabilities_info.merge(plistInfo.backgroundModes)
|
71
|
+
capabilities_info = capabilities_info.merge(plistInfo.enabled_capabilities)
|
72
|
+
capabilities_info
|
73
|
+
end
|
74
|
+
|
75
|
+
def mobileprovision
|
76
|
+
return unless mobileprovision?
|
77
|
+
return @mobileprovision if @mobileprovision
|
78
|
+
@mobileprovision = MobileProvision.new(mobileprovision_path)
|
79
|
+
end
|
80
|
+
|
81
|
+
def plistInfo
|
82
|
+
@plistInfo ||= InfoPlist.new(info_path)
|
83
|
+
end
|
84
|
+
|
85
|
+
def entitlementsInfo
|
86
|
+
@entitlementsInfo ||= EntitlementsPlist.new(entitlements_path)
|
87
|
+
end
|
88
|
+
|
89
|
+
|
90
|
+
def mobileprovision?
|
91
|
+
File.exist?(mobileprovision_path)
|
92
|
+
end
|
93
|
+
|
94
|
+
def mobileprovision_path
|
95
|
+
filename = 'embedded.mobileprovision'
|
96
|
+
@mobileprovision_path ||= File.join(@file, filename)
|
97
|
+
unless File.exist?(@mobileprovision_path)
|
98
|
+
@mobileprovision_path = File.join(app_path, filename)
|
99
|
+
end
|
100
|
+
|
101
|
+
@mobileprovision_path
|
102
|
+
end
|
103
|
+
|
104
|
+
|
105
|
+
def contents
|
106
|
+
@contents ||= unarchive(@file, path: 'ios')
|
107
|
+
end
|
108
|
+
|
109
|
+
def info_path
|
110
|
+
@info_path ||= File.join(app_path, 'Info.plist')
|
111
|
+
end
|
112
|
+
|
113
|
+
def entitlements_path
|
114
|
+
@entitlements_path ||= File.join(app_path, 'Runner.entitlements')
|
115
|
+
end
|
116
|
+
|
117
|
+
def app_path
|
118
|
+
@app_path ||= Dir.glob(File.join(contents, 'Payload', '*.app')).first
|
119
|
+
end
|
120
|
+
|
121
|
+
def clear!
|
122
|
+
return unless @contents
|
123
|
+
|
124
|
+
FileUtils.rm_rf(@contents)
|
125
|
+
|
126
|
+
@contents = nil
|
127
|
+
@app_path = nil
|
128
|
+
@info_path = nil
|
129
|
+
@info = nil
|
130
|
+
@pre_version = nil
|
131
|
+
end
|
132
|
+
|
133
|
+
end
|
134
|
+
|
135
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
require 'tmpdir'
|
2
|
+
require 'crimp'
|
3
|
+
|
4
|
+
module AppPermissionStatistics
|
5
|
+
|
6
|
+
module Helper
|
7
|
+
|
8
|
+
module Sigflat
|
9
|
+
def createsig(body)
|
10
|
+
return Crimp.signature(body)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
module Utils
|
15
|
+
|
16
|
+
def defalut_store_path(app_identifier)
|
17
|
+
root_path = "#{Dir.home}/appInfo-#{app_identifier}"
|
18
|
+
FileUtils.mkdir_p(root_path) unless File.exists?(root_path)
|
19
|
+
root_path
|
20
|
+
end
|
21
|
+
|
22
|
+
def entitlements_yaml_name(v,app_identifier,path: nil)
|
23
|
+
dir_path = path ? "#{path}-#{app_identifier}" : defalut_store_path(app_identifier)
|
24
|
+
yaml_name = "#{dir_path}/entitlements_#{v}.yml";
|
25
|
+
yaml_name
|
26
|
+
end
|
27
|
+
|
28
|
+
def versions_yaml_name(app_identifier,path: nil)
|
29
|
+
dir_path = path ? "#{path}-#{app_identifier}" : defalut_store_path(app_identifier)
|
30
|
+
yaml_name = "#{dir_path}/entitlements_versions.yml";
|
31
|
+
yaml_name
|
32
|
+
end
|
33
|
+
|
34
|
+
def report_file_name(path: nil)
|
35
|
+
path = path ? "#{path}/entitlements" : nil
|
36
|
+
if !path.nil?
|
37
|
+
FileUtils.mkdir_p(path) unless File.exists?(path)
|
38
|
+
end
|
39
|
+
file_name = path ? "#{path}/analyze_report" : "analyze_report";
|
40
|
+
file_name
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
44
|
+
|
45
|
+
module Archive
|
46
|
+
require 'fileutils'
|
47
|
+
require 'securerandom'
|
48
|
+
require 'zip'
|
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 AppPermissionStatistics
|
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
|
+
if !value.nil?
|
41
|
+
return {
|
42
|
+
"BackgroundModes" => {
|
43
|
+
"UIBackgroundModes" => value
|
44
|
+
}
|
45
|
+
}
|
46
|
+
end
|
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
|