heitt 0.4.6 → 0.5.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2682eb074a7763036e54775706be2ae16221acd2d4cd7d416b387723e1b51aa2
4
- data.tar.gz: 4739a64851b2e071217eed0b12e3e71327670a4abcdebd218fdb3020df4aa47a
3
+ metadata.gz: ae7835969de375994bf7a0f9bf03c453f1ccd77d9086b4712a13e755084cb01c
4
+ data.tar.gz: 5754d899e34e5a9e8162c451fcef359939165221a0a758348e9ec608e9c0cc5a
5
5
  SHA512:
6
- metadata.gz: d6084258bca3bc35f40aecf9402dffbace9a419561f886e02a73939e821a7369798d4878b3e6483b4293687082ff169f7206c5046d94fc64ad38085718c758f5
7
- data.tar.gz: 362c46e00ba6dde92e1763e39b2f965e7782fb46a2073ab25974bcbb9d9213bfa03719b30b6747a145e9d8fdf8bc200e26a9c8df88c8c1022b4657147ed13f6e
6
+ metadata.gz: 1f2174d59e403b16385946ca733cdc428fb75f09c6c4275f42d6ecb6870725f8a4224d8389934d4d5a0106e6a01f65d9226eb1de30fc6997d98e2b4c81b09ade
7
+ data.tar.gz: 228b63e61c87e07b4f8f5c68afc8121fe95c6e9bffd644408cda410e05d7e6314684d730e14fa640793c14b8d21dc7664ba22ed84c27b5399fdf99141e249c2d
data/bin/heitt CHANGED
@@ -8,14 +8,13 @@ require 'heitt'
8
8
 
9
9
  module HEITT
10
10
  class CLI
11
- attr_accessor :inputs, :extended, :verbose, :output, :json, :database, :show_regex_match, :min_entropy
12
-
11
+ attr_accessor :inputs, :extended, :verbose, :tree, :database, :show_regex_match, :min_entropy, :peel_limit
13
12
  def initialize
14
13
  @inputs = []
15
14
  @extended = false
16
15
  @verbose = false
17
- @output = ""
18
- @json = false
16
+ @tree = false
17
+ @peel_limit = 0
19
18
  @database = ""
20
19
  @show_regex_match = false
21
20
  @min_entropy = 3.5
@@ -46,10 +45,13 @@ module HEITT
46
45
  opts.separator ""
47
46
  opts.separator "OUTPUT OPTIONS:"
48
47
  opts.on("-V", "--verbose", "Show description and notes for each candidate") {@verbose = true}
49
- opts.on("-j", "--json","Output in json format") {@json = true}
50
- opts.on("-o", "--output FILEPATH", String, "File to write output to") {|v| @output = v}
48
+ opts.on("-t", "--tree", "Force tree format when output is redirected"){@tree = true}
51
49
  opts.on("--debug", "Output in debug mode") {HEITT::Logger.enable_debug}
52
50
 
51
+ opts.separator ""
52
+ opts.separator "DECODE OPTIONS:"
53
+ opts.on("-L", "--peel-limit INTEGER", Integer, "Maximum decode layers to peel(supports base64 and hex)") {|v| @peel_limit = v}
54
+
53
55
  opts.separator ""
54
56
  opts.separator "FILTERING OPTIONS:"
55
57
  opts.on("-e", "--extended", "Show extended candidates") {@extended = true}
@@ -64,7 +66,7 @@ module HEITT
64
66
  opts.separator "EXAMPLES:"
65
67
  opts.separator " heitt 634d398e96eb1550956b8128cfeb0747 -r"
66
68
  opts.separator " heitt auth.log"
67
- opts.separator " heitt auth.log --json --output result.json"
69
+ opts.separator " heitt auth.log | cat"
68
70
  opts.separator " heitt auth.log --extended --regex-match"
69
71
  opts.separator " heitt auth.log --min-entropy 4.0"
70
72
  opts.separator " cat auth.log | heitt "
@@ -81,42 +83,13 @@ module HEITT
81
83
  end.parse!
82
84
 
83
85
  @inputs = if ARGV.empty?
