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,38 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ module SecureKeys
4
+ module Core
5
+ module Console
6
+ module Argument
7
+ # Shared fetch/set behaviour for argument handler classes.
8
+ # Include this module inside +class << self+ so the methods become
9
+ # class-level accessors that check the handler's own +arguments+ hash
10
+ # before falling back to environment variables.
11
+ #
12
+ # The including class must expose +arguments+ as a class-level reader
13
+ # (via +attr_reader :arguments+ inside +class << self+).
14
+ module Fetchable
15
+ # Fetch an argument value, falling back to SECURE_KEYS_<KEY>,
16
+ # then the bare <KEY> environment variable, then +default+.
17
+ #
18
+ # @param key [Symbol, Array<Symbol>] The argument key (or nested key path)
19
+ # @param default [Object] The value to return when nothing is found
20
+ # @return [Object] The resolved value
21
+ def fetch(key:, default: nil)
22
+ keys = Array(key).map(&:to_sym)
23
+ joined_keys = keys.join('_').upcase
24
+ arguments.dig(*keys) || ENV["SECURE_KEYS_#{joined_keys}"] || ENV[joined_keys] || default
25
+ end
26
+
27
+ # Update a single argument value
28
+ # @param key [Symbol] The argument key to update
29
+ # @param value [Object] The new value
30
+ # @return [void]
31
+ def set(key:, value:)
32
+ arguments[key.to_sym] = value
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -1,9 +1,13 @@
1
+ require_relative 'fetchable'
2
+
1
3
  module SecureKeys
2
4
  module Core
3
5
  module Console
4
6
  module Argument
5
7
  class Handler
6
8
  class << self
9
+ include Fetchable
10
+
7
11
  attr_reader :arguments
8
12
  end
9
13
 
@@ -16,31 +20,11 @@ module SecureKeys
16
20
  verbose: false,
17
21
  }
18
22
 
19
- # Fetch the argument value by key
20
- # from CLI arguments or environment variables
21
- #
22
- # @param key [Array|Symbol] the argument key
23
- # @param default [String] the default value
24
- #
25
- # @return [String] the argument value
26
- def self.fetch(key:, default: nil)
27
- keys = Array(key).map(&:to_sym)
28
- joined_keys = keys.join('_').upcase
29
- @arguments.dig(*keys) || ENV["SECURE_KEYS_#{joined_keys}"] || ENV[joined_keys] || default
30
- end
31
-
32
- # Set the value of the key
33
- # @param key [Symbol] the key to be updated
34
- # @param value [String] the value to be updated
35
- def self.set(key:, value:)
36
- @arguments[key.to_sym] = value
37
- end
38
-
39
- # Append the argument value by key
23
+ # Append a hash value into a nested key, initialising it when absent
40
24
  # @param key [Symbol] the argument key
41
- # @param value [String] the argument value
25
+ # @param value [Hash] the hash to merge in
42
26
  def self.deep_merge(key:, value:)
43
- @arguments[key.to_sym] ||= {} # Initialize the key if it doesn't exist
27
+ @arguments[key.to_sym] ||= {}
44
28
  @arguments[key.to_sym].merge!(value)
45
29
  end
46
30
  end
@@ -2,8 +2,8 @@
2
2
 
3
3
  require 'optparse'
4
4
  require_relative '../../globals/globals'
5
- require_relative './handler'
6
- require_relative './xcframework/parser'
5
+ require_relative 'handler'
6
+ require_relative 'xcframework/parser'
7
7
 
8
8
  module SecureKeys
9
9
  module Core
@@ -15,6 +15,11 @@ module SecureKeys
15
15
  super('Usage: secure-keys [--options]')
16
16
  separator('')
17
17
 
18
+ # Route known subcommands before processing flags.
19
+ # Like --help and --version, subcommand handlers exit internally,
20
+ # so the generator is never reached.
21
+ route_subcommand!
22
+
18
23
  # Configure the argument parser
19
24
  configure!
20
25
  order!(into: Handler.arguments)
@@ -23,6 +28,27 @@ module SecureKeys
23
28
 
24
29
  private
25
30
 
