Skip to content

Commit 6f97dbe

Browse files
authored
Merge pull request #20372 from cgranleese-r7/add-cache-validation-logic
Adds cache invalidation logic
2 parents 31b9dcd + 604fc95 commit 6f97dbe

File tree

2 files changed

+175
-1
lines changed

2 files changed

+175
-1
lines changed

lib/msf/core/modules/metadata/store.rb

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
require '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
127290
end

lib/msf/ui/console/driver.rb

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,16 @@ def initialize(prompt = DefaultPrompt, prompt_char = DefaultPromptChar, opts = {
7272
elog(e)
7373
end
7474

75+
# Check if files have been modified and force immediate loading if so
76+
has_modified_metasploit_files = !Msf::Modules::Metadata::Store.valid_checksum?
77+
78+
if has_modified_metasploit_files
79+
current_checksum = Msf::Modules::Metadata::Store.get_current_checksum
80+
Msf::Modules::Metadata::Store.update_cache_checksum(current_checksum)
81+
# Force immediate module loading when files have changed
82+
opts['DeferModuleLoads'] = false
83+
end
84+
7585
if opts['DeferModuleLoads'].nil?
7686
opts['DeferModuleLoads'] = Msf::FeatureManager.instance.enabled?(Msf::FeatureManager::DEFER_MODULE_LOADS)
7787
end
@@ -163,7 +173,8 @@ def initialize(prompt = DefaultPrompt, prompt_char = DefaultPromptChar, opts = {
163173
self.framework.init_module_paths(module_paths: opts['ModulePath'], defer_module_loads: opts['DeferModuleLoads'])
164174
end
165175

166-
unless opts['DeferModuleLoads']
176+
# Refresh module cache if modules are modified, or we're not deferring loads
177+
if has_modified_metasploit_files || !opts['DeferModuleLoads']
167178
framework.threads.spawn("ModuleCacheRebuild", true) do
168179
framework.modules.refresh_cache_from_module_files
169180
end

0 commit comments

Comments
 (0)