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,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
|
-
#
|
|
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 [
|
|
25
|
+
# @param value [Hash] the hash to merge in
|
|
42
26
|
def self.deep_merge(key:, value:)
|
|
43
|
-
@arguments[key.to_sym] ||= {}
|
|
27
|
+
@arguments[key.to_sym] ||= {}
|
|
44
28
|
@arguments[key.to_sym].merge!(value)
|
|
45
29
|
end
|
|
46
30
|
end
|
|
@@ -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
|
data/lib/core/environment/ci.rb
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
data/lib/core/generator.rb
CHANGED
|
@@ -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
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
|
|
3
|
+
require_relative '../../services/environment'
|
|
4
|
+
|
|
5
|
+
module SecureKeys
|
|
6
|
+
module Validation
|
|
7
|
+
module Globals
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
# Returns the minimum length for an API key
|
|
11
|
+
# @return [Integer] The minimum length for an API key
|
|
12
|
+
def api_key_length
|
|
13
|
+
Services::Environment.integer(key: :api_key_length, default: 20)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Returns the minimum length for a token
|
|
17
|
+
# @return [Integer] The minimum length for a token
|
|
18
|
+
def token_length
|
|
19
|
+
Services::Environment.integer(key: :token_length, default: 20)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Returns the minimum length for a secret
|
|
23
|
+
# @return [Integer] The minimum length for a secret
|
|
24
|
+
def secret_length
|
|
25
|
+
Services::Environment.integer(key: :secret_length, default: 16)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Returns the minimum length for a password
|
|
29
|
+
# @return [Integer] The minimum length for a password
|
|
30
|
+
def password_length
|
|
31
|
+
Services::Environment.integer(key: :password_length, default: 12)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Returns the minimum length for a generic key
|
|
35
|
+
# @return [Integer] The minimum length for a key
|
|
36
|
+
def key_length
|
|
37
|
+
Services::Environment.integer(key: :key_length, default: 16)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Returns the default file extensions to scan
|
|
41
|
+
# @return [Array<String>] The default file extensions
|
|
42
|
+
def default_scan_extensions
|
|
43
|
+
Services::Environment.fetch(
|
|
44
|
+
key: :scan_extensions,
|
|
45
|
+
default: '.swift,.m,.mm,.h,.rb,.py,.js,.ts,.java,.kt,.yaml,.yml,.json,.env,.plist'
|
|
46
|
+
).split(',')
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Returns the default directory and file names to exclude from scanning
|
|
50
|
+
# @return [Array<String>] The default exclude patterns
|
|
51
|
+
def default_scan_excludes
|
|
52
|
+
Services::Environment.fetch(
|
|
53
|
+
key: :scan_excludes,
|
|
54
|
+
default: '.git,node_modules,Pods,build,DerivedData,.build,vendor,.bundle,Carthage,.secure-keys,coverage'
|
|
55
|
+
).split(',')
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Returns the maximum directory traversal depth for scanning
|
|
59
|
+
# @return [Integer] The maximum scan depth
|
|
60
|
+
def max_scan_depth
|
|
61
|
+
Services::Environment.integer(key: :max_scan_depth, default: 10)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Returns the minimum Shannon entropy threshold for secret validation
|
|
65
|
+
# @return [Float] The minimum entropy threshold
|
|
66
|
+
def min_entropy_threshold
|
|
67
|
+
Services::Environment.decimal(key: :min_entropy_threshold, default: 3.0)
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
|
|
3
|
+
module SecureKeys
|
|
4
|
+
module Validation
|
|
5
|
+
# Represents a single secret detected during a file or git diff scan
|
|
6
|
+
class Finding
|
|
7
|
+
attr_reader :file, :line, :column, :type, :description, :severity,
|
|
8
|
+
:matched_text, :full_line, :is_addition
|
|
9
|
+
|
|
10
|
+
# Initialize a new finding
|
|
11
|
+
# @param file [String] The file path where the secret was found
|
|
12
|
+
# @param line [Integer] The line number where the secret was found
|
|
13
|
+
# @param column [Integer] The column offset of the match within the line
|
|
14
|
+
# @param type [Symbol] The pattern type that matched (e.g. :github_token, :aws_access_key)
|
|
15
|
+
# @param description [String] A human-readable description of the secret type
|
|
16
|
+
# @param severity [Symbol] The severity level (:low, :medium, :high, :critical)
|
|
17
|
+
# @param matched_text [String] The masked matched text, safe for display
|
|
18
|
+
# @param full_line [String] The full trimmed line of code containing the secret
|
|
19
|
+
# @param is_addition [Boolean] Whether this line is an addition in a git diff (default: false)
|
|
20
|
+
def initialize(file:, line:, column:, type:, description:, severity:,
|
|
21
|
+
matched_text:, full_line:, is_addition: false)
|
|
22
|
+
@file = file
|
|
23
|
+
@line = line
|
|
24
|
+
@column = column
|
|
25
|
+
@type = type
|
|
26
|
+
@description = description
|
|
27
|
+
@severity = severity
|
|
28
|
+
@matched_text = matched_text
|
|
29
|
+
@full_line = full_line
|
|
30
|
+
@is_addition = is_addition
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Check if this finding came from a git diff addition
|
|
34
|
+
# @return [Boolean] true if the line is a git diff addition
|
|
35
|
+
def addition?
|
|
36
|
+
is_addition
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Returns a one-line string representation of the finding
|
|
40
|
+
# @return [String] The formatted finding string
|
|
41
|
+
def to_s
|
|
42
|
+
"#{severity_icon} #{file}:#{line}:#{column} [#{type}] #{description} — #{matched_text}"
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Returns a hash representation of the finding
|
|
46
|
+
# @return [Hash] The hash representation
|
|
47
|
+
def to_h
|
|
48
|
+
{
|
|
49
|
+
file:,
|
|
50
|
+
line:,
|
|
51
|
+
column:,
|
|
52
|
+
type:,
|
|
53
|
+
description:,
|
|
54
|
+
severity:,
|
|
55
|
+
matched_text:,
|
|
56
|
+
full_line:,
|
|
57
|
+
is_addition:,
|
|
58
|
+
}
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
private
|
|
62
|
+
|
|
63
|
+
# Returns the appropriate icon for the severity level
|
|
64
|
+
# @return [String] The severity icon
|
|
65
|
+
def severity_icon
|
|
66
|
+
case severity
|
|
67
|
+
when :critical then '🔴'
|
|
68
|
+
when :high then '🟠'
|
|
69
|
+
when :medium then '🟡'
|
|
70
|
+
when :low then '🔵'
|
|
71
|
+
else '⚪'
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|