31
+ # Known positional subcommands mapped to their handler lambdas.
32
+ # Each lambda is expected to handle its own output and exit.
33
+ SUBCOMMANDS = {
34
+ 'validate' => lambda {
35
+ require_relative '../../../validation/console/arguments/parser'
36
+ Validation::Console::Argument::Parser.new.execute
37
+ },
38
+ }.freeze
39
+
40
+ # Detect a known positional subcommand as the first ARGV token and delegate
41
+ # to its handler. Like --help and --version, the handler exits internally so
42
+ # the generator is never reached.
43
+ # @return [void]
44
+ def route_subcommand!
45
+ token = ARGV.first
46
+ return unless SUBCOMMANDS.key?(token)
47
+
48
+ ARGV.shift
49
+ SUBCOMMANDS[token].call
50
+ end
51
+
26
52
  # Configure the argument parser
27
53
  def configure!
28
54
  on('-h', '--help', 'Use the provided commands to select the params') do
@@ -2,7 +2,7 @@
2
2
 
3
3
  require 'optparse'
4
4
  require_relative '../../../globals/globals'
5
- require_relative './handler'
5
+ require_relative 'handler'
6
6
 
7
7
  module SecureKeys
8
8
  module Core
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
3
  require 'open3'
4
- require_relative './logger'
4
+ require_relative 'logger'
5
5
  require_relative '../globals/globals'
6
6
 
7
7
  module SecureKeys
@@ -49,8 +49,6 @@ module SecureKeys
49
49
  return yield(exit_status || $CHILD_STATUS, output, command) if block_given?
50
50
 
51
51
  [output, exit_status || $CHILD_STATUS, command]
52
- rescue StandardError => e
53
- raise e
54
52
  ensure
55
53
  Encoding.default_external = previous_encoding.first
56
54
  Encoding.default_internal = previous_encoding.last
@@ -1,5 +1,7 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
+ require_relative '../console/logger'
4
+
3
5
  module SecureKeys
4
6
  module Core
5
7
  module Environment
@@ -8,9 +10,20 @@ module SecureKeys
8
10
  # @param key [String] the key of the environment variable to fetch
9
11
  # @return [String] the value of the environment variable
10
12
  def fetch(key:)
11
- ENV[key]
13
+ normalized_key = key.to_s.tr('-', '_').upcase
14
+
15
+ ENV[key.to_s] ||
16
+ ENV[normalized_key] ||
17
+ ENV["SECURE_KEYS_#{normalized_key}"] ||
18
+ inline_identifier_value(key)
12
19
  rescue StandardError
13
- puts "Error fetching the key: #{key} from ENV variables"
20
+ Core::Console::Logger.error(message: "Error fetching the key '#{key}' from ENV variables")
21
+ end
22
+
23
+ private
24
+
25
+ def inline_identifier_value(key)
26
+ key if key.to_s.include?(SecureKeys::Globals.key_delimiter)
14
27
  end
15
28
  end
16
29
  end
@@ -1,13 +1,13 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
- require_relative './globals/globals'
4
- require_relative './environment/ci'
5
- require_relative './environment/keychain'
6
- require_relative './utils/swift/writer'
7
- require_relative './utils/swift/package'
8
- require_relative './utils/swift/swift'
9
- require_relative './utils/swift/xcframework'
10
- require_relative './utils/openssl/cipher'
3
+ require_relative 'globals/globals'
4
+ require_relative 'environment/ci'
5
+ require_relative 'environment/keychain'
6
+ require_relative 'utils/swift/writer'
7
+ require_relative 'utils/swift/package'
8
+ require_relative 'utils/swift/swift'
9
+ require_relative 'utils/swift/xcframework'
10
+ require_relative 'utils/openssl/cipher'
11
11
 
12
12
  module SecureKeys
13
13
  module Core
@@ -45,10 +45,10 @@ module SecureKeys
45
45
  generate_swift_package
46
46
  write_keys
47
47
  xcframework.generate
48
+ post_actions
48
49
  end
49
50
 
50
51
  xcframework.configure_xcframework_to_xcodeproj
51
- post_actions if Globals.generate_xcframework?
52
52
  end
53
53
 
54
54
  private
@@ -1,5 +1,5 @@
1
- require_relative './string/to_bool'
2
- require_relative './string/camelize'
1
+ require_relative 'string/to_bool'
2
+ require_relative 'string/camelize'
3
3
 
4
4
  module Kernel
5
5
  include BooleanCasting
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
- require_relative './swift'
3
+ require_relative 'swift'
4
4
  require_relative '../../console/shell'
5
5
 
6
6
  module SecureKeys
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env ruby
2
2
  # rubocop:disable Layout/HeredocIndentation
