1
1
require 'json'
2
+ require 'parallel'
3
+ require 'zlib'
2
4
3
5
#
4
6
# Handles storage of module metadata on disk. A base metadata file is always included - this was added to ensure a much
@@ -14,6 +16,7 @@ def initialize
14
16
15
17
BaseMetaDataFile = 'modules_metadata_base.json'
16
18
UserMetaDataFile = 'modules_metadata.json'
19
+ CacheMetaDataFile = 'module_metadata_cache.json'
17
20
18
21
#
19
22
# Initializes from user store (under ~/store/.msf4) if it exists. else base file (under $INSTALL_ROOT/db) is copied and loaded.
@@ -124,4 +127,164 @@ def load_cache_from_file_store
124
127
}
125
128
end
126
129
130
+ # This method checks if the current module and library files match the cached checksum.
131
+ # It uses a per-file CRC32 cache to avoid recalculating checksums for files that haven't changed.
132
+ # If no cache exists, it will create one in the user's directory.
133
+ #
134
+ # @return [Boolean] True if the current checksum matches the cached one
135
+ def self . valid_checksum?
136
+ current_checksum = get_current_checksum
137
+ cached_sha = get_cached_checksum
138
+
139
+ # If no cached checksum exists, create the cache file with current checksum
140
+ if cached_sha . nil?
141
+ update_cache_checksum ( current_checksum )
142
+ return false
143
+ end
144
+
145
+ checksums_match? ( current_checksum , cached_sha )
146
+ end
147
+
148
+ # Calculate the current checksum for all module and library files
149
+ # This calculates checksums for each file and generates an overall checksum
150
+ # from the individual file checksums. Does NOT update the cached checksum.
151
+ #
152
+ # @return [Integer] The current overall checksum
153
+ def self . get_current_checksum
154
+ files = collect_files_to_check
155
+ cache_file = get_cache_path
156
+ cache_data = load_combined_cache ( cache_file )
157
+
158
+ files_lookup = { }
159
+ cache_data [ 'files' ] . each { |entry | files_lookup [ entry [ 'path' ] ] = entry }
160
+
161
+ file_crc32s_with_metadata = calculate_file_checksums ( files , files_lookup )
162
+
163
+ file_crc32s = file_crc32s_with_metadata . map { |_ , meta | meta [ 'crc32' ] } . sort
164
+
165
+ overall_checksum = calculate_overall_checksum ( file_crc32s )
166
+
167
+ overall_checksum
168
+ end
169
+
170
+ # Compare the current checksum with the cached checksum
171
+ # @param [String] current_checksum The calculated checksum for the current state
172
+ # @param [String] cached_checksum The checksum retrieved from cache
173
+ # @return [Boolean] True if checksums match, false otherwise
174
+ def self . checksums_match? ( current_checksum , cached_checksum )
175
+ current_checksum == cached_checksum
176
+ end
177
+
178
+ # Calculate the overall checksum from individual file checksums
179
+ # @param [Array<Integer>] file_crc32s Array of individual file CRC32 values
180
+ # @return [Integer] The overall CRC32 as an integer
181
+ def self . calculate_overall_checksum ( file_crc32s )
182
+ Zlib . crc32 ( file_crc32s . join ( ',' ) , 0 )
183
+ end
184
+
185
+ # Collect all files that need to be checked for checksums
186
+ # @return [Array<String>] List of file paths
187
+ def self . collect_files_to_check
188
+ # Define the directories to scan for files
189
+ modules_dir = File . join ( Msf ::Config . install_root , 'modules' , '**' , '*' )
190
+ local_modules_dir = File . join ( Msf ::Config . user_module_directory , '**' , '*' )
191
+ lib_dir = File . join ( Msf ::Config . install_root , 'lib' , '**' , '*' )
192
+ # Gather all files from the specified directories
193
+ Dir . glob ( [ modules_dir , lib_dir , local_modules_dir ] ) . select { |f | File . file? ( f ) } . sort
194
+ end
195
+
196
+ # Calculate checksums for all files, using the cache when possible
197
+ # @param [Array<String>] files List of file paths to check
198
+ # @param [Hash] cache Current cache data
199
+ # @return [Array<Array>] Array of [file_path, metadata] pairs
200
+ def self . calculate_file_checksums ( files , cache )
201
+ Parallel . map ( files , in_threads : Etc . nprocessors * 2 ) do |file |
202
+ # Get file metadata (size and last modified time)
203
+ file_metadata = File . stat ( file )
204
+ cache_entry = cache [ file ]
205
+ # Use cached CRC32 if mtime and size match, otherwise recalculate
206
+ if cache_entry && cache_entry [ 'mtime' ] == file_metadata . mtime . to_i && cache_entry [ 'size' ] == file_metadata . size
207
+ crc32 = cache_entry [ 'crc32' ]
208
+ else
209
+ crc32 = File . open ( file , 'rb' ) { |fd | Zlib . crc32 ( fd . read , 0 ) }
210
+ end
211
+ # Return file and its metadata for later aggregation
212
+ [ file , {
213
+ 'crc32' => crc32 ,
214
+ 'mtime' => file_metadata . mtime . to_i ,
215
+ 'size' => file_metadata . size
216
+ } ]
217
+ end
218
+ end
219
+
220
+ # Get the path to the cache file
221
+ # @return [String] Path to the cache file
222
+ def self . get_cache_path
223
+ File . join ( Msf ::Config . config_directory , "store" , CacheMetaDataFile )
224
+ end
225
+
226
+ # Load the combined cache from disk (contains both files and checksum)
227
+ # @param [String] cache_file Path to the cache file
228
+ # @return [Hash] The loaded cache with 'files' and 'checksum' keys, or empty structure if file doesn't exist
229
+ def self . load_combined_cache ( cache_file )
230
+ if File . exist? ( cache_file )
231
+ cache_content = JSON . parse ( File . read ( cache_file ) )
232
+ # Ensure the cache has the expected structure
233
+ {
234
+ 'files' => cache_content [ 'files' ] || [ ] ,
235
+ 'checksum' => cache_content [ 'checksum' ]
236
+ }
237
+ else
238
+ { 'files' => [ ] , 'checksum' => nil }
239
+ end
240
+ end
241
+
242
+ # Save the combined cache to disk (files and checksum in one file)
243
+ # @param [String] cache_file Path to the cache file
244
+ # @param [Hash] files_cache The per-file cache data
245
+ # @param [Integer] overall_checksum The overall checksum
246
+ # @return [void]
247
+ def self . save_combined_cache ( cache_file , files_cache , overall_checksum )
248
+ # Ensure the directory for the cache file exists before writing
249
+ FileUtils . mkdir_p ( File . dirname ( cache_file ) )
250
+
251
+ cache_content = {
252
+ 'checksum' => overall_checksum ,
253
+ 'files' => files_cache
254
+ }
255
+
256
+ File . write ( cache_file , JSON . pretty_generate ( cache_content ) )
257
+ end
258
+
259
+ # Get the cached checksum value from the combined cache file
260
+ # @return [Integer, nil] The cached checksum value or nil if no cache exists
261
+ def self . get_cached_checksum
262
+ cache_path = get_cache_path
263
+ cache_data = load_combined_cache ( cache_path )
264
+ cache_data [ 'checksum' ]
265
+ end
266
+
267
+ # Update the cache with the current checksum and file data
268
+ # @param [Integer] current_checksum The current checksum to store in the cache
269
+ # @return [void]
270
+ def self . update_cache_checksum ( current_checksum )
271
+ # Recalculate file checksums and update both overall checksum and file cache
272
+ files = collect_files_to_check
273
+ cache_file = get_cache_path
274
+ cache_data = load_combined_cache ( cache_file )
275
+
276
+ files_lookup = { }
277
+ cache_data [ 'files' ] . each { |entry | files_lookup [ entry [ 'path' ] ] = entry }
278
+
279
+ file_crc32s_with_metadata = calculate_file_checksums ( files , files_lookup )
280
+
281
+ updated_files_cache = file_crc32s_with_metadata . map do |file_path , metadata |
282
+ metadata . merge ( 'path' => file_path )
283
+ end
284
+
285
+ updated_files_cache . sort_by! { |entry | entry [ 'path' ] }
286
+
287
+ # Save both the updated file cache and the new overall checksum
288
+ save_combined_cache ( cache_file , updated_files_cache , current_checksum )
289
+ end
127
290
end
0 commit comments