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.
@@ -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.1.7'.freeze
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.1.7
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: 2025-05-11 00:00:00.000000000 Z
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: []