3
3
 
4
- require_relative './swift'
4
+ require_relative 'swift'
5
5
 
6
6
  module SecureKeys
7
7
  module Swift
@@ -25,9 +25,7 @@ module SecureKeys
25
25
  # Write the keys to the file
26
26
  def write
27
27
  # Write the file
28
- File.open("#{key_directory}/#{key_file}", 'w') do |file|
29
- file.write(key_swift_file_template(content: formatted_keys))
30
- end
28
+ File.write("#{key_directory}/#{key_file}", key_swift_file_template(content: formatted_keys))
31
29
  end
32
30
 
33
31
  # Generate the formatted keys content using Swift code format
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
- require_relative './swift'
3
+ require_relative 'swift'
4
4
  require_relative '../../globals/globals'
5
5
  require_relative '../../console/shell'
6
6
  require_relative '../../console/logger'
data/lib/keys.rb CHANGED
@@ -1,11 +1,11 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
- require_relative './core/utils/extensions/kernel'
4
- require_relative './core/generator'
5
- require_relative './core/globals/globals'
6
- require_relative './core/console/logger'
7
- require_relative './core/console/arguments/parser'
8
- require_relative './core/utils/swift/xcframework'
3
+ require_relative 'core/utils/extensions/kernel'
4
+ require_relative 'core/generator'
5
+ require_relative 'core/globals/globals'
6
+ require_relative 'core/console/logger'
7
+ require_relative 'core/console/arguments/parser'
8
+ require_relative 'core/utils/swift/xcframework'
9
9
 
10
10
  module SecureKeys
11
11
  def self.run
