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.
@@ -0,0 +1,125 @@
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 verbose
47
+ if candidate[:description] && !candidate[:description].empty?
48
+ children << {text: "Description: #{candidate[:description]}", children: []}
49
+ end
50
+
51
+ if candidate[:notes] && !candidate[:notes].empty?
52
+ children << {text: "Notes:", children: candidate[:notes].map {|note| {text: note, children: []}}}
53
+ end
54
+ end
55
+ {
56
+ 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)}",
57
+ children: children
58
+ }
59
+ end.compact
60
+ result += render_tree(candidate_nodes, "", false, false) unless candidate_nodes.nil? || candidate_nodes.empty?
61
+ end
62
+ result
63
+ end
64
+
65
+ def self.json(groups, extended: false, show_regex_match: false)
66
+ visible_groups = groups.select do |group|
67
+ has_non_extended = group[:candidates].any? {|c| c[:extended] || extended}
68
+ has_non_regex = group[:candidates].any? {|c| c[:confidence] != "regex-match" || show_regex_match}
69
+ has_non_extended && has_non_regex
70
+ end
71
+ #Renumber after filtering
72
+ renumbered_groups = visible_groups.each_with_index.map { |group, index| group.merge(cluster_id: index+1)}
73
+
74
+ JSON.pretty_generate(
75
+ renumbered_groups.map do |group|
76
+ visible_candidates = group[:candidates].select do |c|
77
+ (!c[:extended] || extended) && (c[:confidence] != "regex-match" || show_regex_match)
78
+ end
79
+ {
80
+ cluster_id: group[:cluster_id],
81
+ count: group[:count],
82
+ hashes: group[:hashes],
83
+ candidates: visible_candidates.map do |candidate|
84
+ {
85
+ name: candidate[:name],
86
+ hashcat: candidate[:hashcat],
87
+ john: candidate[:john],
88
+ confidence: candidate[:confidence],
89
+ description: candidate[:description]
90
+ }
91
+ end
92
+ }
93
+ end
94
+ )
95
+ end
96
+
97
+ private
98
+ def self.render_tree(items, prefix = "", parent_is_last=true, is_root=true)
99
+ result = ""
100
+
101
+ items.each_with_index do |node, i|
102
+ is_last_item = (i == items.length - 1)
103
+
104
+ line = if is_root
105
+ "#{node[:text]}\n"
106
+ else
107
+ "#{HEITT::Color.colorize(prefix, :blue)}#{HEITT::Color.colorize((is_last_item ? '└── ' : '├── '), :blue)}#{node[:text]}\n"
108
+ end
109
+
110
+ child_prefix = if is_root
111
+ ""
112
+ else
113
+ "#{HEITT::Color.colorize(prefix, :bold, :blue)}#{HEITT::Color.colorize((is_last_item ? " " : "│ "), :bold, :blue)}"
114
+ end
115
+ result += line
116
+ result += render_tree(node[:children], child_prefix, is_last_item, false) if node[:children].any?
117
+
118
+ if is_last_item && !is_root and !node[:children].any?
119
+ result += "#{HEITT::Color.colorize(prefix, :bold, :blue)} \n"
120
+ end
121
+ end
122
+ result
123
+ end
124
+ end
125
+ end
@@ -0,0 +1,22 @@
1
+ require_relative 'utils'
2
+ module HEITT
3
+ module Grouper
4
+
5
+ def self.group(results)
6
+ clusters = {}
7
+
8
+ clusters = results.group_by {|r| r[:candidates].first[:name]}
9
+ groups = clusters.each_with_index.map do |(name, group), index|
10
+ hashes = group.map {|r| r[:hash]}
11
+ {
12
+ cluster_id: index+1,
13
+ hashes: hashes,
14
+ candidates: group.first[:candidates],
15
+ count: hashes.size
16
+ }
17
+ end
18
+ HEITT::Logger.debug("Hashes grouped successfully") unless groups.empty? || groups.nil?
19
+ groups
20
+ end
21
+ end
22
+ 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,65 @@
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
+ HEITT::Logger.debug("Extracting prefix..")
29
+ delim_prefix = HEITT::Analyzer.extract_prefix(text, offset)
30
+ HEITT::Logger.debug("Extracted prefix: #{delim_prefix.length <= 1 ? "NULL" : delim_prefix}")
31
+
32
+ candidates = HEITT::Analyzer.score_candidates(modes, delim_prefix, context_scores)
33
+ #score = candidates.first[:score]
34
+
35
+ found[matched] ||= {hash: matched, candidates: []}
36
+ found[matched][:candidates].concat(candidates)
37
+ end
38
+ end
39
+
40
+ found.each_value do |result|
41
+ result[:candidates] = result[:candidates]
42
+ .group_by {|c| c[:name]}
43
+ .map {|name, dupes| dupes.max_by {|c| c[:score]}}
44
+ .sort_by {|c| -c[:score]}
45
+
46
+ # Re-assign confidence based on final merged scores
47
+ scores_hash = result[:candidates].map {|c| [c[:name], c[:score]]}.to_h
48
+ confidences = Analyzer.assign_confidence(scores_hash)
49
+ result[:candidates] = result[:candidates].map {|c| c.merge(confidence: confidences[c[:name]])}
50
+ end
51
+ found.values
52
+ end
53
+
54
+ private
55
+
56
+ def self.get_regex(entry)
57
+ entry[:extract_regex] || entry[:regex] || entry[:pattern] || entry[:regexp]
58
+ end
59
+
60
+ def self.get_modes(entry)
61
+ entry[:modes] || entry[:algorithms] || entry[:hashes] ||
62
+ entry[:candidates] || entry[:types] || entry[:hashtypes]
63
+ end
64
+ end
65
+ 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,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module HEITT
4
- VERSION = "0.4.5"
4
+ VERSION = "0.4.6"
5
5
  GITHUB = "https://github.com/jobotow/heitt"
6
6
  end