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.
@@ -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