dependency-timeline-audit 0.0.1 → 0.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|