-
-
Notifications
You must be signed in to change notification settings - Fork 9.3k
/
audit.rb
287 lines (239 loc) 路 9.99 KB
/
audit.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
require "hbc/checkable"
require "hbc/download"
require "digest"
require "utils/git"
module Hbc
class Audit
include Checkable
attr_reader :cask, :commit_range, :download
def initialize(cask, download: false, check_token_conflicts: false, commit_range: nil, command: SystemCommand)
@cask = cask
@download = download
@commit_range = commit_range
@check_token_conflicts = check_token_conflicts
@command = command
end
def check_token_conflicts?
@check_token_conflicts
end
def run!
check_required_stanzas
check_version_and_checksum
check_version
check_sha256
check_appcast
check_url
check_generic_artifacts
check_token_conflicts
check_download
check_single_pre_postflight
check_single_uninstall_zap
self
rescue StandardError => e
odebug "#{e.message}\n#{e.backtrace.join("\n")}"
add_error "exception while auditing #{cask}: #{e.message}"
self
end
def success?
!(errors? || warnings?)
end
def summary_header
"audit for #{cask}"
end
private
def check_single_pre_postflight
odebug "Auditing preflight and postflight stanzas"
if cask.artifacts.count { |k| k.is_a?(Hbc::Artifact::PreflightBlock) && k.directives.key?(:preflight) } > 1
add_warning "only a single preflight stanza is allowed"
end
return unless cask.artifacts.count { |k| k.is_a?(Hbc::Artifact::PostflightBlock) && k.directives.key?(:postflight) } > 1
add_warning "only a single postflight stanza is allowed"
end
def check_single_uninstall_zap
odebug "Auditing single uninstall_* and zap stanzas"
if cask.artifacts.count { |k| k.is_a?(Hbc::Artifact::Uninstall) } > 1
add_warning "only a single uninstall stanza is allowed"
end
if cask.artifacts.count { |k| k.is_a?(Hbc::Artifact::PreflightBlock) && k.directives.key?(:uninstall_preflight) } > 1
add_warning "only a single uninstall_preflight stanza is allowed"
end
if cask.artifacts.count { |k| k.is_a?(Hbc::Artifact::PostflightBlock) && k.directives.key?(:uninstall_postflight) } > 1
add_warning "only a single uninstall_postflight stanza is allowed"
end
return unless cask.artifacts.count { |k| k.is_a?(Hbc::Artifact::Zap) } > 1
add_warning "only a single zap stanza is allowed"
end
def check_required_stanzas
odebug "Auditing required stanzas"
[:version, :sha256, :url, :homepage].each do |sym|
add_error "a #{sym} stanza is required" unless cask.send(sym)
end
add_error "at least one name stanza is required" if cask.name.empty?
# TODO: specific DSL knowledge should not be spread around in various files like this
# TODO: nested_container should not still be a pseudo-artifact at this point
installable_artifacts = cask.artifacts.reject { |k| [:uninstall, :zap, :nested_container].include?(k) }
add_error "at least one activatable artifact stanza is required" if installable_artifacts.empty?
end
def check_version_and_checksum
return if @cask.sourcefile_path.nil?
tap = Tap.select { |t| t.cask_file?(@cask.sourcefile_path) }.first
return if tap.nil?
return if commit_range.nil?
previous_cask_contents = Git.last_revision_of_file(tap.path, @cask.sourcefile_path, before_commit: commit_range)
return if previous_cask_contents.empty?
begin
previous_cask = CaskLoader.load(previous_cask_contents)
return unless previous_cask.version == cask.version
return if previous_cask.sha256 == cask.sha256
add_error "only sha256 changed (see: https://github.com/caskroom/homebrew-cask/blob/master/doc/cask_language_reference/stanzas/sha256.md)"
rescue CaskError => e
add_warning "Skipped version and checksum comparison. Reading previous version failed: #{e}"
end
end
def check_version
return unless cask.version
check_no_string_version_latest
check_no_file_separator_in_version
end
def check_no_string_version_latest
odebug "Verifying version :latest does not appear as a string ('latest')"
return unless cask.version.raw_version == "latest"
add_error "you should use version :latest instead of version 'latest'"
end
def check_no_file_separator_in_version
odebug "Verifying version does not contain '#{File::SEPARATOR}'"
return unless cask.version.raw_version.is_a?(String)
return unless cask.version.raw_version.include?(File::SEPARATOR)
add_error "version should not contain '#{File::SEPARATOR}'"
end
def check_sha256
return unless cask.sha256
check_sha256_no_check_if_latest
check_sha256_actually_256
check_sha256_invalid
end
def check_sha256_no_check_if_latest
odebug "Verifying sha256 :no_check with version :latest"
return unless cask.version.latest? && cask.sha256 != :no_check
add_error "you should use sha256 :no_check when version is :latest"
end
def check_sha256_actually_256(sha256: cask.sha256, stanza: "sha256")
odebug "Verifying #{stanza} string is a legal SHA-256 digest"
return unless sha256.is_a?(String)
return if sha256.length == 64 && sha256[/^[0-9a-f]+$/i]
add_error "#{stanza} string must be of 64 hexadecimal characters"
end
def check_sha256_invalid(sha256: cask.sha256, stanza: "sha256")
odebug "Verifying #{stanza} is not a known invalid value"
empty_sha256 = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
return unless sha256 == empty_sha256
add_error "cannot use the sha256 for an empty string in #{stanza}: #{empty_sha256}"
end
def check_appcast
return unless cask.appcast
odebug "Auditing appcast"
check_appcast_has_checkpoint
return unless cask.appcast.checkpoint
check_sha256_actually_256(sha256: cask.appcast.checkpoint, stanza: "appcast :checkpoint")
check_sha256_invalid(sha256: cask.appcast.checkpoint, stanza: "appcast :checkpoint")
return unless download
check_appcast_http_code
check_appcast_checkpoint_accuracy
end
def check_appcast_has_checkpoint
odebug "Verifying appcast has :checkpoint key"
add_error "a checkpoint sha256 is required for appcast" unless cask.appcast.checkpoint
end
def check_appcast_http_code
odebug "Verifying appcast returns 200 HTTP response code"
curl_executable, *args = curl_args(
"--compressed", "--location", "--fail",
"--write-out", "%{http_code}",
"--output", "/dev/null",
cask.appcast,
user_agent: :fake
)
result = @command.run(curl_executable, args: args, print_stderr: false)
if result.success?
http_code = result.stdout.chomp
add_warning "unexpected HTTP response code retrieving appcast: #{http_code}" unless http_code == "200"
else
add_warning "error retrieving appcast: #{result.stderr}"
end
end
def check_appcast_checkpoint_accuracy
odebug "Verifying appcast checkpoint is accurate"
result = cask.appcast.calculate_checkpoint
actual_checkpoint = result[:checkpoint]
if actual_checkpoint.nil?
add_warning "error retrieving appcast: #{result[:command_result].stderr}"
else
expected = cask.appcast.checkpoint
add_warning <<~EOS unless expected == actual_checkpoint
appcast checkpoint mismatch
Expected: #{expected}
Actual: #{actual_checkpoint}
EOS
end
end
def check_url
return unless cask.url
check_download_url_format
end
def check_download_url_format
odebug "Auditing URL format"
if bad_sourceforge_url?
add_warning "SourceForge URL format incorrect. See https://github.com/caskroom/homebrew-cask/blob/master/doc/cask_language_reference/stanzas/url.md#sourceforgeosdn-urls"
elsif bad_osdn_url?
add_warning "OSDN URL format incorrect. See https://github.com/caskroom/homebrew-cask/blob/master/doc/cask_language_reference/stanzas/url.md#sourceforgeosdn-urls"
end
end
def bad_url_format?(regex, valid_formats_array)
return false unless cask.url.to_s =~ regex
valid_formats_array.none? { |format| cask.url.to_s =~ format }
end
def bad_sourceforge_url?
bad_url_format?(/sourceforge/,
[
%r{\Ahttps://sourceforge\.net/projects/[^/]+/files/latest/download\Z},
%r{\Ahttps://downloads\.sourceforge\.net/(?!(project|sourceforge)\/)},
# special cases: cannot find canonical format URL
%r{\Ahttps?://brushviewer\.sourceforge\.net/brushviewql\.zip\Z},
%r{\Ahttps?://doublecommand\.sourceforge\.net/files/},
%r{\Ahttps?://excalibur\.sourceforge\.net/get\.php\?id=},
])
end
def bad_osdn_url?
bad_url_format?(/osd/, [%r{\Ahttps?://([^/]+.)?dl\.osdn\.jp/}])
end
def check_generic_artifacts
cask.artifacts.select { |a| a.is_a?(Hbc::Artifact::Artifact) }.each do |artifact|
unless artifact.target.absolute?
add_error "target must be absolute path for #{artifact.class.english_name} #{artifact.source}"
end
end
end
def check_token_conflicts
return unless check_token_conflicts?
return unless core_formula_names.include?(cask.token)
add_warning "possible duplicate, cask token conflicts with Homebrew core formula: #{core_formula_url}"
end
def core_tap
@core_tap ||= CoreTap.instance
end
def core_formula_names
core_tap.formula_names
end
def core_formula_url
"#{core_tap.default_remote}/blob/master/Formula/#{cask.token}.rb"
end
def check_download
return unless download && cask.url
odebug "Auditing download"
downloaded_path = download.perform
Verify.all(cask, downloaded_path)
rescue => e
add_error "download not possible: #{e.message}"
end
end
end