86
+
84
87
  if $stdin.tty?
85
- database = @database.empty? ? HEITT::DATABASE : load_custom_database(@database)
86
- # Interactive mode
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
-
100
- loop do
101
- print "heitt> "
102
- input = $stdin.gets&.strip
103
- break if input.nil? || input.empty?
104
- results = HEITT::Scanner.scan(input, min_entropy: @min_entropy)
105
- groups = HEITT::Grouper.group(results)
106
- format = @json ? :json : :tree
107
- output = case format
108
- when :json
109
- HEITT::Formatter.json(groups, extended: @extended, show_regex_match: @show_regex_match)
110
- else
111
- HEITT::Formatter.tree(groups, extended: @extended, show_regex_match: @show_regex_match)
112
- end
113
- puts output
114
- puts ""
115
- end
88
+ # instead of hanging, make it interactive
89
+ interactive_loop
116
90
  else
117
91
  [$stdin.read]
118
92
  end
119
-
120
93
 
121
94
  else
122
95
  ARGV.dup
@@ -124,35 +97,81 @@ module HEITT
124
97
  self
125
98
  end
126
99
 
100
+ def interactive_loop
101
+ database = @database.empty? ? HEITT::DATABASE : load_custom_database(@database)
102
+ # Interactive mode
103
+ puts header("=== HEITT INTERACTIVE MODE ====")
104
+ #print "[debug:#{HEITT::Logger.debug_enabled? ? "on": "off"} || "
105
+ print "extended: #{@extended ? "on": "off"} || "
106
+ print "peel-limit: #{@peel_limit} || "
107
+ print "verbose: #{@verbose ? "on": "off"} || "
108
+ print "regex-match: #{@show_regex_match ? "on": "off"} || "
109
+ print "min-entropy: #{@min_entropy} || "
110
+ print "database: #{@database.empty? ? "default": database} || "
111
+ print "output-format: #{@json ? "json": "tree"}]\n"
112
+ puts ""
113
+ puts "Enter a hash or filepath. Press Ctrl+C or Enter to exit"
114
+ #puts "Extended: true" unless
115
+ # puts used flags here
116
+
117
+ loop do
118
+ print "heitt> "
119
+ input = $stdin.gets&.strip
120
+ break if input.nil? || input.empty?
121
+ results = HEITT::Scanner.scan(input, min_entropy: @min_entropy)
122
+ groups = HEITT::Grouper.group(results)
123
+
124
+ format = :tree
125
+
126
+ output = case format
127
+ when :tree
128
+ HEITT::Formatter.json(groups, extended: @extended, show_regex_match: @show_regex_match)
129
+ else
130
+ HEITT::Formatter.tree(groups, verbose: @verbose, extended: @extended, show_regex_match: @show_regex_match)
131
+ end
132
+ puts output
133
+ puts ""
134
+ end
135
+ end
136
+
137
+
127
138
  def run
128
- HEITT::Logger.debug("Loading database...")
129
139
  database = @database.empty? ? HEITT::DATABASE : load_custom_database(@database)
130
- HEITT::Logger.debug("Database (#{@database.empty? ? "default" : @database}) loaded")
131
140
 
132
- format = @json || !$stdout.tty? ? :json : :tree
141
+ format = if @tree
142
+ :tree
143
+ elsif !$stdout.tty?
144
+ :json
145
+ else
146
+ :tree
147
+ end
148
+
133
149
  output = @inputs.map do |input|
134
- HEITT::Logger.debug("Scanning #{input}...")
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 ...")
150
+ text = read_input(input)
151
+ text = HEITT::Decoder.peel(text, peel_limit: @peel_limit)[:final] if @peel_limit > 0
152
+ results = HEITT::Scanner.scan(text, database: database, min_entropy: @min_entropy)
138
153
  groups = HEITT::Grouper.group(results)
139
154
 
140
155
  case format
141
156
  when :json
142
157
  HEITT::Formatter.json(groups, extended: @extended, show_regex_match: @show_regex_match)
143
158
  else
