diff --git a/README.md b/README.md index 8da2815..084ee5f 100644 --- a/README.md +++ b/README.md @@ -236,11 +236,12 @@ module MyEngine end ``` -## Checking for outdated or vulnerable packages +## Checking for outdated, vulnerable or altered packages Importmap for Rails provides two commands to check your pinned packages: - `./bin/importmap outdated` checks the NPM registry for new versions - `./bin/importmap audit` checks the NPM registry for known security issues +- `./bin/importmap verify` checks the vendored files against a fresh download ## License diff --git a/lib/importmap/commands.rb b/lib/importmap/commands.rb index 350cc42..4a7afc2 100644 --- a/lib/importmap/commands.rb +++ b/lib/importmap/commands.rb @@ -73,6 +73,30 @@ def audit end end + desc "verify", "Verify that vendored files are identical to the pinned remote" + desc "pin [*PACKAGES]", "Pin new packages" + option :env, type: :string, aliases: :e, default: "production" + option :from, type: :string, aliases: :f, default: "jspm" + def verify(*packages) + if packages.empty? + packages = npm.packages_with_versions.map do |p, v| + v.blank? ? p : [p, v].join("@") + end + end + + if imports = packager.import(*packages, env: options[:env], from: options[:from]) + imports.each do |package, url| + puts %(Verifying "#{package}" download from #{url}) + packager.verify(package, url) + end + else + puts "No packages found" + end + rescue Importmap::Packager::VerifyError => error + puts error.message + exit 1 + end + desc "outdated", "Check for outdated packages" def outdated if (outdated_packages = npm.outdated_packages).any? diff --git a/lib/importmap/packager.rb b/lib/importmap/packager.rb index 76f0661..2882088 100644 --- a/lib/importmap/packager.rb +++ b/lib/importmap/packager.rb @@ -4,6 +4,7 @@ class Importmap::Packager Error = Class.new(StandardError) + VerifyError = Class.new(Error) HTTPError = Class.new(Error) ServiceError = Error.new(Error) @@ -51,6 +52,14 @@ def packaged?(package) importmap.match(/^pin ["']#{package}["'].*$/) end + def verify(package, url) + ensure_vendor_directory_exists + + if vendored_package_path(package).file? + verify_vendored_package(package, url) + end + end + def download(package, url) ensure_vendor_directory_exists remove_existing_package_file(package) @@ -95,7 +104,6 @@ def importmap @importmap ||= File.read(@importmap_path) end - def ensure_vendor_directory_exists FileUtils.mkdir_p @vendor_path end @@ -114,20 +122,29 @@ def remove_package_from_importmap(package) end def download_package_file(package, url) + body = load_package_file(package, url) + save_vendored_package(package, url, body) + end + + def load_package_file(package, url) response = Net::HTTP.get_response(URI(url)) if response.code == "200" - save_vendored_package(package, url, response.body) + format_vendored_package(package, url, response.body) else handle_failure_response(response) end end + def format_vendored_package(package, url, source) + formatted = "// #{package}#{extract_package_version_from(url)} downloaded from #{url}\n\n" + formatted.concat remove_sourcemap_comment_from(source).force_encoding("UTF-8") + formatted + end + def save_vendored_package(package, url, source) File.open(vendored_package_path(package), "w+") do |vendored_package| - vendored_package.write "// #{package}#{extract_package_version_from(url)} downloaded from #{url}\n\n" - - vendored_package.write remove_sourcemap_comment_from(source).force_encoding("UTF-8") + vendored_package.write source end end @@ -135,6 +152,15 @@ def remove_sourcemap_comment_from(source) source.gsub(/^\/\/# sourceMappingURL=.*/, "") end + def verify_vendored_package(package, url) + vendored_body = vendored_package_path(package).read.strip + remote_body = load_package_file(package, url).strip + + return true if vendored_body == remote_body + + raise VerifyError.new("Vendored #{package}#{extract_package_version_from(url)} does not match remote #{url}") + end + def vendored_package_path(package) @vendor_path.join(package_filename(package)) end diff --git a/test/packager_integration_test.rb b/test/packager_integration_test.rb index 3600065..81b3c04 100644 --- a/test/packager_integration_test.rb +++ b/test/packager_integration_test.rb @@ -34,15 +34,25 @@ class Importmap::PackagerIntegrationTest < ActiveSupport::TestCase vendored_package_file = Pathname.new(vendor_dir).join("@github--webauthn-json.js") assert File.exist?(vendored_package_file) assert_equal "// @github/webauthn-json@0.5.7 downloaded from #{package_url}", File.readlines(vendored_package_file).first.strip + assert @packager.verify("@github/webauthn-json", package_url) package_url = "https://ga.jspm.io/npm:react@17.0.2/index.js" vendored_package_file = Pathname.new(vendor_dir).join("react.js") @packager.download("react", package_url) assert File.exist?(vendored_package_file) assert_equal "// react@17.0.2 downloaded from #{package_url}", File.readlines(vendored_package_file).first.strip - + assert @packager.verify("react", package_url) + + File.write(vendored_package_file, "// altered content") + + assert_raises(Importmap::Packager::VerifyError) do + @packager.verify("react", package_url) + end + @packager.remove("react") assert_not File.exist?(Pathname.new(vendor_dir).join("react.js")) + + refute @packager.verify("react", package_url) end end end