pass-station 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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