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