headdesk 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'headdesk/apk/field'
4
+ require 'headdesk/apk/method'
5
+
6
+ module Headdesk
7
+ class Apk
8
+ #
9
+ # A Smali bytecode class
10
+ #
11
+ class Class
12
+ # Formats:
13
+ # android/content/Context
14
+ # android.content.Context
15
+ def self.path_for(decl)
16
+ File.join(*decl.split(%r{[\/,\.]}))
17
+ end
18
+
19
+ def initialize(smali_file)
20
+ @smali = File.read(smali_file)
21
+ end
22
+
23
+ def method?(name)
24
+ method(name) != false
25
+ end
26
+
27
+ def method(name)
28
+ matchdata = /(^\.method .* #{name}.*$[\s\S]*?\.end method)/.match(@smali)
29
+ return nil unless matchdata
30
+
31
+ Method.new(matchdata)
32
+ end
33
+
34
+ def field?(name)
35
+ field(name) != false
36
+ end
37
+
38
+ def field(name)
39
+ matchdata = /^\.field .* #{name}.* = "(.*)"$/.match(@smali)
40
+ return nil unless matchdata
41
+
42
+ Field.new(matchdata)
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Headdesk
4
+ class Apk
5
+ #
6
+ # A Smali bytecode field
7
+ #
8
+ class Field
9
+ attr_reader :code, :value
10
+
11
+ def initialize(matchdata)
12
+ @code = matchdata[0]
13
+ @value = matchdata[1]
14
+
15
+ @value.extend(ExtraMethods)
16
+ end
17
+ end
18
+
19
+ #
20
+ # Extra methods for 'value'
21
+ #
22
+ module ExtraMethods
23
+ def to_version
24
+ /^(\d+\.)?(\d+\.)?(\*|\d+)$/.match(self).captures.map(&:to_i)
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Headdesk
4
+ class Apk
5
+ #
6
+ # A Smali bytecode method
7
+ #
8
+ class Method
9
+ attr_reader :code
10
+
11
+ def initialize(matchdata)
12
+ @code = matchdata[0]
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'ostruct'
4
+
5
+ module Headdesk
6
+ class Apk
7
+ #
8
+ # Android Resources searching
9
+ #
10
+ class Resources
11
+ def initialize(path)
12
+ @path = path
13
+ end
14
+
15
+ def values(modifiers = {})
16
+ XmlCollection.new(@path, 'values', modifiers)
17
+ end
18
+
19
+ #
20
+ # Collection of XML values for specific locale/api/etc
21
+ #
22
+ class XmlCollection
23
+ # :reek:NestedIterators and :reek:TooManyStatements
24
+ def initialize(path, type, modifiers = {})
25
+ @resources = {}
26
+ globspec = File.join(path, 'res', "#{type}{#{XmlCollection.api_versions(modifiers).join(',')}}", '*.xml')
27
+ Dir.glob(globspec).each do |file_name|
28
+ xml = File.open(file_name) { |file| Nokogiri::XML(file) }
29
+
30
+ @resources.merge! XmlCollection.named_elements(xml)
31
+ @resources.merge! XmlCollection.item_elements(xml)
32
+ end
33
+ end
34
+
35
+ def respond_to_missing?(method_name, include_all)
36
+ @resources.include?(method_name.to_s) || super
37
+ end
38
+
39
+ def method_missing(method_name, *arguments, &block)
40
+ super unless @resources.include?(method_name.to_s)
41
+
42
+ OpenStruct.new(@resources[method_name.to_s])
43
+ end
44
+
45
+ private
46
+
47
+ def self.api_versions(modifiers)
48
+ mods = [nil]
49
+ if modifiers.key?(:v)
50
+ (1..modifiers[:v].to_i).each do |api_version|
51
+ mods << "-v#{api_version}"
52
+ end
53
+ end
54
+ mods
55
+ end
56
+
57
+ # :reek:TooManyStatements
58
+ def self.named_elements(xml)
59
+ named_elements = %i[string integer color bool]
60
+
61
+ resources = {}
62
+ xml.xpath("//#{named_elements.join('|//')}").each do |elem|
63
+ type = elem.name.to_s
64
+ name = elem.attributes['name'].to_s
65
+
66
+ resources[type] ||= {}
67
+ resources[type][name] = case type
68
+ when 'bool'
69
+ elem.text == true.to_s
70
+ when 'integer'
71
+ elem.text.to_i
72
+ else
73
+ elem.text
74
+ end
75
+ end
76
+ resources
77
+ end
78
+
79
+ # :reek:TooManyStatements
80
+ def self.item_elements(xml)
81
+ item_elements = %i[drawable]
82
+ resources = {}
83
+ xml.xpath("//item[#{item_elements.map { |elem| "contains(@type, '#{elem}')" }.join('or')}]").each do |elem|
84
+ type = elem.attributes['type'].to_s
85
+ name = elem.attributes['name'].to_s
86
+
87
+ resources[type] ||= {}
88
+ resources[type][name] = elem.text
89
+ end
90
+ resources
91
+ end
92
+ end
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'open3'
4
+
5
+ module Headdesk
6
+ #
7
+ # Wrapper around using https://ibotpeaches.github.io/Apktool/
8
+ #
9
+ class ApkTool
10
+ def self.apktool_jar
11
+ "ext/apktool_#{APKTOOL_VERSION}.jar"
12
+ end
13
+
14
+ #
15
+ # Run apktool command
16
+ #
17
+ # :reek:TooManyStatements
18
+ def self.cmd(*args)
19
+ _stdin, stdout, stderr, wait_thr = Open3.popen3('java', '-jar', apktool_jar, *args)
20
+ r_stdout = stdout.gets(nil)
21
+ stdout.close
22
+ r_stderr = stderr.gets(nil)
23
+ stderr.close
24
+ r_exit_code = wait_thr.value
25
+
26
+ [r_stdout, r_stderr, r_exit_code]
27
+ end
28
+
29
+ #
30
+ # Unpacks an APK to the specified path
31
+ #
32
+ # :reek:TooManyStatements
33
+ def self.unpack_to(path, destination)
34
+ throw CliError.new("File not found: #{path}") unless File.exist?(path)
35
+ throw CliError.new("Path not found: #{destination}") unless Dir.exist?(destination)
36
+
37
+ args = ['d', '--force', '--output', destination]
38
+
39
+ stdout, stderr, exit_code = Headdesk::ApkTool.cmd(*args, path)
40
+ raise stderr unless exit_code.to_i.zero?
41
+
42
+ stdout
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,184 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'headdesk/descriptionator'
4
+
5
+ module Headdesk
6
+ #
7
+ # Check for a potential issue in an apk or ipa
8
+ #
9
+ # :reek:ModuleInitialize
10
+ module Check
11
+ attr_reader :report, :status, :apk, :ipa
12
+
13
+ def self.for_apk
14
+ APK.all
15
+ end
16
+
17
+ def self.for_ipa
18
+ IPA.all
19
+ end
20
+
21
+ def self.included(klass)
22
+ klass.extend(ClassMethods)
23
+ end
24
+
25
+ #
26
+ # Class methods for Check
27
+ #
28
+ module ClassMethods
29
+ def describe(desc = nil)
30
+ @last_desc = desc if desc
31
+ @last_desc
32
+ end
33
+ end
34
+
35
+ def initialize(bundle)
36
+ @apk = bundle
37
+ @ipa = bundle
38
+ @report = {
39
+ description: self.class.describe,
40
+ steps: [],
41
+ export: {}
42
+ }
43
+ @status = :skip
44
+ end
45
+
46
+ def describe(desc = nil)
47
+ @last_desc = desc if desc
48
+ @last_desc
49
+ end
50
+
51
+ # :reek:ManualDispatch and :reek:TooManyStatements and :reek:FeatureEnvy
52
+ def check_control_flow(status_to_assign, conditions = nil)
53
+ pass = !conditions || conditions.empty?
54
+ raise ArgumentError, 'Do not specify both if: and unless:' if
55
+ conditions.key?(:if) && conditions.key?(:unless)
56
+
57
+ pass = Check.condition?(conditions, :if) if conditions.key? :if
58
+ pass = !Check.condition?(conditions, :unless) if conditions.key? :unless
59
+
60
+ skip = false
61
+ raise ArgumentError, 'Do not specify both skip_if: and skip_unless:' if
62
+ conditions.key?(:skip_if) && conditions.key?(:skip_unless)
63
+
64
+ skip = Check.condition?(conditions, :skip_if) if conditions.key? :skip_if
65
+ skip = !Check.condition?(conditions, :skip_unless) if conditions.key? :skip_unless
66
+
67
+ # TODO: greater_than, less_than, equals
68
+
69
+ # rubocop:disable RescueStandardError
70
+ # Try and get an auto-description
71
+ default_description = describe.to_s
72
+ description = begin
73
+ if conditions[:unless].respond_to?(:call)
74
+ descriptionator = Headdesk::Descriptionator.new(:unless)
75
+ desc = descriptionator.instance_exec(&conditions[:unless])
76
+ desc.is_a?(String) ? desc : default_description
77
+ elsif conditions[:if].respond_to?(:call)
78
+ descriptionator = Headdesk::Descriptionator.new(:if)
79
+ desc = descriptionator.instance_exec(&conditions[:if])
80
+ desc.is_a?(String) ? desc : default_description
81
+ else
82
+ default_description
83
+ end
84
+ rescue
85
+ default_description
86
+ end
87
+ # rubocop:enable RescueStandardError
88
+
89
+ @status = status_to_assign if pass && !skip
90
+ @report[:steps] << {
91
+ description: description,
92
+ status: skip ? :skip : @status
93
+ }
94
+ return unless pass
95
+
96
+ throw :halt_check
97
+ end
98
+
99
+ def skip_check(conditions = {})
100
+ check_control_flow(:skip, conditions)
101
+ end
102
+
103
+ def fail_check(conditions = {})
104
+ check_control_flow(:fail, conditions)
105
+ end
106
+
107
+ def export(merge = {})
108
+ @report[:export].merge! merge
109
+ end
110
+
111
+ def preconditions?
112
+ true
113
+ end
114
+
115
+ def run
116
+ before
117
+ catch(:halt_check) do
118
+ call
119
+ end
120
+ after
121
+
122
+ @status
123
+ end
124
+
125
+ # :reek:ManualDispatch
126
+ def process
127
+ return report unless respond_to?(:call) && preconditions?
128
+
129
+ @status = :success
130
+ report[:status] = run
131
+ report
132
+ end
133
+
134
+ def before; end
135
+
136
+ def after; end
137
+
138
+ #
139
+ # Check applies to APKs
140
+ #
141
+ module APK
142
+ def self.all
143
+ @all
144
+ end
145
+
146
+ def self.included(klass)
147
+ @all ||= []
148
+ @all << klass
149
+ klass.include(Check)
150
+ end
151
+ end
152
+
153
+ #
154
+ # Check applies to IPAs
155
+ #
156
+ module IPA
157
+ def self.all
158
+ @all
159
+ end
160
+
161
+ def self.included(klass)
162
+ @all ||= []
163
+ @all << klass
164
+ klass.include(Check)
165
+ end
166
+ end
167
+
168
+ # :reek:ManualDispatch
169
+ def self.condition?(conditions, key)
170
+ condition = conditions.fetch(key, nil)
171
+ if !condition
172
+ false
173
+ elsif condition.respond_to? :call
174
+ condition.call
175
+ elsif %w[true false].include?(condition.to_s)
176
+ condition.to_s == 'true'
177
+ else
178
+ raise ArgumentError, 'fail_check and skip_check only accept truthy, falsy, nil, or Proc arguments'
179
+ end
180
+ end
181
+ end
182
+ end
183
+
184
+ Dir[File.dirname(__FILE__) + '/checks/*.rb'].each { |file| require file }
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Headdesk
4
+ module Checks
5
+ #
6
+ # APKs should all target API 26+
7
+ #
8
+ # :reek:UncommunicativeModuleName
9
+ class Api26
10
+ include Check::APK
11
+
12
+ describe 'targetSdkVersion must be at least 26'
13
+ def call
14
+ fail_check unless: -> { apk.targets_sdk 26 }
15
+
16
+ klass_def = 'android/support/v4/app/NotificationCompat$Builder'
17
+ skip_check unless: -> { apk.class?(klass_def) }
18
+ klass = apk.find_class(klass_def)
19
+
20
+ describe 'support-v4 version is 26.1+'
21
+ fail_check unless: -> { klass.method?('setChannelId') }
22
+
23
+ # TODO: AndroidX/JetPack
24
+ end
25
+ end
26
+ end
27
+ end