@@ -0,0 +1,38 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ module SecureKeys
4
+ module Services
5
+ module Environment
6
+ module_function
7
+
8
+ # Fetches the value of an environment variable with support for SecureKeys prefix
9
+ # @param key [Symbol] The environment variable key to fetch
10
+ # @param default [Object] The default value to return if the environment variable is not set
11
+ # @return [Object, nil] The value of the environment variable or the default value
12
+ def fetch(key:, default: nil)
13
+ formatted_key = key.to_s.upcase
14
+ ENV[formatted_key] || ENV["SECURE_KEYS_#{formatted_key}"] || default
15
+ end
16
+
17
+ # Fetches the integer value of an environment variable with support for SecureKeys prefix
18
+ # @param key [Symbol] The environment variable key to fetch
19
+ # @param default [Object] The default value to return if the environment variable is not set or cannot be converted to an integer
20
+ # @return [Integer, nil] The integer value of the environment variable or the default value
21
+ def integer(key:, default: nil)
22
+ value = fetch(key:, default:)
23
+ Integer(value)
24
+ rescue ArgumentError, TypeError
25
+ # Returns default if it's nil or integer, otherwise, force return nil
26
+ default.is_a?(Integer) || default.nil? ? default : nil
27
+ end
28
+
29
+ def decimal(key:, default: nil)
30
+ value = fetch(key:, default:)
31
+ Float(value)
32
+ rescue ArgumentError, TypeError
33
+ # Returns default if it's nil or float, otherwise, force return nil
34
+ default.is_a?(Float) || default.nil? ? default : nil
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,126 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'json'
4
+ require_relative '../console/arguments/scan/handler'
5
+ require_relative '../../core/console/logger'
6
+ require_relative '../scanner'
7
+
8
+ module SecureKeys
9
+ module Validation
10
+ module Actions
11
+ # Executes the `validate scan` action: runs the scanner, prints a formatted
12
+ # report to the console, optionally saves a JSON report, and exits with an
13
+ # appropriate code (0 = clean, 1 = findings present).
14
+ class Scan
15
+ # Run the scan, print the report, and exit
16
+ # @return [void]
17
+ def run
18
+ result = execute_scan
19
+ print_result(result:)
20
+ save_report(result:) if Console::Argument::Scan::Handler.fetch(key: :output)
21
+ exit(result.clean? ? 0 : 1)
22
+ end
23
+
24
+ private
25
+
26
+ # Build optional scanner overrides from CLI arguments via Handler.fetch
27
+ # @return [Hash] A sparse options hash (only keys explicitly provided by the user)
28
+ def scanner_options
29
+ options = {}
30
+
31
+ if (extensions = Console::Argument::Scan::Handler.fetch(key: :extensions))
32
+ options[:extensions] = extensions.split(',').map(&:strip)
33
+ end
34
+
35
+ if (excludes = Console::Argument::Scan::Handler.fetch(key: :excludes))
36
+ options[:excludes] = excludes.split(',').map(&:strip)
37
+ end
38
+
39
+ options
40
+ end
41
+
42
+ # Run the appropriate scan based on the --staged flag
43
+ # @return [ScanResult] The result of the scan
44
+ def execute_scan
45
+ scanner = Scanner.new(options: scanner_options)
46
+
47
+ if Console::Argument::Scan::Handler.fetch(key: :staged, default: false)
48
+ Core::Console::Logger.important(message: 'Scanning staged git changes...')
49
+ scanner.scan_git_diff(staged_only: true)
50
+ else
51
+ path = Console::Argument::Scan::Handler.fetch(key: :path, default: '.')
52
+ Core::Console::Logger.important(message: "Scanning directory: #{path}")
53
+ scanner.scan_directory(path:)
54
+ end
55
+ end
56
+
57
+ # Print a formatted scan report to the console
58
+ # @param result [ScanResult] The scan result to display
59
+ # @return [void]
60
+ def print_result(result:)
61
+ separator = '-' * 70
62
+
63
+ Core::Console::Logger.message(message: separator)
64
+ Core::Console::Logger.message(message: "\tFiles scanned: #{result.files_count}")
65
+ Core::Console::Logger.message(message: "\tFindings: #{result.findings.length}")
66
+ Core::Console::Logger.message(message: separator)
67
+
68
+ if result.clean?
69
+ Core::Console::Logger.success(message: "\t✅ No secrets found")
70
+ else
71
+ print_severity_summary(result:)
72
+ Core::Console::Logger.message(message: separator)
73
+ print_findings(result:)
74
+ end
75
+
76
+ Core::Console::Logger.message(message: separator)
77
+ end
78
+
79
+ # Print a per-severity count breakdown
80
+ # @param result [ScanResult] The scan result
81
+ # @return [void]
82
+ def print_severity_summary(result:)
83
+ counts = {
84
+ critical: result.by_severity(severity: :critical).length,
85
+ high: result.by_severity(severity: :high).length,
86
+ medium: result.by_severity(severity: :medium).length,
87
+ low: result.by_severity(severity: :low).length,
88
+ }
89
+
90
+ Core::Console::Logger.error(message: "\t🔴 Critical: #{counts[:critical]}") if counts[:critical].positive?
91
+ Core::Console::Logger.warning(message: "\t🟠 High: #{counts[:high]}") if counts[:high].positive?
92
+ Core::Console::Logger.warning(message: "\t🟡 Medium: #{counts[:medium]}") if counts[:medium].positive?
93
+ Core::Console::Logger.message(message: "\t🔵 Low: #{counts[:low]}") if counts[:low].positive?
94
+ end
95
+
96
+ # Print each finding grouped by severity level
97
+ # @param result [ScanResult] The scan result
98
+ # @return [void]
99
+ def print_findings(result:)
100
+ %i[critical high medium low].each do |severity|
101
+ findings = result.by_severity(severity:)
102
+ next if findings.empty?
103
+
104
+ Core::Console::Logger.message(message: "\t#{severity.to_s.upcase} (#{findings.length}):")
105
+
106
+ findings.each do |finding|
107
+ Core::Console::Logger.message(message: "\t\t#{finding}")
108
+ Core::Console::Logger.message(message: "\t\t└ #{finding.full_line}")
109
+ end
110
+
111
+ Core::Console::Logger.message(message: '')
112
+ end
113
+ end
114
+
115
+ # Save the scan result as a pretty-printed JSON report
116
+ # @param result [ScanResult] The scan result to serialise
117
+ # @return [void]
118
+ def save_report(result:)
119
+ output = Console::Argument::Scan::Handler.fetch(key: :output)
120
+ File.write(output, JSON.pretty_generate(result.to_h))
121
+ Core::Console::Logger.success(message: "Report saved to: #{output}")
122
+ end
123
+ end
124
+ end
125
+ end
126
+ end
@@ -0,0 +1,65 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'optparse'
4
+ require_relative '../../../core/console/logger'
5
+ require_relative 'scan/handler'
6
+ require_relative 'scan/parser'
7
+ require_relative '../../actions/scan'
8
+
9
+ module SecureKeys
10
+ module Validation
11
+ module Console
12
+ module Argument
13
+ # Routes the `validate` subcommand to the appropriate sub-parser and action.
14
+ # The constructor reads the subcommand token from ARGV; +execute+ then
15
+ # delegates to the matching sub-parser and runs the action.
16
+ class Parser < OptionParser
17
+ # Initialize the validate argument parser and capture the subcommand token
18
+ def initialize
19
+ super('Usage: secure-keys validate [subcommand] [--options]')
20
+ separator('')
21
+ configure!
22
+ @subcommand = ARGV.shift
23
+ end
24
+
25
+ # Dispatch to the correct sub-parser and action based on the subcommand
26
+ # @return [void]
27
+ def execute
28
+ case @subcommand
29
+ when 'scan'
30
+ Scan::Parser.new
31
+ Actions::Scan.new.run
32
+ when nil, '--help', '-h'
33
+ puts self
34
+ exit(0)
35
+ else
36
+ Core::Console::Logger.error(message: "Unknown validate subcommand: '#{@subcommand}'")
37
+ puts self
38
+ exit(1)
39
+ end
40
+ end
41
+
42
+ private
43
+
44
+ # Configure the validate-level help text and available subcommands
45
+ # @return [void]
46
+ def configure!
47
+ on('-h', '--help', 'Show help for the validate command') do
48
+ puts self
49
+ exit(0)
50
+ end
51
+ separator('')
52
+ separator('Subcommands:')
53
+ separator("\tscan [path] Scan a directory or staged git changes for exposed secrets")
54
+ separator('')
55
+ separator('Examples:')
56
+ separator("\tsecure-keys validate scan")
57
+ separator("\tsecure-keys validate scan ./src")
58
+ separator("\tsecure-keys validate scan --staged")
59
+ separator("\tsecure-keys validate scan --output report.json")
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,31 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require_relative '../../../../core/console/arguments/fetchable'
4
+
5
+ module SecureKeys
6
+ module Validation
7
+ module Console
8
+ module Argument
9
+ module Scan
10
+ # Stores and provides access to the resolved CLI arguments for the scan subcommand
11
+ class Handler
12
+ class << self
13
+ include Core::Console::Argument::Fetchable
14
+
15
+ attr_reader :arguments
16
+ end
17
+
18
+ # Default argument values for the scan subcommand
19
+ @arguments = {
20
+ path: '.',
21
+ staged: false,
22
+ output: nil,
23
+ extensions: nil,
24
+ excludes: nil,
25
+ }
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,61 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'optparse'
4
+ require_relative 'handler'
5
+ require_relative '../../../../core/console/arguments/handler'
6
+
7
+ module SecureKeys
8
+ module Validation
9
+ module Console
10
+ module Argument
11
+ module Scan
12
+ # Parses CLI options for the `secure-keys validate scan` subcommand.
13
+ # Uses +parse!+ so options and positional arguments may appear in any order;
14
+ # after parsing, any remaining ARGV token is treated as the scan path.
15
+ class Parser < OptionParser
16
+ # Initialize the scan parser, process ARGV, and store results in Handler
17
+ def initialize
18
+ super('Usage: secure-keys validate scan [path] [--options]')
19
+ separator('')
20
+ configure!
21
+ parse!(into: Handler.arguments)
22
+ Handler.set(key: :path, value: ARGV.shift) unless ARGV.empty?
23
+ end
24
+
25
+ private
26
+
27
+ # Define all accepted options for the scan subcommand
28
+ # @return [void]
29
+ def configure!
30
+ on('-h', '--help', 'Show help for the scan subcommand') do
31
+ puts self
32
+ exit(0)
33
+ end
34
+ on('--staged', 'Scan staged git changes instead of a directory (default: false)') do
35
+ Handler.set(key: :staged, value: true)
36
+ end
37
+ on(
38
+ '-o', '--output FILE',
39
+ String,
40
+ 'Save the scan report as JSON to FILE'
41
+ )
42
+ on(
43
+ '--extensions EXTENSIONS',
44
+ String,
45
+ 'Comma-separated file extensions to scan (e.g. .rb,.swift)'
46
+ )
47
+ on(
48
+ '--excludes EXCLUDES',
49
+ String,
50
+ 'Comma-separated directory names to exclude from the scan'
51
+ )
52
+ on('--verbose', 'Enable verbose output (default: false)') do
53
+ Core::Console::Argument::Handler.set(key: :verbose, value: true)
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end