dependency-timeline-audit 0.0.1 → 0.0.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/bin/dependency-timeline-audit +30 -1
- data/lib/dependency-timeline-audit/api.rb +57 -0
- data/lib/dependency-timeline-audit/check.rb +18 -65
- data/lib/dependency-timeline-audit/config.rb +27 -0
- data/lib/dependency-timeline-audit/gem.rb +37 -0
- data/lib/dependency-timeline-audit/gem_cache.rb +24 -0
- data/lib/dependency-timeline-audit/gem_version.rb +47 -0
- data/lib/dependency-timeline-audit/gem_version_cache.rb +11 -0
- data/lib/dependency-timeline-audit/text_format.rb +51 -0
- data/lib/dependency-timeline-audit/version.rb +1 -1
- data/lib/dependency-timeline-audit.rb +10 -4
- metadata +9 -3
- data/lib/dependency-timeline-audit/gem_info.rb +0 -41
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 3ac86eaf5319d8a10dc42fd001c0f32a15a5421bd861ce24faf2b4bcb462493d
|
4
|
+
data.tar.gz: 9a67aac946d8265cb908d521ed90f2de25fe245bb0f141766faae266e4fb678d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: af4097c658cefeb2853b4fe6d4e1f517a976a7d6bde91eaf69660cd9da95f80f0cf392f3986b7298dafbaa34259b526f501c5170d481dcf2e0be84d306a68853
|
7
|
+
data.tar.gz: 70a6bbfd8e7c40c39c427133d8e528a00fc54d10bdb68784aadac12a3ad60b1fc1c34aca1fe73d4e88b9fe852282b50f3a527aeca69be8498bff84c6926c78ac
|
@@ -1,5 +1,34 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
|
3
3
|
require 'dependency-timeline-audit'
|
4
|
+
require 'optparse'
|
4
5
|
|
5
|
-
|
6
|
+
# See: https://docs.ruby-lang.org/en/master/OptionParser.html
|
7
|
+
|
8
|
+
begin
|
9
|
+
options = {}
|
10
|
+
OptionParser.new do |opts|
|
11
|
+
opts.banner = "Usage: dependency-timeline-audit [options]\n"
|
12
|
+
|
13
|
+
opts.on('-i', '--interactive-ignore', 'Allows interactively generating an ignore file')
|
14
|
+
opts.on('-v', '--verbose', 'Provides more verbose output')
|
15
|
+
opts.on('--lockfile=LOCKFILE', 'Allows overwriting where the lockfile is located (default: "Gemfile.lock")')
|
16
|
+
opts.on('--no-lockfile', "Don't use a lockfile")
|
17
|
+
opts.on('--outdated-threshold=YEARS', Integer, 'Allows overwriting the number of years before a gem is considered outdated (default: 1)')
|
18
|
+
opts.on_tail('-h', '--help', 'Prints this help') do
|
19
|
+
puts opts
|
20
|
+
exit
|
21
|
+
end
|
22
|
+
opts.on('-V', '--version', 'Prints the version of dependency-timeline-audit') do
|
23
|
+
puts "Dependency Timeline Audit (Ruby) - version: #{DependencyTimelineAudit.gem_version}"
|
24
|
+
exit
|
25
|
+
end
|
26
|
+
end.parse!(into: options)
|
27
|
+
rescue OptionParser::InvalidOption, OptionParser::MissingArgument => e
|
28
|
+
puts e.message
|
29
|
+
exit(1)
|
30
|
+
end
|
31
|
+
|
32
|
+
config = DependencyTimelineAudit::Config.new(options)
|
33
|
+
checker = DependencyTimelineAudit::Check.new(config: config)
|
34
|
+
checker.check
|
@@ -0,0 +1,57 @@
|
|
1
|
+
require 'net/http'
|
2
|
+
require 'json'
|
3
|
+
|
4
|
+
module DependencyTimelineAudit
|
5
|
+
class API
|
6
|
+
API_URL = 'https://rubygems.org/api/v1/versions/'
|
7
|
+
CACHE_DIRECTORY = "#{Dir.pwd}/.dependency-timeline-audit/cache/ruby/"
|
8
|
+
EXCEPTIONS_DIRECTORY = "#{Dir.pwd}/.dependency-timeline-audit/exceptions/ruby/"
|
9
|
+
@@gem_cache = {}
|
10
|
+
|
11
|
+
def self.fetch_gem_info(gem_name)
|
12
|
+
if !cached?(gem_name) || cache_outdated?(gem_name)
|
13
|
+
update_cache(gem_name)
|
14
|
+
end
|
15
|
+
|
16
|
+
gem_cache(gem_name)
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.cached?(gem_name)
|
20
|
+
File.exist?(gem_cache_file(gem_name))
|
21
|
+
end
|
22
|
+
|
23
|
+
# TODO: Implement cache_outdated? method using config.cache_expires_after
|
24
|
+
def self.cache_outdated?(gem_name)
|
25
|
+
false
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.gem_cache_file(gem_name)
|
29
|
+
File.join(CACHE_DIRECTORY, "#{gem_name}.json")
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.update_cache(gem_name)
|
33
|
+
response = rubygems_api_response(gem_name)
|
34
|
+
gem_cache = GemCache.new(JSON.parse(response))
|
35
|
+
|
36
|
+
FileUtils.mkdir_p(CACHE_DIRECTORY) unless File.directory?(CACHE_DIRECTORY)
|
37
|
+
File.open(gem_cache_file(gem_name), 'w') do |file|
|
38
|
+
file.write(JSON.pretty_generate(gem_cache.to_h))
|
39
|
+
end
|
40
|
+
|
41
|
+
gem_cache(gem_name)
|
42
|
+
end
|
43
|
+
|
44
|
+
def self.rubygems_api_response(gem_name)
|
45
|
+
url = URI("#{API_URL}#{gem_name}.json")
|
46
|
+
Net::HTTP.get(url)
|
47
|
+
end
|
48
|
+
|
49
|
+
def self.gem_cache(gem_name)
|
50
|
+
if @@gem_cache[gem_name].nil? && cached?(gem_name)
|
51
|
+
@@gem_cache[gem_name] = GemCache.new(JSON.parse(File.read(gem_cache_file(gem_name))))
|
52
|
+
end
|
53
|
+
|
54
|
+
@@gem_cache[gem_name]
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -1,31 +1,30 @@
|
|
1
1
|
require 'date'
|
2
|
-
require 'active_support/all'
|
3
2
|
|
4
3
|
module DependencyTimelineAudit
|
5
4
|
class Check
|
6
|
-
|
7
|
-
|
8
|
-
|
5
|
+
attr_reader :config
|
6
|
+
|
7
|
+
def initialize(config:)
|
8
|
+
@config = config
|
9
9
|
end
|
10
10
|
|
11
|
-
def
|
11
|
+
def check
|
12
12
|
outdated_versions = []
|
13
13
|
locked_gems.each do |gem|
|
14
|
-
|
15
|
-
|
16
|
-
outdated_versions.push(gem[:name]) if gem_outdated?(lock_released_at)
|
17
|
-
print_info(gem, lock_released_at, latest_version)
|
14
|
+
outdated_versions.push(gem) if gem.locked_version.outdated?
|
15
|
+
gem.print_info if config.verbose
|
18
16
|
end
|
19
17
|
|
18
|
+
print "\n" if config.verbose
|
19
|
+
|
20
20
|
if outdated_versions.any?
|
21
|
-
|
22
|
-
puts "
|
21
|
+
TextFormat.color = :red
|
22
|
+
puts "Outdated gems detected!"
|
23
23
|
puts " - #{outdated_versions.join(', ')}"
|
24
24
|
|
25
25
|
exit(1) # Failure
|
26
26
|
else
|
27
|
-
|
28
|
-
puts "\nAll gems are within the accepted threshold!"
|
27
|
+
puts "All gems are within the accepted threshold!"
|
29
28
|
|
30
29
|
exit(0) # Success
|
31
30
|
end
|
@@ -33,61 +32,15 @@ module DependencyTimelineAudit
|
|
33
32
|
|
34
33
|
private
|
35
34
|
|
36
|
-
def
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
puts "Gem: \e[1m#{gem[:name]}\e[0m"
|
42
|
-
set_text_color(lock_released_at, gem[:locked_version] == latest_version[:version])
|
43
|
-
puts " - Locked to: #{gem[:locked_version]} (Released: #{format_date(lock_released_at)})"
|
44
|
-
set_text_color(latest_version[:created_at])
|
45
|
-
puts " - Latest: #{latest_version[:version]} (Released: #{format_date(latest_version[:created_at])})"
|
46
|
-
reset_text_style
|
47
|
-
end
|
48
|
-
|
49
|
-
def self.set_text_color(released_at, using_latest = true)
|
50
|
-
if gem_outdated?(released_at)
|
51
|
-
set_text_color_red
|
52
|
-
else
|
53
|
-
if using_latest
|
54
|
-
set_text_color_green
|
35
|
+
def locked_gems
|
36
|
+
lockfile_parser = Bundler::LockfileParser.new(File.read(config.lockfile))
|
37
|
+
lockfile_parser.specs.map do |spec|
|
38
|
+
if spec.source.is_a?(Bundler::Source::Git)
|
39
|
+
Gem.new(config: config, name: spec.name, locked_version: spec.source.revision)
|
55
40
|
else
|
56
|
-
|
41
|
+
Gem.new(config: config, name: spec.name, locked_version: spec.version)
|
57
42
|
end
|
58
43
|
end
|
59
44
|
end
|
60
|
-
|
61
|
-
def self.set_text_bold
|
62
|
-
print "\e[1m"
|
63
|
-
end
|
64
|
-
|
65
|
-
def self.set_text_color_red
|
66
|
-
print "\e[31m"
|
67
|
-
end
|
68
|
-
|
69
|
-
def self.set_text_color_green
|
70
|
-
print "\e[32m"
|
71
|
-
end
|
72
|
-
|
73
|
-
def self.set_text_color_yellow
|
74
|
-
print "\e[33m"
|
75
|
-
end
|
76
|
-
|
77
|
-
def self.reset_text_style
|
78
|
-
print "\e[0m"
|
79
|
-
end
|
80
|
-
|
81
|
-
def self.locked_gems
|
82
|
-
lockfile = Bundler::LockfileParser.new(File.read('Gemfile.lock'))
|
83
|
-
lockfile.specs.map do |gem|
|
84
|
-
{ name: gem.name, locked_version: gem.version.to_s }
|
85
|
-
end
|
86
|
-
end
|
87
|
-
|
88
|
-
def self.format_date(date_string)
|
89
|
-
date = Date.parse(date_string)
|
90
|
-
date.strftime("%Y-%m-%d")
|
91
|
-
end
|
92
45
|
end
|
93
46
|
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'active_support/all'
|
2
|
+
|
3
|
+
module DependencyTimelineAudit
|
4
|
+
class Config
|
5
|
+
attr_reader :verbose, :lockfile, :outdated_threshold
|
6
|
+
|
7
|
+
def initialize(options)
|
8
|
+
@verbose = value_or_default(options[:verbose], false)
|
9
|
+
@lockfile = value_or_default(options[:lockfile], 'Gemfile.lock')
|
10
|
+
@outdated_threshold = value_or_default(options[:outdated_threshold], 3)
|
11
|
+
|
12
|
+
# FIXME: There is probably a better way to handle the guard clauses and type casting
|
13
|
+
if @outdated_threshold.to_i <= 0
|
14
|
+
raise InvalidOption, "Outdated Threshold must be an integer greater than 0"
|
15
|
+
end
|
16
|
+
|
17
|
+
# TODO: activesupport is kinda hefty for just grabbing X.years.ago, remove
|
18
|
+
@outdated_threshold = @outdated_threshold.to_i.years.ago
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def value_or_default(value, default)
|
24
|
+
!value.nil? ? value : default
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module DependencyTimelineAudit
|
2
|
+
class Gem
|
3
|
+
attr_reader :name, :locked_version, :latest_version, :config
|
4
|
+
|
5
|
+
def initialize(config:, name:, locked_version:)
|
6
|
+
@config = config
|
7
|
+
@name = name
|
8
|
+
@locked_version = GemVersion.new(gem: self, name: locked_version)
|
9
|
+
@latest_version = GemVersion.new(gem: self, name: api.latest_version)
|
10
|
+
end
|
11
|
+
|
12
|
+
def to_s
|
13
|
+
name.to_s
|
14
|
+
end
|
15
|
+
|
16
|
+
def print_info
|
17
|
+
puts "Gem: #{TextFormat.bold}#{name}#{TextFormat.reset}"
|
18
|
+
TextFormat.color = locked_version.color
|
19
|
+
puts " - Locked to: #{locked_version.name} (Released: #{format_date(locked_version.released_at)})"
|
20
|
+
TextFormat.color = latest_version.color
|
21
|
+
puts " - Latest: #{latest_version.name} (Released: #{format_date(latest_version.released_at)})"
|
22
|
+
TextFormat.reset!
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def api
|
28
|
+
API.fetch_gem_info(name)
|
29
|
+
end
|
30
|
+
|
31
|
+
def format_date(date_string)
|
32
|
+
return 'Unknown' if date_string.nil?
|
33
|
+
date = Date.parse(date_string)
|
34
|
+
date.strftime("%Y-%m-%d")
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module DependencyTimelineAudit
|
2
|
+
class GemCache
|
3
|
+
attr_reader :versions
|
4
|
+
|
5
|
+
def initialize(gem_info)
|
6
|
+
@versions = gem_info || []
|
7
|
+
end
|
8
|
+
|
9
|
+
def latest_version
|
10
|
+
latest = versions.first # The first entry is the latest version
|
11
|
+
version_number = latest['number']
|
12
|
+
end
|
13
|
+
|
14
|
+
# Find the version that matches the requested version string
|
15
|
+
def version(version_number)
|
16
|
+
version_info = versions.find { |v| v['number'] == version_number }
|
17
|
+
GemVersionCache.new(version_info)
|
18
|
+
end
|
19
|
+
|
20
|
+
def to_h
|
21
|
+
versions
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
module DependencyTimelineAudit
|
2
|
+
class GemVersion
|
3
|
+
attr_reader :gem, :config, :name
|
4
|
+
|
5
|
+
def initialize(gem:, name:)
|
6
|
+
@gem = gem
|
7
|
+
@config = gem.config
|
8
|
+
@name = name.to_s
|
9
|
+
end
|
10
|
+
|
11
|
+
def ==(other)
|
12
|
+
return name == other.name if other.is_a?(GemVersion)
|
13
|
+
|
14
|
+
nil
|
15
|
+
end
|
16
|
+
|
17
|
+
def to_s
|
18
|
+
name.to_s
|
19
|
+
end
|
20
|
+
|
21
|
+
# If release date is unknown, leave it as not outdated
|
22
|
+
def outdated?
|
23
|
+
return false if released_at.nil?
|
24
|
+
released_at <= config.outdated_threshold
|
25
|
+
end
|
26
|
+
|
27
|
+
def latest?
|
28
|
+
gem.latest_version == self
|
29
|
+
end
|
30
|
+
|
31
|
+
def color
|
32
|
+
return :red if outdated?
|
33
|
+
return :green if latest?
|
34
|
+
:yellow
|
35
|
+
end
|
36
|
+
|
37
|
+
def released_at
|
38
|
+
api.released_at
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def api
|
44
|
+
API.fetch_gem_info(gem.name).version(name)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
module DependencyTimelineAudit
|
2
|
+
class TextFormat
|
3
|
+
class << self
|
4
|
+
def color=(color)
|
5
|
+
case color
|
6
|
+
when :red
|
7
|
+
print red
|
8
|
+
when :green
|
9
|
+
print green
|
10
|
+
when :yellow
|
11
|
+
print yellow
|
12
|
+
else
|
13
|
+
raise ArgumentError, "Unknown color: #{color}"
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def style=(style)
|
18
|
+
case style
|
19
|
+
when :bold
|
20
|
+
print bold
|
21
|
+
else
|
22
|
+
raise ArgumentError, "Unknown style: #{style}"
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def reset!
|
27
|
+
print reset
|
28
|
+
end
|
29
|
+
|
30
|
+
def reset
|
31
|
+
"\e[0m"
|
32
|
+
end
|
33
|
+
|
34
|
+
def bold
|
35
|
+
"\e[1m"
|
36
|
+
end
|
37
|
+
|
38
|
+
def red
|
39
|
+
"\e[31m"
|
40
|
+
end
|
41
|
+
|
42
|
+
def green
|
43
|
+
"\e[32m"
|
44
|
+
end
|
45
|
+
|
46
|
+
def yellow
|
47
|
+
"\e[33m"
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -1,9 +1,15 @@
|
|
1
1
|
module DependencyTimelineAudit
|
2
|
-
autoload :
|
3
|
-
autoload :
|
4
|
-
autoload :
|
2
|
+
autoload :API, 'dependency-timeline-audit/api'
|
3
|
+
autoload :Check, 'dependency-timeline-audit/check'
|
4
|
+
autoload :Config, 'dependency-timeline-audit/config'
|
5
|
+
autoload :GemCache, 'dependency-timeline-audit/gem_cache'
|
6
|
+
autoload :GemVersionCache, 'dependency-timeline-audit/gem_version_cache'
|
7
|
+
autoload :GemVersion, 'dependency-timeline-audit/gem_version'
|
8
|
+
autoload :Gem, 'dependency-timeline-audit/gem'
|
9
|
+
autoload :TextFormat, 'dependency-timeline-audit/text_format'
|
10
|
+
autoload :VERSION, 'dependency-timeline-audit/version'
|
5
11
|
|
6
12
|
def self.gem_version
|
7
|
-
Gem::Version.new VERSION::STRING
|
13
|
+
::Gem::Version.new VERSION::STRING
|
8
14
|
end
|
9
15
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: dependency-timeline-audit
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.3
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Josh Buker
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-
|
11
|
+
date: 2024-11-05 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activesupport
|
@@ -47,8 +47,14 @@ extra_rdoc_files: []
|
|
47
47
|
files:
|
48
48
|
- bin/dependency-timeline-audit
|
49
49
|
- lib/dependency-timeline-audit.rb
|
50
|
+
- lib/dependency-timeline-audit/api.rb
|
50
51
|
- lib/dependency-timeline-audit/check.rb
|
51
|
-
- lib/dependency-timeline-audit/
|
52
|
+
- lib/dependency-timeline-audit/config.rb
|
53
|
+
- lib/dependency-timeline-audit/gem.rb
|
54
|
+
- lib/dependency-timeline-audit/gem_cache.rb
|
55
|
+
- lib/dependency-timeline-audit/gem_version.rb
|
56
|
+
- lib/dependency-timeline-audit/gem_version_cache.rb
|
57
|
+
- lib/dependency-timeline-audit/text_format.rb
|
52
58
|
- lib/dependency-timeline-audit/version.rb
|
53
59
|
homepage: https://github.com/CloudSecurityAlliance/Dependency-Timeline-Audit
|
54
60
|
licenses:
|
@@ -1,41 +0,0 @@
|
|
1
|
-
require 'net/http'
|
2
|
-
require 'json'
|
3
|
-
|
4
|
-
module DependencyTimelineAudit
|
5
|
-
# Define a class for interacting with the RubyGems API
|
6
|
-
class GemInfo
|
7
|
-
API_URL = 'https://rubygems.org/api/v1/versions/'
|
8
|
-
@@gem_cache = {}
|
9
|
-
|
10
|
-
# Method to fetch the gem data and cache it
|
11
|
-
def self.fetch_gem_data(gem_name)
|
12
|
-
# Check if gem info is already cached
|
13
|
-
unless @@gem_cache[gem_name]
|
14
|
-
url = URI("#{API_URL}#{gem_name}.json")
|
15
|
-
response = Net::HTTP.get(url)
|
16
|
-
@@gem_cache[gem_name] = JSON.parse(response)
|
17
|
-
end
|
18
|
-
|
19
|
-
# Return cached gem info
|
20
|
-
@@gem_cache[gem_name]
|
21
|
-
end
|
22
|
-
|
23
|
-
# Method to fetch the latest version and its created_at timestamp
|
24
|
-
def self.latest_version(gem_name)
|
25
|
-
versions = fetch_gem_data(gem_name)
|
26
|
-
latest = versions.first # The first entry is the latest version
|
27
|
-
version_number = latest['number']
|
28
|
-
created_at = latest['created_at']
|
29
|
-
{ version: version_number, created_at: created_at }
|
30
|
-
end
|
31
|
-
|
32
|
-
# Method to fetch the created_at timestamp for a specific version
|
33
|
-
def self.version_created_at(gem_name, version)
|
34
|
-
versions = fetch_gem_data(gem_name)
|
35
|
-
# Find the version that matches the requested version string
|
36
|
-
version_info = versions.find { |v| v['number'] == version }
|
37
|
-
|
38
|
-
version_info['created_at']
|
39
|
-
end
|
40
|
-
end
|
41
|
-
end
|