144
- HEITT::Formatter.tree(groups, extended: @extended, show_regex_match: @show_regex_match)
159
+ HEITT::Formatter.tree(groups, verbose: @verbose, extended: @extended, show_regex_match: @show_regex_match)
145
160
  end
146
161
  end.join("\n")
147
162
 
148
- unless @output.empty?
149
- File.write(@output, output)
150
- else
151
- puts output
152
- end
163
+ #unless @output.empty?
164
+ # File.write(@output, output)
165
+ #else
166
+ puts output
167
+ #end
153
168
  end
154
169
 
155
170
  private
171
+ def read_input(input)
172
+ File.exist?(File.expand_path(input)) ? File.read(File.expand_path(input)) : input
173
+ end
174
+
156
175
  def header(text)
157
176
  terminal_width = IO.console.winsize[1] rescue 70
158
177
  text.center(terminal_width)
@@ -162,9 +181,9 @@ module HEITT
162
181
 
163
182
  def load_custom_database(file_path)
164
183
  filepath = File.expand_path(file_path)
165
- HEITT::Logger.debug("#{label} filepath: #{filepath}")
184
+ HEITT::Logger.debug("filepath: #{filepath}")
166
185
  unless File.exist?(filepath)
167
- HEITT::Logger.error("#{label} file #{filepath} does not exist")
186
+ HEITT::Logger.error("file #{filepath} does not exist")
168
187
  exit(1)
169
188
  end
170
189
  begin
@@ -175,26 +194,22 @@ module HEITT
175
194
  exit(1)
176
195
  end
177
196
  end
178
-
179
- # def decode(text, decoders)
180
- #Check if result is printable ascii after decoding
181
- # decoders.each do |decoder|
182
- # case decoder
183
- # when "b64"
184
- # text = text.gsub(/[A-Za-z0-9+\/]{4,}={0,2}/) do |match|
185
- # current = match
186
- # loop do
187
- # #until word is non printable ascii continue decoding
188
- # decoded = Base64.decode64(word) #rescue break
189
- # current = decoded.strip.match?(/\A[[:print:]]+\z/) ? decoded.strip : break
190
- #puts "IS PRINTABLE?: #{decoded.match?(/\A[[:print:]]+\z/)}"
191
- #puts "RESULT: #{result}"
192
- # end
193
- # current
194
- # end
195
- # when "hex"
196
- # text = tesxt.gsub(/)
197
197
  end
198
198
  end
199
199
 
200
200
  cli = HEITT::CLI.new.parse!.run #if __FILE__ == $0
201
+
202
+ ## Some flags will be removed.
203
+ # output flag will be replaced by tree flag. it existed because
204
+ # tree couldnt show when output was redirected.
205
+ # json flag will be removed, although it was created so its format will be seen
206
+ # it now looks redundant to the automatic version.
207
+ # -r flag wont be used for everything. it was made
208
+ # to filter out low confidence types from high confidence ones and show when asked for.
209
+ # in case of all being "regex-matched", -r shouldnt be necessary.
210
+ # this will make it convenient to check single hashes without always needing -r flag
211
+ #auto decoding hasn't being dropped, a library "codeberg.org/jobotow/lookslike" is in the making to help with encoded string identification.
212
+ # it will decode and check it it's a binary blob. if first decode shows binary blob, then it certainly isn't a hash hex or proper b64.
213
+ # else it should continue decoding till ut also meets binary blob before returning back to previous text before last decode"
214
+ # from the context that a text that's encoded once cant be decoded twice because the binary blob will show that the second text undergoing decoding was the actual answer.
215
+ # binary blob will be the eyes as it's uncertain the number of times a text has being encoded.
@@ -1,12 +1,9 @@
1
- require_relative 'utils'
2
1
  require_relative 'profiles'
3
2
 
4
3
  module HEITT
5
4
  module Analyzer
6
5
  def self.analyze(text, profiles: HEITT::PROFILES)#database: HEITT::DATABASE)
7
- HEITT::Logger.debug("Counting keywords...")
8
6
  keyword_counts = keyword_counts(text.downcase, profiles: profiles)
9
- HEITT::Logger.debug("Counted keywords: #{keyword_counts}")
10
7
  algorithm_scores(keyword_counts, profiles: profiles)
