-
Notifications
You must be signed in to change notification settings - Fork 120
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add outdated and audit commands (#109)
- Loading branch information
Showing
6 changed files
with
310 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,113 @@ | ||
require "net/http" | ||
require "uri" | ||
require "json" | ||
|
||
class Importmap::Npm | ||
Error = Class.new(StandardError) | ||
HTTPError = Class.new(Error) | ||
|
||
singleton_class.attr_accessor :base_uri | ||
self.base_uri = URI("https://registry.npmjs.org") | ||
|
||
def initialize(importmap_path = "config/importmap.rb") | ||
@importmap_path = Pathname.new(importmap_path) | ||
end | ||
|
||
def outdated_packages | ||
packages_with_versions.each.with_object([]) do |(package, current_version), outdated_packages| | ||
outdated_package = OutdatedPackage.new(name: package, | ||
current_version: current_version) | ||
|
||
if !(response = get_package(package)) | ||
outdated_package.error = 'Response error' | ||
elsif (error = response['error']) | ||
outdated_package.error = error | ||
else | ||
latest_version = find_latest_version(response) | ||
next unless outdated?(current_version, latest_version) | ||
|
||
outdated_package.latest_version = latest_version | ||
end | ||
|
||
outdated_packages << outdated_package | ||
end.sort_by(&:name) | ||
end | ||
|
||
def vulnerable_packages | ||
get_audit.flat_map do |package, vulnerabilities| | ||
vulnerabilities.map do |vulnerability| | ||
VulnerablePackage.new(name: package, | ||
severity: vulnerability['severity'], | ||
vulnerable_versions: vulnerability['vulnerable_versions'], | ||
vulnerability: vulnerability['title']) | ||
end | ||
end.sort_by { |p| [p.name, p.severity] } | ||
end | ||
|
||
private | ||
OutdatedPackage = Struct.new(:name, :current_version, :latest_version, :error, keyword_init: true) | ||
VulnerablePackage = Struct.new(:name, :severity, :vulnerable_versions, :vulnerability, keyword_init: true) | ||
|
||
def packages_with_versions | ||
# We cannot use the name after "pin" because some dependencies are loaded from inside packages | ||
# Eg. pin "buffer", to: "https://ga.jspm.io/npm:@jspm/[email protected]/nodelibs/browser/buffer.js" | ||
|
||
importmap.scan(/^pin .*(?<=npm:|npm\/|skypack\.dev\/|unpkg\.com\/)(.*)(?=@\d+\.\d+\.\d+)@(\d+\.\d+\.\d+(?:[^\/\s"]*)).*$/) | | ||
importmap.scan(/^pin "([^"]*)".* #.*@(\d+\.\d+\.\d+(?:[^\s]*)).*$/) | ||
end | ||
|
||
def importmap | ||
@importmap ||= File.read(@importmap_path) | ||
end | ||
|
||
def get_package(package) | ||
uri = self.class.base_uri.dup | ||
uri.path = "/" + package | ||
response = get_json(uri) | ||
|
||
JSON.parse(response) | ||
rescue JSON::ParserError | ||
nil | ||
end | ||
|
||
def get_json(uri) | ||
Net::HTTP.get(uri, "Content-Type" => "application/json") | ||
rescue => error | ||
raise HTTPError, "Unexpected transport error (#{error.class}: #{error.message})" | ||
end | ||
|
||
def find_latest_version(response) | ||
latest_version = response.dig('dist-tags', 'latest') | ||
return latest_version if latest_version | ||
|
||
return unless response['versions'] | ||
|
||
response['versions'].keys.map { |v| Gem::Version.new(v) rescue nil }.compact.sort.last | ||
end | ||
|
||
def outdated?(current_version, latest_version) | ||
Gem::Version.new(current_version) < Gem::Version.new(latest_version) | ||
rescue ArgumentError | ||
current_version.to_s < latest_version.to_s | ||
end | ||
|
||
def get_audit | ||
uri = self.class.base_uri.dup | ||
uri.path = "/-/npm/v1/security/advisories/bulk" | ||
|
||
body = packages_with_versions.each.with_object({}) { |(package, version), data| | ||
data[package] ||= [] | ||
data[package] << version | ||
} | ||
return {} if body.empty? | ||
|
||
response = post_json(uri, body) | ||
JSON.parse(response.body) | ||
end | ||
|
||
def post_json(uri, body) | ||
Net::HTTP.post(uri, body.to_json, "Content-Type" => "application/json") | ||
rescue => error | ||
raise HTTPError, "Unexpected transport error (#{error.class}: #{error.message})" | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
pin "md5", to: "https://cdn.skypack.dev/[email protected]", preload: true |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
pin "is-svg", to: "https://cdn.skypack.dev/[email protected]", preload: true |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
require "test_helper" | ||
require "importmap/npm" | ||
|
||
class Importmap::NpmIntegrationTest < ActiveSupport::TestCase | ||
test "successful outdated packages against live service" do | ||
file = file_fixture("outdated_import_map.rb") | ||
npm = Importmap::Npm.new(file) | ||
|
||
outdated_packages = npm.outdated_packages | ||
|
||
assert_equal(1, outdated_packages.size) | ||
assert_equal("md5", outdated_packages[0].name) | ||
assert_equal("2.2.0", outdated_packages[0].current_version) | ||
assert_match(/\d+\.\d+\.\d+/, outdated_packages[0].latest_version) | ||
end | ||
|
||
test "failed outdated packages request against live bad domain" do | ||
file = file_fixture("outdated_import_map.rb") | ||
npm = Importmap::Npm.new(file) | ||
|
||
original_base_uri = Importmap::Npm.base_uri | ||
Importmap::Npm.base_uri = URI("https://invalid.error") | ||
|
||
assert_raises(Importmap::Npm::HTTPError) do | ||
npm.outdated_packages | ||
end | ||
ensure | ||
Importmap::Npm.base_uri = original_base_uri | ||
end | ||
|
||
test "successful vulnerable packages against live service" do | ||
file = file_fixture("vulnerable_import_map.rb") | ||
npm = Importmap::Npm.new(file) | ||
|
||
vulnerable_packages = npm.vulnerable_packages | ||
|
||
assert(vulnerable_packages.size >= 2) | ||
|
||
assert_equal("is-svg", vulnerable_packages[0].name) | ||
assert_equal("is-svg", vulnerable_packages[1].name) | ||
|
||
severities = vulnerable_packages.map(&:severity) | ||
assert_includes(severities, "high") | ||
|
||
vulnerabilities = vulnerable_packages.map(&:vulnerability) | ||
assert_includes(vulnerabilities, "ReDOS in IS-SVG") | ||
assert_includes(vulnerabilities, "Regular Expression Denial of Service (ReDoS)") | ||
|
||
vulnerable_versions = vulnerable_packages.map(&:vulnerable_versions) | ||
assert_includes(vulnerable_versions, ">=2.1.0 <4.3.0") | ||
assert_includes(vulnerable_versions, ">=2.1.0 <4.2.2") | ||
end | ||
|
||
test "failed vulnerable packages request against live bad domain" do | ||
file = file_fixture("vulnerable_import_map.rb") | ||
npm = Importmap::Npm.new(file) | ||
|
||
original_base_uri = Importmap::Npm.base_uri | ||
Importmap::Npm.base_uri = URI("https://invalid.error") | ||
|
||
assert_raises(Importmap::Npm::HTTPError) do | ||
npm.vulnerable_packages | ||
end | ||
ensure | ||
Importmap::Npm.base_uri = original_base_uri | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
require "test_helper" | ||
require "importmap/npm" | ||
require "minitest/mock" | ||
|
||
class Importmap::NpmTest < ActiveSupport::TestCase | ||
setup { @npm = Importmap::Npm.new(file_fixture("outdated_import_map.rb")) } | ||
|
||
test "successful outdated packages with mock" do | ||
response = { "dist-tags" => { "latest" => '2.3.0' } }.to_json | ||
|
||
@npm.stub(:get_json, response) do | ||
outdated_packages = @npm.outdated_packages | ||
|
||
assert_equal(1, outdated_packages.size) | ||
assert_equal('md5', outdated_packages[0].name) | ||
assert_equal('2.2.0', outdated_packages[0].current_version) | ||
assert_equal('2.3.0', outdated_packages[0].latest_version) | ||
end | ||
end | ||
|
||
test "missing outdated packages with mock" do | ||
response = { "error" => "Not found" }.to_json | ||
|
||
@npm.stub(:get_json, response) do | ||
outdated_packages = @npm.outdated_packages | ||
|
||
assert_equal(1, outdated_packages.size) | ||
assert_equal('md5', outdated_packages[0].name) | ||
assert_equal('2.2.0', outdated_packages[0].current_version) | ||
assert_equal('Not found', outdated_packages[0].error) | ||
end | ||
end | ||
|
||
test "failed outdated packages request with mock" do | ||
Net::HTTP.stub(:get, proc { raise "Unexpected Error" }) do | ||
assert_raises(Importmap::Npm::HTTPError) do | ||
@npm.outdated_packages | ||
end | ||
end | ||
end | ||
|
||
test "successful vulnerable packages with mock" do | ||
response = Class.new do | ||
def body | ||
{ "md5" => [{ "title" => "Unsafe hashing", "severity" => "high", "vulnerable_versions" => "<42.0.0" }] }.to_json | ||
end | ||
|
||
def code() "200" end | ||
end.new | ||
|
||
@npm.stub(:post_json, response) do | ||
vulnerable_packages = @npm.vulnerable_packages | ||
|
||
assert_equal(1, vulnerable_packages.size) | ||
assert_equal('md5', vulnerable_packages[0].name) | ||
assert_equal('Unsafe hashing', vulnerable_packages[0].vulnerability) | ||
assert_equal('high', vulnerable_packages[0].severity) | ||
assert_equal('<42.0.0', vulnerable_packages[0].vulnerable_versions) | ||
end | ||
end | ||
|
||
test "failed vulnerable packages request with mock" do | ||
Net::HTTP.stub(:post, proc { raise "Unexpected Error" }) do | ||
assert_raises(Importmap::Npm::HTTPError) do | ||
@npm.vulnerable_packages | ||
end | ||
end | ||
end | ||
end |