pass-station 1.0.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,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Project internal
4
+ require 'pass_station/source'
5
+ require 'pass_station/parse'
6
+ require 'pass_station/search'
7
+ require 'pass_station/version'
8
+
9
+ # Pass Station module
10
+ module PassStation
11
+ # Constants
12
+ include Version
13
+
14
+ # Password database handling
15
+ class DB
16
+ # Get / set storage location, where will be stored the password database.
17
+ # @return [String] database storage location. Default to +data/+.
18
+ # @example
19
+ # PassStation.storage_location = '/srv/downloads/'
20
+ attr_accessor :storage_location
21
+
22
+ # Get / set the password database name
23
+ # @return [String] password database filename. Default to
24
+ # +DefaultCreds-Cheat-Sheet.csv+.
25
+ attr_accessor :database_name
26
+
27
+ # Get the password database in +CSV::Table+ format
28
+ # @return [CSV::Table] pasword database
29
+ attr_reader :data
30
+
31
+ # A new instance of Pass Station
32
+ def initialize
33
+ @storage_location = 'data/'
34
+ @database_name = 'DefaultCreds-Cheat-Sheet.csv'
35
+ @database_path = @storage_location + @database_name
36
+ database_exists?
37
+ @config = {}
38
+ csv_config
39
+ @data = nil
40
+ @search_result = []
41
+ end
42
+
43
+ # Check if the password database exists
44
+ # @return [Boolean] +true+ if the file exists
45
+ def database_exists?
46
+ exists = File.file?(@database_path)
47
+ raise "Database does not exist: #{@database_path}" unless exists
48
+
49
+ exists
50
+ end
51
+
52
+ protected :database_exists?
53
+ end
54
+ end
@@ -0,0 +1,204 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Ruby internal
4
+ require 'csv'
5
+ require 'json'
6
+ require 'yaml'
7
+ # External dependencies
8
+ require 'paint'
9
+
10
+ # Pass Station module
11
+ module PassStation
12
+ # Password database handling
13
+ class DB
14
+ # Output the data in the chosen format
15
+ # @param formatter [String] Engine to use to format the data: +table+, +'pretty-table'+, +JSON+, +CSV+, +YAML+
16
+ # @param data [CSV::Table]
17
+ # @return [Array<String>] formatted output
18
+ def output(formatter, data)
19
+ # Convert string to class
20
+ Object.const_get("PassStation::Output::#{normalize(formatter)}").format(data)
21
+ end
22
+
23
+ # Output the data in the chosen format (list command)
24
+ # @param formatter [String] Engine to use to format the data: +table+, +'pretty-table'+, +JSON+, +CSV+, +YAML+
25
+ # @return [Array<String>] formatted output
26
+ def output_list(formatter)
27
+ data_nil?
28
+ output(formatter, @data)
29
+ end
30
+
31
+ # Output the data in the chosen format (search command)
32
+ # @param formatter [String] Engine to use to format the data: +table+, +'pretty-table'+, +JSON+, +CSV+, +YAML+
33
+ # @return [Array<String>] formatted output
34
+ def output_search(formatter)
35
+ return '[-] No result' if @search_result.empty?
36
+
37
+ output(formatter, @search_result)
38
+ end
39
+
40
+ # Highlight (colorize) a searched term in the input
41
+ # When used with the search command, it will ignore in which column the
42
+ # search was made, and will instead colorize in every columns.
43
+ # @param term [String] the searched term
44
+ # @param text [String] the output in which the colorization must be made
45
+ # @param sensitive [Boolean] case sensitive or not
46
+ # @return [Array<String>] colorized output
47
+ def highlight_found(term, text, sensitive)
48
+ text.map do |x|
49
+ rgxp = build_regexp(term, sensitive: sensitive)
50
+ x.gsub(rgxp) { |s| Paint[s, :red] }
51
+ end
52
+ end
53
+
54
+ # Normalize string to be class name compatible
55
+ # Join splitted words and capitalize
56
+ # @param formatter [String] formatter name
57
+ # @return [String] normalized name (class compatible)
58
+ def normalize(formatter)
59
+ formatter.split('-').map(&:capitalize).join
60
+ end
61
+
62
+ protected :normalize
63
+ end
64
+
65
+ # Output handling module containing all formatter engines
66
+ # Meant to be used by the CLI binary but could be re-used in many other ways
67
+ module Output
68
+ # Simple table formatter
69
+ class Table
70
+ class << self
71
+ # Format the +CSV::Table+ into a simple table with justified columns
72
+ # @param table [CSV::Table] a +CSV::Table+
73
+ # @return [Array<String>] the formatted table ready to be printed
74
+ def format(table)
75
+ out = []
76
+ colsizes = colsizes_count(table)
77
+ out.push(headers(colsizes))
78
+ table.each do |r|
79
+ out.push(justify_row(r, colsizes))
80
+ end
81
+ out
82
+ end
83
+
84
+ # Calculate column size (max item size)
85
+ # @param table [CSV::Table]
86
+ # @param column [Symbol] the symbol of the column
87
+ # @return [Integer] the column size
88
+ def colsize_count(table, column)
89
+ table.map { |i| i[column].nil? ? 0 : i[column].size }.max + 1
90
+ end
91
+
92
+ # Calculate the size of all columns (max item size)
93
+ # @param table [CSV::Table]
94
+ # @return [Hash] keys are columns name, values are columns size
95
+ def colsizes_count(table)
96
+ colsizes = table.first.to_h.keys.each_with_object({}) do |c, h|
97
+ h[c] = colsize_count(table, c)
98
+ end
99
+ correct_min_colsizes(colsizes)
100
+ end
101
+
102
+ # Correct colsizes to be at least of the size of the headers (case when
103
+ # values are shorter than headers and breaks the table display)
104
+ # @param colsizes [Hash] hash containing the column size for each column as returned by {colsizes_count}
105
+ # @return [Hash] fixed colsizes, keys are columns name, values are columns size
106
+ def correct_min_colsizes(colsizes)
107
+ min_colsizes = {
108
+ productvendor: 14,
109
+ username: 9,
110
+ password: 9
111
+ }
112
+ min_colsizes.each_with_object({}) { |(k, v), h| h[k] = [v, colsizes[k]].max }
113
+ end
114
+
115
+ # Left justify an element of the column
116
+ # @param row [CSV::Row] +CSV::Row+
117
+ # @param column [Symbol] the symbol of the column
118
+ # @param colsizes [Hash] hash containing the column size for each column as returned by {colsizes_count}
119
+ # @return [String] the justified element
120
+ def justify(row, column, colsizes)
121
+ row[column].to_s.ljust(colsizes[column])
122
+ end
123
+
124
+ # Left justify all elements of the column
125
+ # @param row [CSV::Row] +CSV::Row+
126
+ # @param colsizes [Hash] hash containing the column size for each column as returned by {colsizes_count}
127
+ # @return [String] the justified row
128
+ def justify_row(row, colsizes)
129
+ out = ''
130
+ row.to_h.each_key do |col|
131
+ out += justify(row, col, colsizes)
132
+ end
133
+ out
134
+ end
135
+
136
+ # Generate justified headers
137
+ # @param colsizes [Hash] hash containing the column size for each column as returned by {colsizes_count}
138
+ # @return [String] the justified headers
139
+ def headers(colsizes)
140
+ colsizes.map { |k, v| k.to_s.ljust(v) }.join.to_s
141
+ end
142
+
143
+ protected :colsize_count, :colsizes_count, :justify, :justify_row, :headers, :correct_min_colsizes
144
+ end
145
+ end
146
+
147
+ # Pretty table with ASCII borders formatter
148
+ class PrettyTable < Table
149
+ class << self
150
+ # Format the +CSV::Table+ into a simple table with justified columns
151
+ # @param table [CSV::Table] a +CSV::Table+
152
+ # @return [Array<String>] the formatted table ready to be printed
153
+ def format(table)
154
+ out = []
155
+ colsizes = colsizes_count(table)
156
+ out.push(dividers(colsizes))
157
+ out.push(headers(colsizes))
158
+ out.push(dividers(colsizes))
159
+ table.each do |r|
160
+ out.push(justify_row(r, colsizes))
161
+ end
162
+ out.push(dividers(colsizes))
163
+ end
164
+
165
+ # Left justify an element of the column
166
+ # @param row [CSV::Row] +CSV::Row+
167
+ # @param column [Symbol] the symbol of the column
168
+ # @param colsizes [Hash] hash containing the column size for each column as returned by {colsizes_count}
169
+ # @return [String] the justified element
170
+ def justify(row, column, colsizes)
171
+ row[column].to_s.ljust(colsizes[column] - 1)
172
+ end
173
+
174
+ # Left justify all elements of the column
175
+ # @param row [CSV::Row] +CSV::Row+
176
+ # @param colsizes [Hash] hash containing the column size for each column as returned by {colsizes_count}
177
+ # @return [String] the justified row
178
+ def justify_row(row, colsizes)
179
+ out = '| '
180
+ row.to_h.each_key do |col|
181
+ out += "#{justify(row, col, colsizes)} | "
182
+ end
183
+ out
184
+ end
185
+
186
+ # Generate dividers
187
+ # @param colsizes [Hash] hash containing the column size for each column as returned by {colsizes_count}
188
+ # @return [String] divider line
189
+ def dividers(colsizes)
190
+ "+#{colsizes.map { |_, cs| '-' * (cs + 1) }.join('+')}+"
191
+ end
192
+
193
+ # Generate justified headers
194
+ # @param colsizes [Hash] hash containing the column size for each column as returned by {colsizes_count}
195
+ # @return [String] the justified headers
196
+ def headers(colsizes)
197
+ "| #{colsizes.map { |k, v| k.to_s.ljust(v - 1) }.join(' | ')} |"
198
+ end
199
+
200
+ protected :dividers, :headers, :justify_row, :justify
201
+ end
202
+ end
203
+ end
204
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Ruby internal
4
+ require 'csv'
5
+
6
+ # Pass Station module
7
+ module PassStation
8
+ # Password database handling
9
+ class DB
10
+ # Register CSV converters for parsing
11
+ def csv_config
12
+ strip_converter = proc { |field| field.strip }
13
+ CSV::Converters[:strip] = strip_converter
14
+ # https://github.com/ruby/csv/issues/208
15
+ # @config[:strip] = true
16
+ # @config[:liberal_parsing] = true
17
+ @config[:headers] = true
18
+ @config[:converters] = :strip
19
+ @config[:header_converters] = :symbol
20
+ @config[:empty_value] = '<blank>'
21
+ @config[:nil_value] = '<blank>'
22
+ end
23
+
24
+ # Parse, sort and sanitize the password database
25
+ # @param sort [Symbol] column name to sort by: +:productvendor+, +:username+, +:password+
26
+ # @return [CSV::Table] table of +CSV::Row+, each row contains three
27
+ # attributes: :productvendor, :username, :password
28
+ def parse(sort = :productvendor)
29
+ @data = CSV.table(@database_path, @config).sort_by do |s|
30
+ s[sort].downcase
31
+ end
32
+ end
33
+
34
+ protected :csv_config
35
+ end
36
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Pass Station module
4
+ module PassStation
5
+ # Password database handling
6
+ class DB
7
+ # Search term in the data table
8
+ # @param term [String] the searched term
9
+ # @param col [Symbol] the column to search in: :productvendor | :username | :password | :all (all columns)
10
+ # @see build_regexp for +opts+ param description
11
+ # @return [CSV::Table] table of +CSV::Row+, each row contains three
12
+ # attributes: :productvendor, :username, :password
13
+ def search(term, col, opts = {})
14
+ r1 = prepare_search(term, opts)
15
+ condition = column_selector(col, r1)
16
+ @data.each do |row|
17
+ @search_result.push(row) if condition.call(row)
18
+ end
19
+ @search_result
20
+ end
21
+
22
+ # Choose in which column the search will be performed and build the
23
+ # condition to use
24
+ # @param col [Symbol] the column to search in: :productvendor | :username | :password | :all (all columns)
25
+ # @param rgxp [Regexp] the search regexp (generated by {build_regexp})
26
+ # @return [Proc] the proc condition to use for searching
27
+ def column_selector(col, rgxp)
28
+ proc { |row|
29
+ if col == :all
30
+ row[:productvendor].match?(rgxp) || row[:username].match?(rgxp) ||
31
+ row[:password].match?(rgxp)
32
+ else
33
+ row[col].match?(rgxp)
34
+ end
35
+ }
36
+ end
37
+
38
+ # Prepare search query
39
+ # Check if data available, prepare the regexp, and clean previous search
40
+ # results
41
+ # @see build_regexp for parameters and return value
42
+ def prepare_search(term, opts = {})
43
+ data_nil?
44
+ r1 = build_regexp(term, opts)
45
+ @search_result = []
46
+ r1
47
+ end
48
+
49
+ # Manage search options to build a search regexp
50
+ # @param term [String] the searched term
51
+ # @param opts [Hash] the option hash
52
+ # @option opts [Boolean] :sensitive is the search case sensitive, default: false
53
+ # @return [Regexp] the search regexp
54
+ def build_regexp(term, opts = {})
55
+ opts[:sensitive] ||= false
56
+ if opts[:sensitive]
57
+ Regexp.new(term, Regexp::EXTENDED)
58
+ else
59
+ Regexp.new(term, Regexp::EXTENDED | Regexp::IGNORECASE)
60
+ end
61
+ end
62
+
63
+ # Raise an error is data attribute is nil
64
+ def data_nil?
65
+ raise 'You must use the parse method to polutate the data attribute first' if @data.nil?
66
+ end
67
+
68
+ protected :build_regexp, :prepare_search, :column_selector, :data_nil?
69
+ end
70
+ end
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Ruby internal
4
+ require 'net/https'
5
+ require 'tmpdir'
6
+
7
+ # Pass Station module
8
+ module PassStation
9
+ # Password database handling
10
+ class DB
11
+ UPSTREAM_DATABASE = {
12
+ URL: 'https://raw.githubusercontent.com/ihebski/DefaultCreds-cheat-sheet/main/DefaultCreds-Cheat-Sheet.csv',
13
+ HASH: 'de6a9f7e7ac94fbcd142ec5817efb71d3a0027076266c87ead5158a4960ec708'
14
+ }.freeze
15
+
16
+ class << self
17
+ # Download upstream password database
18
+ # @param destination_path [String] the destination path (may
19
+ # overwrite existing file).
20
+ # @param opts [Hash] the optional downlaod parameters.
21
+ # @option opts [String] :sha256 the SHA256 hash to check, if the file
22
+ # already exist and the hash matches then the download will be skipped.
23
+ # @return [String|nil] the saved file path.
24
+ def download_upstream(destination_path, opts = {})
25
+ download_file(UPSTREAM_DATABASE[:URL], destination_path, opts)
26
+ end
27
+
28
+ # Chek if an update is available
29
+ # @return [Boolean] +true+ if there is, +false+ else.
30
+ def check_for_update
31
+ file = download_file(UPSTREAM_DATABASE[:URL], Dir.mktmpdir)
32
+ # Same hash = no update
33
+ !check_hash(file, UPSTREAM_DATABASE[:HASH])
34
+ end
35
+
36
+ # Download a file.
37
+ # @param file_url [String] the URL of the file.
38
+ # @param destination_path [String] the destination path (may
39
+ # overwrite existing file).
40
+ # @param opts [Hash] the optional downlaod parameters.
41
+ # @option opts [String] :sha256 the SHA256 hash to check, if the file
42
+ # already exist and the hash matches then the download will be skipped.
43
+ # @return [String|nil] the saved file path.
44
+ def download_file(file_url, destination_path, opts = {})
45
+ opts[:sha256] ||= nil
46
+
47
+ destination_path += '/' unless destination_path[-1] == '/'
48
+ uri = URI(file_url)
49
+ filename = uri.path.split('/').last
50
+ destination_file = destination_path + filename
51
+ # Verify hash to see if it is the latest
52
+ skip_download = check_hash(destination_file, opts[:sha256])
53
+ write_file(destination_file, fetch_file(uri)) unless skip_download
54
+ end
55
+
56
+ # Check if a file match a SHA256 hash
57
+ # @param file [String] the path of the file.
58
+ # @param hash [String] tha SHA256 hash to check against.
59
+ # @return [Boolean] if the hash of the file matched the one provided (+true+)
60
+ # or not (+false+).
61
+ def check_hash(file, hash)
62
+ if !hash.nil? && File.file?(file)
63
+ computed_h = Digest::SHA256.file(file)
64
+ true if hash.casecmp(computed_h.hexdigest).zero?
65
+ else
66
+ false
67
+ end
68
+ end
69
+
70
+ # Just fetch a file
71
+ # @param uri [URI] the URI to of the file
72
+ # @return [Bytes] the content of the file
73
+ def fetch_file(uri)
74
+ res = Net::HTTP.get_response(uri)
75
+ raise "#{file_url} ended with #{res.code} #{res.message}" unless res.is_a?(Net::HTTPSuccess)
76
+
77
+ res.body
78
+ end
79
+
80
+ # Write a file to disk
81
+ # @param destination_file [String] the file path where the fiel will be
82
+ # written to disk
83
+ # @param file_content [String] the content to write in the file
84
+ # @return [String] destination file path
85
+ def write_file(destination_file, file_content)
86
+ File.open(destination_file, 'wb') do |file|
87
+ file.write(file_content)
88
+ end
89
+ destination_file
90
+ end
91
+
92
+ # https://github.com/lsegal/yard/issues/1372
93
+ protected :download_file, :check_hash, :fetch_file, :write_file
94
+ end
95
+ end
96
+ end