11
8
  end
12
9
 
@@ -39,6 +36,7 @@ module HEITT
39
36
  john: mode[:john],
40
37
  extended: mode[:extended],
41
38
  description: profile[:description],
39
+ notes: profile[:notes],
42
40
  score: score
43
41
  }
44
42
  end
@@ -49,7 +47,6 @@ module HEITT
49
47
 
50
48
  confidences = assign_confidence(scores_hash, prefix_matched_mode)
51
49
  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
50
  scored_candidates
54
51
  end
55
52
 
@@ -153,4 +150,4 @@ module HEITT
153
150
  end
154
151
  end
155
152
  #private_constant :Analyzer
156
- end
153
+ end
@@ -0,0 +1,48 @@
1
+ require 'base64'
2
+ module HEITT
3
+ module Decoder
4
+ ENCODED_B64 = '[A-Za-z0-9+\/]'
5
+ BASE_HEXBYTE = '[[:xdigit:]]'
6
+
7
+ def self.binary?(string)
8
+ return true if string.encoding == Encoding::ASCII_8BIT && !string.valid_encoding?
9
+ return true if string.include?("\x00")
10
+ sample = string.bytes.first(4096)
11
+ non_printable = sample.count { |b| b < 32 || b > 126}
12
+ (non_printable.to_f / sample.size) > 0.30
13
+ end
14
+
15
+ def self.likely_base64?(string)
16
+ string.match(/\A(?:#{ENCODED_B64}{4})*(?:#{ENCODED_B64}{2}==|#{ENCODED_B64}{3}=)?\z/)
17
+ end
18
+
19
+ def self.likely_hex?(string)
20
+ string.match(/\A#{BASE_HEXBYTE}+\z/) && string.length.even?
21
+ end
22
+
23
+ def self.decode_layer(string)
24
+ return [:base64, Base64.decode64(string)] if likely_base64?(string)
25
+ return [:hex, [string].pack("H*")] if likely_hex?(string)
26
+ nil
27
+ end
28
+
29
+
30
+ def self.peel(string, peel_limit: 12)
31
+ steps = []
32
+ current = string.gsub(/\s+/, '')
33
+
34
+ peel_limit.times do
35
+ result = decode_layer(current)
36
+ break unless result
37
+ method, decoded = result
38
+ decoded = decoded.gsub(/\s+/, '')
39
+ break if binary?(decoded)
40
+
41
+ steps << { method: method, result: decoded }
42
+ current = decoded
43
+ end
44
+ { final: current, steps: steps }
45
+ end
46
+ end
47
+ end
48
+
@@ -43,6 +43,10 @@ module HEITT
43
43
  {text: "John Format: #{candidate[:john] || "--"}", children: []}
44
44
  ]
45
45
 
46
+ if extended
47
+ children << {text: "Extended: #{candidate[:extended]}", children: []}
48
+ end
49
+
46
50
  if verbose
47
51
  if candidate[:description] && !candidate[:description].empty?
48
52
  children << {text: "Description: #{candidate[:description]}", children: []}
@@ -86,7 +90,9 @@ module HEITT
86
90
  hashcat: candidate[:hashcat],
87
91
  john: candidate[:john],
88
92
  confidence: candidate[:confidence],
89
- description: candidate[:description]
93
+ extended: candidate[:extended],
94
+ description: candidate[:description],
95
+ notes: candidate[:notes]
90
96
  }
91
97
  end
92
98
  }
@@ -122,4 +128,4 @@ module HEITT
122
128
  result
123
129
  end
124
130
  end
125
- end
131
+ end
data/lib/heitt/grouper.rb CHANGED
@@ -1,4 +1,3 @@
1
- require_relative 'utils'
2
1
  module HEITT
3
2
  module Grouper
4
3
 
@@ -15,8 +14,7 @@ module HEITT
15
14
  count: hashes.size
16
15
  }
17
16
  end
18
- HEITT::Logger.debug("Hashes grouped successfully") unless groups.empty? || groups.nil?
19
17
  groups
20
18
  end
21
19
  end
