bluewall 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/README.md +109 -0
- data/bin/bluewall +21 -0
- data/lib/bluewall.rb +1400 -0
- metadata +78 -0
data/lib/bluewall.rb
ADDED
@@ -0,0 +1,1400 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
# BlueWall Firewall Auditor (pfSense/OpenSense - Full XML Support)
|
3
|
+
# Created by :cillia
|
4
|
+
require 'yaml'
|
5
|
+
require 'nokogiri'
|
6
|
+
require 'set'
|
7
|
+
require 'json'
|
8
|
+
|
9
|
+
class BlueWall
|
10
|
+
Rule = Struct.new(:id, :action, :interface, :direction, :protocol, :source, :destination, :dport, :comment, :quick, :schedule, :gateway, :state_type, :nat_rule) do
|
11
|
+
def to_s
|
12
|
+
parts = ["[#{id}] #{action} #{direction} on #{interface}"]
|
13
|
+
parts << "proto=#{protocol}" if protocol && protocol != 'any'
|
14
|
+
parts << "src=#{source}" if source && source != 'any'
|
15
|
+
parts << "dst=#{destination}" if destination && destination != 'any'
|
16
|
+
parts << "dport=#{dport}" if dport
|
17
|
+
parts << "(Comment: #{comment})" if comment
|
18
|
+
parts << "(Quick)" if quick
|
19
|
+
parts << "(Schedule: #{schedule})" if schedule
|
20
|
+
parts << "(Gateway: #{gateway})" if gateway
|
21
|
+
parts << "(State: #{state_type})"
|
22
|
+
parts << "(NAT Rule)" if nat_rule
|
23
|
+
parts.join(" ")
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
AuditResult = Struct.new(:firewall_type, :rules, :strengths, :weaknesses, :score, :details, :simulated_attacks, :framework_assessments) do
|
28
|
+
def to_s
|
29
|
+
blue_wall_logo = <<~LOGO.chomp
|
30
|
+
██████╗ ██╗ ██╗ ██╗███████╗██╗ ██╗ █████╗ ██╗ ██╗ ███╗███╗
|
31
|
+
██╔══██╗██║ ██║ ██║██╔════╝██║ ██║██╔══██╗██║ ██║ ██╔╝╚██║
|
32
|
+
██████╔╝██║ ██║ ██║█████╗ ██║ █╗ ██║███████║██║ ██║ ██║ ██║
|
33
|
+
██╔══██╗██║ ██║ ██║██╔══╝ ██║███╗██║██╔══██║██║ ██║ ██║ ██║
|
34
|
+
██████╔╝███████╗╚██████╔╝███████╗╚███╔███╔╝██║ ██║███████╗███████╗ ███╗███║
|
35
|
+
╚═════╝ ╚══════╝ ╚═════╝ ╚══════╝ ╚══╝╚══╝ ╚═╝ ╚═╝╚══════╝╚══════╝ ╚══╝╚══╝
|
36
|
+
created by :cillia
|
37
|
+
LOGO
|
38
|
+
|
39
|
+
<<~AUDIT_REPORT
|
40
|
+
#{blue_wall_logo}
|
41
|
+
--- BlueWall Audit Report ---
|
42
|
+
Firewall Type: #{firewall_type}
|
43
|
+
---------------------------
|
44
|
+
Strengths:
|
45
|
+
#{strengths.empty? ? ' None identified.' : strengths.map { |s| " - #{s}" }.join("\n")}
|
46
|
+
Weaknesses:
|
47
|
+
#{weaknesses.empty? ? ' None identified.' : weaknesses.map { |w| " - #{w}" }.join("\n")}
|
48
|
+
Overall Security Score (1-10): #{score.round(3)}
|
49
|
+
Details: #{details}
|
50
|
+
---------------------------
|
51
|
+
Simulated Attack Scenarios:
|
52
|
+
#{simulated_attacks.empty? ? ' No scenarios tested or results found.' : simulated_attacks.map { |s| " - #{s}" }.join("\n")}
|
53
|
+
AUDIT_REPORT
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def initialize
|
58
|
+
@supported_firewall_types = {
|
59
|
+
'PFSENSE_LIKE' => {
|
60
|
+
indicators: ['/pfsense/interfaces', '/pfsense/filter/rule'],
|
61
|
+
description: 'A pfSense-like XML structure or configuration.'
|
62
|
+
},
|
63
|
+
'OPENSENSE_LIKE' => {
|
64
|
+
indicators: ['/opnsense/interfaces', '/opnsense/filter/rule'],
|
65
|
+
description: 'An OpenSense-like XML configuration.'
|
66
|
+
}
|
67
|
+
}
|
68
|
+
|
69
|
+
@weights = {
|
70
|
+
strength_explicit_wan_deny: 2.5,
|
71
|
+
strength_restricted_mgmt_access: 4.0,
|
72
|
+
strength_restricted_ssh_access: 4.0,
|
73
|
+
strength_no_insecure_services_allowed: 2.5,
|
74
|
+
strength_granular_lan_outbound: 1.5,
|
75
|
+
strength_specific_wan_inbound_rule: 0.3,
|
76
|
+
strength_simulated_attack_explicitly_blocked: 1.5,
|
77
|
+
strength_simulated_exfiltration_blocked: 1.5,
|
78
|
+
strength_simulated_legitimate_allowed: 0.8,
|
79
|
+
strength_all_external_attacks_prevented_overall: 2.0,
|
80
|
+
|
81
|
+
weakness_no_explicit_wan_deny: -1.5,
|
82
|
+
weakness_wan_mgmt_from_any: -15.0,
|
83
|
+
weakness_wan_ssh_from_any: -15.0,
|
84
|
+
weakness_overly_permissive_wan: -20.0,
|
85
|
+
weakness_insecure_service_allowed: -4.0,
|
86
|
+
weakness_broad_lan_outbound: -1.0,
|
87
|
+
weakness_simulated_attack_allowed: -12.0,
|
88
|
+
weakness_simulated_attack_implicitly_blocked: -1.0,
|
89
|
+
weakness_simulated_exfiltration_allowed: -10.0,
|
90
|
+
weakness_simulated_legitimate_blocked: -3.0,
|
91
|
+
weakness_no_rules_found: -25.0,
|
92
|
+
weakness_stateless_rule: -1.8,
|
93
|
+
weakness_nat_insecure_service: -6.0
|
94
|
+
}
|
95
|
+
|
96
|
+
@max_raw_score_contribution = @weights.values.select { |v| v > 0 }.sum
|
97
|
+
@min_raw_score_contribution = @weights.values.select { |v| v < 0 }.sum
|
98
|
+
@score_range_buffer = 0.5
|
99
|
+
end
|
100
|
+
|
101
|
+
def detect_firewall_type_from_xml(xml_doc)
|
102
|
+
@supported_firewall_types.each do |type, info|
|
103
|
+
if info[:indicators].all? { |xpath| xml_doc.at_xpath(xpath) }
|
104
|
+
return type
|
105
|
+
end
|
106
|
+
end
|
107
|
+
'UNKNOWN'
|
108
|
+
end
|
109
|
+
|
110
|
+
def extract_interfaces_from_xml(xml_doc)
|
111
|
+
interfaces = {}
|
112
|
+
xml_doc.xpath('//interfaces/*/ipaddr').each do |ip_node|
|
113
|
+
interface_name = ip_node.parent.name
|
114
|
+
interfaces[interface_name.to_sym] = {
|
115
|
+
ip: ip_node.content || '',
|
116
|
+
net: ip_node.parent.at_xpath('subnet')&.content || ''
|
117
|
+
}
|
118
|
+
end
|
119
|
+
interfaces
|
120
|
+
end
|
121
|
+
|
122
|
+
def extract_system_ip_from_xml(xml_doc)
|
123
|
+
(xml_doc.at_xpath('//system/wan/ipaddr')&.content ||
|
124
|
+
xml_doc.at_xpath('//system/general/hostname')&.content || '')
|
125
|
+
end
|
126
|
+
|
127
|
+
def extract_aliases_from_xml(xml_doc)
|
128
|
+
aliases = {}
|
129
|
+
xml_doc.xpath('//aliases/alias').each do |a|
|
130
|
+
name = a.at_xpath('name')&.content
|
131
|
+
type = a.at_xpath('type')&.content
|
132
|
+
address = a.at_xpath('address')&.content || ''
|
133
|
+
descr = a.at_xpath('descr')&.content || ''
|
134
|
+
next unless name
|
135
|
+
aliases[name] = { type: type, address: address.split(/\s+/), description: descr }
|
136
|
+
end
|
137
|
+
aliases
|
138
|
+
end
|
139
|
+
|
140
|
+
def extract_schedules_from_xml(xml_doc)
|
141
|
+
schedules = {}
|
142
|
+
xml_doc.xpath('//schedules/schedule').each do |s|
|
143
|
+
name = s.at_xpath('name')&.content
|
144
|
+
descr = s.at_xpath('descr')&.content || ''
|
145
|
+
times = s.at_xpath('times')&.content || ''
|
146
|
+
weekdays = s.at_xpath('weekdays')&.content || ''
|
147
|
+
months = s.at_xpath('months')&.content || ''
|
148
|
+
next unless name
|
149
|
+
schedules[name] = { descr: descr, times: times, weekdays: weekdays, months: months }
|
150
|
+
end
|
151
|
+
schedules
|
152
|
+
end
|
153
|
+
|
154
|
+
def extract_nat_rules_from_xml(xml_doc, aliases)
|
155
|
+
nat_rules = []
|
156
|
+
xml_doc.xpath('//nat/rule').each_with_index do |rule_node, index|
|
157
|
+
rule_id = rule_node.at_xpath('id')&.content || "nat_rule_#{index + 1}"
|
158
|
+
action = 'ALLOW'
|
159
|
+
interface = rule_node.at_xpath('interface')&.content || 'any'
|
160
|
+
direction = 'in'
|
161
|
+
protocol = rule_node.at_xpath('protocol')&.content || 'any'
|
162
|
+
|
163
|
+
source_node = rule_node.at_xpath('source')
|
164
|
+
source = resolve_alias(source_node&.at_xpath('address')&.content, aliases) || 'any'
|
165
|
+
|
166
|
+
destination_node = rule_node.at_xpath('destination')
|
167
|
+
destination = resolve_alias(destination_node&.at_xpath('address')&.content, aliases) || 'any'
|
168
|
+
|
169
|
+
dport = rule_node.at_xpath('destination/port')&.content&.to_i
|
170
|
+
dport = resolve_alias(dport.to_s, aliases).to_i if dport && aliases.key?(dport.to_s)
|
171
|
+
comment = rule_node.at_xpath('descr')&.content
|
172
|
+
quick = true
|
173
|
+
schedule = nil
|
174
|
+
gateway = rule_node.at_xpath('gateway')&.content
|
175
|
+
state_type = 'keep state'
|
176
|
+
|
177
|
+
nat_rules << Rule.new(rule_id, action, interface, direction, protocol, source, destination, dport, comment, quick, schedule, gateway, state_type, true)
|
178
|
+
end
|
179
|
+
nat_rules
|
180
|
+
end
|
181
|
+
|
182
|
+
def resolve_alias(value, aliases)
|
183
|
+
return value unless value && aliases.key?(value)
|
184
|
+
aliases[value][:address].first
|
185
|
+
end
|
186
|
+
|
187
|
+
def parse_config_from_xml(xml_doc, aliases)
|
188
|
+
rules = []
|
189
|
+
xml_doc.xpath('//filter/rule').each_with_index do |rule_node, index|
|
190
|
+
rule_id = rule_node.at_xpath('id')&.content || rule_node.at_xpath('descr')&.content || "xml_rule_#{index + 1}"
|
191
|
+
action = rule_node.at_xpath('type')&.content == 'block' ? 'DENY' : 'ALLOW'
|
192
|
+
interface = rule_node.at_xpath('interface')&.content || 'any'
|
193
|
+
direction = rule_node.at_xpath('direction')&.content || 'in'
|
194
|
+
protocol = rule_node.at_xpath('protocol')&.content || 'any'
|
195
|
+
|
196
|
+
source_node = rule_node.at_xpath('source/network') || rule_node.at_xpath('source/address')
|
197
|
+
source = resolve_alias(source_node&.content, aliases) || 'any'
|
198
|
+
source = 'any' if rule_node.at_xpath('source/any')
|
199
|
+
|
200
|
+
destination_node = rule_node.at_xpath('destination/network') || rule_node.at_xpath('destination/address')
|
201
|
+
destination = resolve_alias(destination_node&.content, aliases) || 'any'
|
202
|
+
destination = 'any' if rule_node.at_xpath('destination/any')
|
203
|
+
|
204
|
+
dport = rule_node.at_xpath('destination/port')&.content&.to_i
|
205
|
+
dport = resolve_alias(dport.to_s, aliases).to_i if dport && aliases.key?(dport.to_s)
|
206
|
+
comment = rule_node.at_xpath('descr')&.content
|
207
|
+
quick = rule_node.at_xpath('quick')&.content == 'on'
|
208
|
+
schedule = rule_node.at_xpath('sched')&.content
|
209
|
+
gateway = rule_node.at_xpath('gateway')&.content
|
210
|
+
state_type = rule_node.at_xpath('statetype')&.content || 'keep state'
|
211
|
+
|
212
|
+
rules << Rule.new(rule_id, action, interface, direction, protocol, source, destination, dport, comment, quick, schedule, gateway, state_type, false)
|
213
|
+
end
|
214
|
+
rules
|
215
|
+
end
|
216
|
+
|
217
|
+
def match_ip_or_network(packet_ip_str, rule_ip_or_network_str, interface_ips)
|
218
|
+
packet_ip_str = packet_ip_str.to_s
|
219
|
+
rule_ip_or_network_str = rule_ip_or_network_str.to_s
|
220
|
+
return true if rule_ip_or_network_str == 'any'
|
221
|
+
return true if packet_ip_str == rule_ip_or_network_str
|
222
|
+
|
223
|
+
if rule_ip_or_network_str == 'self'
|
224
|
+
return true if packet_ip_str == interface_ips[:wan_ip] || packet_ip_str == interface_ips[:lan_ip]
|
225
|
+
end
|
226
|
+
|
227
|
+
if rule_ip_or_network_str.include?('/')
|
228
|
+
rule_base_ip, rule_cidr_mask = rule_ip_or_network_str.split('/')
|
229
|
+
if rule_cidr_mask == '24' && !rule_base_ip.empty?
|
230
|
+
packet_octets = packet_ip_str.split('.')
|
231
|
+
rule_octets = rule_base_ip.split('.')
|
232
|
+
return packet_octets.size >= 3 && rule_octets.size >= 3 && packet_octets[0..2].join('.') == rule_octets[0..2].join('.')
|
233
|
+
end
|
234
|
+
end
|
235
|
+
|
236
|
+
false
|
237
|
+
end
|
238
|
+
|
239
|
+
def simulate_connection_attempt(rules, packet, system_meta)
|
240
|
+
interface_ips = {
|
241
|
+
wan_ip: system_meta[:interfaces][:wan][:ip],
|
242
|
+
lan_ip: system_meta[:interfaces][:lan][:ip]
|
243
|
+
}
|
244
|
+
|
245
|
+
rules.each do |rule|
|
246
|
+
next unless rule.interface == 'any' || rule.interface == packet[:interface]
|
247
|
+
next unless rule.direction == 'any' || rule.direction == packet[:direction]
|
248
|
+
next unless rule.protocol == 'any' || rule.protocol == packet[:protocol]
|
249
|
+
next unless match_ip_or_network(packet[:src_ip], rule.source, interface_ips)
|
250
|
+
next unless match_ip_or_network(packet[:dst_ip], rule.destination, interface_ips)
|
251
|
+
|
252
|
+
if rule.dport && packet[:dst_port]
|
253
|
+
next unless rule.dport == packet[:dst_port]
|
254
|
+
elsif rule.dport && !packet[:dst_port]
|
255
|
+
next
|
256
|
+
end
|
257
|
+
|
258
|
+
return rule.action if rule.quick
|
259
|
+
return rule.action
|
260
|
+
end
|
261
|
+
|
262
|
+
'IMPLICITLY_BLOCKED'
|
263
|
+
end
|
264
|
+
|
265
|
+
def calculate_entropy(strings)
|
266
|
+
text = strings.join('').downcase
|
267
|
+
freq = Hash.new(0)
|
268
|
+
text.each_char { |c| freq[c] += 1 }
|
269
|
+
total = text.length.to_f
|
270
|
+
return 0 if total == 0
|
271
|
+
-freq.values.map { |count| (count / total) * Math.log2(count / total) }.sum
|
272
|
+
end
|
273
|
+
|
274
|
+
def calculate_defense_depth(rules)
|
275
|
+
interface_count = rules.map(&:interface).uniq.size
|
276
|
+
action_diversity = rules.map(&:action).uniq.size
|
277
|
+
protocol_diversity = rules.map(&:protocol).uniq.size
|
278
|
+
port_specificity = rules.count { |r| r.dport && r.dport > 0 } / rules.size.to_f
|
279
|
+
|
280
|
+
score = (interface_count / 5.0) * 1.0 +
|
281
|
+
(action_diversity / 2.0) * 1.0 +
|
282
|
+
(protocol_diversity / 5.0) * 1.0 +
|
283
|
+
port_specificity * 2.0
|
284
|
+
|
285
|
+
[[score, 5.0].min, 0.0].max
|
286
|
+
end
|
287
|
+
|
288
|
+
def audit_rules(rules, firewall_type, system_meta, aliases, nat_rules)
|
289
|
+
strengths = []
|
290
|
+
weaknesses = []
|
291
|
+
raw_score = 0.0
|
292
|
+
simulated_attacks = []
|
293
|
+
|
294
|
+
system_ip = system_meta[:system_ip].to_s.downcase
|
295
|
+
lan_net = system_meta[:interfaces][:lan][:net].to_s.downcase rescue nil
|
296
|
+
wan_ip = system_meta[:interfaces][:wan][:ip].to_s.downcase rescue nil
|
297
|
+
|
298
|
+
# --- Explicit WAN Deny ---
|
299
|
+
wan_explicit_block_all = rules.any? do |r|
|
300
|
+
r.interface == 'wan' && r.direction == 'in' && r.action == 'DENY' &&
|
301
|
+
r.protocol == 'any' && r.source == 'any' && r.destination == 'any'
|
302
|
+
end
|
303
|
+
|
304
|
+
if wan_explicit_block_all
|
305
|
+
strengths << "Explicit 'DENY all' inbound rule on WAN detected, enhancing clarity and reinforcing default deny."
|
306
|
+
raw_score += @weights[:strength_explicit_wan_deny]
|
307
|
+
else
|
308
|
+
weaknesses << "No explicit 'DENY all' inbound rule on WAN. Relying on implicit deny can lead to oversight."
|
309
|
+
raw_score += @weights[:weakness_no_explicit_wan_deny]
|
310
|
+
end
|
311
|
+
|
312
|
+
broad_lan_outbound_weakness_added = false
|
313
|
+
critical_weakness_types_found = Set.new
|
314
|
+
|
315
|
+
rules.each do |rule|
|
316
|
+
if rule.action == 'ALLOW' && rule.direction == 'in' &&
|
317
|
+
(rule.destination == 'self' || rule.destination == system_ip || rule.destination == wan_ip) &&
|
318
|
+
[80, 443].include?(rule.dport)
|
319
|
+
|
320
|
+
if rule.interface == 'wan' && rule.source == 'any'
|
321
|
+
weaknesses << "Rule [ID:#{rule.id}] allows firewall management (HTTP/HTTPS) from 'any' source on WAN. **Critical risk!**"
|
322
|
+
raw_score += @weights[:weakness_wan_mgmt_from_any]
|
323
|
+
critical_weakness_types_found << :wan_mgmt_from_any
|
324
|
+
elsif rule.interface == 'wan' && rule.source != 'any'
|
325
|
+
strengths << "Rule [ID:#{rule.id}] restricts firewall management access to specific trusted sources on WAN."
|
326
|
+
raw_score += @weights[:strength_restricted_mgmt_access]
|
327
|
+
end
|
328
|
+
end
|
329
|
+
|
330
|
+
if rule.action == 'ALLOW' && rule.direction == 'in' &&
|
331
|
+
(rule.destination == 'self' || rule.destination == system_ip || rule.destination == wan_ip) &&
|
332
|
+
rule.dport == 22
|
333
|
+
|
334
|
+
if rule.interface == 'wan' && rule.source == 'any'
|
335
|
+
weaknesses << "Rule [ID:#{rule.id}] allows SSH access to firewall from 'any' source on WAN. **Critical risk!**"
|
336
|
+
raw_score += @weights[:weakness_wan_ssh_from_any]
|
337
|
+
critical_weakness_types_found << :wan_ssh_from_any
|
338
|
+
elsif rule.interface == 'wan' && rule.source != 'any'
|
339
|
+
strengths << "Rule [ID:#{rule.id}] restricts SSH access to firewall to specific trusted sources on WAN."
|
340
|
+
raw_score += @weights[:strength_restricted_ssh_access]
|
341
|
+
end
|
342
|
+
end
|
343
|
+
|
344
|
+
if rule.action == 'ALLOW' && rule.interface == 'wan' && rule.direction == 'in' &&
|
345
|
+
(rule.source == 'any' || rule.source.nil?) && (rule.destination == 'any' || rule.destination.nil?) &&
|
346
|
+
rule.protocol == 'any'
|
347
|
+
weaknesses << "Rule [ID:#{rule.id}] is an overly permissive 'ALLOW' rule from WAN to 'any' destination. **Major vulnerability!**"
|
348
|
+
raw_score += @weights[:weakness_overly_permissive_wan]
|
349
|
+
critical_weakness_types_found << :overly_permissive_wan
|
350
|
+
end
|
351
|
+
|
352
|
+
if rule.action == 'ALLOW' && rule.direction == 'in' && [21, 23, 445, 139].include?(rule.dport)
|
353
|
+
weaknesses << "Rule [ID:#{rule.id}] on interface '#{rule.interface}' allows insecure service (port #{rule.dport}). Consider disabling or securing alternatives."
|
354
|
+
raw_score += @weights[:weakness_insecure_service_allowed]
|
355
|
+
critical_weakness_types_found << :insecure_service_allowed
|
356
|
+
end
|
357
|
+
|
358
|
+
if rule.action == 'ALLOW' && rule.interface == 'lan' && rule.direction == 'out' &&
|
359
|
+
(rule.source == lan_net || rule.source == 'any') && rule.destination == 'any' && rule.protocol == 'any'
|
360
|
+
unless broad_lan_outbound_weakness_added
|
361
|
+
weaknesses << "A broad 'ALLOW all' outbound rule from LAN exists. Review to ensure no unnecessary egress traffic."
|
362
|
+
raw_score += @weights[:weakness_broad_lan_outbound]
|
363
|
+
broad_lan_outbound_weakness_added = true
|
364
|
+
end
|
365
|
+
end
|
366
|
+
|
367
|
+
if rule.action == 'ALLOW' && rule.interface == 'wan' && rule.direction == 'in' && rule.dport &&
|
368
|
+
![80, 443, 22, 21, 23, 445, 139].include?(rule.dport)
|
369
|
+
if rule.source != 'any' && rule.destination != 'any' && rule.protocol != 'any'
|
370
|
+
strengths << "Rule [ID:#{rule.id}] provides granular access for a specific service (Port #{rule.dport})."
|
371
|
+
raw_score += @weights[:strength_specific_wan_inbound_rule]
|
372
|
+
end
|
373
|
+
end
|
374
|
+
end
|
375
|
+
|
376
|
+
unless broad_lan_outbound_weakness_added
|
377
|
+
strengths << "LAN outbound rules appear granular, promoting better control over egress."
|
378
|
+
raw_score += @weights[:strength_granular_lan_outbound]
|
379
|
+
end
|
380
|
+
|
381
|
+
if rules.empty? && firewall_type != 'UNKNOWN'
|
382
|
+
weaknesses << "No firewall rules found in the configuration. This implies an 'allow all' or unknown state."
|
383
|
+
raw_score += @weights[:weakness_no_rules_found]
|
384
|
+
critical_weakness_types_found << :no_rules_found
|
385
|
+
end
|
386
|
+
|
387
|
+
# --- Simulated Attack Scenarios ---
|
388
|
+
require 'set'
|
389
|
+
simulated_scenarios = [
|
390
|
+
{ name: "WAN to LAN SSH (Port 22)",
|
391
|
+
packet: { src_ip: '203.0.113.1', dst_ip: '10.0.0.100', dst_port: 22, protocol: 'tcp', interface: 'wan', direction: 'in' }, type: :attack, randomize_port: false },
|
392
|
+
{ name: "WAN to LAN RDP (Port 3389)",
|
393
|
+
packet: { src_ip: '203.0.113.1', dst_ip: '10.0.0.100', dst_port: 3389, protocol: 'tcp', interface: 'wan', direction: 'in' }, type: :attack, randomize_port: false },
|
394
|
+
{ name: "WAN to Firewall HTTP Management (Port 80)",
|
395
|
+
packet: { src_ip: '203.0.113.1', dst_ip: system_ip, dst_port: 80, protocol: 'tcp', interface: 'wan', direction: 'in' }, type: :attack, randomize_port: false },
|
396
|
+
{ name: "WAN to Firewall HTTPS Management (Port 443)",
|
397
|
+
packet: { src_ip: '203.0.113.1', dst_ip: system_ip, dst_port: 443, protocol: 'tcp', interface: 'wan', direction: 'in' }, type: :attack, randomize_port: false },
|
398
|
+
{ name: "WAN to Internal FTP Server (Port 21)",
|
399
|
+
packet: { src_ip: '203.0.113.1', dst_ip: '10.0.0.200', dst_port: 21, protocol: 'tcp', interface: 'wan', direction: 'in' }, type: :attack, randomize_port: false },
|
400
|
+
{ name: "WAN to Internal SMB Share (Port 445)",
|
401
|
+
packet: { src_ip: '203.0.113.1', dst_ip: '10.0.0.200', dst_port: 445, protocol: 'tcp', interface: 'wan', direction: 'in' }, type: :attack, randomize_port: false },
|
402
|
+
{ name: "LAN to External Web (Port 80 - Expected Allowed)",
|
403
|
+
packet: { src_ip: '10.0.0.50', dst_ip: '8.8.8.8', dst_port: 80, protocol: 'tcp', interface: 'lan', direction: 'out' }, type: :legitimate, randomize_port: false },
|
404
|
+
{ name: "WAN to Firewall SSH Brute-force (Random Port)",
|
405
|
+
packet: { src_ip: '185.10.10.10', dst_ip: system_ip, protocol: 'tcp', interface: 'wan', direction: 'in' }, type: :attack, randomize_port: true, base_port: 22 },
|
406
|
+
{ name: "LAN to External Exfiltration (Random High Port)",
|
407
|
+
packet: { src_ip: '10.0.0.100', dst_ip: '1.2.3.4', protocol: 'tcp', interface: 'lan', direction: 'out' }, type: :exfiltration, randomize_port: true, port_range: (49152..65535) },
|
408
|
+
{ name: "DMZ to LAN Database Access (Random Port)",
|
409
|
+
packet: { src_ip: '172.16.0.50', dst_ip: '10.0.0.150', protocol: 'tcp', interface: 'dmz', direction: 'in' }, type: :attack, randomize_port: true, base_port: 1433 },
|
410
|
+
{ name: "WAN to Internal Reverse Shell (Random High Port)",
|
411
|
+
packet: { src_ip: '203.0.113.1', dst_ip: '10.0.0.100', protocol: 'tcp', interface: 'wan', direction: 'in' }, type: :attack, randomize_port: true, port_range: (49152..65535) },
|
412
|
+
{ name: "WAN to Internal Netcat Listener (Random High Port)",
|
413
|
+
packet: { src_ip: '203.0.113.1', dst_ip: '10.0.0.100', protocol: 'tcp', interface: 'wan', direction: 'in' }, type: :attack, randomize_port: true, port_range: (49152..65535) },
|
414
|
+
]
|
415
|
+
|
416
|
+
wan_dmz_attacks_prevented_count = 0
|
417
|
+
wan_dmz_total_attacks_in_scenarios = 0
|
418
|
+
|
419
|
+
simulated_scenarios.each do |scenario|
|
420
|
+
num_loops = scenario[:randomize_port] ? 5 : 1
|
421
|
+
scenario_outcomes = []
|
422
|
+
scenario_allowed_any_time = false
|
423
|
+
|
424
|
+
num_loops.times do
|
425
|
+
current_packet = scenario[:packet].dup
|
426
|
+
if scenario[:randomize_port]
|
427
|
+
if scenario[:base_port]
|
428
|
+
current_packet[:dst_port] = [1, [scenario[:base_port] - 100 + rand(201), 65535].min].max
|
429
|
+
elsif scenario[:port_range]
|
430
|
+
current_packet[:dst_port] = rand(scenario[:port_range])
|
431
|
+
else
|
432
|
+
current_packet[:dst_port] = rand(1024..65535)
|
433
|
+
end
|
434
|
+
scenario_name_with_port = "#{scenario[:name]} (Port #{current_packet[:dst_port]})"
|
435
|
+
else
|
436
|
+
scenario_name_with_port = scenario[:name]
|
437
|
+
end
|
438
|
+
|
439
|
+
outcome = simulate_connection_attempt(rules + nat_rules, current_packet, system_meta)
|
440
|
+
scenario_outcomes << "#{scenario_name_with_port}: #{outcome}"
|
441
|
+
scenario_allowed_any_time = true if outcome == 'ALLOW'
|
442
|
+
end
|
443
|
+
|
444
|
+
simulated_attacks.concat(scenario_outcomes)
|
445
|
+
|
446
|
+
is_external_attack_scenario = (scenario[:name].include?("WAN to") || scenario[:name].include?("DMZ to")) && scenario[:type] == :attack
|
447
|
+
is_exfiltration_scenario = scenario[:type] == :exfiltration
|
448
|
+
is_legitimate_scenario = scenario[:type] == :legitimate
|
449
|
+
|
450
|
+
if is_external_attack_scenario
|
451
|
+
wan_dmz_total_attacks_in_scenarios += 1
|
452
|
+
end
|
453
|
+
|
454
|
+
if scenario_allowed_any_time
|
455
|
+
if is_legitimate_scenario
|
456
|
+
strengths << "Simulated legitimate traffic: '#{scenario[:name]}' was ALLOWED (as expected) in at least one test."
|
457
|
+
raw_score += @weights[:strength_simulated_legitimate_allowed]
|
458
|
+
elsif is_external_attack_scenario
|
459
|
+
weaknesses << "Simulated attack: '#{scenario[:name]}' was ALLOWED in at least one randomized test. **Major exposure!**"
|
460
|
+
raw_score += @weights[:weakness_simulated_attack_allowed]
|
461
|
+
critical_weakness_types_found << :simulated_attack_allowed
|
462
|
+
elsif is_exfiltration_scenario
|
463
|
+
weaknesses << "Simulated exfiltration: '#{scenario[:name]}' was ALLOWED in at least one randomized test. Review outbound rules for data leakage prevention."
|
464
|
+
raw_score += @weights[:weakness_simulated_exfiltration_allowed]
|
465
|
+
critical_weakness_types_found << :simulated_exfiltration_allowed
|
466
|
+
end
|
467
|
+
else
|
468
|
+
if is_legitimate_scenario
|
469
|
+
weaknesses << "Simulated legitimate traffic: '#{scenario[:name]}' was BLOCKED/IMPLICITLY_BLOCKED unexpectedly in all tests. This might indicate a functional issue."
|
470
|
+
raw_score += @weights[:weakness_simulated_legitimate_blocked]
|
471
|
+
elsif is_external_attack_scenario
|
472
|
+
if scenario_outcomes.any? { |o| o.include?('DENY') }
|
473
|
+
strengths << "Simulated attack: '#{scenario[:name]}' was EXPLICITLY_BLOCKED in tests (strong security)."
|
474
|
+
raw_score += @weights[:strength_simulated_attack_explicitly_blocked]
|
475
|
+
else
|
476
|
+
weaknesses << "Simulated attack: '#{scenario[:name]}' was IMPLICITLY_BLOCKED in all tests. Consider explicit block rules for clarity and robustness."
|
477
|
+
raw_score += @weights[:weakness_simulated_attack_implicitly_blocked]
|
478
|
+
end
|
479
|
+
wan_dmz_attacks_prevented_count += 1
|
480
|
+
elsif is_exfiltration_scenario
|
481
|
+
if scenario_outcomes.any? { |o| o.include?('DENY') }
|
482
|
+
strengths << "Simulated exfiltration: '#{scenario[:name]}' was BLOCKED in tests. Good for data leakage prevention."
|
483
|
+
raw_score += @weights[:strength_simulated_exfiltration_blocked]
|
484
|
+
else
|
485
|
+
weaknesses << "Simulated exfiltration: '#{scenario[:name]}' was IMPLICITLY_BLOCKED. Consider explicit block rules for data leakage prevention."
|
486
|
+
raw_score += @weights[:weakness_simulated_exfiltration_allowed]
|
487
|
+
end
|
488
|
+
end
|
489
|
+
end
|
490
|
+
end
|
491
|
+
|
492
|
+
if wan_dmz_total_attacks_in_scenarios > 0 && wan_dmz_attacks_prevented_count == wan_dmz_total_attacks_in_scenarios
|
493
|
+
strengths << "All simulated external attack attempts were successfully prevented (either explicitly or implicitly blocked)."
|
494
|
+
raw_score += @weights[:strength_all_external_attacks_prevented_overall]
|
495
|
+
end
|
496
|
+
|
497
|
+
raw_score += (critical_weakness_types_found.size * -5.0)
|
498
|
+
|
499
|
+
effective_min_raw_score = @min_raw_score_contribution - @score_range_buffer
|
500
|
+
effective_max_raw_score = @max_raw_score_contribution + @score_range_buffer
|
501
|
+
range = effective_max_raw_score - effective_min_raw_score
|
502
|
+
normalized_score = range.abs < 1e-9 ? 0.5 : (raw_score - effective_min_raw_score) / range
|
503
|
+
final_score = (normalized_score * 9) + 1
|
504
|
+
final_score = [1.0, [final_score, 10.0].min].max
|
505
|
+
|
506
|
+
framework_assessments = _assess_frameworks(strengths, weaknesses, final_score)
|
507
|
+
|
508
|
+
{ strengths: strengths, weaknesses: weaknesses, score: final_score, simulated_attacks: simulated_attacks, framework_assessments: framework_assessments }
|
509
|
+
end
|
510
|
+
|
511
|
+
def _assess_frameworks(strengths, weaknesses, score)
|
512
|
+
assessments = {}
|
513
|
+
|
514
|
+
weakness_categories = {
|
515
|
+
critical_exposure: weaknesses.any? { |w| w.include?('**Critical risk!**') || w.include?('**Major vulnerability!**') ||
|
516
|
+
(w.include?('Simulated attack:') && w.include?('ALLOW')) ||
|
517
|
+
(w.include?('Simulated exfiltration:') && w.include?('ALLOW')) },
|
518
|
+
insecure_services: weaknesses.any? { |w| w.include?('insecure service') && w.include?('ALLOW') },
|
519
|
+
broad_wan_rules: weaknesses.any? { |w| w.include?('overly permissive') && w.include?('WAN') },
|
520
|
+
no_explicit_deny: weaknesses.any? { |w| w.include?('No explicit \'DENY all\' inbound rule on WAN') },
|
521
|
+
functional_issues: weaknesses.any? { |w| w.include?('functional issue') },
|
522
|
+
no_rules_at_all: weaknesses.any? { |w| w.include?('No firewall rules found in the configuration.') },
|
523
|
+
nat_vulnerability: weaknesses.any? { |w| w.include?('NAT Rule') && (w.include?('insecure service') || w.include?('sensitive service')) }
|
524
|
+
}
|
525
|
+
|
526
|
+
# --- NIST CSF ---
|
527
|
+
nist_score = 5.0
|
528
|
+
nist_reasons = []
|
529
|
+
nist_reasons << "Overall security score is low (#{sprintf("%.2f", score)}/10)." if score < 6.0
|
530
|
+
if weakness_categories[:critical_exposure] || weakness_categories[:nat_vulnerability]
|
531
|
+
nist_score -= 2.0
|
532
|
+
nist_reasons << "Critical exposures (e.g., exposed management, allowed simulated attacks) detected."
|
533
|
+
end
|
534
|
+
if weakness_categories[:no_explicit_deny]
|
535
|
+
nist_score -= 1.0
|
536
|
+
nist_reasons << "Lack of explicit 'DENY all' inbound rule on WAN."
|
537
|
+
end
|
538
|
+
if weakness_categories[:insecure_services]
|
539
|
+
nist_score -= 1.5
|
540
|
+
nist_reasons << "Insecure services are allowed."
|
541
|
+
end
|
542
|
+
if weakness_categories[:functional_issues]
|
543
|
+
nist_score -= 0.5
|
544
|
+
nist_reasons << "Functional issues detected, potentially impacting system availability."
|
545
|
+
end
|
546
|
+
nist_score = [1.0, nist_score].max
|
547
|
+
nist_status = nist_score >= 3.0 ? 'Pass' : 'Fail'
|
548
|
+
assessments['NIST CSF'] = {
|
549
|
+
status: nist_status,
|
550
|
+
score: nist_score.round(1),
|
551
|
+
reason: nist_reasons.empty? ? 'Generally aligns with NIST CSF principles.' : 'Significant weaknesses in core protective controls and risk management.',
|
552
|
+
reason_details: nist_reasons
|
553
|
+
}
|
554
|
+
|
555
|
+
# --- ISO/IEC 27001 ---
|
556
|
+
iso_score = 5.0
|
557
|
+
iso_reasons = []
|
558
|
+
if weakness_categories[:critical_exposure]
|
559
|
+
iso_score -= 2.0
|
560
|
+
iso_reasons << "Critical exposures impacting information security objectives."
|
561
|
+
end
|
562
|
+
if weakness_categories[:insecure_services]
|
563
|
+
iso_score -= 1.5
|
564
|
+
iso_reasons << "Insecure services are allowed, violating control objectives."
|
565
|
+
end
|
566
|
+
if weakness_categories[:broad_wan_rules]
|
567
|
+
iso_score -= 1.0
|
568
|
+
iso_reasons << "Overly broad WAN rules reduce control effectiveness."
|
569
|
+
end
|
570
|
+
if weakness_categories[:no_rules_at_all]
|
571
|
+
iso_score -= 3.0
|
572
|
+
iso_reasons << "No firewall rules found, indicating a lack of basic security controls."
|
573
|
+
end
|
574
|
+
iso_score = [1.0, iso_score].max
|
575
|
+
iso_status = iso_score >= 3.0 ? 'Pass' : 'Fail'
|
576
|
+
assessments['ISO/IEC 27001'] = {
|
577
|
+
status: iso_status,
|
578
|
+
score: iso_score.round(1),
|
579
|
+
reason: iso_reasons.empty? ? 'Basic technical controls appear to be in place.' : 'Fundamental information security controls are not adequately implemented.',
|
580
|
+
reason_details: iso_reasons
|
581
|
+
}
|
582
|
+
|
583
|
+
# --- CIS Controls ---
|
584
|
+
cis_score = 5.0
|
585
|
+
cis_reasons = []
|
586
|
+
if weakness_categories[:critical_exposure] || weakness_categories[:nat_vulnerability]
|
587
|
+
cis_score -= 2.5
|
588
|
+
cis_reasons << "Violations of critical security controls (e.g., exposed management, allowed attacks)."
|
589
|
+
end
|
590
|
+
if weakness_categories[:insecure_services]
|
591
|
+
cis_score -= 1.5
|
592
|
+
cis_reasons << "Failure to block insecure services (CIS Control 1)."
|
593
|
+
end
|
594
|
+
if weakness_categories[:no_explicit_deny]
|
595
|
+
cis_score -= 1.0
|
596
|
+
cis_reasons << "Lack of explicit deny-all rule (CIS Control 9)."
|
597
|
+
end
|
598
|
+
if weakness_categories[:broad_wan_rules]
|
599
|
+
cis_score -= 1.0
|
600
|
+
cis_reasons << "Overly permissive inbound rules (CIS Control 9)."
|
601
|
+
end
|
602
|
+
cis_score = [1.0, cis_score].max
|
603
|
+
cis_status = cis_score >= 3.5 ? 'Pass' : 'Fail'
|
604
|
+
assessments['CIS Controls'] = {
|
605
|
+
status: cis_status,
|
606
|
+
score: cis_score.round(1),
|
607
|
+
reason: cis_reasons.empty? ? 'Adheres to many foundational CIS Controls.' : 'Violations of critical security controls identified.',
|
608
|
+
reason_details: cis_reasons
|
609
|
+
}
|
610
|
+
|
611
|
+
# --- PCI DSS ---
|
612
|
+
pci_score = 5.0
|
613
|
+
pci_reasons = []
|
614
|
+
if weakness_categories[:insecure_services]
|
615
|
+
pci_score -= 3.0
|
616
|
+
pci_reasons << "Insecure services (e.g., FTP, Telnet, SMB) are allowed, which is a direct PCI DSS violation."
|
617
|
+
end
|
618
|
+
if weakness_categories[:broad_wan_rules]
|
619
|
+
pci_score -= 2.0
|
620
|
+
pci_reasons << "Overly permissive WAN rules violate PCI DSS requirement for strict access control."
|
621
|
+
end
|
622
|
+
if weakness_categories[:critical_exposure] || weakness_categories[:nat_vulnerability]
|
623
|
+
pci_score -= 2.0
|
624
|
+
pci_reasons << "Critical exposures (e.g., allowed simulated attacks) indicate insufficient segmentation/access controls."
|
625
|
+
end
|
626
|
+
pci_score = [1.0, pci_score].max
|
627
|
+
pci_status = pci_score >= 4.0 ? 'Pass' : 'Fail'
|
628
|
+
assessments['PCI DSS'] = {
|
629
|
+
status: pci_status,
|
630
|
+
score: pci_score.round(1),
|
631
|
+
reason: pci_reasons.empty? ? 'No obvious firewall-related PCI DSS violations detected.' : '**Highly likely to fail PCI DSS.** Critical vulnerabilities present.',
|
632
|
+
reason_details: pci_reasons
|
633
|
+
}
|
634
|
+
|
635
|
+
# --- SOC 2 ---
|
636
|
+
soc2_score = 5.0
|
637
|
+
soc2_reasons = []
|
638
|
+
if weakness_categories[:critical_exposure]
|
639
|
+
soc2_score -= 2.0
|
640
|
+
soc2_reasons << "Critical exposures impact security and confidentiality criteria."
|
641
|
+
end
|
642
|
+
if weakness_categories[:broad_wan_rules]
|
643
|
+
soc2_score -= 1.0
|
644
|
+
soc2_reasons << "Broad WAN rules impact security and processing integrity."
|
645
|
+
end
|
646
|
+
if weakness_categories[:functional_issues]
|
647
|
+
soc2_score -= 0.8
|
648
|
+
soc2_reasons << "Functional issues (e.g., blocked legitimate traffic) impact availability."
|
649
|
+
end
|
650
|
+
if weakness_categories[:insecure_services]
|
651
|
+
soc2_score -= 1.2
|
652
|
+
soc2_reasons << "Allowing insecure services violates confidentiality and processing integrity."
|
653
|
+
end
|
654
|
+
soc2_score = [1.0, soc2_score].max
|
655
|
+
soc2_status = soc2_score >= 3.5 ? 'Pass' : 'Fail'
|
656
|
+
assessments['SOC 2'] = {
|
657
|
+
status: soc2_status,
|
658
|
+
score: soc2_score.round(1),
|
659
|
+
reason: soc2_reasons.empty? ? 'Basic security controls appear adequate.' : 'Significant control deficiencies related to Trust Services Criteria.',
|
660
|
+
reason_details: soc2_reasons
|
661
|
+
}
|
662
|
+
|
663
|
+
# --- COBIT 2019 ---
|
664
|
+
cobit_score = 5.0
|
665
|
+
cobit_reasons = []
|
666
|
+
if score < 6.0
|
667
|
+
cobit_score -= 1.0
|
668
|
+
cobit_reasons << "Overall security score is low (#{sprintf("%.2f", score)}/10)."
|
669
|
+
end
|
670
|
+
if weakness_categories[:no_explicit_deny]
|
671
|
+
cobit_score -= 1.0
|
672
|
+
cobit_reasons << "Lack of explicit deny policy impacts governance over network access."
|
673
|
+
end
|
674
|
+
if weakness_categories[:broad_wan_rules]
|
675
|
+
cobit_score -= 1.5
|
676
|
+
cobit_reasons << "Overly permissive rules indicate poor risk management and control design."
|
677
|
+
end
|
678
|
+
if weakness_categories[:critical_exposure]
|
679
|
+
cobit_score -= 2.0
|
680
|
+
cobit_reasons << "Critical exposures reflect failure in MEA (Monitor, Evaluate, and Assess) processes."
|
681
|
+
end
|
682
|
+
if weakness_categories[:insecure_services]
|
683
|
+
cobit_score -= 1.0
|
684
|
+
cobit_reasons << "Allowing insecure services violates DSS05 (Managed Security Services)."
|
685
|
+
end
|
686
|
+
cobit_score = [1.0, cobit_score].max
|
687
|
+
cobit_status = cobit_score >= 3.0 ? 'Pass' : 'Fail'
|
688
|
+
assessments['COBIT 2019'] = {
|
689
|
+
status: cobit_status,
|
690
|
+
score: cobit_score.round(1),
|
691
|
+
reason: cobit_reasons.empty? ? 'Basic governance of network security is present.' : 'Significant gaps in IT governance and control framework implementation.',
|
692
|
+
reason_details: cobit_reasons
|
693
|
+
}
|
694
|
+
|
695
|
+
assessments
|
696
|
+
end
|
697
|
+
|
698
|
+
def conduct_audit(config_file_path)
|
699
|
+
puts "Starting BlueWall audit..."
|
700
|
+
unless File.exist?(config_file_path)
|
701
|
+
puts "Error: Configuration file not found at '#{config_file_path}'"
|
702
|
+
return AuditResult.new('N/A', [], [], ["Configuration file not found: #{config_file_path}"], 1.0, 'Audit failed.', [], {})
|
703
|
+
end
|
704
|
+
|
705
|
+
begin
|
706
|
+
xml_content = File.read(config_file_path)
|
707
|
+
xml_doc = Nokogiri::XML(xml_content) { |c| c.options = Nokogiri::XML::ParseOptions::NOBLANKS }
|
708
|
+
|
709
|
+
system_meta = {
|
710
|
+
interfaces: extract_interfaces_from_xml(xml_doc),
|
711
|
+
system_ip: extract_system_ip_from_xml(xml_doc)
|
712
|
+
}
|
713
|
+
|
714
|
+
firewall_type = detect_firewall_type_from_xml(xml_doc)
|
715
|
+
if firewall_type == 'UNKNOWN'
|
716
|
+
return AuditResult.new(firewall_type, [], [], ['Unrecognized XML structure.'], 1.0, 'Cannot perform audit.', [], {})
|
717
|
+
end
|
718
|
+
|
719
|
+
puts "Detected firewall type: #{firewall_type}"
|
720
|
+
|
721
|
+
aliases = extract_aliases_from_xml(xml_doc)
|
722
|
+
schedules = extract_schedules_from_xml(xml_doc)
|
723
|
+
nat_rules = extract_nat_rules_from_xml(xml_doc, aliases)
|
724
|
+
rules = parse_config_from_xml(xml_doc, aliases)
|
725
|
+
|
726
|
+
puts "Parsed #{rules.count} firewall rules, #{aliases.size} aliases, #{nat_rules.size} NAT rules."
|
727
|
+
|
728
|
+
if rules.empty?
|
729
|
+
return AuditResult.new(firewall_type, [], [], ['No firewall rules found.'], 1.0, 'Cannot audit empty ruleset.', [], {})
|
730
|
+
end
|
731
|
+
|
732
|
+
audit_findings = audit_rules(rules, firewall_type, system_meta, aliases, nat_rules)
|
733
|
+
details = "BlueWall audit completed based on common cybersecurity principles tailored for #{firewall_type} (inspired by CIS Controls and NIST CSF). Score reflects adherence to least privilege, rule specificity, handling of insecure services, and interface-specific security."
|
734
|
+
|
735
|
+
AuditResult.new(
|
736
|
+
firewall_type, rules, audit_findings[:strengths], audit_findings[:weaknesses],
|
737
|
+
audit_findings[:score], details, audit_findings[:simulated_attacks], audit_findings[:framework_assessments]
|
738
|
+
)
|
739
|
+
rescue Nokogiri::XML::SyntaxError => e
|
740
|
+
puts "Error parsing XML: #{e.message}"
|
741
|
+
AuditResult.new('N/A', [], [], ["XML error: #{e.message}"], 1.0, 'Parse failed.', [], {})
|
742
|
+
rescue StandardError => e
|
743
|
+
puts "Unexpected error: #{e.message}"
|
744
|
+
AuditResult.new('ERROR', [], [], ["Unexpected error: #{e.message}"], 1.0, 'Audit failed.', [], {})
|
745
|
+
end
|
746
|
+
end
|
747
|
+
end
|
748
|
+
|
749
|
+
# --- Main Execution ---
|
750
|
+
if ARGV.empty?
|
751
|
+
puts "Usage: ruby bluewall.rb <config.xml>"
|
752
|
+
exit(1)
|
753
|
+
end
|
754
|
+
|
755
|
+
config_file = ARGV[0]
|
756
|
+
auditor = BlueWall.new
|
757
|
+
audit_result = auditor.conduct_audit(config_file)
|
758
|
+
|
759
|
+
puts audit_result.to_s.split("--- BlueWall Audit Report ---").first
|
760
|
+
|
761
|
+
puts "\n---------------------------"
|
762
|
+
puts "Framework Compliance Assessment (Simplified):"
|
763
|
+
audit_result.framework_assessments.each do |framework, result|
|
764
|
+
status = result[:status] == 'Pass' ? "\e[32mPASS\e[0m" : "\e[31mFAIL\e[0m"
|
765
|
+
puts " - #{framework}: #{status} - #{result[:reason]}"
|
766
|
+
|
767
|
+
end
|
768
|
+
|
769
|
+
print "\nWould you like to see the detailed framework assessment in the console? (yes/no): "
|
770
|
+
$stdout.flush
|
771
|
+
user_detail_choice = STDIN.gets.chomp.downcase
|
772
|
+
if user_detail_choice == 'yes'
|
773
|
+
puts "\n" + "="*80
|
774
|
+
puts " DETAILED FRAMEWORK COMPLIANCE ASSESSMENT"
|
775
|
+
puts "="*80
|
776
|
+
|
777
|
+
audit_result.framework_assessments.each do |framework, result|
|
778
|
+
score = sprintf("%.1f", result[:score])
|
779
|
+
status = result[:status]
|
780
|
+
status_color = status == 'Pass' ? "\e[32m#{status}\e[0m" : "\e[31m#{status}\e[0m"
|
781
|
+
bar_width = 20
|
782
|
+
filled = (score.to_f / 5.0 * bar_width).round
|
783
|
+
progress_bar = "#" * filled + "-" * (bar_width - filled)
|
784
|
+
score_bar = "[#{progress_bar}] #{score}/5.0"
|
785
|
+
|
786
|
+
puts "\n[Framework] #{framework}"
|
787
|
+
puts " Score: #{score_bar}"
|
788
|
+
puts " Status: #{status_color}"
|
789
|
+
puts " Summary: #{result[:reason]}"
|
790
|
+
|
791
|
+
unless result[:reason_details].empty?
|
792
|
+
puts " Breakdown:"
|
793
|
+
result[:reason_details].each do |detail|
|
794
|
+
# Add plain-text indicators based on content
|
795
|
+
indicator = if detail.include?("Critical") || detail.include?("Major")
|
796
|
+
"[CRITICAL]"
|
797
|
+
elsif detail.include?("Lack") || detail.include?("No")
|
798
|
+
"[WARNING]"
|
799
|
+
elsif detail.include?("violates") || detail.include?("exposure")
|
800
|
+
"[FAIL]"
|
801
|
+
elsif detail.include?("Good") || detail.include?("adheres")
|
802
|
+
"[PASS]"
|
803
|
+
else
|
804
|
+
" -"
|
805
|
+
end
|
806
|
+
puts " #{indicator} #{detail}"
|
807
|
+
end
|
808
|
+
end
|
809
|
+
|
810
|
+
# Add mitigation advice for failed frameworks
|
811
|
+
if status == 'Fail'
|
812
|
+
puts " Recommendations:"
|
813
|
+
case framework
|
814
|
+
when 'NIST CSF'
|
815
|
+
puts " • Implement explicit deny-all inbound rules on WAN"
|
816
|
+
puts " • Harden management access (SSH/HTTP) from external sources"
|
817
|
+
puts " • Review and block insecure services (e.g., FTP, Telnet)"
|
818
|
+
when 'CIS Controls'
|
819
|
+
puts " • Enforce least privilege in firewall rules"
|
820
|
+
puts " • Enable logging and monitoring of rule hits"
|
821
|
+
puts " • Apply 'quick' rules for high-priority blocks"
|
822
|
+
when 'PCI DSS'
|
823
|
+
puts " • Disable or restrict access to insecure services (ports 21, 139, 445)"
|
824
|
+
puts " • Implement strict segmentation between cardholder and other zones"
|
825
|
+
puts " • Conduct regular firewall rule reviews"
|
826
|
+
when 'SOC 2'
|
827
|
+
puts " • Document firewall change management process"
|
828
|
+
puts " • Ensure availability of legitimate traffic"
|
829
|
+
puts " • Implement automated rule cleanup policies"
|
830
|
+
when 'COBIT 2019'
|
831
|
+
puts " • Align firewall policy with governance objectives"
|
832
|
+
puts " • Define ownership and review cycles for rules"
|
833
|
+
puts " • Integrate firewall audits into risk management"
|
834
|
+
else
|
835
|
+
puts " • Review rule specificity and default deny posture"
|
836
|
+
puts " • Limit broad ALLOW rules from untrusted interfaces"
|
837
|
+
end
|
838
|
+
end
|
839
|
+
end
|
840
|
+
|
841
|
+
# Summary stats
|
842
|
+
total = audit_result.framework_assessments.size
|
843
|
+
passed = audit_result.framework_assessments.count { |_, r| r[:status] == 'Pass' }
|
844
|
+
failed = total - passed
|
845
|
+
compliance_rate = ((passed.to_f / total) * 100).round(1)
|
846
|
+
|
847
|
+
puts "\n" + "-"*50
|
848
|
+
puts "Compliance Summary:"
|
849
|
+
puts " Passed: #{passed} framework#{passed == 1 ? '' : 's'}"
|
850
|
+
puts " Failed: #{failed} framework#{failed == 1 ? '' : 's'}"
|
851
|
+
puts " Overall Compliance: #{compliance_rate}%"
|
852
|
+
puts "-"*50
|
853
|
+
|
854
|
+
# Final advisory
|
855
|
+
if failed > 0
|
856
|
+
puts "\nACTION REQUIRED: #{failed} framework#{failed == 1 ? ' has' : 's have'} critical gaps. Review recommendations above."
|
857
|
+
else
|
858
|
+
puts "\nEXCELLENT: All frameworks meet minimum compliance thresholds."
|
859
|
+
end
|
860
|
+
end
|
861
|
+
|
862
|
+
print "
|
863
|
+
Would you like to save this report as an HTML file with a score graph? (yes/no): "
|
864
|
+
$stdout.flush
|
865
|
+
user_html_choice = STDIN.gets.chomp.downcase
|
866
|
+
if user_html_choice == 'yes'
|
867
|
+
html_filename = "bluewall_report_#{Time.now.strftime('%Y%m%d_%H%M%S')}.html"
|
868
|
+
pre_content = audit_result.to_s.split("--- BlueWall Audit Report ---").first.strip
|
869
|
+
.gsub('<', '<').gsub('>', '>').gsub('&', '&').gsub('`', '\\`')
|
870
|
+
total_strengths = audit_result.strengths.count
|
871
|
+
total_weaknesses = audit_result.weaknesses.count
|
872
|
+
total_sim_blocked = audit_result.simulated_attacks.count { |a| !a.include?('ALLOW') }
|
873
|
+
total_sim_allowed = audit_result.simulated_attacks.count { |a| a.include?('ALLOW') }
|
874
|
+
framework_data = audit_result.framework_assessments.map do |name, result|
|
875
|
+
{
|
876
|
+
name: name,
|
877
|
+
score: result[:score].to_f,
|
878
|
+
status: result[:status],
|
879
|
+
reason: result[:reason].gsub(/'/, "\\'"),
|
880
|
+
details: result[:reason_details].map { |d| d.gsub(/'/, "\\'") }
|
881
|
+
}
|
882
|
+
end
|
883
|
+
defense_depth = auditor.calculate_defense_depth(audit_result.rules)
|
884
|
+
entropy_score = auditor.calculate_entropy(audit_result.rules.map(&:to_s))
|
885
|
+
html_content = <<~HTML
|
886
|
+
<!DOCTYPE html>
|
887
|
+
<html lang="en">
|
888
|
+
<head>
|
889
|
+
<meta charset="UTF-8" />
|
890
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
891
|
+
<title>BlueWall Firewall Audit Report</title>
|
892
|
+
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
893
|
+
<style id="light-theme">
|
894
|
+
:root {
|
895
|
+
--bg: #f9fafa;
|
896
|
+
--text: #333;
|
897
|
+
--card-bg: #fff;
|
898
|
+
--header-bg: #1a3b5d;
|
899
|
+
--accent: #3498db;
|
900
|
+
--pass: #27ae60;
|
901
|
+
--fail: #e74c3c;
|
902
|
+
--warn: #f39c12;
|
903
|
+
--border: #ddd;
|
904
|
+
--pre-bg: #1e1e1e;
|
905
|
+
--pre-color: #00ff00;
|
906
|
+
}
|
907
|
+
body {
|
908
|
+
font-family: 'Segoe UI', sans-serif;
|
909
|
+
background-color: var(--bg);
|
910
|
+
color: var(--text);
|
911
|
+
margin: 0;
|
912
|
+
padding: 0;
|
913
|
+
line-height: 1.6;
|
914
|
+
transition: background-color 0.3s, color 0.3s;
|
915
|
+
}
|
916
|
+
header {
|
917
|
+
background: var(--header-bg);
|
918
|
+
color: white;
|
919
|
+
text-align: center;
|
920
|
+
padding: 20px;
|
921
|
+
border-bottom: 5px solid var(--accent);
|
922
|
+
}
|
923
|
+
.main-content {
|
924
|
+
max-width: 1200px;
|
925
|
+
margin: 20px auto;
|
926
|
+
padding: 20px;
|
927
|
+
background: var(--card-bg);
|
928
|
+
border-radius: 10px;
|
929
|
+
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
|
930
|
+
}
|
931
|
+
.tabs {
|
932
|
+
display: flex;
|
933
|
+
margin-bottom: 20px;
|
934
|
+
border-bottom: 1px solid var(--border);
|
935
|
+
}
|
936
|
+
.tab-button {
|
937
|
+
padding: 10px 20px;
|
938
|
+
cursor: pointer;
|
939
|
+
background: #eee;
|
940
|
+
border: 1px solid var(--border);
|
941
|
+
border-bottom: none;
|
942
|
+
border-radius: 5px 5px 0 0;
|
943
|
+
margin-right: 5px;
|
944
|
+
font-weight: 600;
|
945
|
+
}
|
946
|
+
.tab-button.active {
|
947
|
+
background: var(--accent);
|
948
|
+
color: white;
|
949
|
+
}
|
950
|
+
.tab-content {
|
951
|
+
display: none;
|
952
|
+
padding: 20px;
|
953
|
+
border: 1px solid var(--border);
|
954
|
+
border-radius: 5px;
|
955
|
+
background: #fafafa;
|
956
|
+
}
|
957
|
+
.tab-content.active {
|
958
|
+
display: block;
|
959
|
+
}
|
960
|
+
.chart-wrapper {
|
961
|
+
width: 100%;
|
962
|
+
height: 300px;
|
963
|
+
position: relative;
|
964
|
+
margin: 10px 0;
|
965
|
+
}
|
966
|
+
.chart-wrapper canvas {
|
967
|
+
width: 100% !important;
|
968
|
+
height: 100% !important;
|
969
|
+
}
|
970
|
+
.summary-box {
|
971
|
+
display: grid;
|
972
|
+
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
973
|
+
gap: 15px;
|
974
|
+
margin: 20px 0;
|
975
|
+
}
|
976
|
+
.box {
|
977
|
+
padding: 15px;
|
978
|
+
border-radius: 8px;
|
979
|
+
color: white;
|
980
|
+
font-weight: bold;
|
981
|
+
text-align: center;
|
982
|
+
}
|
983
|
+
.box.score { background: var(--accent); }
|
984
|
+
.box.pass { background: var(--pass); }
|
985
|
+
.box.fail { background: var(--fail); }
|
986
|
+
.box.warn { background: var(--warn); }
|
987
|
+
ul {
|
988
|
+
padding-left: 15px;
|
989
|
+
}
|
990
|
+
li {
|
991
|
+
margin: 5px 0;
|
992
|
+
}
|
993
|
+
pre {
|
994
|
+
background: var(--pre-bg);
|
995
|
+
color: var(--pre-color);
|
996
|
+
padding: 15px;
|
997
|
+
border-radius: 8px;
|
998
|
+
overflow-x: auto;
|
999
|
+
font-family: 'Courier New', monospace;
|
1000
|
+
white-space: pre;
|
1001
|
+
}
|
1002
|
+
footer {
|
1003
|
+
text-align: center;
|
1004
|
+
margin-top: 30px;
|
1005
|
+
color: #7f8c8d;
|
1006
|
+
font-size: 0.9em;
|
1007
|
+
}
|
1008
|
+
.theme-toggle {
|
1009
|
+
position: fixed;
|
1010
|
+
top: 20px;
|
1011
|
+
right: 20px;
|
1012
|
+
background: var(--accent);
|
1013
|
+
color: white;
|
1014
|
+
border: none;
|
1015
|
+
padding: 10px 15px;
|
1016
|
+
border-radius: 5px;
|
1017
|
+
cursor: pointer;
|
1018
|
+
font-weight: bold;
|
1019
|
+
z-index: 100;
|
1020
|
+
}
|
1021
|
+
.critical-risk {
|
1022
|
+
background: #fef6f6;
|
1023
|
+
border: 1px solid #e74c3c;
|
1024
|
+
border-radius: 8px;
|
1025
|
+
padding: 15px;
|
1026
|
+
margin: 20px 0;
|
1027
|
+
}
|
1028
|
+
.critical-risk h3 {
|
1029
|
+
color: #e74c3c;
|
1030
|
+
margin-top: 0;
|
1031
|
+
}
|
1032
|
+
.metrics-table {
|
1033
|
+
width: 100%;
|
1034
|
+
border-collapse: collapse;
|
1035
|
+
font-size: 0.9em;
|
1036
|
+
margin: 20px 0;
|
1037
|
+
}
|
1038
|
+
.metrics-table td {
|
1039
|
+
padding: 8px;
|
1040
|
+
border-bottom: 1px solid #ddd;
|
1041
|
+
}
|
1042
|
+
</style>
|
1043
|
+
<style id="dark-theme" disabled>
|
1044
|
+
:root {
|
1045
|
+
--bg: #121212;
|
1046
|
+
--text: #e0e0e0;
|
1047
|
+
--card-bg: #1e1e1e;
|
1048
|
+
--header-bg: #0d47a1;
|
1049
|
+
--accent: #1976d2;
|
1050
|
+
--pass: #4caf50;
|
1051
|
+
--fail: #f44336;
|
1052
|
+
--warn: #ff9800;
|
1053
|
+
--border: #444;
|
1054
|
+
--pre-bg: #000;
|
1055
|
+
--pre-color: #00ff00;
|
1056
|
+
}
|
1057
|
+
body {
|
1058
|
+
background-color: var(--bg);
|
1059
|
+
color: var(--text);
|
1060
|
+
}
|
1061
|
+
.tab-button {
|
1062
|
+
background: #333;
|
1063
|
+
color: white;
|
1064
|
+
border-color: var(--border);
|
1065
|
+
}
|
1066
|
+
.tab-button.active {
|
1067
|
+
background: var(--accent);
|
1068
|
+
}
|
1069
|
+
.tab-content {
|
1070
|
+
background: #2a2a2a;
|
1071
|
+
border-color: var(--border);
|
1072
|
+
}
|
1073
|
+
pre {
|
1074
|
+
background: var(--pre-bg);
|
1075
|
+
color: var(--pre-color);
|
1076
|
+
}
|
1077
|
+
.critical-risk {
|
1078
|
+
background: #380909;
|
1079
|
+
border-color: #c62828;
|
1080
|
+
}
|
1081
|
+
.critical-risk h3 {
|
1082
|
+
color: #e57373;
|
1083
|
+
}
|
1084
|
+
.metrics-table td {
|
1085
|
+
border-bottom: 1px solid #555;
|
1086
|
+
}
|
1087
|
+
</style>
|
1088
|
+
</head>
|
1089
|
+
<body>
|
1090
|
+
<button class="theme-toggle" onclick="toggleDarkMode()">🌙 Dark Mode</button>
|
1091
|
+
<header>
|
1092
|
+
<h1>🔐 BlueWall Firewall Audit Report</h1>
|
1093
|
+
<p>Generated on #{Time.now.strftime('%Y-%m-%d at %H:%M:%S')}</p>
|
1094
|
+
</header>
|
1095
|
+
<div class="main-content">
|
1096
|
+
<pre id="ascii-art"></pre>
|
1097
|
+
<div class="summary-box">
|
1098
|
+
<div class="box score">
|
1099
|
+
Overall Score<br><strong>#{sprintf("%.2f", audit_result.score)}/10</strong>
|
1100
|
+
</div>
|
1101
|
+
<div class="box pass">
|
1102
|
+
Strengths<br><strong>#{total_strengths}</strong>
|
1103
|
+
</div>
|
1104
|
+
<div class="box fail">
|
1105
|
+
Weaknesses<br><strong>#{total_weaknesses}</strong>
|
1106
|
+
</div>
|
1107
|
+
<div class="box warn">
|
1108
|
+
Type<br><strong>#{audit_result.firewall_type}</strong>
|
1109
|
+
</div>
|
1110
|
+
</div>
|
1111
|
+
<div class="tabs">
|
1112
|
+
<div class="tab-button active" onclick="openTab(event, 'overview')">📊 Overview</div>
|
1113
|
+
<div class="tab-button" onclick="openTab(event, 'frameworks')">🎯 Frameworks</div>
|
1114
|
+
<div class="tab-button" onclick="openTab(event, 'findings')">🔍 Findings</div>
|
1115
|
+
<div class="tab-button" onclick="openTab(event, 'simulations')">🧪 Simulations</div>
|
1116
|
+
</div>
|
1117
|
+
<div id="overview" class="tab-content active">
|
1118
|
+
<h2>Security Overview</h2>
|
1119
|
+
<div class="critical-risk" id="critical-risk-container" style="display: none;">
|
1120
|
+
<h3>🚨 Critical Risks</h3>
|
1121
|
+
<ul id="critical-risk-list"></ul>
|
1122
|
+
</div>
|
1123
|
+
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 25px; margin: 20px 0;">
|
1124
|
+
<div class="chart-wrapper"><canvas id="overallScoreChart"></canvas></div>
|
1125
|
+
<div class="chart-wrapper"><canvas id="findingsChart"></canvas></div>
|
1126
|
+
<div class="chart-wrapper"><canvas id="interfaceDistributionChart"></canvas></div>
|
1127
|
+
<div class="chart-wrapper"><canvas id="actionDistributionChart"></canvas></div>
|
1128
|
+
<div class="chart-wrapper"><canvas id="protocolDistributionChart"></canvas></div>
|
1129
|
+
<div class="chart-wrapper"><canvas id="topPortsChart"></canvas></div>
|
1130
|
+
</div>
|
1131
|
+
<div style="margin: 30px 0; padding: 20px; background: #f8f9fa; border-radius: 8px;">
|
1132
|
+
<h3>🔍 Advanced Security Metrics</h3>
|
1133
|
+
<table class="metrics-table">
|
1134
|
+
<tr><td><strong>Defense Depth Score</strong></td><td>#{sprintf("%.2f", defense_depth)}/5.0</td></tr>
|
1135
|
+
<tr><td><strong>Rule Entropy (Complexity)</strong></td><td>#{sprintf("%.2f", entropy_score)}/3.0</td></tr>
|
1136
|
+
<tr><td><strong>Scheduled Rules</strong></td><td>#{audit_result.rules.count { |r| r.schedule }} rules use time-based access control</td></tr>
|
1137
|
+
<tr><td><strong>Stateless Rules</strong></td><td>#{audit_result.rules.count { |r| r.state_type != 'keep state' }}</td></tr>
|
1138
|
+
<tr><td><strong>Quick Rules</strong></td><td>#{audit_result.rules.count { |r| r.quick }} rules use 'quick' evaluation</td></tr>
|
1139
|
+
</table>
|
1140
|
+
</div>
|
1141
|
+
</div>
|
1142
|
+
<div id="frameworks" class="tab-content">
|
1143
|
+
<h2>Framework Compliance Assessment</h2>
|
1144
|
+
<p>Each chart shows compliance score (out of 5).</p>
|
1145
|
+
<div id="frameworkCharts" style="display: flex; flex-wrap: wrap; justify-content: center; gap: 20px;">
|
1146
|
+
#{framework_data.map do |data|
|
1147
|
+
chart_id = data[:name].gsub(/\W+/, '_')
|
1148
|
+
status_class = data[:status] == 'Pass' ? 'pass' : 'fail'
|
1149
|
+
<<~HTML
|
1150
|
+
<div style="text-align:center; width: 30%;">
|
1151
|
+
<h3>#{data[:name]}</h3>
|
1152
|
+
<div class="chart-wrapper">
|
1153
|
+
<canvas id="#{chart_id}_chart"></canvas>
|
1154
|
+
</div>
|
1155
|
+
<p><strong>Status:</strong> <span class="#{status_class}">#{data[:status]}</span> (#{sprintf("%.1f", data[:score])}/5)</p>
|
1156
|
+
<p style="font-size:0.9em; color:#555;"><em>#{data[:reason]}</em></p>
|
1157
|
+
<details style="margin-top:10px; font-size:0.85em;">
|
1158
|
+
<summary>Why this score?</summary>
|
1159
|
+
<ul>
|
1160
|
+
#{data[:details].map { |d| "<li>#{d}</li>" }.join('')}
|
1161
|
+
</ul>
|
1162
|
+
</details>
|
1163
|
+
</div>
|
1164
|
+
HTML
|
1165
|
+
end.join("\n")}
|
1166
|
+
</div>
|
1167
|
+
</div>
|
1168
|
+
<div id="findings" class="tab-content">
|
1169
|
+
<h2>Strengths & Weaknesses</h2>
|
1170
|
+
<h3>✅ Strengths</h3>
|
1171
|
+
<ul>#{audit_result.strengths.map { |s| "<li>#{s}</li>" }.join('')}</ul>
|
1172
|
+
<h3>❌ Weaknesses</h3>
|
1173
|
+
<ul>#{audit_result.weaknesses.map { |w| "<li>#{w}</li>" }.join('')}</ul>
|
1174
|
+
</div>
|
1175
|
+
<div id="simulations" class="tab-content">
|
1176
|
+
<h2>Attack Simulation Results</h2>
|
1177
|
+
<div class="chart-wrapper"><canvas id="simulationsChart"></canvas></div>
|
1178
|
+
<ul>#{audit_result.simulated_attacks.map { |s| "<li>#{s}</li>" }.join('')}</ul>
|
1179
|
+
</div>
|
1180
|
+
<footer>
|
1181
|
+
Report generated by <strong>BlueWall</strong> — created by :cillia
|
1182
|
+
</footer>
|
1183
|
+
</div>
|
1184
|
+
<script>
|
1185
|
+
function toggleDarkMode() {
|
1186
|
+
const darkTheme = document.getElementById('dark-theme');
|
1187
|
+
const button = document.querySelector('.theme-toggle');
|
1188
|
+
if (darkTheme.disabled) {
|
1189
|
+
darkTheme.disabled = false;
|
1190
|
+
button.textContent = '☀️ Light Mode';
|
1191
|
+
localStorage.setItem('darkMode', 'enabled');
|
1192
|
+
} else {
|
1193
|
+
darkTheme.disabled = true;
|
1194
|
+
button.textContent = '🌙 Dark Mode';
|
1195
|
+
localStorage.setItem('darkMode', 'disabled');
|
1196
|
+
}
|
1197
|
+
}
|
1198
|
+
|
1199
|
+
window.addEventListener('DOMContentLoaded', () => {
|
1200
|
+
if (localStorage.getItem('darkMode') === 'enabled') {
|
1201
|
+
document.getElementById('dark-theme').disabled = false;
|
1202
|
+
document.querySelector('.theme-toggle').textContent = '☀️ Light Mode';
|
1203
|
+
}
|
1204
|
+
|
1205
|
+
const criticalRisks = #{JSON.generate(audit_result.weaknesses.select { |w| w.include?('**Critical risk!**') })};
|
1206
|
+
const container = document.getElementById('critical-risk-container');
|
1207
|
+
const list = document.getElementById('critical-risk-list');
|
1208
|
+
if (criticalRisks.length > 0) {
|
1209
|
+
container.style.display = 'block';
|
1210
|
+
criticalRisks.forEach(risk => {
|
1211
|
+
const li = document.createElement('li');
|
1212
|
+
li.innerHTML = '<strong>' + risk.replace(/\*\*/g, '') + '</strong>';
|
1213
|
+
list.appendChild(li);
|
1214
|
+
});
|
1215
|
+
}
|
1216
|
+
});
|
1217
|
+
|
1218
|
+
function openTab(evt, tabName) {
|
1219
|
+
document.querySelectorAll('.tab-content').forEach(t => t.classList.remove('active'));
|
1220
|
+
document.querySelectorAll('.tab-button').forEach(t => t.classList.remove('active'));
|
1221
|
+
document.getElementById(tabName).classList.add('active');
|
1222
|
+
evt.currentTarget.classList.add('active');
|
1223
|
+
}
|
1224
|
+
|
1225
|
+
document.addEventListener('DOMContentLoaded', function () {
|
1226
|
+
document.getElementById('ascii-art').textContent = `#{pre_content}`;
|
1227
|
+
|
1228
|
+
// 1. Overall Score
|
1229
|
+
new Chart(document.getElementById('overallScoreChart'), {
|
1230
|
+
type: 'bar',
|
1231
|
+
data: {
|
1232
|
+
labels: ['Security Score'],
|
1233
|
+
datasets: [{
|
1234
|
+
label: 'Score (1-10)',
|
1235
|
+
data: [#{audit_result.score}],
|
1236
|
+
backgroundColor: #{audit_result.score} >= 8 ? '#27ae60' : #{audit_result.score} >= 5 ? '#f39c12' : '#e74c3c',
|
1237
|
+
borderColor: '#2c3e50',
|
1238
|
+
borderWidth: 2
|
1239
|
+
}]
|
1240
|
+
},
|
1241
|
+
options: {
|
1242
|
+
responsive: true,
|
1243
|
+
maintainAspectRatio: false,
|
1244
|
+
indexAxis: 'y',
|
1245
|
+
scales: { x: { min: 0, max: 10 } },
|
1246
|
+
plugins: { legend: { display: false } }
|
1247
|
+
}
|
1248
|
+
});
|
1249
|
+
|
1250
|
+
// 2. Findings
|
1251
|
+
new Chart(document.getElementById('findingsChart'), {
|
1252
|
+
type: 'bar',
|
1253
|
+
data: {
|
1254
|
+
labels: ['Findings'],
|
1255
|
+
datasets: [
|
1256
|
+
{ label: 'Strengths', data: [#{total_strengths}], backgroundColor: '#27ae60', stack: 'stack0' },
|
1257
|
+
{ label: 'Weaknesses', data: [#{total_weaknesses}], backgroundColor: '#e74c3c', stack: 'stack0' }
|
1258
|
+
]
|
1259
|
+
},
|
1260
|
+
options: {
|
1261
|
+
responsive: true,
|
1262
|
+
maintainAspectRatio: false,
|
1263
|
+
scales: { x: { stacked: true }, y: { stacked: true, beginAtZero: true } },
|
1264
|
+
plugins: { legend: { position: 'top' } }
|
1265
|
+
}
|
1266
|
+
});
|
1267
|
+
|
1268
|
+
// 3. Interface Distribution
|
1269
|
+
const interfaceData = #{JSON.generate(audit_result.rules.group_by(&:interface).map { |k, v| [k.to_s, v.size] }.to_h)};
|
1270
|
+
new Chart(document.getElementById('interfaceDistributionChart'), {
|
1271
|
+
type: 'pie',
|
1272
|
+
data: {
|
1273
|
+
labels: Object.keys(interfaceData),
|
1274
|
+
datasets: [{
|
1275
|
+
data: Object.values(interfaceData),
|
1276
|
+
backgroundColor: ['#3498db', '#e74c3c', '#f39c12', '#9b59b6', '#1abc9c']
|
1277
|
+
}]
|
1278
|
+
},
|
1279
|
+
options: {
|
1280
|
+
responsive: true,
|
1281
|
+
maintainAspectRatio: false,
|
1282
|
+
plugins: { legend: { position: 'bottom' } }
|
1283
|
+
}
|
1284
|
+
});
|
1285
|
+
|
1286
|
+
// 4. Action Distribution
|
1287
|
+
const actionData = {
|
1288
|
+
ALLOW: #{audit_result.rules.count { |r| r.action == 'ALLOW' }},
|
1289
|
+
DENY: #{audit_result.rules.count { |r| r.action == 'DENY' }}
|
1290
|
+
};
|
1291
|
+
new Chart(document.getElementById('actionDistributionChart'), {
|
1292
|
+
type: 'bar',
|
1293
|
+
data: {
|
1294
|
+
labels: ['Actions'],
|
1295
|
+
datasets: [
|
1296
|
+
{ label: 'ALLOW', data: [actionData.ALLOW], backgroundColor: '#27ae60' },
|
1297
|
+
{ label: 'DENY', data: [actionData.DENY], backgroundColor: '#e74c3c' }
|
1298
|
+
]
|
1299
|
+
},
|
1300
|
+
options: {
|
1301
|
+
responsive: true,
|
1302
|
+
maintainAspectRatio: false,
|
1303
|
+
scales: { x: { stacked: true }, y: { stacked: true, beginAtZero: true } },
|
1304
|
+
plugins: { legend: { position: 'top' } }
|
1305
|
+
}
|
1306
|
+
});
|
1307
|
+
|
1308
|
+
// 5. Protocol Distribution
|
1309
|
+
const protocolData = #{JSON.generate(audit_result.rules.group_by(&:protocol).map { |k, v| [k.to_s, v.size] }.to_h)};
|
1310
|
+
new Chart(document.getElementById('protocolDistributionChart'), {
|
1311
|
+
type: 'doughnut',
|
1312
|
+
data: {
|
1313
|
+
labels: Object.keys(protocolData),
|
1314
|
+
datasets: [{
|
1315
|
+
data: Object.values(protocolData),
|
1316
|
+
backgroundColor: ['#3498db', '#e74c3c', '#f39c12', '#9b59b6']
|
1317
|
+
}]
|
1318
|
+
},
|
1319
|
+
options: {
|
1320
|
+
responsive: true,
|
1321
|
+
maintainAspectRatio: false,
|
1322
|
+
cutout: '60%',
|
1323
|
+
plugins: { legend: { position: 'bottom' } }
|
1324
|
+
}
|
1325
|
+
});
|
1326
|
+
|
1327
|
+
// 6. Top Ports
|
1328
|
+
const portCounts = #{JSON.generate(Hash.new(0).tap { |h| audit_result.rules.each { |r| h[r.dport || 'any'] += 1 if r.dport } }.sort_by { |k, v| -v }.take(8).to_h.transform_keys(&:to_s))};
|
1329
|
+
new Chart(document.getElementById('topPortsChart'), {
|
1330
|
+
type: 'bar',
|
1331
|
+
data: {
|
1332
|
+
labels: Object.keys(portCounts),
|
1333
|
+
datasets: [{
|
1334
|
+
label: 'Rule Count',
|
1335
|
+
data: Object.values(portCounts),
|
1336
|
+
backgroundColor: '#3498db'
|
1337
|
+
}]
|
1338
|
+
},
|
1339
|
+
options: {
|
1340
|
+
indexAxis: 'y',
|
1341
|
+
responsive: true,
|
1342
|
+
maintainAspectRatio: false,
|
1343
|
+
scales: { x: { beginAtZero: true } },
|
1344
|
+
plugins: { title: { display: true, text: 'Top Targeted Ports' } }
|
1345
|
+
}
|
1346
|
+
});
|
1347
|
+
|
1348
|
+
// 7. Simulations
|
1349
|
+
const totalSimBlocked = #{total_sim_blocked};
|
1350
|
+
const totalSimAllowed = #{total_sim_allowed};
|
1351
|
+
new Chart(document.getElementById('simulationsChart'), {
|
1352
|
+
type: 'pie',
|
1353
|
+
data: {
|
1354
|
+
labels: ['Blocked', 'Allowed'],
|
1355
|
+
datasets: [{
|
1356
|
+
data: [totalSimBlocked, totalSimAllowed],
|
1357
|
+
backgroundColor: ['#27ae60', '#e74c3c']
|
1358
|
+
}]
|
1359
|
+
},
|
1360
|
+
options: {
|
1361
|
+
responsive: true,
|
1362
|
+
maintainAspectRatio: false
|
1363
|
+
}
|
1364
|
+
});
|
1365
|
+
|
1366
|
+
// 8. Framework Charts
|
1367
|
+
#{framework_data.map do |data|
|
1368
|
+
chart_id = data[:name].gsub(/\W+/, '_')
|
1369
|
+
score = data[:score]
|
1370
|
+
remaining = (5.0 - score).round(2)
|
1371
|
+
<<~JS
|
1372
|
+
new Chart(document.getElementById('#{chart_id}_chart'), {
|
1373
|
+
type: 'doughnut',
|
1374
|
+
data: {
|
1375
|
+
labels: ['Score', 'Remaining'],
|
1376
|
+
datasets: [{
|
1377
|
+
data: [#{score}, #{remaining}],
|
1378
|
+
backgroundColor: ['#{data[:status] == 'Pass' ? '#27ae60' : '#e74c3c'}', '#ecf0f1']
|
1379
|
+
}]
|
1380
|
+
},
|
1381
|
+
options: {
|
1382
|
+
responsive: true,
|
1383
|
+
maintainAspectRatio: false,
|
1384
|
+
cutout: '70%',
|
1385
|
+
plugins: { legend: { display: false } }
|
1386
|
+
}
|
1387
|
+
});
|
1388
|
+
JS
|
1389
|
+
end.join("\n")}
|
1390
|
+
});
|
1391
|
+
</script>
|
1392
|
+
</body>
|
1393
|
+
</html>
|
1394
|
+
HTML
|
1395
|
+
|
1396
|
+
File.write(html_filename, html_content)
|
1397
|
+
puts "Interactive HTML report saved to #{html_filename}"
|
1398
|
+
else
|
1399
|
+
puts "HTML report not saved."
|
1400
|
+
end
|