denmark 0.0.2 → 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/README.md +5 -2
- data/bin/denmark +15 -4
- data/lib/denmark/monkeypatches.rb +11 -0
- data/lib/denmark/plugins/metadata.rb +13 -5
- data/lib/denmark/plugins/timeline.rb +91 -0
- data/lib/denmark/repository.rb +57 -4
- data/lib/denmark/version.rb +1 -1
- data/lib/denmark.rb +20 -3
- metadata +20 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f9f7e41d628d1cb52bd1af3055c8f34ada3edcc8092e2d1c779fd30a37dd72ad
|
4
|
+
data.tar.gz: 76829cb396c0115321efeccbe91c51adaf8f77cac48657b75399579c1c6e94ea
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f3ae368e8233da41ac9926f0163184ee5f580750245e8c7af82554efc21bf2a1593202a9d05163c3b1238ef0dcd1064cfe8be89be353faeb5d8e05742b5dd628
|
7
|
+
data.tar.gz: e320a6024a23c1c707627352b7f7ec4ded0bad2bbf397b5f2f2c65d83ea10ad31d8d92f6cb936f7d7d10998345cfa8fde5435f5d57c5a31f3c2352bd4bf80bf1
|
data/README.md
CHANGED
@@ -6,9 +6,12 @@
|
|
6
6
|
I'm sure you've had the experience of evaluating modules on the Puppet Forge. Maybe
|
7
7
|
you were comparing a handful that all claimed to meet your needs, or maybe you were
|
8
8
|
just determining whether a specific module met your standards for deploying into
|
9
|
-
your production environment.
|
9
|
+
your production environment. With tools like `puppet-lint` and the PDK, it's fairly
|
10
|
+
straightforward to evaluate code quality. But what about the health of the project
|
11
|
+
itself? Is it actively maintained? Are Forge releases kept up to date? What are the
|
12
|
+
chances that the latest release was compromised with a hidden bitcoin miner?
|
10
13
|
|
11
|
-
How
|
14
|
+
How do you answer those sorts of questions? You probably
|
12
15
|
|
13
16
|
* Skimmed the module's README for signs of the author's diligence.
|
14
17
|
* Poked through the issue list and pull requests on the repository hosting the module
|
data/bin/denmark
CHANGED
@@ -6,8 +6,9 @@ require 'denmark/version'
|
|
6
6
|
class Denmark
|
7
7
|
extend GLI::App
|
8
8
|
|
9
|
-
program_desc
|
10
|
-
version
|
9
|
+
program_desc 'A simple tool for checking Puppet Forge modules for maintenance smells'
|
10
|
+
version Denmark::VERSION
|
11
|
+
wrap_help_text :verbatim
|
11
12
|
|
12
13
|
pre do |global, command, options, args|
|
13
14
|
Denmark.config = YAML.load_file("#{Dir.home}/.config/denmark.yaml") rescue {}
|
@@ -30,8 +31,18 @@ class Denmark
|
|
30
31
|
end
|
31
32
|
end
|
32
33
|
|
33
|
-
desc 'Smell test a module using all enabled tests'
|
34
|
-
|
34
|
+
desc 'Smell test a module using all enabled tests.'
|
35
|
+
long_desc <<~DESC
|
36
|
+
Pass this command the name of a module, the path to a module or its
|
37
|
+
`metadata.json`, or simply execute it within the root directory of a module.
|
38
|
+
|
39
|
+
Examples:
|
40
|
+
$ denmark binford2k-node_encrypt
|
41
|
+
$ denmark /Users/ben/Projects/binford2k-node_encrypt
|
42
|
+
$ denmark /Users/ben/Projects/binford2k-node_encrypt/metadata.json
|
43
|
+
$ cd Projects/binford2k-node_encrypt && denmark
|
44
|
+
DESC
|
45
|
+
command [:smell,:check] do |c|
|
35
46
|
c.desc 'Lists of tests to enable'
|
36
47
|
c.flag [:enable, :e], :type => Array
|
37
48
|
|
@@ -1,8 +1,11 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
+
require 'paint'
|
3
|
+
require 'paint/shortcuts'
|
2
4
|
|
3
5
|
class Array
|
4
6
|
def percent_of(digits = nil)
|
5
7
|
raise "Select the items you want to count using a block that returns a boolean" unless block_given?
|
8
|
+
return 0 if self.empty?
|
6
9
|
|
7
10
|
count = self.size
|
8
11
|
match = 0
|
@@ -17,3 +20,11 @@ class Array
|
|
17
20
|
end
|
18
21
|
end
|
19
22
|
end
|
23
|
+
|
24
|
+
Paint::SHORTCUTS[:color] = {
|
25
|
+
:red => Paint.color(:red),
|
26
|
+
:orange => Paint.color('orange', :bright),
|
27
|
+
:yellow => Paint.color(:yellow),
|
28
|
+
:green => Paint.color(:green),
|
29
|
+
}
|
30
|
+
include Paint::Color::Prefix::ColorName
|
@@ -29,13 +29,21 @@ class Denmark::Plugins::Metadata
|
|
29
29
|
|
30
30
|
if (Date.today - release_date) > 365
|
31
31
|
response << {
|
32
|
-
severity: :
|
32
|
+
severity: :green,
|
33
33
|
message: "The most current module release is more than a year old.",
|
34
|
-
explanation: "Sometimes
|
34
|
+
explanation: "Sometimes a module not seeing regular updates is a sign that it's no longer being maintained. You might consider contacting the maintainer to determine the status of the project.",
|
35
|
+
}
|
36
|
+
end
|
37
|
+
|
38
|
+
if (Date.today - release_date) < 15
|
39
|
+
response << {
|
40
|
+
severity: :green,
|
41
|
+
message: "The latest module release is less than two weeks old.",
|
42
|
+
explanation: "Sometimes it's a good idea to let the early adopters shake out the bugs with a new release.",
|
35
43
|
}
|
36
44
|
end
|
37
45
|
|
38
|
-
if version != repo_metadata[
|
46
|
+
if version != repo_metadata['version']
|
39
47
|
response << {
|
40
48
|
severity: :red,
|
41
49
|
message: "The version released on the Forge does not match the version in the repository.",
|
@@ -51,7 +59,7 @@ class Denmark::Plugins::Metadata
|
|
51
59
|
}
|
52
60
|
end
|
53
61
|
|
54
|
-
|
62
|
+
unless [version, "v#{version}"].include? latest_tag
|
55
63
|
response << {
|
56
64
|
severity: :yellow,
|
57
65
|
message: "The version released on the Forge does not match the latest tag in the repo.",
|
@@ -71,7 +79,7 @@ class Denmark::Plugins::Metadata
|
|
71
79
|
response << {
|
72
80
|
severity: :green,
|
73
81
|
message: "There was a gap of at least a year between the last two releases.",
|
74
|
-
explanation: "A large gap between releases often shows sporadic maintenance. This
|
82
|
+
explanation: "A large gap between releases often shows sporadic maintenance. This doesn't necessarily indicate anything wrong, but attackers do sometimes target stagnant projects in the hope that they'll be undetected for a longer time period.",
|
75
83
|
}
|
76
84
|
end
|
77
85
|
|
@@ -0,0 +1,91 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# environments plugin
|
4
|
+
class Denmark::Plugins::Timeline
|
5
|
+
def self.description
|
6
|
+
# This is a Ruby squiggle heredoc; just a multi-line string with indentation removed
|
7
|
+
<<~DESCRIPTION
|
8
|
+
This smell test infers trends a module base on its timeline of issues and commits and whatnot.
|
9
|
+
DESCRIPTION
|
10
|
+
end
|
11
|
+
def self.setup
|
12
|
+
# run just before evaluating this plugin
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.run(mod, repo)
|
16
|
+
# return an array of hashes representing any smells discovered
|
17
|
+
response = Array.new
|
18
|
+
|
19
|
+
unreleased = repo.commits_since_tag.size
|
20
|
+
new_issues = repo.issues_since_tag.size
|
21
|
+
taggers = repo.committers(repo.tags)
|
22
|
+
last_tagger = taggers.shift
|
23
|
+
|
24
|
+
unsigned_commits = repo.commits.percent_of {|i| not repo.verified(i) }
|
25
|
+
unsigned_tags = repo.tags.percent_of {|i| not repo.verified(i) }
|
26
|
+
|
27
|
+
unless taggers.include? last_tagger
|
28
|
+
response << {
|
29
|
+
severity: :yellow,
|
30
|
+
message: "The last tag was pushed by #{last_tagger}, who has not tagged any other release.",
|
31
|
+
explanation: "This often indicates that a project has recently changed owners. Check to ensure you still know who's maintaining the project.",
|
32
|
+
}
|
33
|
+
end
|
34
|
+
|
35
|
+
unless repo.verified(repo.tags.first)
|
36
|
+
response << {
|
37
|
+
severity: :yellow,
|
38
|
+
message: "The last tag was not verified.",
|
39
|
+
explanation: "Many authors don't bother to sign their tags. This means you have no way to ensure who creates them.",
|
40
|
+
}
|
41
|
+
end
|
42
|
+
|
43
|
+
# this smell would be more accurate if we weighted more recent commits
|
44
|
+
if (25..75).include? unsigned_commits
|
45
|
+
response << {
|
46
|
+
severity: :green,
|
47
|
+
message: "#{unsigned_commits}% of the commits in this repo are not signed.",
|
48
|
+
explanation: "The repository is using signed commits, but some of the contributions are unverified.",
|
49
|
+
}
|
50
|
+
end
|
51
|
+
|
52
|
+
# this smell would be more accurate if we weighted more recent tags
|
53
|
+
if (15..85).include? unsigned_tags
|
54
|
+
response << {
|
55
|
+
severity: :green,
|
56
|
+
message: "#{unsigned_tags}% of the tags in this repo are not signed.",
|
57
|
+
explanation: "The repository is using signed tags, but a significant number are unverified.",
|
58
|
+
}
|
59
|
+
end
|
60
|
+
|
61
|
+
if unsigned_tags > 85 and not repo.verified(repo.tags.first)
|
62
|
+
response << {
|
63
|
+
severity: :red,
|
64
|
+
message: "Most tags in this repo are signed, but not the latest one.",
|
65
|
+
explanation: "At best, this means a sloppy release. But it could also mean a compromised release.",
|
66
|
+
}
|
67
|
+
end
|
68
|
+
|
69
|
+
if unreleased > 10
|
70
|
+
response << {
|
71
|
+
severity: :yellow,
|
72
|
+
message: "There are #{unreleased} commits since the last release.",
|
73
|
+
explanation: "Sometimes maintainers forget to make a release. Maybe you should remind them?",
|
74
|
+
}
|
75
|
+
end
|
76
|
+
|
77
|
+
if new_issues > 5
|
78
|
+
response << {
|
79
|
+
severity: :yellow,
|
80
|
+
message: "There have been #{new_issues} issues since the last tagged release.",
|
81
|
+
explanation: "Many issues on a release might indicate that there's a problem with it.",
|
82
|
+
}
|
83
|
+
end
|
84
|
+
|
85
|
+
response
|
86
|
+
end
|
87
|
+
|
88
|
+
def self.cleanup
|
89
|
+
# run just after evaluating this plugin
|
90
|
+
end
|
91
|
+
end
|
data/lib/denmark/repository.rb
CHANGED
@@ -54,6 +54,45 @@ class Denmark::Repository
|
|
54
54
|
end
|
55
55
|
end
|
56
56
|
|
57
|
+
def issues_since_tag(tag = nil)
|
58
|
+
tag ||= tags[0]
|
59
|
+
case @flavor
|
60
|
+
when :github
|
61
|
+
issues_since(commit_date(tag.commit.sha))
|
62
|
+
when :gitlab
|
63
|
+
issues_since(tag.commit.created_at)
|
64
|
+
else
|
65
|
+
Array.new
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def issues_since(date)
|
70
|
+
case @flavor
|
71
|
+
when :github
|
72
|
+
@client.issues(@repo, {:state => 'open', :since=> date}).reject {|i| i[:pull_request] }
|
73
|
+
when :gitlab
|
74
|
+
@client.issues(@repo, updated_after: date, scope: 'all')
|
75
|
+
else
|
76
|
+
Array.new
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def committers(list)
|
81
|
+
list = Array(list)
|
82
|
+
case @flavor
|
83
|
+
when :github
|
84
|
+
list.reduce(Array.new) do |acc, item|
|
85
|
+
acc << (item.author&.login || commit(item.commit.sha).author.login)
|
86
|
+
end
|
87
|
+
when :gitlab
|
88
|
+
list.reduce(Array.new) do |acc, item|
|
89
|
+
acc << item.commit.author_name
|
90
|
+
end
|
91
|
+
else
|
92
|
+
Array.new
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
57
96
|
def tags
|
58
97
|
case @flavor
|
59
98
|
when :github, :gitlab
|
@@ -74,6 +113,22 @@ class Denmark::Repository
|
|
74
113
|
end
|
75
114
|
end
|
76
115
|
|
116
|
+
def verified(item)
|
117
|
+
case @flavor
|
118
|
+
when :github
|
119
|
+
if item.commit.verification.nil?
|
120
|
+
commit(item.commit.sha).commit.verification.verified
|
121
|
+
else
|
122
|
+
item.commit.verification&.verified
|
123
|
+
end
|
124
|
+
when :gitlab
|
125
|
+
commit(tag.commit.id).verification.verification_status == 'verified'
|
126
|
+
else
|
127
|
+
false
|
128
|
+
end
|
129
|
+
|
130
|
+
end
|
131
|
+
|
77
132
|
def commit(sha)
|
78
133
|
case @flavor
|
79
134
|
when :github, :gitlab
|
@@ -99,10 +154,8 @@ class Denmark::Repository
|
|
99
154
|
when :gitlab
|
100
155
|
@client.commit(@repo, sha).commit.created_at.to_date
|
101
156
|
else
|
102
|
-
|
157
|
+
nil
|
103
158
|
end
|
104
|
-
|
105
|
-
|
106
159
|
end
|
107
160
|
|
108
161
|
def commits_since_tag(tag = nil)
|
@@ -110,7 +163,7 @@ class Denmark::Repository
|
|
110
163
|
|
111
164
|
case @flavor
|
112
165
|
when :github
|
113
|
-
@client.commits_since(@repo, tag.commit.
|
166
|
+
@client.commits_since(@repo, commit_date(tag.commit.sha))
|
114
167
|
when :gitlab
|
115
168
|
@client.commits(@repo, since: tag.commit.created_at)
|
116
169
|
else
|
data/lib/denmark/version.rb
CHANGED
data/lib/denmark.rb
CHANGED
@@ -1,7 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'json'
|
4
|
-
require 'colorize'
|
5
4
|
require 'httpclient'
|
6
5
|
require 'puppet_forge'
|
7
6
|
require 'denmark/plugins'
|
@@ -31,7 +30,8 @@ class Denmark
|
|
31
30
|
|
32
31
|
def self.evaluate(slug, options)
|
33
32
|
@options = options
|
34
|
-
slug
|
33
|
+
slug = resolve_slug(slug)
|
34
|
+
|
35
35
|
begin
|
36
36
|
mod = PuppetForge::Module.find(slug)
|
37
37
|
rescue Faraday::BadRequestError, Faraday::ResourceNotFound
|
@@ -51,6 +51,23 @@ class Denmark
|
|
51
51
|
end
|
52
52
|
end
|
53
53
|
|
54
|
+
def self.resolve_slug(path)
|
55
|
+
begin
|
56
|
+
if path.nil?
|
57
|
+
path = JSON.parse(File.read('metadata.json'))['name']
|
58
|
+
elsif File.directory?(path)
|
59
|
+
path = JSON.parse(File.read("#{path}/metadata.json"))['name']
|
60
|
+
elsif path.end_with?('metadata.json')
|
61
|
+
path = JSON.parse(File.read(path))['name']
|
62
|
+
end
|
63
|
+
rescue Errno::ENOENT => e
|
64
|
+
raise "Cannot load metadata from '#{path}'. Pass this tool the name of a module, or the local path to a module."
|
65
|
+
end
|
66
|
+
|
67
|
+
# if we get this far, assume it's the name of a module and normalize it
|
68
|
+
path.sub('/', '-')
|
69
|
+
end
|
70
|
+
|
54
71
|
def self.generate_report(data)
|
55
72
|
if data.empty?
|
56
73
|
puts "Congrats, no smells discovered"
|
@@ -60,7 +77,7 @@ class Denmark
|
|
60
77
|
alerts = data.select {|i| i[:severity] == severity}
|
61
78
|
next unless alerts.size > 0
|
62
79
|
|
63
|
-
puts "[#{severity.upcase}] alerts:".
|
80
|
+
puts "[#{severity.upcase}] alerts:".color_name(severity)
|
64
81
|
alerts.each do |alert|
|
65
82
|
puts " #{alert[:message]}"
|
66
83
|
puts " > #{alert[:explanation]}" if @options[:detail]
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: denmark
|
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
|
- Ben Ford
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2022-02-
|
11
|
+
date: 2022-02-11 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: json
|
@@ -123,19 +123,33 @@ dependencies:
|
|
123
123
|
- !ruby/object:Gem::Version
|
124
124
|
version: '4.0'
|
125
125
|
- !ruby/object:Gem::Dependency
|
126
|
-
name:
|
126
|
+
name: paint
|
127
127
|
requirement: !ruby/object:Gem::Requirement
|
128
128
|
requirements:
|
129
129
|
- - "~>"
|
130
130
|
- !ruby/object:Gem::Version
|
131
|
-
version: '0
|
131
|
+
version: '2.0'
|
132
132
|
type: :runtime
|
133
133
|
prerelease: false
|
134
134
|
version_requirements: !ruby/object:Gem::Requirement
|
135
135
|
requirements:
|
136
136
|
- - "~>"
|
137
137
|
- !ruby/object:Gem::Version
|
138
|
-
version: '0
|
138
|
+
version: '2.0'
|
139
|
+
- !ruby/object:Gem::Dependency
|
140
|
+
name: paint-shortcuts
|
141
|
+
requirement: !ruby/object:Gem::Requirement
|
142
|
+
requirements:
|
143
|
+
- - "~>"
|
144
|
+
- !ruby/object:Gem::Version
|
145
|
+
version: '2.0'
|
146
|
+
type: :runtime
|
147
|
+
prerelease: false
|
148
|
+
version_requirements: !ruby/object:Gem::Requirement
|
149
|
+
requirements:
|
150
|
+
- - "~>"
|
151
|
+
- !ruby/object:Gem::Version
|
152
|
+
version: '2.0'
|
139
153
|
description: |
|
140
154
|
Denmark will check a Puppet module for things you should be concerned about, like signs of an
|
141
155
|
unmaintained module. It uses the Puppet Forge API and GitHub/GitLab APIs to discover information
|
@@ -155,6 +169,7 @@ files:
|
|
155
169
|
- lib/denmark/plugins/issues.rb
|
156
170
|
- lib/denmark/plugins/metadata.rb
|
157
171
|
- lib/denmark/plugins/pull_requests.rb
|
172
|
+
- lib/denmark/plugins/timeline.rb
|
158
173
|
- lib/denmark/repository.rb
|
159
174
|
- lib/denmark/version.rb
|
160
175
|
homepage: https://github.com/binford2k/denmark
|