secure-keys 1.1.7 → 1.2.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/README.md +360 -148
- data/lib/core/console/arguments/fetchable.rb +38 -0
- data/lib/core/console/arguments/handler.rb +7 -23
- data/lib/core/console/arguments/parser.rb +26 -0
- data/lib/core/environment/ci.rb +15 -2
- data/lib/core/generator.rb +1 -1
- data/lib/services/environment.rb +38 -0
- data/lib/validation/actions/scan.rb +126 -0
- data/lib/validation/console/arguments/parser.rb +65 -0
- data/lib/validation/console/arguments/scan/handler.rb +31 -0
- data/lib/validation/console/arguments/scan/parser.rb +61 -0
- data/lib/validation/globals/globals.rb +71 -0
- data/lib/validation/models/finding.rb +76 -0
- data/lib/validation/models/scan_result.rb +47 -0
- data/lib/validation/scanner.rb +269 -0
- data/lib/validation/utils/entropy.rb +24 -0
- data/lib/validation/utils/min_length.rb +16 -0
- data/lib/validation/utils/patterns.rb +204 -0
- data/lib/validation/utils/weak_secrets.rb +13 -0
- data/lib/validation/validation_issue.rb +55 -0
- data/lib/validation/validation_result.rb +117 -0
- data/lib/validation/validator.rb +203 -0
- data/lib/version.rb +1 -1
- metadata +20 -2
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
|
|
3
|
+
require_relative '../core/console/logger'
|
|
4
|
+
|
|
5
|
+
module SecureKeys
|
|
6
|
+
module Validation
|
|
7
|
+
# Encapsulates the result of validating a single secret value
|
|
8
|
+
class ValidationResult
|
|
9
|
+
private
|
|
10
|
+
|
|
11
|
+
attr_writer :key, :value, :issues, :detected_type
|
|
12
|
+
|
|
13
|
+
public
|
|
14
|
+
|
|
15
|
+
attr_reader :key, :value, :issues, :detected_type
|
|
16
|
+
|
|
17
|
+
# Initialize a new validation result
|
|
18
|
+
# @param key [Symbol] The key identifier that was validated
|
|
19
|
+
# @param value [String] The value that was validated
|
|
20
|
+
# @param issues [Array<ValidationIssue>] The list of issues found during validation
|
|
21
|
+
# @param detected_type [Hash, nil] The detected secret type config, if any pattern matched
|
|
22
|
+
def initialize(key:, value:, issues:, detected_type: nil)
|
|
23
|
+
@key = key
|
|
24
|
+
@value = value
|
|
25
|
+
@issues = issues
|
|
26
|
+
@detected_type = detected_type
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Check if validation passed with no errors or critical issues
|
|
30
|
+
# @return [Boolean] true if no critical or error issues were found
|
|
31
|
+
def valid?
|
|
32
|
+
!errors? && !critical?
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Check if any critical-severity issues were found
|
|
36
|
+
# @return [Boolean] true if critical issues exist
|
|
37
|
+
def critical?
|
|
38
|
+
issues.any? { |issue| issue.severity == :critical }
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Check if any error-severity issues were found
|
|
42
|
+
# @return [Boolean] true if error issues exist
|
|
43
|
+
def errors?
|
|
44
|
+
issues.any? { |issue| issue.severity == :error }
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Check if any warning-severity issues were found
|
|
48
|
+
# @return [Boolean] true if warning issues exist
|
|
49
|
+
def warnings?
|
|
50
|
+
issues.any? { |issue| issue.severity == :warning }
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Returns the highest severity level across all issues
|
|
54
|
+
# @return [Symbol] :critical, :error, :warning, or :ok
|
|
55
|
+
def severity_level
|
|
56
|
+
return :critical if critical?
|
|
57
|
+
return :error if errors?
|
|
58
|
+
return :warning if warnings?
|
|
59
|
+
|
|
60
|
+
:ok
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Returns a one-line summary of the validation outcome
|
|
64
|
+
# @return [String] The summary string
|
|
65
|
+
def summary
|
|
66
|
+
return "✅ '#{key}' passed validation" if valid?
|
|
67
|
+
|
|
68
|
+
"#{severity_icon} '#{key}' has #{issues.length} issue(s)"
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Prints the full validation result to the console via Logger
|
|
72
|
+
def print
|
|
73
|
+
Core::Console::Logger.message(message: "\nValidation Result for '#{key}':")
|
|
74
|
+
Core::Console::Logger.message(message: '-' * 70)
|
|
75
|
+
|
|
76
|
+
if detected_type
|
|
77
|
+
Core::Console::Logger.message(message: "Detected Type: #{detected_type[:description]}")
|
|
78
|
+
Core::Console::Logger.message(message: "Severity: #{detected_type[:severity]}")
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
if issues.empty?
|
|
82
|
+
Core::Console::Logger.success(message: '✅ No issues found')
|
|
83
|
+
else
|
|
84
|
+
Core::Console::Logger.message(message: '')
|
|
85
|
+
issues.each { |issue| Core::Console::Logger.message(message: " #{issue}") }
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
Core::Console::Logger.message(message: '-' * 70)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Returns a hash representation of the validation result
|
|
92
|
+
# @return [Hash] The hash representation
|
|
93
|
+
def to_h
|
|
94
|
+
{
|
|
95
|
+
key:,
|
|
96
|
+
valid: valid?,
|
|
97
|
+
severity: severity_level,
|
|
98
|
+
detected_type:,
|
|
99
|
+
issues: issues.map(&:to_h),
|
|
100
|
+
}
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
private
|
|
104
|
+
|
|
105
|
+
# Returns the appropriate icon for the current severity level
|
|
106
|
+
# @return [String] The severity icon
|
|
107
|
+
def severity_icon
|
|
108
|
+
case severity_level
|
|
109
|
+
when :critical then '🔴'
|
|
110
|
+
when :error then '❌'
|
|
111
|
+
when :warning then '⚠️'
|
|
112
|
+
else '✅'
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
|
|
3
|
+
require_relative 'globals/globals'
|
|
4
|
+
require_relative 'utils/weak_secrets'
|
|
5
|
+
require_relative 'utils/patterns'
|
|
6
|
+
require_relative 'utils/min_length'
|
|
7
|
+
require_relative 'utils/entropy'
|
|
8
|
+
require_relative 'validation_issue'
|
|
9
|
+
require_relative 'validation_result'
|
|
10
|
+
|
|
11
|
+
module SecureKeys
|
|
12
|
+
module Validation
|
|
13
|
+
# Validates individual secret values against known patterns and security rules
|
|
14
|
+
class Validator
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
attr_accessor :issues
|
|
18
|
+
|
|
19
|
+
public
|
|
20
|
+
|
|
21
|
+
# Initialize a new validator
|
|
22
|
+
def initialize
|
|
23
|
+
self.issues = []
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Validate a single secret value against all configured rules
|
|
27
|
+
# @param key [Symbol] The key identifier for the secret
|
|
28
|
+
# @param value [String] The value to validate
|
|
29
|
+
# @param options [Hash] Additional validation options
|
|
30
|
+
# @option options [Boolean] :check_entropy Enable Shannon entropy checking (default: false)
|
|
31
|
+
# @option options [Boolean] :allow_production Skip production key warnings (default: false)
|
|
32
|
+
# @option options [Boolean] :warn_on_pattern Emit informational notices for matched patterns (default: false)
|
|
33
|
+
# @return [ValidationResult] The result of the validation
|
|
34
|
+
def validate(key:, value:, options: {})
|
|
35
|
+
self.issues = []
|
|
36
|
+
|
|
37
|
+
check_empty(key:, value:)
|
|
38
|
+
check_weak_secret(key:, value:)
|
|
39
|
+
check_minimum_length(key:, value:)
|
|
40
|
+
check_pattern_match(key:, value:, options:)
|
|
41
|
+
check_entropy(key:, value:) if options[:check_entropy]
|
|
42
|
+
|
|
43
|
+
ValidationResult.new(key:, value:, issues:, detected_type: detect_type(value:))
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Detect the secret type of a value by matching against known patterns
|
|
47
|
+
# @param value [String] The value to analyze
|
|
48
|
+
# @return [Hash, nil] The matching pattern config merged with :type key, or nil if no match
|
|
49
|
+
def detect_type(value:)
|
|
50
|
+
PATTERNS.each do |type, config|
|
|
51
|
+
return config.merge(type:) if value.to_s.match?(config[:pattern])
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
nil
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Returns security recommendations for a given key name
|
|
58
|
+
# @param key [Symbol] The key identifier
|
|
59
|
+
# @return [Array<String>] List of actionable recommendations
|
|
60
|
+
def recommendations(key:)
|
|
61
|
+
result = []
|
|
62
|
+
formatted_key = key.to_s.downcase
|
|
63
|
+
|
|
64
|
+
if formatted_key.include?('github')
|
|
65
|
+
result << 'Use GitHub Personal Access Tokens with minimal required scopes'
|
|
66
|
+
result << 'Consider fine-grained tokens with repository-specific access'
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
if formatted_key.include?('aws')
|
|
70
|
+
result << 'Use AWS IAM roles instead of long-lived access keys when possible'
|
|
71
|
+
result << 'Enable MFA for all IAM users with access keys'
|
|
72
|
+
result << 'Rotate AWS access keys every 90 days'
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
if formatted_key.include?('stripe')
|
|
76
|
+
result << 'Never commit live Stripe keys to version control'
|
|
77
|
+
result << 'Use Stripe test keys for development and staging'
|
|
78
|
+
result << 'Consider Stripe restricted keys with minimal permissions'
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
if formatted_key.include?('api') || formatted_key.include?('key')
|
|
82
|
+
result << 'Rotate this key regularly (every 90 days recommended)'
|
|
83
|
+
result << 'Use environment-specific keys for dev, staging, and production'
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
result
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
private
|
|
90
|
+
|
|
91
|
+
# Check if the value is empty
|
|
92
|
+
# @param key [Symbol] The key identifier
|
|
93
|
+
# @param value [String] The value to check
|
|
94
|
+
def check_empty(key:, value:)
|
|
95
|
+
return unless value.to_s.empty?
|
|
96
|
+
|
|
97
|
+
issues << ValidationIssue.new(
|
|
98
|
+
severity: :error,
|
|
99
|
+
type: :empty_value,
|
|
100
|
+
message: "Key '#{key}' has an empty value",
|
|
101
|
+
recommendation: 'Provide a non-empty secret value'
|
|
102
|
+
)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Check if the value is a known weak or placeholder secret
|
|
106
|
+
# @param key [Symbol] The key identifier
|
|
107
|
+
# @param value [String] The value to check
|
|
108
|
+
def check_weak_secret(key:, value:)
|
|
109
|
+
return unless value
|
|
110
|
+
|
|
111
|
+
formatted_value = value.downcase
|
|
112
|
+
|
|
113
|
+
WEAK_SECRETS.each do |weak|
|
|
114
|
+
next unless formatted_value == weak || formatted_value.include?(weak)
|
|
115
|
+
|
|
116
|
+
issues << ValidationIssue.new(
|
|
117
|
+
severity: :critical,
|
|
118
|
+
type: :weak_secret,
|
|
119
|
+
message: "Key '#{key}' uses a weak or placeholder value matching '#{weak}'",
|
|
120
|
+
recommendation: 'Replace with a strong, randomly generated secret'
|
|
121
|
+
)
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Check if the value meets the minimum length requirement for its inferred key type
|
|
126
|
+
# @param key [Symbol] The key identifier
|
|
127
|
+
# @param value [String] The value to check
|
|
128
|
+
def check_minimum_length(key:, value:)
|
|
129
|
+
return unless value
|
|
130
|
+
|
|
131
|
+
key_type = determine_key_type(key:)
|
|
132
|
+
min_length = MIN_LENGTHS[key_type] || MIN_LENGTHS[:key]
|
|
133
|
+
|
|
134
|
+
return unless value.length < min_length
|
|
135
|
+
|
|
136
|
+
issues << ValidationIssue.new(
|
|
137
|
+
severity: :warning,
|
|
138
|
+
type: :too_short,
|
|
139
|
+
message: "Key '#{key}' is too short (#{value.length} chars, minimum is #{min_length})",
|
|
140
|
+
recommendation: "Use a longer secret (recommended: #{min_length * 2}+ characters)"
|
|
141
|
+
)
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# Check if the value matches a known production secret pattern
|
|
145
|
+
# @param key [Symbol] The key identifier
|
|
146
|
+
# @param value [String] The value to check
|
|
147
|
+
# @param options [Hash] Validation options controlling severity behaviour
|
|
148
|
+
def check_pattern_match(key:, value:, options:)
|
|
149
|
+
return unless value
|
|
150
|
+
|
|
151
|
+
detected = detect_type(value:)
|
|
152
|
+
return unless detected
|
|
153
|
+
|
|
154
|
+
if detected[:severity] == :critical && !options[:allow_production]
|
|
155
|
+
issues << ValidationIssue.new(
|
|
156
|
+
severity: :critical,
|
|
157
|
+
type: :production_key_detected,
|
|
158
|
+
message: "Key '#{key}' appears to be a live #{detected[:description]}",
|
|
159
|
+
recommendation: 'Use test or development keys locally. Store production keys in your CI/CD secrets manager.'
|
|
160
|
+
)
|
|
161
|
+
elsif options[:warn_on_pattern]
|
|
162
|
+
issues << ValidationIssue.new(
|
|
163
|
+
severity: :info,
|
|
164
|
+
type: :pattern_detected,
|
|
165
|
+
message: "Key '#{key}' matches the pattern for: #{detected[:description]}",
|
|
166
|
+
recommendation: nil
|
|
167
|
+
)
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# Check if the value has sufficient Shannon entropy to be a strong secret
|
|
172
|
+
# @param key [Symbol] The key identifier
|
|
173
|
+
# @param value [String] The value to check
|
|
174
|
+
def check_entropy(key:, value:)
|
|
175
|
+
return unless value
|
|
176
|
+
|
|
177
|
+
entropy = Entropy.calculate(string: value)
|
|
178
|
+
return unless entropy < Globals.min_entropy_threshold
|
|
179
|
+
|
|
180
|
+
issues << ValidationIssue.new(
|
|
181
|
+
severity: :warning,
|
|
182
|
+
type: :low_entropy,
|
|
183
|
+
message: "Key '#{key}' has low entropy (#{entropy.round(2)})",
|
|
184
|
+
recommendation: 'Use a more random secret with a wider variety of characters'
|
|
185
|
+
)
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# Infer the semantic key type from the key name for minimum-length lookup
|
|
189
|
+
# @param key [Symbol] The key identifier
|
|
190
|
+
# @return [Symbol] One of :api_key, :token, :secret, :password, or :key
|
|
191
|
+
def determine_key_type(key:)
|
|
192
|
+
formatted_key = key.to_s.downcase
|
|
193
|
+
|
|
194
|
+
return :api_key if formatted_key.include?('api') && formatted_key.include?('key')
|
|
195
|
+
return :token if formatted_key.include?('token')
|
|
196
|
+
return :secret if formatted_key.include?('secret')
|
|
197
|
+
return :password if formatted_key.include?('password') || formatted_key.include?('pwd')
|
|
198
|
+
|
|
199
|
+
:key
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
end
|
data/lib/version.rb
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env ruby
|
|
2
2
|
|
|
3
3
|
module SecureKeys
|
|
4
|
-
VERSION = '1.
|
|
4
|
+
VERSION = '1.2.0'.freeze
|
|
5
5
|
SUMMARY = 'Secure Keys is a simple tool for managing your secret keys'.freeze
|
|
6
6
|
DESCRIPTION = 'Secure Keys is a simple tool to manage your secret keys in your iOS project'.freeze
|
|
7
7
|
HOMEPAGE_URI = 'https://github.com/derian-cordoba/secure-keys'.freeze
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: secure-keys
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Derian Córdoba
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date:
|
|
11
|
+
date: 2026-06-21 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: base64
|
|
@@ -172,6 +172,7 @@ executables:
|
|
|
172
172
|
extensions: []
|
|
173
173
|
extra_rdoc_files: []
|
|
174
174
|
files:
|
|
175
|
+
- "./lib/core/console/arguments/fetchable.rb"
|
|
175
176
|
- "./lib/core/console/arguments/handler.rb"
|
|
176
177
|
- "./lib/core/console/arguments/parser.rb"
|
|
177
178
|
- "./lib/core/console/arguments/xcframework/handler.rb"
|
|
@@ -192,6 +193,22 @@ files:
|
|
|
192
193
|
- "./lib/core/utils/swift/xcframework.rb"
|
|
193
194
|
- "./lib/core/utils/swift/xcodeproj.rb"
|
|
194
195
|
- "./lib/keys.rb"
|
|
196
|
+
- "./lib/services/environment.rb"
|
|
197
|
+
- "./lib/validation/actions/scan.rb"
|
|
198
|
+
- "./lib/validation/console/arguments/parser.rb"
|
|
199
|
+
- "./lib/validation/console/arguments/scan/handler.rb"
|
|
200
|
+
- "./lib/validation/console/arguments/scan/parser.rb"
|
|
201
|
+
- "./lib/validation/globals/globals.rb"
|
|
202
|
+
- "./lib/validation/models/finding.rb"
|
|
203
|
+
- "./lib/validation/models/scan_result.rb"
|
|
204
|
+
- "./lib/validation/scanner.rb"
|
|
205
|
+
- "./lib/validation/utils/entropy.rb"
|
|
206
|
+
- "./lib/validation/utils/min_length.rb"
|
|
207
|
+
- "./lib/validation/utils/patterns.rb"
|
|
208
|
+
- "./lib/validation/utils/weak_secrets.rb"
|
|
209
|
+
- "./lib/validation/validation_issue.rb"
|
|
210
|
+
- "./lib/validation/validation_result.rb"
|
|
211
|
+
- "./lib/validation/validator.rb"
|
|
195
212
|
- "./lib/version.rb"
|
|
196
213
|
- README.md
|
|
197
214
|
- bin/secure-keys
|
|
@@ -204,6 +221,7 @@ metadata:
|
|
|
204
221
|
homepage_uri: https://github.com/derian-cordoba/secure-keys
|
|
205
222
|
source_code_uri: https://github.com/derian-cordoba/secure-keys
|
|
206
223
|
changelog_uri: https://github.com/derian-cordoba/secure-keys/releases
|
|
224
|
+
allowed_push_host: https://rubygems.org
|
|
207
225
|
rubygems_mfa_required: 'true'
|
|
208
226
|
post_install_message:
|
|
209
227
|
rdoc_options: []
|