heitt 0.4.5 → 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.
@@ -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
+
@@ -0,0 +1,131 @@
1
+ require 'json'
2
+ require_relative 'utils'
3
+
4
+ module HEITT
5
+ module Formatter
6
+
7
+
8
+ def self.tree(groups, verbose: false, extended: false, show_regex_match: false)
9
+ result = ""
10
+
11
+ #Filter out groups with extended candidates as true
12
+ visible_groups = groups.select do |group|
13
+ has_non_extended = group[:candidates].any? {|c| !c[:extended] || extended}
14
+ has_non_regex = group[:candidates].any? {|c| c[:confidence] != "regex-match" || show_regex_match}
15
+ has_non_extended && has_non_regex
16
+ end
17
+ #Renumber after filtering
18
+ renumbered_groups= visible_groups.each_with_index.map { |group, index| group.merge(cluster_id: index + 1) }
19
+
20
+ root = {
21
+ text: "#{HEITT::Color.colorize("\n\n[", :bold, :blue)}#{HEITT::Color.colorize("CLUSTERED HASHES", :green)}#{HEITT::Color.colorize("]", :bold, :blue)}",
22
+ children: renumbered_groups.map do |group|
23
+ {
24
+ text: HEITT::Color.colorize("HASH CLUSTER #{group[:cluster_id]}", :magenta, :bold),
25
+ children: group[:hashes].map{|h| {text: h, children: []}}
26
+ }
27
+ end
28
+ }
29
+
30
+ result += render_tree([root])
31
+
32
+ renumbered_groups.each do |group|
33
+ 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: []}
34
+ candidate_nodes = (group[:candidates]).each_with_index.map do |candidate, idx|
35
+ next if candidate.nil?
36
+ next if candidate[:name].nil?
37
+ next if candidate[:extended] && !extended
38
+ next if candidate[:confidence] == "regex-match" && !show_regex_match
39
+ confidence = candidate[:confidence] ? " — CONFIDENCE: #{candidate[:confidence].upcase}" : ""
40
+
41
+ children = [
42
+ {text: "Hashcat Mode: #{candidate[:hashcat] || "--"}", children: []},
43
+ {text: "John Format: #{candidate[:john] || "--"}", children: []}
44
+ ]
45
+
46
+ if extended
47
+ children << {text: "Extended: #{candidate[:extended]}", children: []}
48
+ end
49
+
50
+ if verbose
51
+ if candidate[:description] && !candidate[:description].empty?
52
+ children << {text: "Description: #{candidate[:description]}", children: []}
53
+ end
54
+
55
+ if candidate[:notes] && !candidate[:notes].empty?
56
+ children << {text: "Notes:", children: candidate[:notes].map {|note| {text: note, children: []}}}
57
+ end
58
+ end
59
+ {
60
+ 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)}",
61
+ children: children
62
+ }
63
+ end.compact
64
+ result += render_tree(candidate_nodes, "", false, false) unless candidate_nodes.nil? || candidate_nodes.empty?
65
+ end
66
+ result
67
+ end
68
+
69
+ def self.json(groups, extended: false, show_regex_match: false)
70
+ visible_groups = groups.select do |group|
71
+ has_non_extended = group[:candidates].any? {|c| c[:extended] || extended}
72
+ has_non_regex = group[:candidates].any? {|c| c[:confidence] != "regex-match" || show_regex_match}
73
+ has_non_extended && has_non_regex
74
+ end
75
+ #Renumber after filtering
76
+ renumbered_groups = visible_groups.each_with_index.map { |group, index| group.merge(cluster_id: index+1)}
77
+
78
+ JSON.pretty_generate(
79
+ renumbered_groups.map do |group|
80
+ visible_candidates = group[:candidates].select do |c|
81
+ (!c[:extended] || extended) && (c[:confidence] != "regex-match" || show_regex_match)
82
+ end
83
+ {
84
+ cluster_id: group[:cluster_id],
85
+ count: group[:count],
86
+ hashes: group[:hashes],
87
+ candidates: visible_candidates.map do |candidate|
88
+ {
89
+ name: candidate[:name],
90
+ hashcat: candidate[:hashcat],
91
+ john: candidate[:john],
92
+ confidence: candidate[:confidence],
93
+ extended: candidate[:extended],
94
+ description: candidate[:description],
95
+ notes: candidate[:notes]
96
+ }
97
+ end
98
+ }
99
+ end
100
+ )
101
+ end
102
+
103
+ private
104
+ def self.render_tree(items, prefix = "", parent_is_last=true, is_root=true)
105
+ result = ""
106
+
107
+ items.each_with_index do |node, i|
108
+ is_last_item = (i == items.length - 1)
109
+
110
+ line = if is_root
111
+ "#{node[:text]}\n"
112
+ else
113
+ "#{HEITT::Color.colorize(prefix, :blue)}#{HEITT::Color.colorize((is_last_item ? '└── ' : '├── '), :blue)}#{node[:text]}\n"
114
+ end
115
+
116
+ child_prefix = if is_root
117
+ ""
118
+ else
119
+ "#{HEITT::Color.colorize(prefix, :bold, :blue)}#{HEITT::Color.colorize((is_last_item ? " " : "│ "), :bold, :blue)}"
120
+ end
121
+ result += line
122
+ result += render_tree(node[:children], child_prefix, is_last_item, false) if node[:children].any?
123
+
124
+ if is_last_item && !is_root and !node[:children].any?
125
+ result += "#{HEITT::Color.colorize(prefix, :bold, :blue)} \n"
126
+ end
127
+ end
128
+ result
129
+ end
130
+ end
131
+ end
@@ -0,0 +1,20 @@
1
+ module HEITT
2
+ module Grouper
3
+
4
+ def self.group(results)
5
+ clusters = {}
6
+
7
+ clusters = results.group_by {|r| r[:candidates].first[:name]}
8
+ groups = clusters.each_with_index.map do |(name, group), index|
9
+ hashes = group.map {|r| r[:hash]}
10
+ {
11
+ cluster_id: index+1,
12
+ hashes: hashes,
13
+ candidates: group.first[:candidates],
14
+ count: hashes.size
15
+ }
16
+ end
17
+ groups
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,184 @@
1
+ module HEITT
2
+ PROFILES = {
3
+ "CRC-16-CCITT" => {
4
+ description: "Cyclic Redundancy Check 16-bit Consultative Commitee for International Telegraph and Telephone",
5
+ notes: ["Used for error detection in communication and storage systems", "Data Integrity and verification", "Memory checks integrity", "Not cryptographic"],
6
+ context: ["checksum", "telecom", "bluetooth"],
7
+ common_sources: ["V.41", "X.25", "HDLC", "Bluetooth"]
8
+ },
9
+ "CRC-16" => {
10
+ description: "Cyclic Redundancy Check 16-bit — 4 hexadecimal chars, basic checksum",
11
+ notes: ["Error detection in data transmission", "Data storage integrity checks", "Not cryptographic", "Low collision resistance"],
12
+ context: ["checksum", "networking"],
13
+ prefixes: ["crc-16"],
14
+ common_sources: ["file verification", "network protocols", "embedded systems"]
15
+ },
16
+ "FCS-16" => {
17
+ description: "Frame Check Sequence 6-bit — 4 hexadecimal chars, data link layer",
18
+ notes: ["Not cryptographic"],
19
+ prefixes: ["fcs-16"],
20
+ context: ["checksum", "networking"],
21
+ common_sources: ["Ethernet frames", "PPP"]
22
+ },
23
+ "Adler-32" => {
24
+ description: "Adler-32 checksum — 8 hex chars, zlib compression",
25
+ common_sources: ["zlib", "PNG files", "RSYNC"],
26
+ context: ["checksum", "compression"]
27
+ },
28
+ "CRC-32B" => {
29
+ description: "CRC-32 IEEE 802.3 variant — 8 hex chars, Ethernet standard" ,
30
+ notes: ["Not cryptographic"],
31
+ common_sources: ["Ethernet", "MPEG-2", "PKZIP"],
32
+ context: ["checksum", "networking"]
33
+ },
34
+ "FCS-32" => {
35
+ description: "Frame Check Sequence 32-bit — 8 hex chars, advanced networking",
36
+ common_sources: ["advanced networking protocols"],
37
+ context: ["checksum", "networking"]
38
+ },
39
+ "GHash-32-3" => {
40
+ description: "G-Hash 32-bit 3-round — 8 hex chars, experimental hash",
41
+ common_sources: ["research", "academic"],
42
+ context: ["experimental"]
43
+ },
44
+ "GHash-32-5" => {
45
+ description: "G-Hash 32-bit 5-round — 8 hex chars, experimental hash",
46
+ common_sources: ["research", "academic"],
47
+ context: ["experimental"]
48
+ },
49
+ "FNV-132" => {
50
+ description: "Fowler-Noll-Vo hash 32-bit — 8 hex chars, fast non-crypto hash",
51
+ common_sources: ["DNS", "database indexing", "hash tables"],
52
+ context: ["checksum", "programming"]
53
+ },
54
+ "Fletcher-32" => {
55
+ description: "Fletcher's checksum 32-bit — 8 hex chars, error detection",
56
+ common_sources: ["OSTA UDF", "ISO/IEC 8473-1"],
57
+ context: ["checksum", "storage"]
58
+ },
59
+ "Joaat" => {
60
+ description: "Jenkins one-at-a-time hash — 8 hex chars, simple string hash",
61
+ common_sources: ["Perl", "Apache", "various applications"],
62
+ context: ["programming", "hashing"]
63
+ },
64
+ "ELF-32" => {
65
+ description: "ELF-32 hash for object files — 8 hex chars, Unix/Linux object files",
66
+ context: ["executable", "system"],
67
+ mime_types: ["application/octet-stream"]
68
+ },
69
+ "XOR-32" => {
70
+ description: "Simple XOR-based 32-bit hash — 8 hex chars, basic XOR operation",
71
+ common_sources: ["simple applications", "embedded systems"],
72
+ context: ["basic", "embedded"]
73
+ },
74
+ "CRC-24" => {
75
+ description: "Cyclic Redundancy Check 24-bits — 6 hexadecimal chars, OpenPGP standard",
76
+ notes: ["Not cryptographic"],
77
+ context: ["checksum"],
78
+ common_sources: ["OpenPGP", "RFID", "some file formats"]
79
+ },
80
+ "CRC-32" => {
81
+ description: "Cyclic Redundancy Check 32-bit — 8 hex chars, most common checksum",
82
+ notes: ["Not cryptographic"]
83
+ },
84
+ "DES(Unix)" => {
85
+ description: "DES-based Unix crypt — 13 chars, traditional Unix passwords",
86
+ notes: ["Only 8 char passwords", "weak salt"],
87
+ common_sources: ["/etc/passwd", "old Unix systems"],
88
+ context: ["unix", "legacy"]
89
+ },
90
+ "DEScrypt" => {
91
+ description: "DES crypt implementation — 13 chars",
92
+ notes: ["Traditional Unix password hashing"],
93
+ common_sources: ["old Unix/Linux"],
94
+ context: ["unix", "legacy"]
95
+ },
96
+ "MySQL323" => {
97
+ description: "MySQL 3.23 password hash — 16 chars typical, but can be padded to 32 (hexadecimals)",
98
+ notes: ["Used in old MySQL databases", "Can be broken in seconds", "Susceptible to rainbow tables", "Limited to 8 character passwords", "Deprecated since MySQL 4.1"]
99
+ },
100
+ "DES(Oracle)" => {
101
+ description: "Oracle DES-based hash — 16 hex chars, Oracle specific"
102
+ },
103
+ "Half MD5" => {
104
+ description: "First half of MD5 hash — 16 hex chars, MD5 truncated",
105
+ notes: ["Weaker than full MD5"]
106
+ },
107
+ "FNV-164" => {
108
+ description: "Fowler-Noll-Vo hash 64-bit — 16 hex chars, 64-bit version",
109
+ notes: ["Not cryptographic"]
110
+ },
111
+ "CRC-64" => {
112
+ description: "Cyclic Redundancy Check 64-bit — 16 hex chars, ISO 3309",
113
+ notes: ["Not cryptographic"]
114
+ },
115
+ "CRC-96(ZIP)" => {
116
+ description: "CRC-96 used in some ZIP variants — 24 hex chars, extended CRC",
117
+ notes: ["Not cryptographic", "For some archive formats"]
118
+ },
119
+ "Crypt16" => {
120
+ description: "Extended crypt16 implementation",
121
+ characteristics: "24 chars, extended DES crypt",
122
+ notes: ["Rarely used", "Used by some Unix variants"]
123
+ },
124
+ "BigCrypt" => {
125
+ description: "Extended DES crypt — 13+ chars, extended length",
126
+ notes: ["Rarely used", "Used in some Unix variants"],
127
+ common_sources: ["some Unix variants"],
128
+ context: ["unix", "extended"]
129
+ },
130
+ "MD5" => {
131
+ description: "MD5 cryptographic hash function",
132
+ characteristics: "32 chars, hexadecimal, unsalted",
133
+ notes: ["Used as checksum to verify data or file integrity", "MD5 is cryptographically broken as it is vulnerable to collision attacks"],
134
+ context: ["web", "checksum", "legacy", "password", "hash", "md5"],
135
+ prefixes: ["md5", "hash", "checksum", "password"],
136
+ file_types: ["shadow", "htpasswd", "logs"],
137
+ mime_types: ["text/plain", "text/x-passwd"],
138
+ common_sources: ["web applications", "file integrity checks", "checksums", "legacy systems"]
139
+ },
140
+ "MD4" => {
141
+ characteristics: "32 chars, legacy Microsoft systems",
142
+ prefixes: ["hash"],
143
+ context: ["hash"],
144
+ common_sources: ["Old Windows systems", "legacy applications"]
145
+ },
146
+ "LM" => {
147
+ description: "Windows LAN Manager hash",
148
+ characteristics: "16 hex chars, all uppercase, split password",
149
+ notes: ["Mainly found in Windows SAM files(legacy Windows)", "Very weak", "no lowercase", "split passwords"],
150
+ common_sources: ["Windows SAM", "legacy Windows systems"],
151
+ context: ["windows", "SAM"]
152
+ },
153
+ "NTLM" => {
154
+ description: "Windows NTLM authentication hash",
155
+ characteristics: "32 chars, Windows authentication, based on MD4",
156
+ notes: ["Hashcat Mode: 5600 (NetNTLMv2) - if network captured", "Hashcat Mode: 5500 (NetNTLMv1/NetNTLMv1+ESS) - legacy versions", "John Format: netntlm (for network hashes)", "John Format: netntlmv2 (v2 hashes)"],
157
+ context: ["windows", "SAM", "LSASS", "nt", "ntlm"],
158
+ prefixes: ["nt"],
159
+ file_types: ["ntds", "logs"],
160
+ mime_types: ["text/plain", "application/octet-stream"],
161
+ common_sources: ["Windows SAM", "Active Directory", "LSASS memory"]
162
+ },
163
+ "SHA-1" => {
164
+ description: "SHA-1 cryptographic hash function",
165
+ characteristics: "40 chars, hexadecimal, unsalted",
166
+ notes: ["Used for file verification", "found in git commits and legacy certificates"],
167
+ prefixes: ["sha1", "hash"],
168
+ context: ["sha1", "hash"]
169
+ },
170
+ "RIPEMD-160" => {
171
+ characteristics: "40 chars, Bitcoin addresses, digital signatures",
172
+ notes: ["Rarely used for passwords"]
173
+ },
174
+ "Android PIN" => {
175
+ description: "Android PIN/Password hash",
176
+ characteristics: "40 chars hash + 16 chars salt, SHA1 + MD5",
177
+ notes: ["found in android gesture.key files"]
178
+ },
179
+ "SHA-512 Crypt" => {
180
+ characteristics: "$6$ prefix, includes salt, 96-106 chars",
181
+ notes: ["Industry standard for modern Linux systems"]
182
+ },
183
+ }
184
+ end
@@ -0,0 +1,62 @@
1
+ require 'strscan'
2
+ require 'set'
3
+ require_relative 'analyzer'
4
+ require_relative 'database'
5
+ require_relative 'profiles'
6
+
7
+ module HEITT
8
+ module Scanner
9
+
10
+ def self.scan(input, database: HEITT::DATABASE, profiles: HEITT::PROFILES, min_entropy: 3.5)
11
+ text = File.exist?(File.expand_path(input)) ? File.read(File.expand_path(input)) : input
12
+ context_scores = HEITT::Analyzer.analyze(text, profiles: profiles)#database: database)
13
+ found = {}
14
+ #seen = {}
15
+
16
+
17
+ database.each do |entry|
18
+ regex = get_regex(entry)
19
+ modes = get_modes(entry)
20
+ next unless regex && modes && !modes.empty?
21
+ pattern = regex.is_a?(Regexp) ? regex : Regexp.new(regex)
22
+ scanner = StringScanner.new(text)
23
+
24
+ while scanner.scan_until(pattern)
25
+ matched = scanner.matched
26
+ next unless matched.length < 8 || HEITT::Analyzer.high_entropy?(matched, min_entropy)
27
+ offset = scanner.pos - matched.length
28
+ delim_prefix = HEITT::Analyzer.extract_prefix(text, offset)
29
+ candidates = HEITT::Analyzer.score_candidates(modes, delim_prefix, context_scores)
30
+ #score = candidates.first[:score]
31
+
32
+ found[matched] ||= {hash: matched, candidates: []}
33
+ found[matched][:candidates].concat(candidates)
34
+ end
35
+ end
36
+
37
+ found.each_value do |result|
38
+ result[:candidates] = result[:candidates]
39
+ .group_by {|c| c[:name]}
40
+ .map {|name, dupes| dupes.max_by {|c| c[:score]}}
41
+ .sort_by {|c| -c[:score]}
42
+
43
+ # Re-assign confidence based on final merged scores
44
+ scores_hash = result[:candidates].map {|c| [c[:name], c[:score]]}.to_h
45
+ confidences = Analyzer.assign_confidence(scores_hash)
46
+ result[:candidates] = result[:candidates].map {|c| c.merge(confidence: confidences[c[:name]])}
47
+ end
48
+ found.values
49
+ end
50
+
51
+ private
52
+
53
+ def self.get_regex(entry)
54
+ entry[:extract_regex] || entry[:regex] || entry[:pattern] || entry[:regexp]
55
+ end
56
+
57
+ def self.get_modes(entry)
58
+ entry[:modes] || entry[:algorithms] || entry[:hashes] ||
59
+ entry[:candidates] || entry[:types] || entry[:hashtypes]
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,57 @@
1
+ require 'logger'
2
+ require 'colorize'
3
+
4
+ module HEITT
5
+ module Logger
6
+ @debug = false
7
+ #LEVELS = { debug: 0, verbose: 1, info: 2, warn: 3, error: 4 }
8
+ #@level = :warn
9
+
10
+ #def self.set_level(lvl)
11
+ # @level = lvl
12
+ #end
13
+ def self.enable_debug
14
+ @debug = true
15
+ end
16
+
17
+ def self.disable_debug
18
+ @debug = false
19
+ end
20
+
21
+ def self.debug_enabled?
22
+ @debug
23
+ end
24
+
25
+ def self.debug(msg)
26
+ log("[DEBUG] #{msg}", :cyan)
27
+ end
28
+
29
+ def self.warn(msg)
30
+ log("[WARN] #{msg}", :yellow)
31
+ end
32
+
33
+ def self.error(msg)
34
+ log("[ERROR] #{msg}", :red)
35
+ end
36
+
37
+ private
38
+ def self.log(msg, color)
39
+ return unless @debug
40
+ $stderr.puts HEITT::Color.colorize(msg, color)
41
+ end
42
+ end
43
+
44
+
45
+ module Color
46
+ def self.colorize(text, color, *styles)
47
+ return text unless STDOUT.isatty #&& !(defined?(Flags) && Flags.no_color)
48
+
49
+ colored = text.colorize(color)
50
+ styles.each do |style|
51
+ colored = colored.send(style)
52
+ end
53
+ colored
54
+ end
55
+ end
56
+ #private_constant :Color
57
+ end
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.5"
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