headdesk 0.1.0
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/.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
|