secure-keys 1.1.6 → 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,204 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # rubocop:disable Lint/Syntax, Metrics/ModuleLength
4
+
5
+ module SecureKeys
6
+ module Validation
7
+ # Known secret patterns with descriptions
8
+ PATTERNS = {
9
+ # GitHub
10
+ github_token: {
11
+ pattern: /ghp_[a-zA-Z0-9]{36}/,
12
+ description: 'GitHub Personal Access Token',
13
+ severity: :high,
14
+ example: 'ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
15
+ },
16
+ github_oauth: {
17
+ pattern: /gho_[a-zA-Z0-9]{36}/,
18
+ description: 'GitHub OAuth Access Token',
19
+ severity: :high,
20
+ example: 'gho_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
21
+ },
22
+ github_app: {
23
+ pattern: /(ghu|ghs)_[a-zA-Z0-9]{36}/,
24
+ description: 'GitHub App Token',
25
+ severity: :high,
26
+ example: 'ghu_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
27
+ },
28
+ github_refresh: {
29
+ pattern: /ghr_[a-zA-Z0-9]{36}/,
30
+ description: 'GitHub Refresh Token',
31
+ severity: :high,
32
+ example: 'ghr_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
33
+ },
34
+
35
+ # AWS
36
+ aws_access_key: {
37
+ pattern: /AKIA[0-9A-Z]{16}/,
38
+ description: 'AWS Access Key ID',
39
+ severity: :critical,
40
+ example: 'AKIAIOSFODNN7EXAMPLE'
41
+ },
42
+ aws_secret_key: {
43
+ pattern: %r{(?i)aws(.{0,20})?['\"][0-9a-zA-Z/+]{40}['\"]},
44
+ description: 'AWS Secret Access Key',
45
+ severity: :critical,
46
+ example: 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY'
47
+ },
48
+ aws_session_token: {
49
+ pattern: %r{(?i)aws(.{0,20})?session(.{0,20})?['\"][0-9a-zA-Z/+]{100,}['\"]},
50
+ description: 'AWS Session Token',
51
+ severity: :high,
52
+ example: 'FwoGZXIvYXdzEBaa...(very long token)'
53
+ },
54
+
55
+ # Google Cloud
56
+ gcp_api_key: {
57
+ pattern: /AIza[0-9A-Za-z\-_]{35}/,
58
+ description: 'Google Cloud API Key',
59
+ severity: :high,
60
+ example: 'AIzaSyDaGmWKa4JsXZ-HjGw7ISLn_3namBGewQe'
61
+ },
62
+ gcp_oauth: {
63
+ pattern: /ya29\.[0-9A-Za-z\-_]+/,
64
+ description: 'Google OAuth Access Token',
65
+ severity: :high,
66
+ example: 'ya29.a0AfH6SMBx...'
67
+ },
68
+
69
+ # Stripe
70
+ stripe_secret_key: {
71
+ pattern: /sk_(live|test)_[0-9a-zA-Z]{24,}/,
72
+ description: 'Stripe Secret Key',
73
+ severity: :critical,
74
+ example: 'sk_live_51H...'
75
+ },
76
+ stripe_publishable_key: {
77
+ pattern: /pk_(live|test)_[0-9a-zA-Z]{24,}/,
78
+ description: 'Stripe Publishable Key',
79
+ severity: :medium,
80
+ example: 'pk_live_51H...'
81
+ },
82
+ stripe_restricted_key: {
83
+ pattern: /rk_(live|test)_[0-9a-zA-Z]{24,}/,
84
+ description: 'Stripe Restricted Key',
85
+ severity: :high,
86
+ example: 'rk_live_51H...'
87
+ },
88
+
89
+ # Slack
90
+ slack_token: {
91
+ pattern: /xox[baprs]-[0-9a-zA-Z]{10,48}/,
92
+ description: 'Slack Token',
93
+ severity: :high,
94
+ example: 'xoxb-1234567890123-1234567890123-xxxxxxxxxxxxx'
95
+ },
96
+ slack_webhook: {
97
+ pattern: %r{https://hooks\.slack\.com/services/T[a-zA-Z0-9_]{8,}/B[a-zA-Z0-9_]{8,}/[a-zA-Z0-9_]{24}},
98
+ description: 'Slack Webhook URL',
99
+ severity: :medium,
100
+ example: 'https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX'
101
+ },
102
+
103
+ # OAuth & JWT
104
+ jwt_token: {
105
+ pattern: %r{eyJ[A-Za-z0-9\-_=]+\.eyJ[A-Za-z0-9\-_=]+\.?[A-Za-z0-9\-_.+/=]*},
106
+ description: 'JWT Token',
107
+ severity: :medium,
108
+ example: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.dozjgNryP4J3jVmNHl0w5N_XgL0n3I9PlFUP0THsR8U'
109
+ },
110
+
111
+ # Generic patterns
112
+ private_key: {
113
+ pattern: /-----BEGIN (RSA |EC |OPENSSH |DSA )?PRIVATE KEY-----/,
114
+ description: 'Private Key',
115
+ severity: :critical,
116
+ example: '-----BEGIN PRIVATE KEY-----'
117
+ },
118
+ generic_api_key: {
119
+ pattern: /(?i)(api[_-]?key|apikey)[\s]*[:=][\s]*['\"]([a-zA-Z0-9_\-]{20,})['\"]/,
120
+ description: 'Generic API Key',
121
+ severity: :medium,
122
+ example: 'api_key: "abcdef1234567890abcdef1234567890"'
123
+ },
124
+ generic_secret: {
125
+ pattern: /(?i)(secret|password|passwd|pwd)[\s]*[:=][\s]*['\"]([^\s'\"]{8,})['\"]/,
126
+ description: 'Generic Secret/Password',
127
+ severity: :medium,
128
+ example: 'secret: "mysecretpassword123"'
129
+ },
130
+
131
+ # Firebase
132
+ firebase_key: {
133
+ pattern: /AIza[0-9A-Za-z\-_]{35}/,
134
+ description: 'Firebase API Key',
135
+ severity: :medium,
136
+ example: 'AIzaSyDaGmWKa4JsXZ-HjGw7ISLn_3namBGewQe'
137
+ },
138
+
139
+ # Twilio
140
+ twilio_api_key: {
141
+ pattern: /SK[a-z0-9]{32}/,
142
+ description: 'Twilio API Key',
143
+ severity: :high,
144
+ example: 'SKxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
145
+ },
146
+ twilio_account_sid: {
147
+ pattern: /AC[a-z0-9]{32}/,
148
+ description: 'Twilio Account SID',
149
+ severity: :low,
150
+ example: 'ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
151
+ },
152
+
153
+ # SendGrid
154
+ sendgrid_api_key: {
155
+ pattern: /SG\.[a-zA-Z0-9_\-]{22}\.[a-zA-Z0-9_\-]{43}/,
156
+ description: 'SendGrid API Key',
157
+ severity: :high,
158
+ example: 'SG.xxxxxxxxxxxxxxxxxxxxxx.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
159
+ },
160
+
161
+ # Mailchimp
162
+ mailchimp_api_key: {
163
+ pattern: /[a-f0-9]{32}-us[0-9]{1,2}/,
164
+ description: 'Mailchimp API Key',
165
+ severity: :medium,
166
+ example: 'abcdef1234567890abcdef1234567890-us19'
167
+ },
168
+
169
+ # Square
170
+ square_access_token: {
171
+ pattern: /sq0atp-[0-9A-Za-z\-_]{22}/,
172
+ description: 'Square Access Token',
173
+ severity: :high,
174
+ example: 'sq0atp-xxxxxxxxxxxxxxxxxxxxxx'
175
+ },
176
+
177
+ # PayPal
178
+ paypal_braintree: {
179
+ pattern: /access_token\$production\$[a-z0-9]{16}\$[a-f0-9]{32}/,
180
+ description: 'PayPal Braintree Access Token',
181
+ severity: :critical,
182
+ example: 'access_token$production$xxxxxxxxxxxxxxxx$xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
183
+ },
184
+
185
+ # Heroku
186
+ heroku_api_key: {
187
+ pattern: /[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}/,
188
+ description: 'Heroku API Key (UUID format)',
189
+ severity: :high,
190
+ example: '12345678-1234-1234-1234-123456789012'
191
+ },
192
+
193
+ # Generic Base64 secrets (often used for keys)
194
+ base64_secret: {
195
+ pattern: %r{(?i)(secret|key|token|password)[\s]*[:=][\s]*['\"]([A-Za-z0-9+/]{40,}={0,2})['\"]},
196
+ description: 'Base64 Encoded Secret',
197
+ severity: :low,
198
+ example: 'secret: "YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXoxMjM0NTY3ODkw"'
199
+ }
200
+ }.freeze
201
+ end
202
+ end
203
+
204
+ # rubocop:enable Lint/Syntax, Metrics/ModuleLength
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ module SecureKeys
4
+ module Validation
5
+ # Common weak or test values that should never be used
6
+ WEAK_SECRETS = %w[
7
+ password password123 123456 secret test demo
8
+ admin root changeme temp default example
9
+ sample placeholder your-key-here your_api_key
10
+ xxxxxxxxxxxx 0000000000 1234567890
11
+ ].freeze
12
+ end
13
+ end
@@ -0,0 +1,55 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ module SecureKeys
4
+ module Validation
5
+ # Represents a single issue detected during secret validation
6
+ class ValidationIssue
7
+ attr_reader :severity, :type, :message, :recommendation
8
+
9
+ # Initialize a new validation issue
10
+ # @param severity [Symbol] The severity level (:critical, :error, :warning, :info)
11
+ # @param type [Symbol] The category of the issue (e.g. :empty_value, :weak_secret)
12
+ # @param message [String] A human-readable description of the issue
13
+ # @param recommendation [String, nil] An optional actionable recommendation for the developer
14
+ def initialize(severity:, type:, message:, recommendation:)
15
+ @severity = severity
16
+ @type = type
17
+ @message = message
18
+ @recommendation = recommendation
19
+ end
20
+
21
+ # Returns a string representation of the issue, including the recommendation if present
22
+ # @return [String] The formatted issue string
23
+ def to_s
24
+ text = "#{severity_icon} #{severity.upcase}: #{message}"
25
+ text += "\n\t\t💡 #{recommendation}" if recommendation
26
+ text
27
+ end
28
+
29
+ # Returns a hash representation of the issue
30
+ # @return [Hash] The hash representation
31
+ def to_h
32
+ {
33
+ severity:,
34
+ type:,
35
+ message:,
36
+ recommendation:,
37
+ }
38
+ end
39
+
40
+ private
41
+
42
+ # Returns the appropriate icon for the severity level
43
+ # @return [String] The severity icon
44
+ def severity_icon
45
+ case severity
46
+ when :critical then '🔴'
47
+ when :error then '❌'
48
+ when :warning then '⚠️'
49
+ when :info then 'ℹ️'
50
+ else '•'
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -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,8 +1,9 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
3
  module SecureKeys
4
- VERSION = '1.1.6'.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
8
+ GEM_HOMEPAGE_URI = 'https://rubygems.pkg.github.com'.freeze
8
9
  end