app_permission_statistics 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/app_permission_statistics/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "app_permission_statistics"
7
+ spec.version = AppPermissionStatistics::VERSION
8
+ spec.authors = ["bin"]
9
+ spec.email = ["tang.bin@olaola.chat"]
10
+
11
+ spec.summary = "app permission statistics"
12
+ spec.description = "app permission statistics from ipas"
13
+ spec.homepage = "https://github.com/olaola-chat/cli-app_permission_statistics.git"
14
+ spec.required_ruby_version = Gem::Requirement.new(">= 2.3.0")
15
+
16
+
17
+ spec.metadata["homepage_uri"] = spec.homepage
18
+ spec.metadata["source_code_uri"] = "https://github.com/olaola-chat/cli-app_permission_statistics.git"
19
+
20
+ # Specify which files should be added to the gem when it is released.
21
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
22
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
23
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{\A(?:test|spec|features)/}) }
24
+ end
25
+ spec.bindir = "bin"
26
+ spec.executables = ['app_permission_statistics']
27
+ spec.require_paths = ["lib"]
28
+
29
+ spec.add_runtime_dependency "yaml"
30
+ spec.add_runtime_dependency "crimp"
31
+ spec.add_dependency 'CFPropertyList', '< 3.1.0', '>= 2.3.4'
32
+ spec.add_development_dependency 'bundler', '>= 1.12'
33
+
34
+ # Uncomment to register a new dependency of your gem
35
+ # spec.add_dependency "example-gem", "~> 1.0"
36
+
37
+ # For more information and examples about making a new gem, checkout our
38
+ # guide at: https://bundler.io/guides/creating_gem.html
39
+ end
@@ -0,0 +1,41 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "bundler/setup"
5
+ require "app_permission_statistics"
6
+
7
+ # You can add fixtures and/or initialization code here to make experimenting
8
+ # with your gem easier. You can also use a different console, if you like.
9
+
10
+ # (If you use this, don't forget to add pry to your Gemfile!)
11
+ # require "pry"
12
+ # Pry.start
13
+
14
+ if ARGV.length < 2
15
+ puts "Error: need two ipa path !!"
16
+ abort
17
+ end
18
+
19
+ if !File.exist?(ARGV.first)
20
+ puts "Error: #{ARGV.first} not found"
21
+ abort
22
+ end
23
+
24
+ if !File.exist?(ARGV[1])
25
+ puts "Error: #{ARGV[1]} not found"
26
+ abort
27
+ end
28
+
29
+ if ARGV.length == 1
30
+ actuator = AppPermissionStatistics::Actuator.new(ipaPath)
31
+ actuator.run
32
+ elsif ARGV.length == 2
33
+ actuator = AppPermissionStatistics::Actuator.new(ARGV[0],ARGV[1],report_path: ARGV[2])
34
+ actuator.run
35
+ elsif ARGV.length == 3
36
+ actuator = AppPermissionStatistics::Actuator.new(ARGV[0],ARGV[1],report_path:ARGV[2])
37
+ actuator.run
38
+ elsif ARGV.length >= 4
39
+ actuator = AppPermissionStatistics::Actuator.new(ARGV[0],ARGV[1],report_path:ARGV[2],store_path:ARGV[3])
40
+ actuator.run
41
+ end
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,220 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "helper"
4
+ require 'fileutils'
5
+ require 'yaml'
6
+
7
+ module AppPermissionStatistics
8
+
9
+ class Analyzer
10
+ include Helper::Utils
11
+
12
+ attr_reader :store_path
13
+ attr_reader :report_path
14
+ attr_reader :identifier
15
+
16
+ def initialize(identifier,report_path = nil,store_path = nil)
17
+ @identifier = identifier
18
+ @report_path = report_path
19
+ @store_path = store_path
20
+ end
21
+
22
+ def analyze
23
+ load_version
24
+ return singleGenerate unless @versions.length > 1
25
+ pre_version = @versions[1]
26
+ cur_version = @versions[0]
27
+ pre_entitlements = load_entitlements_yaml(pre_version)
28
+ cur_entitlements = load_entitlements_yaml(cur_version)
29
+ return "" unless pre_entitlements.any?
30
+ return "" unless cur_entitlements.any?
31
+
32
+ capabilities_diff = analyze_capabilities_diff(pre_entitlements,cur_entitlements)
33
+ usage_descs_diff = analyze_usage_descs_diff(pre_entitlements,cur_entitlements);
34
+ report_path = generate(capabilities_diff,usage_descs_diff)
35
+ report_path
36
+ end
37
+
38
+ def singleGenerate
39
+ return "" unless @versions.length > 0
40
+ output = "no more versions to compare !! "
41
+ output += "\n\n\n#{@versions.first} entitlements list: "
42
+ output += "\n" + read_each_line(@versions.first)
43
+ output += "\n" + "----------------------------------------"
44
+ report_path = report_file_name(path: @report_path)
45
+ File.open(report_path, 'w') { |file|
46
+ file.write(output)
47
+ }
48
+ report_path
49
+ end
50
+
51
+
52
+ def generate(capabilities_diff,usage_descs_diff)
53
+ return unless @versions.length > 1
54
+ cur_version = @versions[0]
55
+ pre_version = @versions[1]
56
+
57
+ output = "\n" + "compared #{cur_version} #{pre_version}"
58
+ output += "\n" + capabilities_diff unless capabilities_diff.empty?
59
+ output += "\n" + usage_descs_diff unless usage_descs_diff.empty?
60
+ output += "\n\nThere is no difference between the two versions \n\n" unless (!capabilities_diff.empty? || !usage_descs_diff.empty?)
61
+ puts output
62
+ puts "----------------------------------------"
63
+ output += "\n\n\n#{cur_version} entitlements list: "
64
+ output += "\n" + read_each_line(cur_version)
65
+ output += "\n" + "----------------------------------------"
66
+ output += "\n\n#{pre_version} entitlements list: "
67
+ output += "\n" + read_each_line(pre_version)
68
+ output += "\n" + "----------------------------------------"
69
+
70
+ report_path = report_file_name(path: @report_path)
71
+ File.open(report_path, 'w') { |file|
72
+ file.write(output)
73
+ }
74
+ puts "detail file: #{File.expand_path(report_path)}"
75
+ report_path
76
+ end
77
+
78
+ def read_each_line(version)
79
+ file_name = entitlements_yaml_name(version,@identifier, path: @store_path)
80
+ file_content = ""
81
+ File.open(file_name,"r").each_line do |line|
82
+ file_content += "\n" + line
83
+ end
84
+ file_content
85
+ end
86
+
87
+ def load_version
88
+ yaml_name = versions_yaml_name(@identifier, path: @store_path)
89
+ if File.exist?(yaml_name)
90
+ @versions = YAML.load_file(yaml_name)
91
+ end
92
+ end
93
+
94
+ def load_entitlements_yaml(version)
95
+ yaml_name = entitlements_yaml_name(version,@identifier, path: @store_path)
96
+ yaml_content = [ ]
97
+ if File.exist?(yaml_name)
98
+ yaml_content = YAML.load_file(yaml_name)
99
+ end
100
+ puts "#{yaml_name} not found!!!" unless !yaml_content.empty?
101
+ yaml_content
102
+ end
103
+
104
+ def analyze_capabilities_diff(pre,cur)
105
+ output = ""
106
+ cur_capabilities_summary = cur['Capabilities_Summary']
107
+ pre_capabilities_summary = pre['Capabilities_Summary']
108
+ cur_capabilities = cur['Capabilities']
109
+ pre_capabilities = pre['Capabilities']
110
+ output = ""
111
+ # 修改
112
+ comm_capabilities = cur_capabilities_summary.keys & pre_capabilities_summary.keys
113
+ modifys = modifys_cur = modifys_pre = ""
114
+ comm_capabilities.each do |key|
115
+ if cur_capabilities_summary[key] != pre_capabilities_summary[key]
116
+ modifys_cur += "\n" + '- ' + key + ': ' + cur_capabilities[key].to_s
117
+ modifys_pre += "\n" + '- ' + key + ': ' + pre_capabilities[key].to_s
118
+ end
119
+ end
120
+
121
+ modifys += "\n" + @versions[0] unless modifys_cur.empty?
122
+ modifys += modifys_cur unless modifys_cur.empty?
123
+ modifys += "\n" + @versions[1] unless modifys_pre.empty?
124
+ modifys += modifys_pre unless modifys_pre.empty?
125
+
126
+ #新增
127
+ add_capabilities = cur_capabilities_summary.keys - pre_capabilities_summary.keys
128
+ adds = ""
129
+ add_capabilities.each do |key|
130
+ adds += "\n" + '- ' + key + ': ' + cur_capabilities[key].to_s
131
+ end
132
+
133
+ #移除
134
+ remove_capabilities = pre_capabilities_summary.keys - cur_capabilities_summary.keys
135
+ removes = ""
136
+ remove_capabilities.each do |key|
137
+ removes += "\n" + '- ' + key + ': ' + pre_capabilities[key].to_s
138
+ end
139
+
140
+ if !modifys.empty?
141
+ output += "\n" + "modify capabilitys : "
142
+ output += modifys
143
+ output += "\n------------------------------"
144
+ end
145
+
146
+ if !adds.empty?
147
+ output += "\n" + "add capabilitys : "
148
+ output += adds
149
+ output += "\n------------------------------"
150
+ end
151
+
152
+ if !removes.empty?
153
+ output += "\n" + "remove capabilitys : "
154
+ output += removes
155
+ output += "\n------------------------------"
156
+ end
157
+
158
+ output
159
+ end
160
+
161
+ def analyze_usage_descs_diff(pre,cur)
162
+ output = ""
163
+ cur_usage_descs_summary = cur['PermissionsUsageDescription_Summary']
164
+ pre_usage_descs_summary = pre['PermissionsUsageDescription_Summary']
165
+
166
+ cur_usage_descs = cur['PermissionsUsageDescription']
167
+ pre_usage_descs = pre['PermissionsUsageDescription']
168
+
169
+ # 修改
170
+ comm_usage_descs = cur_usage_descs_summary.keys & pre_usage_descs_summary.keys
171
+ modifys = modifys_cur = modifys_pre = ""
172
+ comm_usage_descs.each do |key|
173
+ if cur_usage_descs_summary[key] != pre_usage_descs_summary[key]
174
+ modifys_cur += "\n" + '- ' + key + ': ' + cur_usage_descs[key].to_s
175
+ modifys_pre += "\n" + '- ' + key + ': ' + pre_usage_descs[key].to_s
176
+ end
177
+ end
178
+
179
+ modifys += "\n" + @versions[0] unless modifys_cur.empty?
180
+ modifys += modifys_cur unless modifys_cur.empty?
181
+ modifys += "\n" + @versions[1] unless modifys_pre.empty?
182
+ modifys += modifys_pre unless modifys_pre.empty?
183
+
184
+ #新增
185
+ add_usage_descs = cur_usage_descs_summary.keys - pre_usage_descs_summary.keys
186
+ adds = ""
187
+ add_usage_descs.each do |key|
188
+ adds += "\n" + '- ' + key + ': ' + cur_usage_descs[key].to_s
189
+ end
190
+
191
+ #移除
192
+ remove_usage_descs = pre_usage_descs_summary.keys - cur_usage_descs_summary.keys
193
+ removes = ""
194
+ remove_usage_descs.each do |key|
195
+ removes += "\n" + '- ' + key + ': ' + pre_usage_descs[key].to_s
196
+ end
197
+
198
+ if !modifys.empty?
199
+ output += "\n" + "modify permissions usage description : "
200
+ output += modifys
201
+ output += "\n------------------------------"
202
+ end
203
+
204
+ if !adds.empty?
205
+ output += "\n" + "add permissions usage description : "
206
+ output += adds
207
+ output += "\n------------------------------"
208
+ end
209
+
210
+ if !removes.empty?
211
+ output += "\n" + "remove permissions usage description : "
212
+ output += removes
213
+ output += "\n------------------------------"
214
+ end
215
+
216
+ output
217
+ end
218
+ end
219
+
220
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AppPermissionStatistics
4
+ module Inflector
5
+ def ai_snakecase
6
+ gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
7
+ .gsub(/([a-z\d])([A-Z])/, '\1_\2')
8
+ .tr('-', '_')
9
+ .gsub(/\s/, '_')
10
+ .gsub(/__+/, '_')
11
+ .downcase
12
+ end
13
+
14
+ def ai_camelcase(first_letter: :upper, separators: ['-', '_', '\s'])
15
+ str = dup
16
+
17
+ separators.each do |s|
18
+ str = str.gsub(/(?:#{s}+)([a-z])/) { $1.upcase }
19
+ end
20
+
21
+ case first_letter
22
+ when :upper, true
23
+ str = str.gsub(/(\A|\s)([a-z])/) { $1 + $2.upcase }
24
+ when :lower, false
25
+ str = str.gsub(/(\A|\s)([A-Z])/) { $1 + $2.downcase }
26
+ end
27
+
28
+ str
29
+ end
30
+ end
31
+ end
32
+
33
+ class String
34
+ include AppPermissionStatistics::Inflector
35
+ end
@@ -0,0 +1,112 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyrights rails
4
+ # Copy from https://github.com/rails/rails/blob/master/activesupport/lib/active_support/core_ext/object/try.rb
5
+
6
+ module AppPermissionStatistics
7
+ module Tryable # :nodoc:
8
+ ##
9
+ # :method: try
10
+ #
11
+ # :call-seq:
12
+ # try(*a, &b)
13
+ #
14
+ # Invokes the public method whose name goes as first argument just like
15
+ # +public_send+ does, except that if the receiver does not respond to it the
16
+ # call returns +nil+ rather than raising an exception.
17
+ #
18
+ # This method is defined to be able to write
19
+ #
20
+ # @person.try(:name)
21
+ #
22
+ # instead of
23
+ #
24
+ # @person.name if @person
25
+ #
26
+ # +try+ calls can be chained:
27
+ #
28
+ # @person.try(:spouse).try(:name)
29
+ #
30
+ # instead of
31
+ #
32
+ # @person.spouse.name if @person && @person.spouse
33
+ #
34
+ # +try+ will also return +nil+ if the receiver does not respond to the method:
35
+ #
36
+ # @person.try(:non_existing_method) # => nil
37
+ #
38
+ # instead of
39
+ #
40
+ # @person.non_existing_method if @person.respond_to?(:non_existing_method) # => nil
41
+ #
42
+ # +try+ returns +nil+ when called on +nil+ regardless of whether it responds
43
+ # to the method:
44
+ #
45
+ # nil.try(:to_i) # => nil, rather than 0
46
+ #
47
+ # Arguments and blocks are forwarded to the method if invoked:
48
+ #
49
+ # @posts.try(:each_slice, 2) do |a, b|
50
+ # ...
51
+ # end
52
+ #
53
+ # The number of arguments in the signature must match. If the object responds
54
+ # to the method the call is attempted and +ArgumentError+ is still raised
55
+ # in case of argument mismatch.
56
+ #
57
+ # If +try+ is called without arguments it yields the receiver to a given
58
+ # block unless it is +nil+:
59
+ #
60
+ # @person.try do |p|
61
+ # ...
62
+ # end
63
+ #
64
+ # You can also call try with a block without accepting an argument, and the block
65
+ # will be instance_eval'ed instead:
66
+ #
67
+ # @person.try { upcase.truncate(50) }
68
+ #
69
+ # Please also note that +try+ is defined on +Object+. Therefore, it won't work
70
+ # with instances of classes that do not have +Object+ among their ancestors,
71
+ # like direct subclasses of +BasicObject+.
72
+ def try(method_name = nil, *args, &block)
73
+ if method_name.nil? && block_given?
74
+ if block.arity.zero?
75
+ instance_eval(&b)
76
+ else
77
+ yield self
78
+ end
79
+ elsif respond_to?(method_name)
80
+ public_send(method_name, *args, &block)
81
+ end
82
+ end
83
+
84
+ ##
85
+ # :method: try!
86
+ #
87
+ # :call-seq:
88
+ # try!(*a, &b)
89
+ #
90
+ # Same as #try, but raises a +NoMethodError+ exception if the receiver is
91
+ # not +nil+ and does not implement the tried method.
92
+ #
93
+ # "a".try!(:upcase) # => "A"
94
+ # nil.try!(:upcase) # => nil
95
+ # 123.try!(:upcase) # => NoMethodError: undefined method `upcase' for 123:Integer
96
+ def try!(method_name = nil, *args, &block)
97
+ if method_name.nil? && block_given?
98
+ if block.arity.zero?
99
+ instance_eval(&block)
100
+ else
101
+ yield self
102
+ end
103
+ else
104
+ public_send(method_name, *args, &block)
105
+ end
106
+ end
107
+ end
108
+ end
109
+
110
+ class Object
111
+ include AppPermissionStatistics::Tryable
112
+ end
@@ -0,0 +1,185 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'forwardable'
4
+ require 'cfpropertylist'
5
+
6
+ module AppPermissionStatistics
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