Skip to content
This repository was archived by the owner on Feb 26, 2021. It is now read-only.

Commit 8888369

Browse files
authored
Expand SSH Finding Model into multiple Categories (#10)
Expand SSH Finding Model into multiple Categories
2 parents 40b4346 + 5139b70 commit 8888369

File tree

6 files changed

+607
-181
lines changed

6 files changed

+607
-181
lines changed

src/main.rb

Lines changed: 12 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,26 +4,24 @@
44
Bundler.setup(:default)
55
require 'ruby-scanner-scaffolding'
66
require 'ruby-scanner-scaffolding/healthcheck'
7-
require_relative "./ssh_worker"
7+
require_relative './ssh_worker'
88

9-
set :port, 8080
9+
set :port, 8_080
1010
set :bind, '0.0.0.0'
1111
set :environment, :production
1212

13-
client = SshWorker.new(
14-
'http://localhost:8080',
15-
'ssh_webserverscan',
16-
['PROCESS_TARGETS']
17-
)
13+
client =
14+
SshWorker.new(
15+
'http://localhost:8080',
16+
'ssh_webserverscan',
17+
%w[PROCESS_TARGETS]
18+
)
1819

1920
healthcheckClient = Healthcheck.new
2021

2122
get '/status' do
22-
status 500
23-
if client.healthy?
24-
status 200
25-
end
26-
content_type :json
27-
healthcheckClient.check(client)
23+
status 500
24+
status 200 if client.healthy?
25+
content_type :json
26+
healthcheckClient.check(client)
2827
end
29-

src/ssh_configuration.rb

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
def is_set(val)
22
if val != ''
3+
34
elsif val.is_a?(Array)
4-
val.length != 0
5-
end
5+
val.length != 0
6+
end
67
end
78

89
class SshConfiguration
@@ -11,14 +12,20 @@ class SshConfiguration
1112
attr_accessor :ssh_policy_file
1213
attr_accessor :ssh_timeout_seconds
1314

14-
15-
def self.from_target(job_id, target, policies_directory = "/securecodebox/static/")
15+
def self.from_target(
16+
job_id, target, policies_directory = '/securecodebox/static/'
17+
)
1618
config = SshConfiguration.new
1719

1820
config.policies_directory = policies_directory
1921
config.job_id = job_id
20-
config.ssh_policy_file = target.dig('attributes','SSH_POLICY_FILE') unless !target.dig('attributes','SSH_POLICY_FILE')
21-
config.ssh_timeout_seconds = target.dig('attributes','SSH_TIMEOUT_SECONDS') unless !target.dig('attributes','SSH_TIMEOUT_SECONDS')
22+
unless !target.dig('attributes', 'SSH_POLICY_FILE')
23+
config.ssh_policy_file = target.dig('attributes', 'SSH_POLICY_FILE')
24+
end
25+
unless !target.dig('attributes', 'SSH_TIMEOUT_SECONDS')
26+
config.ssh_timeout_seconds =
27+
target.dig('attributes', 'SSH_TIMEOUT_SECONDS')
28+
end
2229
config
2330
end
2431

src/ssh_result_transformer.rb

Lines changed: 139 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -3,68 +3,157 @@
33

44
class SshResultTransformer
55
def initialize(uuid_provider = SecureRandom)
6-
@uuid_provider = uuid_provider;
6+
@uuid_provider = uuid_provider
77
end
88

9-
109
def transform(results, timed_out: false)
1110
findings = []
11+
1212
results.each do |r|
13-
findings << {
14-
id: @uuid_provider.uuid,
15-
name: 'SSH Compliance',
16-
description: 'SSH Compliance Information',
17-
category: 'SSH Service',
18-
osi_layer: 'NETWORK',
19-
severity: 'INFORMATIONAL',
20-
reference: {},
21-
hint: '',
22-
location: r.dig('ip'),
23-
attributes: {
24-
hostname: r.dig('hostname'),
25-
server_banner: r.dig('server_banner'),
26-
ssh_version: r.dig('ssh_version'),
27-
os_cpe: r.dig('os_cpe'),
28-
ssh_lib_cpe: r.dig('ssh_lib_cpe'),
29-
compliance_policy: r.dig('compliance', 'policy'),
30-
compliant: r.dig('compliance', 'compliant'),
31-
grade: r.dig('compliance', 'grade'),
32-
start_time: r.dig('start_time'),
33-
end_time: r.dig('end_time'),
34-
scan_duration_seconds: r.dig('scan_duration_seconds'),
35-
references: r.dig('compliance', 'references')
36-
}
37-
}
13+
unless r.has_key? "error"
14+
location = (r.dig('hostname').empty?) ? r.dig('ip') : r.dig('hostname')
15+
hostname = (r.dig('hostname') unless r.dig('hostname').empty?)
16+
findings <<
17+
{
18+
id: @uuid_provider.uuid,
19+
name: 'SSH Service Information',
20+
description: '',
21+
category: 'SSH Service',
22+
osi_layer: 'NETWORK',
23+
severity: 'INFORMATIONAL',
24+
reference: {},
25+
hint: '',
26+
location: location,
27+
attributes: {
28+
hostname: hostname,
29+
ip_address: r.dig('ip'),
30+
server_banner:
31+
(r.dig('server_banner') unless r.dig('server_banner').empty?),
32+
ssh_version: r.dig('ssh_version'),
33+
os_cpe: r.dig('os_cpe'),
34+
ssh_lib_cpe: r.dig('ssh_lib_cpe'),
35+
compliance_policy: r.dig('compliance', 'policy'),
36+
compliant: r.dig('compliance', 'compliant'),
37+
grade: r.dig('compliance', 'grade'),
38+
start_time: r.dig('start_time'),
39+
end_time: r.dig('end_time'),
40+
scan_duration_seconds: r.dig('scan_duration_seconds'),
41+
references: r.dig('compliance', 'references'),
42+
auth_methods: r.dig('auth_methods'),
43+
key_algorithms: r.dig('key_algorithms'),
44+
encryption_algorithms:
45+
r.dig('encryption_algorithms_server_to_client'),
46+
mac_algorithms: r.dig('mac_algorithms_server_to_client'),
47+
compression_algorithms:
48+
r.dig('compression_algorithms_server_to_client')
49+
}
50+
}
3851

39-
unless r.dig('compliance', 'recommendations').nil?
40-
r.dig('compliance', 'recommendations').each do |f|
41-
findings << {
42-
id: @uuid_provider.uuid,
43-
name: f.split(':')[0],
44-
description: f.split(':')[1],
45-
category: 'SSH Service',
46-
osi_layer: 'NETWORK',
47-
severity: decideSeverity(r.dig('compliance', 'grade')),
48-
reference: {},
49-
hint: '',
50-
location: r.dig('ip'),
51-
attributes: {}
52-
}
52+
unless r.dig('compliance', 'recommendations').nil?
53+
r.dig('compliance', 'recommendations')
54+
.each do |policy_violation_message|
55+
findings <<
56+
create_policy_violation_finding(
57+
policy_violation_message,
58+
location,
59+
hostname,
60+
r.dig('ip')
61+
)
62+
end
5363
end
5464
end
5565
end
5666

5767
findings
5868
end
5969

60-
def decideSeverity(grade)
61-
case grade
62-
when 'A', 'B'
63-
'LOW'
64-
when 'C', 'D'
65-
'MEDIUM'
66-
when 'E', 'F'
67-
'HIGH'
70+
# rubocop:disable MethodComplexity
71+
def get_policy_violation_type(message)
72+
type = message.split(': ')[0]
73+
74+
case type
75+
when /^Add these key exchange algorithms/
76+
{
77+
description: 'Good / encouraged SSH key algorithms are missing',
78+
name: 'Missing SSH Key Algorithms'
79+
}
80+
when /^Add these MAC algorithms/
81+
{
82+
description: 'Good / encouraged SSH MAC algorithms are missing',
83+
name: 'Missing SSH MAC Algorithms'
84+
}
85+
when /^Add these encryption ciphers/
86+
{
87+
description: 'Good / encouraged SSH encryption ciphers are missing',
88+
name: 'Missing SSH encryption Ciphers'
89+
}
90+
when /^Add these compression algorithms/
91+
{
92+
description: 'Good / encouraged SSH compression algorithms are missing',
93+
name: 'Missing SSH compression algorithms'
94+
}
95+
when /^Add these authentication methods/
96+
{
97+
description: 'Good / encouraged SSH authentication methods are missing',
98+
name: 'Missing SSH authentication methods'
99+
}
100+
when /^Remove these key exchange algorithms/
101+
{
102+
description: 'Deprecated / discouraged SSH key algorithms are used',
103+
name: 'Insecure SSH Key Algorithms'
104+
}
105+
when /^Remove these MAC algorithms/
106+
{
107+
description: 'Deprecated / discouraged SSH MAC algorithms are used',
108+
name: 'Insecure SSH MAC Algorithms'
109+
}
110+
when /^Remove these encryption ciphers/
111+
{
112+
description: 'Deprecated / discouraged SSH encryption ciphers are used',
113+
name: 'Insecure SSH encryption Ciphers'
114+
}
115+
when /^Remove these compression algorithms/
116+
{
117+
description:
118+
'Deprecated / discouraged SSH compression algorithms are used',
119+
name: 'Insecure SSH compression algorithms'
120+
}
121+
when /^Remove these authentication methods/
122+
{
123+
description: 'Discouraged SSH authentication methods are used',
124+
name: 'Discouraged SSH authentication methods'
125+
}
126+
when /^Update your ssh version to/
127+
{
128+
description: 'Outdated SSH protocol version used',
129+
name: 'Outdated SSH Protocol Version'
130+
}
131+
else
132+
raise Exception.new "Unexpected Policy Violation Type: '#{message}'"
68133
end
69134
end
70-
end
135+
# rubocop:enable MethodComplexity
136+
137+
def create_policy_violation_finding(
138+
message, location, hostname, ip_address
139+
)
140+
policy_violation_type = get_policy_violation_type(message)
141+
142+
payload = message.split(': ')[1].split(', ')
143+
144+
{
145+
id: @uuid_provider.uuid,
146+
name: policy_violation_type[:name],
147+
description: policy_violation_type[:description],
148+
category: 'SSH Policy Violation',
149+
osi_layer: 'NETWORK',
150+
severity: 'MEDIUM',
151+
reference: {},
152+
hint: message,
153+
location: location,
154+
attributes: {
155+
hostname: hostname, ip_address: ip_address, payload: payload
156+
}
157+
}
158+
end
159+
end

src/ssh_scan.rb

Lines changed: 49 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -2,57 +2,61 @@
22
require 'json'
33
require 'logger'
44
require 'pathname'
5+
require 'ruby-scanner-scaffolding'
56

67
require_relative './ssh_result_transformer'
78

89
$logger = Logger.new(STDOUT)
910
$logger.level = if ENV.key? 'DEBUG' then Logger::DEBUG else Logger::INFO end
1011

1112
class SshScan
12-
attr_reader :raw_results
13-
attr_reader :results
14-
attr_reader :errored
15-
16-
def initialize(targetfile, config)
17-
@targetfile = targetfile
18-
@config = config
19-
@errored = false
20-
@transformer = SshResultTransformer.new
21-
end
22-
23-
def start
24-
$logger.info "Running scan for #{File.basename(@targetfile, File.extname(@targetfile))}"
25-
start_scan
26-
$logger.info "Retrieving scan results for #{File.basename(@targetfile, File.extname(@targetfile))}"
27-
get_scan_report
28-
end
29-
30-
def start_scan
31-
begin
32-
resultsFile = File.open("/tmp/raw-results.txt", "w+")
33-
34-
sshCommandLine = "ssh_scan --fingerprint-db /tmp/fingerprint-db.yml -f #{Pathname.new(@targetfile)} -o #{Pathname.new(resultsFile)}"
35-
36-
if not @config.ssh_policy_file.nil?
37-
sshCommandLine += "-P #{@config.filePath} "
38-
end
39-
if not @config.ssh_timeout_seconds.nil?
40-
sshCommandLine += "-T #{@config.ssh_timeout_seconds}"
13+
attr_reader :raw_results
14+
attr_reader :results
15+
attr_reader :errored
16+
17+
def initialize(target_file_path, config)
18+
@target_file_path = target_file_path
19+
@config = config
20+
@errored = false
21+
@transformer = SshResultTransformer.new
22+
end
23+
24+
def start
25+
$logger.info "Running scan for #{@target_file_path.basename}"
26+
start_scan
27+
$logger.info "Retrieving scan results for #{@target_file_path.basename}"
28+
get_scan_report
29+
end
30+
31+
def start_scan
32+
result_file_path = Pathname.new "/tmp/raw-results.txt"
33+
ssh_command_line = "ssh_scan --fingerprint-db /tmp/fingerprint-db.yml -f #{@target_file_path} -o #{result_file_path}"
34+
35+
unless @config.ssh_policy_file.nil?
36+
ssh_command_line += "-P #{@config.filePath} "
4137
end
42-
resultsFile.write(`#{sshCommandLine}`)
43-
@raw_results = JSON.parse(resultsFile.read)
44-
File.delete(resultsFile)
45-
rescue => err
46-
$logger.warn err
47-
raise CamundaIncident.new("Failed to start SSH scan.", "This is most likely related to a error in the configuration. Check the SSH logs for more details.")
48-
end
49-
end
50-
51-
def get_scan_report
52-
begin
53-
@results = @transformer.transform(@raw_results)
54-
rescue => err
55-
$logger.warn err
56-
end
57-
end
38+
unless @config.ssh_timeout_seconds.nil?
39+
ssh_command_line += "-T #{@config.ssh_timeout_seconds}"
40+
end
41+
42+
# Execute the Scanner via command line
43+
`#{ssh_command_line}`
44+
45+
File.open(result_file_path) do |results_file|
46+
@raw_results = JSON.parse(results_file.read)
47+
File.delete(results_file)
48+
end
49+
rescue => err
50+
$logger.warn err
51+
raise CamundaIncident.new(
52+
'Failed to start SSH scan.',
53+
'This is most likely related to a error in the configuration. Check the SSH logs for more details.'
54+
)
55+
end
56+
57+
def get_scan_report
58+
@results = @transformer.transform(@raw_results)
59+
rescue => err
60+
$logger.warn err
61+
end
5862
end

0 commit comments

Comments
 (0)