headdesk 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +10 -0
- data/.reek.yml +10 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/Gemfile +10 -0
- data/Gemfile.lock +86 -0
- data/LICENSE.txt +21 -0
- data/README.md +74 -0
- data/Rakefile +4 -0
- data/bin/console +15 -0
- data/bin/facebook_sdk_versions +24 -0
- data/bin/setup +8 -0
- data/exe/headdesk +5 -0
- data/ext/apktool_2.3.4.jar +0 -0
- data/headdesk.gemspec +44 -0
- data/lib/headdesk.rb +24 -0
- data/lib/headdesk/analize.rb +14 -0
- data/lib/headdesk/apk.rb +78 -0
- data/lib/headdesk/apk/class.rb +46 -0
- data/lib/headdesk/apk/field.rb +28 -0
- data/lib/headdesk/apk/method.rb +16 -0
- data/lib/headdesk/apk/resources.rb +95 -0
- data/lib/headdesk/apktool.rb +45 -0
- data/lib/headdesk/check.rb +184 -0
- data/lib/headdesk/checks/api26.rb +27 -0
- data/lib/headdesk/checks/facebook.rb +41 -0
- data/lib/headdesk/checks/receiver.rb +31 -0
- data/lib/headdesk/checks/teak.rb +52 -0
- data/lib/headdesk/checks/teak/api21_icon.rb +34 -0
- data/lib/headdesk/checks/teak/caching.rb +30 -0
- data/lib/headdesk/checks/teak/teak.rb +74 -0
- data/lib/headdesk/cli.rb +107 -0
- data/lib/headdesk/data/facebook_sdk_versions.yaml +281 -0
- data/lib/headdesk/descriptionator.rb +36 -0
- data/lib/headdesk/report.rb +91 -0
- data/lib/headdesk/version.rb +6 -0
- metadata +180 -0
@@ -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,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
|