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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3b9e17ee55c32c28cf3b8217bbf7ddab45ceafbc611046361ab649b5ede37d42
4
- data.tar.gz: 0a9e935a6e14de9350d1cf9036774da6d5a0e2a8d71967c3d57f581ab3c22bf1
3
+ metadata.gz: 3ac86eaf5319d8a10dc42fd001c0f32a15a5421bd861ce24faf2b4bcb462493d
4
+ data.tar.gz: 9a67aac946d8265cb908d521ed90f2de25fe245bb0f141766faae266e4fb678d
5
5
  SHA512:
6
- metadata.gz: fe745f2316cd6df1ec6022898fd5a6ccb45baf22bfe1f088430a330bd40f58e73b5656515b150ebec40c12423cf829b18b3a9ef8181eec0ddbc592fa9b161f3d
7
- data.tar.gz: f2e7b8e9524a8e9caac06d708e25bdc07c3d90a625a45be93bd0c79d8d3c5446614814a53247c7da6b63628dd7fbc9296033e7753120999b50fdb866c1933839
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
- DependencyTimelineAudit::Check.check
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
- # TODO: activesupport is kinda hefty for just grabbing 1.year.ago, remove
7
- def self.outdated_threshold
8
- 1.year.ago
5
+ attr_reader :config
6
+
7
+ def initialize(config:)
8
+ @config = config
9
9
  end
10
10
 
11
- def self.check(lockfile = 'Gemfile.lock')
11
+ def check
12
12
  outdated_versions = []
13
13
  locked_gems.each do |gem|
14
- lock_released_at = GemInfo.version_created_at(gem[:name], gem[:locked_version])
15
- latest_version = GemInfo.latest_version(gem[:name])
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
- set_text_color_red
22
- puts "\nOutdated gems detected!"
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
- reset_text_style
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 self.gem_outdated?(released_at)
37
- released_at <= outdated_threshold
38
- end
39
-
40
- def self.print_info(gem, lock_released_at, latest_version)
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
- set_text_color_yellow
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,11 @@
1
+ module DependencyTimelineAudit
2
+ class GemVersionCache
3
+ def initialize(version_info)
4
+ @version_info = version_info || {}
5
+ end
6
+
7
+ def released_at
8
+ @version_info['created_at']
9
+ end
10
+ end
11
+ 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
@@ -2,7 +2,7 @@ module DependencyTimelineAudit
2
2
  module VERSION
3
3
  MAJOR = 0
4
4
  MINOR = 0
5
- PATCH = 1
5
+ PATCH = 3
6
6
 
7
7
  STRING = [MAJOR, MINOR, PATCH].join('.')
8
8
  end
@@ -1,9 +1,15 @@
1
1
  module DependencyTimelineAudit
2
- autoload :Check, 'dependency-timeline-audit/check'
3
- autoload :GemInfo, 'dependency-timeline-audit/gem_info'
4
- autoload :VERSION, 'dependency-timeline-audit/version'
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.1
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-09-24 00:00:00.000000000 Z
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/gem_info.rb
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