11require 'json'
2+ require 'parallel'
3+ require 'zlib'
24
35#
46# 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
1416
1517 BaseMetaDataFile = 'modules_metadata_base.json'
1618 UserMetaDataFile = 'modules_metadata.json'
19+ CacheMetaDataFile = 'module_metadata_cache.json'
1720
1821 #
1922 # 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
124127 }
125128 end
126129
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
127290end
0 commit comments