envvariablecheck 0.1.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 +7 -0
- data/Gemfile +5 -0
- data/LICENSE +21 -0
- data/README.md +151 -0
- data/Rakefile +11 -0
- data/exe/envcheck +9 -0
- data/lib/envvariablecheck/auditor.rb +279 -0
- data/lib/envvariablecheck/classifier.rb +50 -0
- data/lib/envvariablecheck/cli.rb +187 -0
- data/lib/envvariablecheck/common_passwords.rb +65 -0
- data/lib/envvariablecheck/entropy.rb +104 -0
- data/lib/envvariablecheck/load.rb +7 -0
- data/lib/envvariablecheck/loader.rb +50 -0
- data/lib/envvariablecheck/parser.rb +176 -0
- data/lib/envvariablecheck/report.rb +146 -0
- data/lib/envvariablecheck/scorer.rb +244 -0
- data/lib/envvariablecheck/version.rb +5 -0
- data/lib/envvariablecheck.rb +37 -0
- data/test/auditor_test.rb +148 -0
- data/test/cli_test.rb +143 -0
- data/test/loader_test.rb +58 -0
- data/test/parser_test.rb +84 -0
- data/test/scorer_test.rb +95 -0
- data/test/test_helper.rb +23 -0
- metadata +73 -0
data/test/cli_test.rb
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'test_helper'
|
|
4
|
+
|
|
5
|
+
class CliTest < Minitest::Test
|
|
6
|
+
STRONG = 'aB3dE6gH9kL2mN5pQ8sT1uV4wX7yZ0cR'
|
|
7
|
+
|
|
8
|
+
def test_help_and_version
|
|
9
|
+
status = nil
|
|
10
|
+
out, err = capture_io { status = EnvVariableCheck::CLI.start([]) }
|
|
11
|
+
assert_equal 0, status
|
|
12
|
+
assert_match(/envcheck run/, out)
|
|
13
|
+
assert_match(/prints the env file in full/, out)
|
|
14
|
+
assert_equal '', err
|
|
15
|
+
|
|
16
|
+
status = nil
|
|
17
|
+
out, _err = capture_io { status = EnvVariableCheck::CLI.start(['version']) }
|
|
18
|
+
assert_equal 0, status
|
|
19
|
+
assert_equal "envvariablecheck #{EnvVariableCheck::VERSION}\n", out
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def test_unknown_command_and_missing_file
|
|
23
|
+
status = nil
|
|
24
|
+
_out, err = capture_io { status = EnvVariableCheck::CLI.start(['nope']) }
|
|
25
|
+
assert_equal 2, status
|
|
26
|
+
assert_match(/Unknown command/, err)
|
|
27
|
+
|
|
28
|
+
status = nil
|
|
29
|
+
_out, err = capture_io { status = EnvVariableCheck::CLI.start(['audit', 'missing.env']) }
|
|
30
|
+
assert_equal 1, status
|
|
31
|
+
assert_match(/not found/, err)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def test_audit_text_and_json_include_the_file
|
|
35
|
+
contents = "DB_PASSWORD=ZzCanary!\nPORT=3000\n"
|
|
36
|
+
with_env_file(contents, mode: 0600) do |_dir, path|
|
|
37
|
+
status = nil
|
|
38
|
+
out, err = capture_io { status = EnvVariableCheck::CLI.start(['audit', '--env', path, '--no-color']) }
|
|
39
|
+
assert_equal 0, status, err
|
|
40
|
+
assert_match(/Overall: Weak/, out)
|
|
41
|
+
assert_match(/DB_PASSWORD/, out)
|
|
42
|
+
assert_match(/PORT/, out)
|
|
43
|
+
assert_includes out, contents
|
|
44
|
+
assert_equal '', err
|
|
45
|
+
|
|
46
|
+
json_status = nil
|
|
47
|
+
json_out, json_err = capture_io do
|
|
48
|
+
json_status = EnvVariableCheck::CLI.start(['audit', '--format', 'json', '--strict', path])
|
|
49
|
+
end
|
|
50
|
+
assert_equal 1, json_status, json_err
|
|
51
|
+
payload = JSON.parse(json_out)
|
|
52
|
+
assert_equal 'Weak', payload['overall_rank']
|
|
53
|
+
assert_equal contents, payload['contents']
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def test_run_injects_env_and_arguments
|
|
58
|
+
with_env_file("FROM_DOTENV=loaded-from-file\nAPI_TOKEN=#{STRONG}\n", mode: 0600) do |dir, path|
|
|
59
|
+
script = File.join(dir, 'app.rb')
|
|
60
|
+
File.write(script, "puts [ENV.fetch('FROM_DOTENV'), ARGV.join(',')].join('|')\n")
|
|
61
|
+
stdout, stderr, status = Open3.capture3(
|
|
62
|
+
RbConfig.ruby, EnvCheckTest::EXE, 'run', '--env', path, script, 'hello', 'world'
|
|
63
|
+
)
|
|
64
|
+
assert status.success?, stderr
|
|
65
|
+
assert_equal "loaded-from-file|hello,world\n", stdout
|
|
66
|
+
assert_match(/injected/, stderr)
|
|
67
|
+
assert_match(/Excellent/, stderr)
|
|
68
|
+
assert_includes stderr, File.read(path)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def test_run_keeps_existing_variables_unless_override
|
|
73
|
+
with_env_file("FROM_DOTENV=from-file\nNEW_ONE=from-file\nAPI_TOKEN=#{STRONG}\n", mode: 0600) do |dir, path|
|
|
74
|
+
script = File.join(dir, 'app.rb')
|
|
75
|
+
File.write(script, "puts [ENV.fetch('FROM_DOTENV'), ENV.fetch('NEW_ONE')].join('|')\n")
|
|
76
|
+
stdout, stderr, status = Open3.capture3(
|
|
77
|
+
{ 'FROM_DOTENV' => 'from-shell' },
|
|
78
|
+
RbConfig.ruby, EnvCheckTest::EXE, 'run', '--quiet', '--env', path, script
|
|
79
|
+
)
|
|
80
|
+
assert status.success?, stderr
|
|
81
|
+
assert_equal "from-shell|from-file\n", stdout
|
|
82
|
+
|
|
83
|
+
stdout, stderr, status = Open3.capture3(
|
|
84
|
+
{ 'FROM_DOTENV' => 'from-shell' },
|
|
85
|
+
RbConfig.ruby, EnvCheckTest::EXE, 'run', '--quiet', '--override', '--env', path, script
|
|
86
|
+
)
|
|
87
|
+
assert status.success?, stderr
|
|
88
|
+
assert_equal "from-file|from-file\n", stdout
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def test_strict_run_does_not_execute_a_weak_file
|
|
93
|
+
secret = 'ZzCanary!'
|
|
94
|
+
with_env_file("DB_PASSWORD=#{secret}\n", mode: 0600) do |dir, path|
|
|
95
|
+
marker = File.join(dir, 'ran.txt')
|
|
96
|
+
script = File.join(dir, 'app.rb')
|
|
97
|
+
File.write(script, "File.write(#{marker.inspect}, 'ran')\n")
|
|
98
|
+
stdout, stderr, status = Open3.capture3(
|
|
99
|
+
RbConfig.ruby, EnvCheckTest::EXE, 'run', '--strict', '--env', path, script
|
|
100
|
+
)
|
|
101
|
+
refute status.success?
|
|
102
|
+
assert_equal '', stdout
|
|
103
|
+
refute File.exist?(marker)
|
|
104
|
+
assert_match(/Critical|Weak/, stderr)
|
|
105
|
+
assert_includes stderr, File.read(path)
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def test_export_command
|
|
110
|
+
with_env_file("FROM_DOTENV=loaded\n", mode: 0600) do |_dir, path|
|
|
111
|
+
status = nil
|
|
112
|
+
out, err = capture_io { status = EnvVariableCheck::CLI.start(['export', path]) }
|
|
113
|
+
assert_equal 0, status, err
|
|
114
|
+
assert_equal "export FROM_DOTENV='loaded'\n", out
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def test_require_load_injects_before_the_script
|
|
119
|
+
with_env_file("FROM_DOTENV=from-require\n", mode: 0600) do |dir, _path|
|
|
120
|
+
stdout, stderr, status = Open3.capture3(
|
|
121
|
+
{ 'HOME' => dir },
|
|
122
|
+
RbConfig.ruby, '-I', EnvCheckTest::LIB, '-r', 'envvariablecheck/load', '-e', 'puts ENV.fetch("FROM_DOTENV")'
|
|
123
|
+
)
|
|
124
|
+
assert status.success?, stderr
|
|
125
|
+
assert_equal "from-require\n", stdout
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def test_audit_reads_home_dotenv_by_default
|
|
130
|
+
contents = "FROM_DOTENV=from-home\nAPI_TOKEN=#{STRONG}\n"
|
|
131
|
+
original_home = ENV['HOME']
|
|
132
|
+
with_env_file(contents, mode: 0600) do |dir, path|
|
|
133
|
+
ENV['HOME'] = dir
|
|
134
|
+
status = nil
|
|
135
|
+
out, err = capture_io { status = EnvVariableCheck::CLI.start(['audit', '--no-color']) }
|
|
136
|
+
assert_equal 0, status, err
|
|
137
|
+
assert_includes out, "File: #{path}"
|
|
138
|
+
assert_includes out, contents
|
|
139
|
+
end
|
|
140
|
+
ensure
|
|
141
|
+
ENV['HOME'] = original_home
|
|
142
|
+
end
|
|
143
|
+
end
|
data/test/loader_test.rb
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'test_helper'
|
|
4
|
+
|
|
5
|
+
class LoaderTest < Minitest::Test
|
|
6
|
+
def teardown
|
|
7
|
+
%w[ENVCHECK_LOAD_A ENVCHECK_LOAD_B ENVCHECK_LOAD_NUL].each { |key| ENV.delete(key) }
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def test_load_keeps_existing_variables_unless_override
|
|
11
|
+
Dir.mktmpdir do |dir|
|
|
12
|
+
path = File.join(dir, '.env')
|
|
13
|
+
File.write(path, "ENVCHECK_LOAD_A=from-file\nENVCHECK_LOAD_B=from-file\n")
|
|
14
|
+
ENV['ENVCHECK_LOAD_A'] = 'from-shell'
|
|
15
|
+
ENV.delete('ENVCHECK_LOAD_B')
|
|
16
|
+
|
|
17
|
+
result = EnvVariableCheck.load!(path: path)
|
|
18
|
+
assert_equal 'from-shell', ENV['ENVCHECK_LOAD_A']
|
|
19
|
+
assert_equal 'from-file', ENV['ENVCHECK_LOAD_B']
|
|
20
|
+
assert_includes result.skipped, 'ENVCHECK_LOAD_A'
|
|
21
|
+
assert_includes result.applied, 'ENVCHECK_LOAD_B'
|
|
22
|
+
|
|
23
|
+
ENV['ENVCHECK_LOAD_A'] = 'from-shell'
|
|
24
|
+
overridden = EnvVariableCheck.load!(path: path, override: true)
|
|
25
|
+
assert_equal 'from-file', ENV['ENVCHECK_LOAD_A']
|
|
26
|
+
assert_includes overridden.applied, 'ENVCHECK_LOAD_A'
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def test_null_bytes_are_skipped
|
|
31
|
+
Dir.mktmpdir do |dir|
|
|
32
|
+
path = File.join(dir, '.env')
|
|
33
|
+
File.binwrite(path, "ENVCHECK_LOAD_NUL=a\0b\n")
|
|
34
|
+
result = EnvVariableCheck.load!(path: path)
|
|
35
|
+
refute ENV.key?('ENVCHECK_LOAD_NUL')
|
|
36
|
+
assert_match(/ENVCHECK_LOAD_NUL/, result.warnings.join)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def test_missing_file
|
|
41
|
+
error = assert_raises(EnvVariableCheck::Error) do
|
|
42
|
+
EnvVariableCheck.load!(path: 'missing-env-file.env')
|
|
43
|
+
end
|
|
44
|
+
assert_match(/not found/, error.message)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def test_shell_exports_round_trip_special_characters
|
|
48
|
+
value = "p@ss'w$rd;$(whoami)`id`"
|
|
49
|
+
Dir.mktmpdir do |dir|
|
|
50
|
+
path = File.join(dir, '.env')
|
|
51
|
+
File.write(path, %q(FOO="p@ss'w\$rd;$(whoami)`id`") + "\n")
|
|
52
|
+
exported = EnvVariableCheck::Loader.shell_exports(EnvVariableCheck::Parser.parse(File.read(path), path: path))
|
|
53
|
+
stdout, stderr, status = Open3.capture3('sh', '-c', 'eval "$1"; printf "%s" "$FOO"', 'sh', exported)
|
|
54
|
+
assert status.success?, stderr
|
|
55
|
+
assert_equal value, stdout
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
data/test/parser_test.rb
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'test_helper'
|
|
4
|
+
|
|
5
|
+
class ParserTest < Minitest::Test
|
|
6
|
+
def parse(contents)
|
|
7
|
+
EnvVariableCheck::Parser.parse(contents)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def test_comments_blanks_export_and_inline_comments
|
|
11
|
+
assignments = parse(<<~ENV)
|
|
12
|
+
# comment
|
|
13
|
+
|
|
14
|
+
export PORT=3000
|
|
15
|
+
NAME=value # trailing
|
|
16
|
+
HASH=value#keep
|
|
17
|
+
ENV
|
|
18
|
+
assert_equal %w[PORT NAME HASH], assignments.map(&:key)
|
|
19
|
+
assert_equal '3000', assignments[0].value
|
|
20
|
+
assert_equal 'value', assignments[1].value
|
|
21
|
+
assert_equal 'value#keep', assignments[2].value
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def test_quotes_escapes_and_multiline
|
|
25
|
+
literal = parse("LITERAL='${KEEP} $KEEP'\n")
|
|
26
|
+
assert_equal '${KEEP} $KEEP', literal[0].value
|
|
27
|
+
|
|
28
|
+
escaped = parse('ESCAPED="a\nb\t\"quote\""' + "\n")
|
|
29
|
+
assert_equal "a\nb\t\"quote\"", escaped[0].value
|
|
30
|
+
|
|
31
|
+
multi = parse("MULTI=\"one\ntwo\"\n")
|
|
32
|
+
assert_equal "one\ntwo", multi[0].value
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def test_interpolation_prefers_earlier_keys_then_env
|
|
36
|
+
ENV['ENVCHECK_PARSER_OUTSIDE'] = 'out'
|
|
37
|
+
assignments = parse(<<~'ENV')
|
|
38
|
+
INSIDE=in
|
|
39
|
+
MIXED=${INSIDE}-$ENVCHECK_PARSER_OUTSIDE
|
|
40
|
+
MISSING=pre-$ENVCHECK_PARSER_MISSING-post
|
|
41
|
+
QUOTED='${INSIDE}'
|
|
42
|
+
ENV
|
|
43
|
+
assert_equal 'in-out', assignments[1].value
|
|
44
|
+
assert_equal 'pre--post', assignments[2].value
|
|
45
|
+
assert_equal '${INSIDE}', assignments[3].value
|
|
46
|
+
ensure
|
|
47
|
+
ENV.delete('ENVCHECK_PARSER_OUTSIDE')
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def test_dollar_escapes_are_literal
|
|
51
|
+
assignments = parse(<<~'ENV')
|
|
52
|
+
BARE=pass\$word
|
|
53
|
+
QUOTED="pass\$word"
|
|
54
|
+
ENV
|
|
55
|
+
assert_equal 'pass$word', assignments[0].value
|
|
56
|
+
assert_equal 'pass$word', assignments[1].value
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def test_crlf_and_bom
|
|
60
|
+
assignments = parse("\uFEFFKEY=value\r\nNEXT=2\r\n")
|
|
61
|
+
assert_equal 'value', assignments[0].value
|
|
62
|
+
assert_equal '2', assignments[1].value
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def test_last_assignment_wins_when_collapsed
|
|
66
|
+
assignments = parse("A=1\nA=2\n")
|
|
67
|
+
collapsed, counts = EnvVariableCheck::Parser.collapse(assignments)
|
|
68
|
+
assert_equal '2', collapsed[0].value
|
|
69
|
+
assert_equal 2, counts['A']
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def test_parse_errors_do_not_include_the_line_contents
|
|
73
|
+
secret = 'canary-secret-value'
|
|
74
|
+
error = assert_raises(EnvVariableCheck::Error) { parse("NOT VALID=#{secret}\n") }
|
|
75
|
+
refute_includes error.message, secret
|
|
76
|
+
assert_match(/:1/, error.message)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def test_unclosed_quote
|
|
80
|
+
error = assert_raises(EnvVariableCheck::Error) { parse("KEY=\"abc\n") }
|
|
81
|
+
assert_match(/Unclosed/, error.message)
|
|
82
|
+
refute_includes error.message, 'abc'
|
|
83
|
+
end
|
|
84
|
+
end
|
data/test/scorer_test.rb
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'test_helper'
|
|
4
|
+
|
|
5
|
+
class ScorerTest < Minitest::Test
|
|
6
|
+
TOKEN = 'aB3dE6gH9kL2mN5pQ8sT1uV4wX7yZ0cR'
|
|
7
|
+
HEX = 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855'
|
|
8
|
+
|
|
9
|
+
def score(value, kind: :password, name: 'PASSWORD', reused: [])
|
|
10
|
+
EnvVariableCheck::Scorer.score(value, kind: kind, name: name, reused_names: reused)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def test_rank_boundaries
|
|
14
|
+
assert_equal 'Critical', EnvVariableCheck::Scorer.rank_for(0)
|
|
15
|
+
assert_equal 'Critical', EnvVariableCheck::Scorer.rank_for(24)
|
|
16
|
+
assert_equal 'Weak', EnvVariableCheck::Scorer.rank_for(25)
|
|
17
|
+
assert_equal 'Weak', EnvVariableCheck::Scorer.rank_for(49)
|
|
18
|
+
assert_equal 'Fair', EnvVariableCheck::Scorer.rank_for(50)
|
|
19
|
+
assert_equal 'Fair', EnvVariableCheck::Scorer.rank_for(74)
|
|
20
|
+
assert_equal 'Strong', EnvVariableCheck::Scorer.rank_for(75)
|
|
21
|
+
assert_equal 'Strong', EnvVariableCheck::Scorer.rank_for(89)
|
|
22
|
+
assert_equal 'Excellent', EnvVariableCheck::Scorer.rank_for(90)
|
|
23
|
+
assert_equal 'Excellent', EnvVariableCheck::Scorer.rank_for(100)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def test_empty_and_common_passwords_are_critical
|
|
27
|
+
assert_equal 0, score('').score
|
|
28
|
+
assert_equal 'Critical', score('').rank
|
|
29
|
+
|
|
30
|
+
common = score('password')
|
|
31
|
+
assert_equal 'Critical', common.rank
|
|
32
|
+
assert_operator common.score, :<=, 12
|
|
33
|
+
assert_match(/commonly used/, common.findings.join)
|
|
34
|
+
|
|
35
|
+
assert_equal 'Critical', score('qwerty').rank
|
|
36
|
+
assert_equal 'Critical', score('changeme').rank
|
|
37
|
+
assert_equal 'Critical', score('correct horse battery staple').rank
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def test_predictable_decoration_does_not_outrank_a_common_base
|
|
41
|
+
decorated = score('Summer2024!')
|
|
42
|
+
assert_equal 'Critical', decorated.rank
|
|
43
|
+
assert_operator decorated.score, :<=, 20
|
|
44
|
+
refute_includes decorated.findings.join, 'Summer2024!'
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def test_length_caps
|
|
48
|
+
eight = score('aB3dE6gH')
|
|
49
|
+
twelve = score('aB3dE6gH9kL2')
|
|
50
|
+
twenty = score('aB3dE6gH9kL2mN5pQ8sT')
|
|
51
|
+
assert_equal 8, 'aB3dE6gH'.length
|
|
52
|
+
assert_equal 12, 'aB3dE6gH9kL2'.length
|
|
53
|
+
assert_equal 20, 'aB3dE6gH9kL2mN5pQ8sT'.length
|
|
54
|
+
|
|
55
|
+
assert_includes %w[Critical Weak], eight.rank
|
|
56
|
+
assert_equal 'Fair', twelve.rank
|
|
57
|
+
assert_includes %w[Strong Excellent], twenty.rank
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def test_random_token_and_hex_are_excellent
|
|
61
|
+
assert_equal 32, TOKEN.length
|
|
62
|
+
assert_equal 64, HEX.length
|
|
63
|
+
token = score(TOKEN, kind: :token, name: 'API_TOKEN')
|
|
64
|
+
hex = score(HEX, kind: :token, name: 'API_TOKEN')
|
|
65
|
+
assert_equal 'Excellent', token.rank, token.findings.inspect
|
|
66
|
+
assert_equal 'Excellent', hex.rank, hex.findings.inspect
|
|
67
|
+
refute_includes token.findings.join, TOKEN
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def test_short_tokens_cannot_rank_above_fair
|
|
71
|
+
short = score('aB3dE6gH9kL2mN5pQ8sT', kind: :token, name: 'API_TOKEN')
|
|
72
|
+
assert_operator short.score, :<=, 74
|
|
73
|
+
assert_match(/32 characters/, short.findings.join)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def test_reuse_blocks_excellent
|
|
77
|
+
reused = score(TOKEN, kind: :token, name: 'API_TOKEN', reused: ['OTHER_TOKEN'])
|
|
78
|
+
assert_operator reused.score, :<=, 89
|
|
79
|
+
refute_equal 'Excellent', reused.rank
|
|
80
|
+
assert_match(/OTHER_TOKEN/, reused.findings.join)
|
|
81
|
+
refute_includes reused.findings.join, TOKEN
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def test_value_matching_the_name_is_critical
|
|
85
|
+
result = score('db_password', name: 'DB_PASSWORD')
|
|
86
|
+
assert_equal 'Critical', result.rank
|
|
87
|
+
assert_match(/variable name/, result.findings.join)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def test_placeholder_and_literal
|
|
91
|
+
assert_equal 'Critical', score('your_api_key').rank
|
|
92
|
+
assert_equal 'Critical', score('true', name: 'API_SECRET').rank
|
|
93
|
+
assert_equal 'Critical', score('xxx').rank
|
|
94
|
+
end
|
|
95
|
+
end
|
data/test/test_helper.rb
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'minitest/autorun'
|
|
4
|
+
require 'open3'
|
|
5
|
+
require 'rbconfig'
|
|
6
|
+
require 'tmpdir'
|
|
7
|
+
require 'json'
|
|
8
|
+
require 'envvariablecheck'
|
|
9
|
+
|
|
10
|
+
module EnvCheckTest
|
|
11
|
+
ROOT = File.expand_path('..', __dir__)
|
|
12
|
+
EXE = File.join(ROOT, 'exe', 'envcheck')
|
|
13
|
+
LIB = File.join(ROOT, 'lib')
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def with_env_file(contents, mode: 0600)
|
|
17
|
+
Dir.mktmpdir('envcheck') do |dir|
|
|
18
|
+
path = File.join(dir, '.env')
|
|
19
|
+
File.write(path, contents)
|
|
20
|
+
File.chmod(mode, path)
|
|
21
|
+
yield dir, path
|
|
22
|
+
end
|
|
23
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: envvariablecheck
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- kellysec
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: exe
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-10-15 00:00:00.000000000 Z
|
|
12
|
+
dependencies: []
|
|
13
|
+
description: |
|
|
14
|
+
envvariablecheck loads ~/.env into the environment before a Ruby
|
|
15
|
+
script runs, and reviews the file's keys and passwords against password best
|
|
16
|
+
practices. It ranks each secret, checks for reused and placeholder values,
|
|
17
|
+
and reports loose file permissions and a missing gitignore entry. The
|
|
18
|
+
review prints the env file in full.
|
|
19
|
+
email:
|
|
20
|
+
- 150852464+kellysec@users.noreply.github.com
|
|
21
|
+
executables:
|
|
22
|
+
- envcheck
|
|
23
|
+
extensions: []
|
|
24
|
+
extra_rdoc_files: []
|
|
25
|
+
files:
|
|
26
|
+
- Gemfile
|
|
27
|
+
- LICENSE
|
|
28
|
+
- README.md
|
|
29
|
+
- Rakefile
|
|
30
|
+
- exe/envcheck
|
|
31
|
+
- lib/envvariablecheck.rb
|
|
32
|
+
- lib/envvariablecheck/auditor.rb
|
|
33
|
+
- lib/envvariablecheck/classifier.rb
|
|
34
|
+
- lib/envvariablecheck/cli.rb
|
|
35
|
+
- lib/envvariablecheck/common_passwords.rb
|
|
36
|
+
- lib/envvariablecheck/entropy.rb
|
|
37
|
+
- lib/envvariablecheck/load.rb
|
|
38
|
+
- lib/envvariablecheck/loader.rb
|
|
39
|
+
- lib/envvariablecheck/parser.rb
|
|
40
|
+
- lib/envvariablecheck/report.rb
|
|
41
|
+
- lib/envvariablecheck/scorer.rb
|
|
42
|
+
- lib/envvariablecheck/version.rb
|
|
43
|
+
- test/auditor_test.rb
|
|
44
|
+
- test/cli_test.rb
|
|
45
|
+
- test/loader_test.rb
|
|
46
|
+
- test/parser_test.rb
|
|
47
|
+
- test/scorer_test.rb
|
|
48
|
+
- test/test_helper.rb
|
|
49
|
+
homepage:
|
|
50
|
+
licenses:
|
|
51
|
+
- MIT
|
|
52
|
+
metadata:
|
|
53
|
+
rubygems_mfa_required: 'true'
|
|
54
|
+
post_install_message:
|
|
55
|
+
rdoc_options: []
|
|
56
|
+
require_paths:
|
|
57
|
+
- lib
|
|
58
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
59
|
+
requirements:
|
|
60
|
+
- - ">="
|
|
61
|
+
- !ruby/object:Gem::Version
|
|
62
|
+
version: 2.6.0
|
|
63
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - ">="
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '0'
|
|
68
|
+
requirements: []
|
|
69
|
+
rubygems_version: 3.0.3.1
|
|
70
|
+
signing_key:
|
|
71
|
+
specification_version: 4
|
|
72
|
+
summary: Inject ~/.env into a Ruby process and rank secret strength.
|
|
73
|
+
test_files: []
|