stackharbinger 0.2.0 → 0.4.0
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/CHANGELOG.md +40 -0
- data/README.md +101 -25
- data/docs/index.html +38 -16
- data/lib/harbinger/analyzers/database_detector.rb +133 -0
- data/lib/harbinger/analyzers/docker_compose_detector.rb +121 -0
- data/lib/harbinger/analyzers/mongo_detector.rb +104 -0
- data/lib/harbinger/analyzers/mysql_detector.rb +90 -0
- data/lib/harbinger/analyzers/postgres_detector.rb +71 -0
- data/lib/harbinger/analyzers/redis_detector.rb +98 -0
- data/lib/harbinger/analyzers/ruby_detector.rb +9 -1
- data/lib/harbinger/cli.rb +362 -48
- data/lib/harbinger/eol_fetcher.rb +22 -8
- data/lib/harbinger/exporters/base_exporter.rb +97 -0
- data/lib/harbinger/exporters/csv_exporter.rb +36 -0
- data/lib/harbinger/exporters/json_exporter.rb +21 -0
- data/lib/harbinger/version.rb +1 -1
- data/lib/harbinger.rb +3 -0
- metadata +15 -3
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "docker_compose_detector"
|
|
4
|
+
|
|
5
|
+
module Harbinger
|
|
6
|
+
module Analyzers
|
|
7
|
+
# Detects MongoDB version from projects
|
|
8
|
+
class MongoDetector
|
|
9
|
+
attr_reader :project_path
|
|
10
|
+
|
|
11
|
+
def initialize(project_path)
|
|
12
|
+
@project_path = project_path
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Main detection method - returns version string or nil
|
|
16
|
+
def detect
|
|
17
|
+
return nil unless mongo_detected?
|
|
18
|
+
|
|
19
|
+
# Try docker-compose.yml first
|
|
20
|
+
version = detect_from_docker_compose
|
|
21
|
+
return version if version
|
|
22
|
+
|
|
23
|
+
# Try shell command
|
|
24
|
+
version = detect_from_shell
|
|
25
|
+
return version if version
|
|
26
|
+
|
|
27
|
+
# Fallback to gem version
|
|
28
|
+
detect_from_gemfile_lock
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Check if MongoDB is used in this project
|
|
32
|
+
def mongo_detected?
|
|
33
|
+
gemfile_has_mongo? || docker_compose_has_mongo?
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def detect_from_docker_compose
|
|
39
|
+
docker = DockerComposeDetector.new(project_path)
|
|
40
|
+
docker.image_version("mongo")
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def detect_from_shell
|
|
44
|
+
# Try mongosh first (modern shell, MongoDB 5+)
|
|
45
|
+
output = `mongosh --version 2>&1`.strip
|
|
46
|
+
return output if $CHILD_STATUS.success? && output.match?(/^\d+\.\d+/)
|
|
47
|
+
|
|
48
|
+
# Try legacy mongo shell
|
|
49
|
+
output = `mongo --version 2>&1`.strip
|
|
50
|
+
if $CHILD_STATUS.success?
|
|
51
|
+
match = output.match(/MongoDB shell version v?(\d+\.\d+(?:\.\d+)?)/)
|
|
52
|
+
return match[1] if match
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Fall back to mongod (server)
|
|
56
|
+
output = `mongod --version 2>&1`.strip
|
|
57
|
+
return nil unless $CHILD_STATUS.success?
|
|
58
|
+
|
|
59
|
+
match = output.match(/db version v(\d+\.\d+(?:\.\d+)?)/)
|
|
60
|
+
match[1] if match
|
|
61
|
+
rescue StandardError
|
|
62
|
+
nil
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def detect_from_gemfile_lock
|
|
66
|
+
content = read_gemfile_lock
|
|
67
|
+
return nil unless content
|
|
68
|
+
|
|
69
|
+
# Look for mongoid gem version first (most common for Rails)
|
|
70
|
+
match = content.match(/^\s{4}mongoid\s+\(([^)]+)\)/)
|
|
71
|
+
return "#{match[1]} (mongoid gem)" if match
|
|
72
|
+
|
|
73
|
+
# Fall back to mongo gem
|
|
74
|
+
match = content.match(/^\s{4}mongo\s+\(([^)]+)\)/)
|
|
75
|
+
return "#{match[1]} (mongo gem)" if match
|
|
76
|
+
|
|
77
|
+
nil
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def gemfile_has_mongo?
|
|
81
|
+
content = read_gemfile_lock
|
|
82
|
+
return false unless content
|
|
83
|
+
|
|
84
|
+
content.include?("mongoid (") || content.include?("mongo (")
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def docker_compose_has_mongo?
|
|
88
|
+
docker = DockerComposeDetector.new(project_path)
|
|
89
|
+
return false unless docker.docker_compose_exists?
|
|
90
|
+
|
|
91
|
+
docker.image_version("mongo") != nil
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def read_gemfile_lock
|
|
95
|
+
path = File.join(project_path, "Gemfile.lock")
|
|
96
|
+
return nil unless File.exist?(path)
|
|
97
|
+
|
|
98
|
+
File.read(path)
|
|
99
|
+
rescue StandardError
|
|
100
|
+
nil
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "database_detector"
|
|
4
|
+
require_relative "docker_compose_detector"
|
|
5
|
+
|
|
6
|
+
module Harbinger
|
|
7
|
+
module Analyzers
|
|
8
|
+
# Detects MySQL version from Rails projects
|
|
9
|
+
# Supports both mysql2 and trilogy adapters
|
|
10
|
+
class MysqlDetector < DatabaseDetector
|
|
11
|
+
protected
|
|
12
|
+
|
|
13
|
+
def adapter_name
|
|
14
|
+
%w[mysql2 trilogy]
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def detect_from_docker_compose
|
|
18
|
+
docker = DockerComposeDetector.new(project_path)
|
|
19
|
+
# Try mysql first, then mariadb
|
|
20
|
+
docker.image_version("mysql") || docker.image_version("mariadb")
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def detect_from_shell
|
|
24
|
+
# Skip shell command if database is remote
|
|
25
|
+
return nil if remote_database?
|
|
26
|
+
|
|
27
|
+
# Try mysql command first, then mysqld
|
|
28
|
+
output = execute_command("mysql --version") || execute_command("mysqld --version")
|
|
29
|
+
return nil unless output
|
|
30
|
+
|
|
31
|
+
# Parse: "mysql Ver 8.0.33" or "mysqld Ver 8.0.33"
|
|
32
|
+
# Also handles MariaDB: "mysql Ver 15.1 Distrib 10.11.2-MariaDB"
|
|
33
|
+
match = output.match(/Ver\s+(?:\d+\.\d+\s+Distrib\s+)?(\d+\.\d+\.\d+)/)
|
|
34
|
+
match[1] if match
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def detect_from_gemfile_lock
|
|
38
|
+
content = parse_gemfile_lock
|
|
39
|
+
return nil unless content
|
|
40
|
+
|
|
41
|
+
# Check which adapter is being used
|
|
42
|
+
config = parse_database_yml
|
|
43
|
+
return nil unless config
|
|
44
|
+
|
|
45
|
+
section = config["production"] || config["default"] || config[config.keys.first]
|
|
46
|
+
return nil unless section
|
|
47
|
+
|
|
48
|
+
adapter = extract_adapter_from_section(section)
|
|
49
|
+
|
|
50
|
+
# Return appropriate gem version based on adapter
|
|
51
|
+
if adapter == "trilogy"
|
|
52
|
+
version = extract_gem_version(content, "trilogy")
|
|
53
|
+
version ? "#{version} (trilogy gem)" : nil
|
|
54
|
+
else
|
|
55
|
+
version = extract_gem_version(content, "mysql2")
|
|
56
|
+
version ? "#{version} (mysql2 gem)" : nil
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
private
|
|
61
|
+
|
|
62
|
+
# Check if database configuration indicates a remote database
|
|
63
|
+
# (same logic as PostgresDetector)
|
|
64
|
+
def remote_database?
|
|
65
|
+
config = parse_database_yml
|
|
66
|
+
return false unless config
|
|
67
|
+
|
|
68
|
+
section = config["production"] || config["default"] || config[config.keys.first]
|
|
69
|
+
return false unless section
|
|
70
|
+
|
|
71
|
+
db_config = if section["adapter"]
|
|
72
|
+
section
|
|
73
|
+
else
|
|
74
|
+
section["primary"] || section.values.find { |v| v.is_a?(Hash) && v["adapter"] }
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
return false unless db_config
|
|
78
|
+
|
|
79
|
+
host = db_config["host"]
|
|
80
|
+
|
|
81
|
+
# No host specified = localhost (Unix socket)
|
|
82
|
+
return false if host.nil? || host.empty?
|
|
83
|
+
|
|
84
|
+
# Explicit localhost indicators
|
|
85
|
+
local_hosts = ["localhost", "127.0.0.1", "::1", "0.0.0.0"]
|
|
86
|
+
!local_hosts.include?(host.to_s.downcase)
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "database_detector"
|
|
4
|
+
require_relative "docker_compose_detector"
|
|
5
|
+
|
|
6
|
+
module Harbinger
|
|
7
|
+
module Analyzers
|
|
8
|
+
# Detects PostgreSQL version from Rails projects
|
|
9
|
+
class PostgresDetector < DatabaseDetector
|
|
10
|
+
protected
|
|
11
|
+
|
|
12
|
+
def adapter_name
|
|
13
|
+
"postgresql"
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def detect_from_docker_compose
|
|
17
|
+
docker = DockerComposeDetector.new(project_path)
|
|
18
|
+
docker.image_version("postgres")
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def detect_from_shell
|
|
22
|
+
# Skip shell command if database is remote
|
|
23
|
+
# (shell gives client version, not server version)
|
|
24
|
+
return nil if remote_database?
|
|
25
|
+
|
|
26
|
+
output = execute_command("psql --version")
|
|
27
|
+
return nil unless output
|
|
28
|
+
|
|
29
|
+
# Parse: "psql (PostgreSQL) 15.3" or "psql (PostgreSQL) 15.3 (Ubuntu 15.3-1)"
|
|
30
|
+
match = output.match(/PostgreSQL\)\s+(\d+\.\d+)/)
|
|
31
|
+
match[1] if match
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def detect_from_gemfile_lock
|
|
35
|
+
content = parse_gemfile_lock
|
|
36
|
+
version = extract_gem_version(content, "pg")
|
|
37
|
+
version ? "#{version} (pg gem)" : nil
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
# Check if database configuration indicates a remote database
|
|
43
|
+
def remote_database?
|
|
44
|
+
config = parse_database_yml
|
|
45
|
+
return false unless config
|
|
46
|
+
|
|
47
|
+
# Get the section with database config
|
|
48
|
+
section = config["production"] || config["default"] || config[config.keys.first]
|
|
49
|
+
return false unless section
|
|
50
|
+
|
|
51
|
+
# Handle multi-database config
|
|
52
|
+
db_config = if section["adapter"]
|
|
53
|
+
section
|
|
54
|
+
else
|
|
55
|
+
section["primary"] || section.values.find { |v| v.is_a?(Hash) && v["adapter"] }
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
return false unless db_config
|
|
59
|
+
|
|
60
|
+
host = db_config["host"]
|
|
61
|
+
|
|
62
|
+
# No host specified = localhost (Unix socket)
|
|
63
|
+
return false if host.nil? || host.empty?
|
|
64
|
+
|
|
65
|
+
# Explicit localhost indicators
|
|
66
|
+
local_hosts = ["localhost", "127.0.0.1", "::1", "0.0.0.0"]
|
|
67
|
+
!local_hosts.include?(host.to_s.downcase)
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "docker_compose_detector"
|
|
4
|
+
|
|
5
|
+
module Harbinger
|
|
6
|
+
module Analyzers
|
|
7
|
+
# Detects Redis version from projects
|
|
8
|
+
class RedisDetector
|
|
9
|
+
attr_reader :project_path
|
|
10
|
+
|
|
11
|
+
def initialize(project_path)
|
|
12
|
+
@project_path = project_path
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Main detection method - returns version string or nil
|
|
16
|
+
def detect
|
|
17
|
+
return nil unless redis_detected?
|
|
18
|
+
|
|
19
|
+
# Try docker-compose.yml first
|
|
20
|
+
version = detect_from_docker_compose
|
|
21
|
+
return version if version
|
|
22
|
+
|
|
23
|
+
# Try shell command
|
|
24
|
+
version = detect_from_shell
|
|
25
|
+
return version if version
|
|
26
|
+
|
|
27
|
+
# Fallback to gem version
|
|
28
|
+
detect_from_gemfile_lock
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Check if Redis is used in this project
|
|
32
|
+
def redis_detected?
|
|
33
|
+
gemfile_has_redis? || docker_compose_has_redis?
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def detect_from_docker_compose
|
|
39
|
+
docker = DockerComposeDetector.new(project_path)
|
|
40
|
+
docker.image_version("redis")
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def detect_from_shell
|
|
44
|
+
# Try redis-cli first (more commonly available)
|
|
45
|
+
output = `redis-cli -v 2>&1`.strip
|
|
46
|
+
if $CHILD_STATUS.success?
|
|
47
|
+
# Parse: "redis-cli 7.0.5"
|
|
48
|
+
match = output.match(/redis-cli\s+(\d+\.\d+(?:\.\d+)?)/)
|
|
49
|
+
return match[1] if match
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Fall back to redis-server
|
|
53
|
+
output = `redis-server --version 2>&1`.strip
|
|
54
|
+
return nil unless $CHILD_STATUS.success?
|
|
55
|
+
|
|
56
|
+
# Parse: "Redis server v=7.2.4 sha=..."
|
|
57
|
+
match = output.match(/v=(\d+\.\d+(?:\.\d+)?)/)
|
|
58
|
+
match[1] if match
|
|
59
|
+
rescue StandardError
|
|
60
|
+
nil
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def detect_from_gemfile_lock
|
|
64
|
+
content = read_gemfile_lock
|
|
65
|
+
return nil unless content
|
|
66
|
+
|
|
67
|
+
# Look for redis gem version
|
|
68
|
+
match = content.match(/^\s{4}redis\s+\(([^)]+)\)/)
|
|
69
|
+
return "#{match[1]} (gem)" if match
|
|
70
|
+
|
|
71
|
+
nil
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def gemfile_has_redis?
|
|
75
|
+
content = read_gemfile_lock
|
|
76
|
+
return false unless content
|
|
77
|
+
|
|
78
|
+
content.include?("redis (")
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def docker_compose_has_redis?
|
|
82
|
+
docker = DockerComposeDetector.new(project_path)
|
|
83
|
+
return false unless docker.docker_compose_exists?
|
|
84
|
+
|
|
85
|
+
docker.image_version("redis") != nil
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def read_gemfile_lock
|
|
89
|
+
path = File.join(project_path, "Gemfile.lock")
|
|
90
|
+
return nil unless File.exist?(path)
|
|
91
|
+
|
|
92
|
+
File.read(path)
|
|
93
|
+
rescue StandardError
|
|
94
|
+
nil
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "docker_compose_detector"
|
|
4
|
+
|
|
3
5
|
module Harbinger
|
|
4
6
|
module Analyzers
|
|
5
7
|
class RubyDetector
|
|
@@ -10,7 +12,8 @@ module Harbinger
|
|
|
10
12
|
def detect
|
|
11
13
|
detect_from_ruby_version ||
|
|
12
14
|
detect_from_gemfile ||
|
|
13
|
-
detect_from_gemfile_lock
|
|
15
|
+
detect_from_gemfile_lock ||
|
|
16
|
+
detect_from_dockerfile
|
|
14
17
|
end
|
|
15
18
|
|
|
16
19
|
def ruby_detected?
|
|
@@ -61,6 +64,11 @@ module Harbinger
|
|
|
61
64
|
# Remove patch level suffix (e.g., "p223")
|
|
62
65
|
version.sub(/p\d+$/, "")
|
|
63
66
|
end
|
|
67
|
+
|
|
68
|
+
def detect_from_dockerfile
|
|
69
|
+
docker = DockerComposeDetector.new(project_path)
|
|
70
|
+
docker.ruby_version_from_dockerfile
|
|
71
|
+
end
|
|
64
72
|
end
|
|
65
73
|
end
|
|
66
74
|
end
|