heitt 0.4.5 → 0.4.6

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.
data/lib/heitt.rb CHANGED
@@ -1,367 +1,16 @@
1
- require 'json'
2
- require 'set'
3
- require 'strscan'
4
- require 'colorize'
1
+ #require 'json'
2
+ #require 'set'
3
+ #require 'strscan'
4
+ #require 'logger'
5
+ #require 'colorize'
6
+ require_relative 'heitt/utils'
7
+ require_relative 'heitt/profiles'
5
8
  require_relative 'heitt/database'
9
+ require_relative 'heitt/analyzer'
10
+ require_relative 'heitt/scanner'
11
+ require_relative 'heitt/grouper'
12
+ require_relative 'heitt/formatter'
6
13
  require_relative 'heitt/version'
7
14
 
8
- module HEITT
9
- module Color
10
- def self.colorize(text, color, *styles)
11
- return text unless STDOUT.isatty #&& !(defined?(Flags) && Flags.no_color)
12
15
 
13
- colored = text.colorize(color)
14
- styles.each do |style|
15
- colored = colored.send(style)
16
- end
17
- colored
18
- end
19
- end
20
- #private_constant :Color
21
-
22
-
23
- module Grouper
24
-
25
- def self.group(results)
26
- clusters = {}
27
-
28
- clusters = results.group_by {|r| r[:candidates].first[:name]}
29
- groups = clusters.each_with_index.map do |(name, group), index|
30
- hashes = group.map {|r| r[:hash]}
31
- {
32
- cluster_id: index+1,
33
- hashes: hashes,
34
- candidates: group.first[:candidates],
35
- count: hashes.size
36
- }
37
- end
38
- groups
39
- end
40
- end
41
-
42
-
43
-
44
- module Analyzer
45
- def self.analyze(text, database: HEITT::DATABASE)
46
- keyword_counts = keyword_counts(text.downcase, database: database)
47
- algorithm_scores(keyword_counts, database: database)
48
- end
49
-
50
- def self.extract_prefix(text, offset)
51
- line_start = text.rindex("\n", offset) || 0
52
- text[line_start...offset]
53
- end
54
-
55
- def self.high_entropy?(text, min_ent)
56
- entropy(text) >= min_ent
57
- end
58
-
59
-
60
- def self.score_candidates(modes, delim_prefix, context_scores)
61
- prefix_matched_mode = nil
62
- #context based scoring
63
- matches = modes.map do |mode|
64
- score_data = context_scores[mode[:name]]
65
- score = score_data || 0
66
-
67
- if prefix_match?(mode, delim_prefix)
68
- #boost score as confidence is high if prefix matched
69
- prefix_matched_mode = mode[:name]
70
- score += 20
71
- end
72
- {
73
- name: mode[:name],
74
- hashcat: mode[:hashcat],
75
- john: mode[:john],
76
- description: mode[:description],
77
- extended: mode[:extended],
78
- score: score
79
- }
80
- end
81
- return [] if matches.empty?
82
-
83
- #calculate confidence
84
- scores_hash = matches.map {|m| [m[:name], m[:score]]}.to_h
85
-
86
- confidences = assign_confidence(scores_hash, prefix_matched_mode)
87
- matches.map{|m| m.merge(confidence: confidences[m[:name]])}.sort_by {|m| -m[:score]}
88
- end
89
-
90
-
91
- private
92
- def self.get_modes(entry)
93
- entry[:modes] || entry[:algorithms] || entry[:hashes] ||
94
- entry[:candidates] || entry[:types] || entry[:hashtypes]
95
- end
96
-
97
- #this code is an inspiration of "https://github.com/chrisjchandler/entropy/blob/main/entropy.go"
98
- def self.entropy(text)
99
- frequency = Hash.new(0)
100
- text.each_char { |ch| frequency[ch] += 1 }
101
-
102
- #calculate the total number of characters
103
- total = text.length.to_f
104
- #caluclate entropy
105
- entropy = 0.0
106
- frequency.each_value do |count|
107
- probability = count.to_f / total
108
- entropy += probability * Math.log2(probability)
109
- end
110
- #negate the sum as entropy is positive
111
- -entropy
112
- end
113
-
114
- def self.keyword_counts(content_lower, database: HEITT::DATABASE)
115
- keywords = database.flat_map do |entry|
116
- modes = get_modes(entry)
117
- next [] unless modes
118
- modes.flat_map {|mode| mode[:context] || []}
119
- end.uniq.map(&:downcase)
120
-
121
- counts = {}
122
- keywords.each do |kw|
123
- count = content_lower.scan(/\b#{Regexp.escape(kw)}\b/).size
124
- counts[kw] = count if count > 0
125
- end
126
- counts
127
- end
128
-
129
-
130
- def self.algorithm_scores(keyword_counts, database: HEITT::DATABASE)
131
- scores = {}
132
- return scores if keyword_counts.nil?
133
-
134
- database.each do |entry|
135
- modes = get_modes(entry)
136
- next unless modes
137
- modes.each do |mode|
138
- contexts = mode[:context] || []
139
- next if contexts.empty?
140
- total = contexts.sum {|kw| keyword_counts[kw.downcase] || 0}
141
- scores[mode[:name]] = total if total > 0
142
- end
143
- end
144
- scores
145
- end
146
-
147
-
148
- def self.prefix_match?(mode, delim_prefix)
149
- prefixes = mode[:prefixes] || []
150
- return false if prefixes.empty?
151
-
152
- delimiters = "= : "
153
- raw_prefix = delim_prefix.strip.split(/[#{Regexp.escape(delimiters)}]/).last&.strip&.downcase
154
- prefixes.map(&:downcase).include?(raw_prefix)
155
- end
156
-
157
-
158
- def self.assign_confidence(scores_hash, prefix_matched_mode=nil)
159
- all_scores = scores_hash.values
160
-
161
- return {} if all_scores.empty?
162
-
163
- avg_score = all_scores.sum.to_f / all_scores.size
164
-
165
- scores_hash.transform_values do |score|
166
- if score == 0
167
- "regex-match"
168
- else
169
- mode_name = scores_hash.key(score)
170
- is_prefix_mode = (prefix_matched_mode == mode_name)
171
- deviation = (score - avg_score) / avg_score
172
-
173
- case deviation
174
- when 2.0..Float::INFINITY
175
- "high"
176
- when 0.5..2.0
177
- is_prefix_mode ? "high" : "medium-high"
178
- else
179
- is_prefix_mode ? "medium-high" : "medium-low"
180
- end
181
- end
182
- end
183
- end
184
- end
185
- #private_constant :Analyzer
186
-
187
-
188
-
189
- module Scanner
190
-
191
- def self.scan(input, database: HEITT::DATABASE, min_entropy: 3.5)
192
- text = File.exist?(input) ? File.read(input) : input
193
- context_scores = HEITT::Analyzer.analyze(text, database: database)
194
- found = {}#[]
195
- seen = {}
196
-
197
-
198
- database.each do |entry|
199
- regex = get_regex(entry)
200
- modes = get_modes(entry)
201
- next unless regex && modes && !modes.empty?
202
- pattern = regex.is_a?(Regexp) ? regex : Regexp.new(regex)
203
- scanner = StringScanner.new(text)
204
-
205
- while scanner.scan_until(pattern)
206
- matched = scanner.matched
207
- next unless matched.length < 8 || HEITT::Analyzer.high_entropy?(matched, min_entropy)
208
- offset = scanner.pos - matched.length
209
- delim_prefix = HEITT::Analyzer.extract_prefix(text, offset)
210
-
211
- candidates = HEITT::Analyzer.score_candidates(modes, delim_prefix, context_scores)
212
- score = candidates.first[:score]
213
-
214
- found[matched] ||= {hash: matched, candidates: []}
215
- found[matched][:candidates].concat(candidates)
216
- end
217
- end
218
-
219
- found.each_value do |result|
220
- result[:candidates] = result[:candidates]
221
- .group_by {|c| c[:name]}
222
- .map {|name, dupes| dupes.max_by {|c| c[:score]}}
223
- .sort_by {|c| -c[:score]}
224
-
225
- # Re-assign confidence based on final merged scores
226
- scores_hash = result[:candidates].map {|c| [c[:name], c[:score]]}.to_h
227
- confidences = Analyzer.assign_confidence(scores_hash)
228
- result[:candidates] = result[:candidates].map {|c| c.merge(confidence: confidences[c[:name]])}
229
- end
230
- found.values
231
- end
232
-
233
- private
234
-
235
- def self.get_regex(entry)
236
- entry[:extract_regex] || entry[:regex] || entry[:pattern] || entry[:regexp]
237
- end
238
-
239
- def self.get_modes(entry)
240
- entry[:modes] || entry[:algorithms] || entry[:hashes] ||
241
- entry[:candidates] || entry[:types] || entry[:hashtypes]
242
- end
243
- end
244
-
245
-
246
- module Formatter
247
-
248
-
249
- def self.tree(groups, verbose: false, extended: false, show_regex_match: false)
250
- result = ""
251
-
252
- #Filter out groups with extended candidates as true
253
- visible_groups = groups.select do |group|
254
- has_non_extended = group[:candidates].any? {|c| !c[:extended] || extended}
255
- has_non_regex = group[:candidates].any? {|c| c[:confidence] != "regex-match" || show_regex_match}
256
- has_non_extended && has_non_regex
257
- end
258
- #Renumber after filtering
259
- renumbered_groups= visible_groups.each_with_index.map { |group, index| group.merge(cluster_id: index + 1) }
260
-
261
- root = {
262
- text: "#{HEITT::Color.colorize("\n\n[", :bold, :blue)}#{HEITT::Color.colorize("CLUSTERED HASHES", :green)}#{HEITT::Color.colorize("]", :bold, :blue)}",
263
- children: renumbered_groups.map do |group|
264
- {
265
- text: HEITT::Color.colorize("HASH CLUSTER #{group[:cluster_id]}", :magenta, :bold),
266
- children: group[:hashes].map{|h| {text: h, children: []}}
267
- }
268
- end
269
- }
270
-
271
- result += render_tree([root])
272
-
273
- renumbered_groups.each do |group|
274
- result += "#{HEITT::Color.colorize("\n\n[", :bold, :blue)}#{HEITT::Color.colorize("HASH CLUSTER #{group[:cluster_id]}", :white, :bold)}#{HEITT::Color.colorize("]\n", :bold, :blue)}"#, children: []}
275
- candidate_nodes = (group[:candidates]).each_with_index.map do |candidate, idx|
276
- next if candidate.nil?
277
- next if candidate[:name].nil?
278
- next if candidate[:extended] && !extended
279
- next if candidate[:confidence] == "regex-match" && !show_regex_match
280
- confidence = candidate[:confidence] ? " — CONFIDENCE: #{candidate[:confidence].upcase}" : ""
281
-
282
- children = [
283
- {text: "Hashcat Mode: #{candidate[:hashcat] || "--"}", children: []},
284
- {text: "John Format: #{candidate[:john] || "--"}", children: []}
285
- ]
286
-
287
- if verbose
288
- if candidate[:description] && !candidate[:description].empty?
289
- children << {text: "Description: #{candidate[:description]}", children: []}
290
- end
291
-
292
- if candidate[:notes] && !candidate[:notes].empty?
293
- children << {text: "Notes:", children: candidate[:notes].map {|note| {text: note, children: []}}}
294
- end
295
- end
296
- {
297
- text: "#{HEITT::Color.colorize("[", :bold, :blue)}#{HEITT::Color.colorize("CANDIDATE #{idx + 1}: ", :bold, :cyan)}#{HEITT::Color.colorize("#{candidate[:name]}#{confidence}", :bold, :cyan)}#{HEITT::Color.colorize("]", :bold, :blue)}",
298
- children: children
299
- }
300
- end.compact
301
- result += render_tree(candidate_nodes, "", false, false) unless candidate_nodes.nil? || candidate_nodes.empty?
302
- end
303
- result
304
- end
305
-
306
- def self.json(groups, extended: false, show_regex_match: false)
307
- visible_groups = groups.select do |group|
308
- has_non_extended = group[:candidates].any? {|c| c[:extended] || extended}
309
- has_non_regex = group[:candidates].any? {|c| c[:confidence] != "regex-match" || show_regex_match}
310
- has_non_extended && has_non_regex
311
- end
312
- #Renumber after filtering
313
- renumbered_groups = visible_groups.each_with_index.map { |group, index| group.merge(cluster_id: index+1)}
314
-
315
- JSON.pretty_generate(
316
- renumbered_groups.map do |group|
317
- visible_candidates = group[:candidates].select do |c|
318
- (!c[:extended] || extended) && (c[:confidence] != "regex-match" || show_regex_match)
319
- end
320
- {
321
- cluster_id: group[:cluster_id],
322
- count: group[:count],
323
- hashes: group[:hashes],
324
- candidates: visible_candidates.map do |candidate|
325
- {
326
- name: candidate[:name],
327
- hashcat: candidate[:hashcat],
328
- john: candidate[:john],
329
- confidence: candidate[:confidence],
330
- description: candidate[:description]
331
- }
332
- end
333
- }
334
- end
335
- )
336
- end
337
-
338
- private
339
- def self.render_tree(items, prefix = "", parent_is_last=true, is_root=true)
340
- result = ""
341
-
342
- items.each_with_index do |node, i|
343
- is_last_item = (i == items.length - 1)
344
-
345
- line = if is_root
346
- "#{node[:text]}\n"
347
- else
348
- "#{HEITT::Color.colorize(prefix, :blue)}#{HEITT::Color.colorize((is_last_item ? '└── ' : '├── '), :blue)}#{node[:text]}\n"
349
- end
350
-
351
- child_prefix = if is_root
352
- ""
353
- else
354
- "#{HEITT::Color.colorize(prefix, :bold, :blue)}#{HEITT::Color.colorize((is_last_item ? " " : "│ "), :bold, :blue)}"
355
- end
356
- result += line
357
- result += render_tree(node[:children], child_prefix, is_last_item, false) if node[:children].any?
358
-
359
- if is_last_item && !is_root and !node[:children].any?
360
- result += "#{HEITT::Color.colorize(prefix, :bold, :blue)} \n"
361
- end
362
- end
363
- result
364
- end
365
- end
366
- end
367
16
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: heitt
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.5
4
+ version: 0.4.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jonathan Botchway Owusu
@@ -33,7 +33,13 @@ extra_rdoc_files: []
33
33
  files:
34
34
  - bin/heitt
35
35
  - lib/heitt.rb
36
+ - lib/heitt/analyzer.rb
36
37
  - lib/heitt/database.rb
38
+ - lib/heitt/formatter.rb
39
+ - lib/heitt/grouper.rb
40
+ - lib/heitt/profiles.rb
41
+ - lib/heitt/scanner.rb
42
+ - lib/heitt/utils.rb
37
43
  - lib/heitt/version.rb
38
44
  homepage: https://github.com/jobotow/heitt
39
45
  licenses: []