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.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/bin/pass-station +79 -0
- data/bin/pass-station_console +7 -0
- data/data/DefaultCreds-Cheat-Sheet.csv +3344 -0
- data/lib/pass_station.rb +54 -0
- data/lib/pass_station/output.rb +204 -0
- data/lib/pass_station/parse.rb +36 -0
- data/lib/pass_station/search.rb +70 -0
- data/lib/pass_station/source.rb +96 -0
- data/lib/pass_station/version.rb +5 -0
- metadata +211 -0
data/lib/pass_station.rb
ADDED
@@ -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
|