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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: aaae42753b6fd3c04375200d118517b8b3f45eb42d0bf6b4fddc9b08c64e5af7
4
- data.tar.gz: ed0b83ef1116ebc291b000985784107a709156df68be16443598d2a41ae59853
3
+ metadata.gz: 2682eb074a7763036e54775706be2ae16221acd2d4cd7d416b387723e1b51aa2
4
+ data.tar.gz: 4739a64851b2e071217eed0b12e3e71327670a4abcdebd218fdb3020df4aa47a
5
5
  SHA512:
6
- metadata.gz: c4f7ea072d396d4c5ba96f9f65402ab006c1a94856b77cd359417d7ddc26bde65bc81928369f64ad36562aa0d7ce4e7b0efbe4d04edb6d04e467c9df4ce5ab6d
7
- data.tar.gz: 3a1c7494156de04f10c2a3f132f85337fdd597197883b81cc4d1df934fd3c9d70dcaf89378729837a7e0f086d6cb2d2c75518675e0af56b60e4ee7c3e2f1f804
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 "#{header("=== HEITT INTERACTIVE MODE ====\n")}"
84
- puts "Enter a hash or file"
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
- database = @database.empty? ? HEITT::DATABASE : load_custom_database(@database)
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
- def load_custom_database(filepath)
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
- puts HEITT::Color.colorize("[ERROR] Database file #{filepath} does not exists", :bold, :red)
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
- puts "#{HEITT::Color.colorize("[ERROR] Invalid JSON in database file: ", :bold, :red)}#{e.message}"
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