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.
- checksums.yaml +4 -4
- data/bin/heitt +29 -6
- data/lib/heitt/analyzer.rb +156 -0
- data/lib/heitt/database.rb +276 -1782
- data/lib/heitt/formatter.rb +125 -0
- data/lib/heitt/grouper.rb +22 -0
- data/lib/heitt/profiles.rb +184 -0
- data/lib/heitt/scanner.rb +65 -0
- data/lib/heitt/utils.rb +57 -0
- data/lib/heitt/version.rb +1 -1
- data/lib/heitt.rb +11 -362
- metadata +7 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2682eb074a7763036e54775706be2ae16221acd2d4cd7d416b387723e1b51aa2
|
|
4
|
+
data.tar.gz: 4739a64851b2e071217eed0b12e3e71327670a4abcdebd218fdb3020df4aa47a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d6084258bca3bc35f40aecf9402dffbace9a419561f886e02a73939e821a7369798d4878b3e6483b4293687082ff169f7206c5046d94fc64ad38085718c758f5
|
|
7
|
+
data.tar.gz: 362c46e00ba6dde92e1763e39b2f965e7782fb46a2073ab25974bcbb9d9213bfa03719b30b6747a145e9d8fdf8bc200e26a9c8df88c8c1022b4657147ed13f6e
|
data/bin/heitt
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
require 'optparse'
|
|
3
3
|
require 'io/console'
|
|
4
4
|
require 'heitt'
|
|
5
|
+
#require_relative '../lib/heitt'
|
|
5
6
|
|
|
6
7
|
|
|
7
8
|
|
|
@@ -47,6 +48,7 @@ module HEITT
|
|
|
47
48
|
opts.on("-V", "--verbose", "Show description and notes for each candidate") {@verbose = true}
|
|
48
49
|
opts.on("-j", "--json","Output in json format") {@json = true}
|
|
49
50
|
opts.on("-o", "--output FILEPATH", String, "File to write output to") {|v| @output = v}
|
|
51
|
+
opts.on("--debug", "Output in debug mode") {HEITT::Logger.enable_debug}
|
|
50
52
|
|
|
51
53
|
opts.separator ""
|
|
52
54
|
opts.separator "FILTERING OPTIONS:"
|
|
@@ -70,6 +72,7 @@ module HEITT
|
|
|
70
72
|
opts.separator ""
|
|
71
73
|
opts.separator "NOTES:"
|
|
72
74
|
opts.separator " JSON format is default when output is redirected or piped."
|
|
75
|
+
opts.separator " Custom database if merged with default database"
|
|
73
76
|
opts.separator " Regex-match candidates are hidden by default, use '-r' to show."
|
|
74
77
|
opts.separator " Running without input starts interactive mode."
|
|
75
78
|
opts.separator "#{header("=========================================================================")}"
|
|
@@ -79,9 +82,21 @@ module HEITT
|
|
|
79
82
|
|
|
80
83
|
@inputs = if ARGV.empty?
|
|
81
84
|
if $stdin.tty?
|
|
85
|
+
database = @database.empty? ? HEITT::DATABASE : load_custom_database(@database)
|
|
82
86
|
# Interactive mode
|
|
83
|
-
puts
|
|
84
|
-
|
|
87
|
+
puts header("=== HEITT INTERACTIVE MODE ====")
|
|
88
|
+
print "[debug:#{HEITT::Logger.debug_enabled? ? "on": "off"} || "
|
|
89
|
+
print "extended: #{@extended ? "on": "off"} || "
|
|
90
|
+
print "verbose: #{@verbose ? "on": "off"} || "
|
|
91
|
+
print "regex-match: #{@show_regex_match ? "on": "off"} || "
|
|
92
|
+
print "min-entropy: #{@min_entropy} || "
|
|
93
|
+
print "database: #{@database.empty? ? "default": database} || "
|
|
94
|
+
print "output-format: #{@json ? "json": "tree"}]\n"
|
|
95
|
+
puts ""
|
|
96
|
+
puts "Enter a hash or filepath. Press Ctrl+C or Enter to exit"
|
|
97
|
+
#puts "Extended: true" unless
|
|
98
|
+
# puts used flags here
|
|
99
|
+
|
|
85
100
|
loop do
|
|
86
101
|
print "heitt> "
|
|
87
102
|
input = $stdin.gets&.strip
|
|
@@ -110,11 +125,16 @@ module HEITT
|
|
|
110
125
|
end
|
|
111
126
|
|
|
112
127
|
def run
|
|
113
|
-
|
|
128
|
+
HEITT::Logger.debug("Loading database...")
|
|
129
|
+
database = @database.empty? ? HEITT::DATABASE : load_custom_database(@database)
|
|
130
|
+
HEITT::Logger.debug("Database (#{@database.empty? ? "default" : @database}) loaded")
|
|
114
131
|
|
|
115
132
|
format = @json || !$stdout.tty? ? :json : :tree
|
|
116
133
|
output = @inputs.map do |input|
|
|
134
|
+
HEITT::Logger.debug("Scanning #{input}...")
|
|
117
135
|
results = HEITT::Scanner.scan(input, database: database, min_entropy: @min_entropy)
|
|
136
|
+
HEITT::Logger.debug("Scanning completed (#{input})")
|
|
137
|
+
HEITT::Logger.debug("Grouping Results ...")
|
|
118
138
|
groups = HEITT::Grouper.group(results)
|
|
119
139
|
|
|
120
140
|
case format
|
|
@@ -139,16 +159,19 @@ module HEITT
|
|
|
139
159
|
end
|
|
140
160
|
|
|
141
161
|
|
|
142
|
-
|
|
162
|
+
|
|
163
|
+
def load_custom_database(file_path)
|
|
164
|
+
filepath = File.expand_path(file_path)
|
|
165
|
+
HEITT::Logger.debug("#{label} filepath: #{filepath}")
|
|
143
166
|
unless File.exist?(filepath)
|
|
144
|
-
|
|
167
|
+
HEITT::Logger.error("#{label} file #{filepath} does not exist")
|
|
145
168
|
exit(1)
|
|
146
169
|
end
|
|
147
170
|
begin
|
|
148
171
|
custom_database = JSON.parse(File.read(filepath), symbolize_names: true)
|
|
149
172
|
HEITT::DATABASE + custom_database
|
|
150
173
|
rescue JSON::ParserError => e
|
|
151
|
-
|
|
174
|
+
HEITT::Logger.error("Invalid JSON in #{label} file: #{e.message}")
|
|
152
175
|
exit(1)
|
|
153
176
|
end
|
|
154
177
|
end
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
require_relative 'utils'
|
|
2
|
+
require_relative 'profiles'
|
|
3
|
+
|
|
4
|
+
module HEITT
|
|
5
|
+
module Analyzer
|
|
6
|
+
def self.analyze(text, profiles: HEITT::PROFILES)#database: HEITT::DATABASE)
|
|
7
|
+
HEITT::Logger.debug("Counting keywords...")
|
|
8
|
+
keyword_counts = keyword_counts(text.downcase, profiles: profiles)
|
|
9
|
+
HEITT::Logger.debug("Counted keywords: #{keyword_counts}")
|
|
10
|
+
algorithm_scores(keyword_counts, profiles: profiles)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def self.extract_prefix(text, offset)
|
|
14
|
+
line_start = text.rindex("\n", offset) || 0
|
|
15
|
+
text[line_start...offset]
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def self.high_entropy?(text, min_ent)
|
|
19
|
+
entropy(text) >= min_ent
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def self.score_candidates(modes, delim_prefix, context_scores, profiles: HEITT::PROFILES)
|
|
24
|
+
prefix_matched_mode = nil
|
|
25
|
+
#context based scoring
|
|
26
|
+
matches = modes.map do |mode|
|
|
27
|
+
profile = profiles[mode[:name]] || {}
|
|
28
|
+
score = context_scores[mode[:name]] || 0
|
|
29
|
+
#score = score_data || 0
|
|
30
|
+
|
|
31
|
+
if prefix_match?(profile, delim_prefix)
|
|
32
|
+
#boost score as confidence is high if prefix matched
|
|
33
|
+
prefix_matched_mode = mode[:name]
|
|
34
|
+
score += 20
|
|
35
|
+
end
|
|
36
|
+
{
|
|
37
|
+
name: mode[:name],
|
|
38
|
+
hashcat: mode[:hashcat],
|
|
39
|
+
john: mode[:john],
|
|
40
|
+
extended: mode[:extended],
|
|
41
|
+
description: profile[:description],
|
|
42
|
+
score: score
|
|
43
|
+
}
|
|
44
|
+
end
|
|
45
|
+
return [] if matches.empty?
|
|
46
|
+
|
|
47
|
+
#calculate confidence
|
|
48
|
+
scores_hash = matches.map {|m| [m[:name], m[:score]]}.to_h
|
|
49
|
+
|
|
50
|
+
confidences = assign_confidence(scores_hash, prefix_matched_mode)
|
|
51
|
+
scored_candidates = matches.map{|m| m.merge(confidence: confidences[m[:name]])}.sort_by {|m| -m[:score]}
|
|
52
|
+
HEITT::Logger.debug("Scored Algorithm: #{scored_candidates.map{|s| s[:name] }} => Calculated Confidence: #{scored_candidates.map{|s| s[:confidence] }}")
|
|
53
|
+
scored_candidates
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
private
|
|
58
|
+
#def self.get_modes(entry)
|
|
59
|
+
# entry[:modes] || entry[:algorithms] || entry[:hashes] ||
|
|
60
|
+
# entry[:candidates] || entry[:types] || entry[:hashtypes]
|
|
61
|
+
#end
|
|
62
|
+
|
|
63
|
+
#this code is an inspiration of "https://github.com/chrisjchandler/entropy/blob/main/entropy.go"
|
|
64
|
+
def self.entropy(text)
|
|
65
|
+
frequency = Hash.new(0)
|
|
66
|
+
text.each_char { |ch| frequency[ch] += 1 }
|
|
67
|
+
|
|
68
|
+
#calculate the total number of characters
|
|
69
|
+
total = text.length.to_f
|
|
70
|
+
#caluclate entropy
|
|
71
|
+
entropy = 0.0
|
|
72
|
+
frequency.each_value do |count|
|
|
73
|
+
probability = count.to_f / total
|
|
74
|
+
entropy += probability * Math.log2(probability)
|
|
75
|
+
end
|
|
76
|
+
#negate the sum as entropy is positive
|
|
77
|
+
-entropy
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def self.keyword_counts(content_lower, profiles: HEITT::PROFILES) #HEITT::DATABASE)
|
|
81
|
+
keywords = profiles.values.flat_map { |p| p[:context] || [] }.uniq.map(&:downcase)
|
|
82
|
+
#database.flat_map do |entry|
|
|
83
|
+
#modes = get_modes(entry)
|
|
84
|
+
#next [] unless modes
|
|
85
|
+
#modes.flat_map {|mode| HEITT::PROFILES[mode[:name]][:context] || []}
|
|
86
|
+
#end#.uniq.map(&:downcase)
|
|
87
|
+
|
|
88
|
+
counts = {}
|
|
89
|
+
keywords.each do |kw|
|
|
90
|
+
count = content_lower.scan(/\b#{Regexp.escape(kw)}\b/).size
|
|
91
|
+
counts[kw] = count if count > 0
|
|
92
|
+
end
|
|
93
|
+
counts
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def self.algorithm_scores(keyword_counts, profiles: HEITT::PROFILES)#database: HEITT::DATABASE)
|
|
98
|
+
scores = {}
|
|
99
|
+
return scores if keyword_counts.nil?
|
|
100
|
+
|
|
101
|
+
profiles.each do |name, profile| #database.each do |entry|
|
|
102
|
+
context = profile[:context] || []
|
|
103
|
+
next if context.empty?
|
|
104
|
+
#modes = get_modes(entry)
|
|
105
|
+
#next unless modes
|
|
106
|
+
#modes.each do |mode|
|
|
107
|
+
# contexts = mode[:context] || []
|
|
108
|
+
# next if contexts.empty?
|
|
109
|
+
total = context.sum {|kw| keyword_counts[kw.downcase] || 0}
|
|
110
|
+
scores[name] = total if total > 0
|
|
111
|
+
# end
|
|
112
|
+
end
|
|
113
|
+
scores
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def self.prefix_match?(profile, delim_prefix)
|
|
118
|
+
#prefixes = mode[:prefixes] || []
|
|
119
|
+
prefixes = profile[:prefixes] || []
|
|
120
|
+
return false if prefixes.empty?
|
|
121
|
+
|
|
122
|
+
delimiters = "= : "
|
|
123
|
+
raw_prefix = delim_prefix.strip.split(/[#{Regexp.escape(delimiters)}]/).last&.strip&.downcase
|
|
124
|
+
prefixes.map(&:downcase).include?(raw_prefix)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def self.assign_confidence(scores_hash, prefix_matched_mode=nil)
|
|
129
|
+
all_scores = scores_hash.values
|
|
130
|
+
|
|
131
|
+
return {} if all_scores.empty?
|
|
132
|
+
|
|
133
|
+
avg_score = all_scores.sum.to_f / all_scores.size
|
|
134
|
+
|
|
135
|
+
scores_hash.transform_values do |score|
|
|
136
|
+
if score == 0
|
|
137
|
+
"regex-match"
|
|
138
|
+
else
|
|
139
|
+
mode_name = scores_hash.key(score)
|
|
140
|
+
is_prefix_mode = (prefix_matched_mode == mode_name)
|
|
141
|
+
deviation = (score - avg_score) / avg_score
|
|
142
|
+
|
|
143
|
+
case deviation
|
|
144
|
+
when 2.0..Float::INFINITY
|
|
145
|
+
"high"
|
|
146
|
+
when 0.5..2.0
|
|
147
|
+
is_prefix_mode ? "high" : "medium-high"
|
|
148
|
+
else
|
|
149
|
+
is_prefix_mode ? "medium-high" : "medium-low"
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
#private_constant :Analyzer
|
|
156
|
+
end
|