22
- end
20
+ end
data/lib/heitt/scanner.rb CHANGED
@@ -25,10 +25,7 @@ module HEITT
25
25
  matched = scanner.matched
26
26
  next unless matched.length < 8 || HEITT::Analyzer.high_entropy?(matched, min_entropy)
27
27
  offset = scanner.pos - matched.length
28
- HEITT::Logger.debug("Extracting prefix..")
29
28
  delim_prefix = HEITT::Analyzer.extract_prefix(text, offset)
30
- HEITT::Logger.debug("Extracted prefix: #{delim_prefix.length <= 1 ? "NULL" : delim_prefix}")
31
-
32
29
  candidates = HEITT::Analyzer.score_candidates(modes, delim_prefix, context_scores)
33
30
  #score = candidates.first[:score]
34
31
 
data/lib/heitt/version.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module HEITT
4
- VERSION = "0.4.6"
5
- GITHUB = "https://github.com/jobotow/heitt"
4
+ # version was changed to 0.50 on 30th Aug, 2026
5
+ VERSION = "0.5.0"
6
+ GITHUB = "https://codeberg.org/jobotow/heitt"
6
7
  end
data/lib/heitt.rb CHANGED
@@ -3,8 +3,8 @@
3
3
  #require 'strscan'
4
4
  #require 'logger'
5
5
  #require 'colorize'
6
- require_relative 'heitt/utils'
7
6
  require_relative 'heitt/profiles'
7
+ require_relative 'heitt/decoder'
8
8
  require_relative 'heitt/database'
9
9
  require_relative 'heitt/analyzer'
10
10
  require_relative 'heitt/scanner'
@@ -12,5 +12,7 @@ require_relative 'heitt/grouper'
12
12
  require_relative 'heitt/formatter'
13
13
  require_relative 'heitt/version'
14
14
 
15
-
16
-
15
+ # module HEITT
16
+ # something like heitt = HEITT.new(database: nil, profiles: nil)
17
+ # heitt.identify("hash_string")
18
+
metadata CHANGED
@@ -1,13 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: heitt
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.6
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jonathan Botchway Owusu
8
+ autorequire:
8
9
  bindir: bin
9
10
  cert_chain: []
10
- date: 1980-01-02 00:00:00.000000000 Z
11
+ date: 2026-08-30 00:00:00.000000000 Z
11
12
  dependencies:
12
13
  - !ruby/object:Gem::Dependency
13
14
  name: colorize
@@ -35,18 +36,20 @@ files:
35
36
  - lib/heitt.rb
36
37
  - lib/heitt/analyzer.rb
37
38
  - lib/heitt/database.rb
39
+ - lib/heitt/decoder.rb
38
40
  - lib/heitt/formatter.rb
39
41
  - lib/heitt/grouper.rb
40
42
  - lib/heitt/profiles.rb
41
43
  - lib/heitt/scanner.rb
42
44
  - lib/heitt/utils.rb
43
45
  - lib/heitt/version.rb
44
- homepage: https://github.com/jobotow/heitt
46
+ homepage: https://codeberg.org/jobotow/heitt
45
47
  licenses: []
46
48
  metadata:
47
- homepage_uri: https://github.com/jobotow/heitt
48
- source_code_uri: https://github.com/jobotow/heitt
49
- changelog_uri: https://github.com/jobotow/heitt/blob/main/CHANGELOG.md
49
+ homepage_uri: https://codeberg.org/jobotow/heitt
50
+ source_code_uri: https://codeberg.org/jobotow/heitt
51
+ changelog_uri: https://codeberg.org/jobotow/heitt/blob/main/CHANGELOG.md
52
+ post_install_message:
50
53
  rdoc_options: []
51
54
  require_paths:
52
55
  - lib
@@ -61,7 +64,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
61
64
  - !ruby/object:Gem::Version
62
65
  version: '0'
63
66
  requirements: []
64
- rubygems_version: 3.6.7
67
+ rubygems_version: 3.4.20
68
+ signing_key:
65
69
  specification_version: 4
66
70
  summary: Hash Extraction, Identification and Triage Tool.
67
71
  test_files: []