hashprint-cli 1.0.0__py3-none-any.whl

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.
HashPrint/__init__.py ADDED
@@ -0,0 +1,7 @@
1
+ # In the Name of God, the Most Compassionate, Most Merciful
2
+
3
+
4
+ from importlib.metadata import version, metadata
5
+
6
+ __version__ = version("HashPrint")
7
+ __project_name__ = metadata("HashPrint").get("Name")
HashPrint/cli.py ADDED
@@ -0,0 +1,100 @@
1
+ # In the Name of God, the Most Compassionate, Most Merciful
2
+
3
+
4
+ import argparse
5
+ import pyfiglet
6
+ from rich_argparse import RichHelpFormatter
7
+ from rich.console import Console
8
+ from rich.theme import Theme
9
+ import sys
10
+ from HashPrint import __project_name__
11
+
12
+ DISCLAIMER_MESSAGE = "Hash identification is based on pattern matching - therefore some hashes cannot be identified with certainty"
13
+ POSSIBILITY_MESSAGE = "The string may not be a real hash"
14
+ SUCCESS_MESSAGE = f"{__project_name__} ran successfully"
15
+
16
+
17
+ class Parser(argparse.ArgumentParser):
18
+ def error(self, message):
19
+ console.print(f"[error]\nError: {message}[/error]")
20
+ console.print(f"[hint]{hint_line}[/hint]")
21
+ sys.exit()
22
+
23
+
24
+ def print_banner():
25
+ banner = pyfiglet.figlet_format(__project_name__, font="slant")
26
+ console.print(f"[banner]{banner}[/banner]")
27
+
28
+
29
+ theme = Theme(
30
+ {
31
+ "success": "#00A86B", # Jade color
32
+ "information": "#00D084", # Bright Jade color
33
+ "error": "bold #FF4A4A", # Coral red color
34
+ "fail": "#EC5B38", # Warm orange-red color
35
+ "hint": "#FF5FCF", # Bright hot pink color
36
+ "banner": "#499A13", # Vida Loca color
37
+ "description": "#24B1B1", # Medium dark teal color
38
+ "usage": "#2F2FE4", # Electric blue color
39
+ "disclaimer": "#B8860B", # Dark goldenrod color
40
+ "warning": "#FFFF00", # Yellow color
41
+ }
42
+ )
43
+
44
+ RichHelpFormatter.styles["argparse.args"] = "#03AED2" # Sky blue color
45
+ RichHelpFormatter.styles["argparse.groups"] = "#FF6D1F" # Bright orange color
46
+ RichHelpFormatter.styles["argparse.text"] = "#24B1B1" # Medium dark teal color
47
+
48
+ console = Console(highlight=False, theme=theme)
49
+
50
+
51
+ description = "\nA forensic CLI tool for hash identification"
52
+ hint_line = "\nRun with -h for usage information"
53
+ program_name_cli = "hashprint"
54
+ format_choices = ["text", "json", "yaml"]
55
+
56
+
57
+ def cli_parser():
58
+ if len(sys.argv) == 1:
59
+ print_banner()
60
+ console.print(f"[description]{description}[/description]")
61
+ console.print(f"[hint]{hint_line}[/hint]")
62
+ sys.exit()
63
+
64
+ parser = Parser(
65
+ prog=program_name_cli,
66
+ description=description,
67
+ formatter_class=RichHelpFormatter,
68
+ color=False,
69
+ )
70
+ parser.add_argument(
71
+ "-f",
72
+ "--hash-file",
73
+ action="append",
74
+ help="path to a text (.txt) file containing one hash per line",
75
+ )
76
+ parser.add_argument(
77
+ "-c",
78
+ "--hash",
79
+ dest="candidate_hash",
80
+ action="append",
81
+ help="hash to identify between single quotes and without the 0x prefix",
82
+ )
83
+ parser.add_argument("-x", "--format", choices=format_choices, help="output format")
84
+ parser.add_argument(
85
+ "-p",
86
+ "--path",
87
+ metavar="PATH",
88
+ help="path of the output file - overwrites the file if it is not empty",
89
+ )
90
+ parser.add_argument(
91
+ "-w",
92
+ "--walk",
93
+ metavar="PATH",
94
+ help="recursively scans a directory for text (.txt) files - containing one hash per line",
95
+ )
96
+ parser.add_argument(
97
+ "-v", "--version", action="store_true", help="displays the version number"
98
+ )
99
+ args = parser.parse_args()
100
+ return args
HashPrint/database.py ADDED
@@ -0,0 +1,660 @@
1
+ # In the Name of God, the Most Compassionate, Most Merciful
2
+
3
+
4
+ import re
5
+
6
+
7
+ class Hash:
8
+ def __init__(self, pattern, name, family, popularity, source=None):
9
+ self.pattern = re.compile(pattern)
10
+ self.name = name
11
+ self.family = family
12
+ self.popularity = popularity
13
+ self.source = source
14
+
15
+
16
+ POPULARITY_RANKING = {
17
+ "Very High": 8,
18
+ "High": 7,
19
+ "Moderate/High": 6,
20
+ "Growing": 5,
21
+ "Moderate": 4,
22
+ "Low/Moderate": 3,
23
+ "Low": 2,
24
+ "Very Low": 1,
25
+ }
26
+
27
+ HASH_DATA = (
28
+ Hash(
29
+ r"^[a-fA-F0-9]{2}$",
30
+ "CRC-8 (Generic)",
31
+ "Error Detection Checksum",
32
+ POPULARITY_RANKING["High"],
33
+ ),
34
+ Hash(
35
+ r"^[a-fA-F0-9]{2}$",
36
+ "CRC-8/SAE-J1850",
37
+ "Error Detection Checksum",
38
+ POPULARITY_RANKING["High"],
39
+ ),
40
+ Hash(
41
+ r"^[a-fA-F0-9]{2}$",
42
+ "CRC-8/MAXIM (MAXIM-DOW)",
43
+ "Error Detection Checksum",
44
+ POPULARITY_RANKING["High"],
45
+ ),
46
+ Hash(
47
+ r"^[a-fA-F0-9]{2}$",
48
+ "CRC-8/CDMA2000",
49
+ "Error Detection Checksum",
50
+ POPULARITY_RANKING["Moderate"],
51
+ ),
52
+ Hash(
53
+ r"^[a-fA-F0-9]{2}$",
54
+ "CRC-8/DARC",
55
+ "Error Detection Checksum",
56
+ POPULARITY_RANKING["Low"],
57
+ ),
58
+ Hash(
59
+ r"^[a-fA-F0-9]{2}$",
60
+ "CRC-8/DVB-S2",
61
+ "Error Detection Checksum",
62
+ POPULARITY_RANKING["Moderate"],
63
+ ),
64
+ Hash(
65
+ r"^[a-fA-F0-9]{2}$",
66
+ "CRC-8/EBU (AES/EBU)",
67
+ "Error Detection Checksum",
68
+ POPULARITY_RANKING["Moderate"],
69
+ ),
70
+ Hash(
71
+ r"^[a-fA-F0-9]{2}$",
72
+ "CRC-8/I-CODE",
73
+ "Error Detection Checksum",
74
+ POPULARITY_RANKING["Low"],
75
+ ),
76
+ Hash(
77
+ r"^[a-fA-F0-9]{2}$",
78
+ "CRC-8/ITU (CRC-8/ATM)",
79
+ "Error Detection Checksum",
80
+ POPULARITY_RANKING["Moderate"],
81
+ ),
82
+ Hash(
83
+ r"^[a-fA-F0-9]{2}$",
84
+ "CRC-8/ROHC",
85
+ "Error Detection Checksum",
86
+ POPULARITY_RANKING["Moderate"],
87
+ ),
88
+ Hash(
89
+ r"^[a-fA-F0-9]{2}$",
90
+ "CRC-8/WCDMA",
91
+ "Error Detection Checksum",
92
+ POPULARITY_RANKING["Moderate"],
93
+ ),
94
+ Hash(
95
+ r"^[a-fA-F0-9]{2}$",
96
+ "Fletcher-8",
97
+ "Error Detection Checksum",
98
+ POPULARITY_RANKING["Low/Moderate"],
99
+ ),
100
+ Hash(
101
+ r"^[a-fA-F0-9]{4}$",
102
+ "CRC-16 (Generic)",
103
+ "Error Detection Checksum",
104
+ POPULARITY_RANKING["High"],
105
+ ),
106
+ Hash(
107
+ r"^[a-fA-F0-9]{4}$",
108
+ "CRC-16/CCITT (CRC-16/CCITT-FALSE)",
109
+ "Error Detection Checksum",
110
+ POPULARITY_RANKING["Very High"],
111
+ ),
112
+ Hash(
113
+ r"^[a-fA-F0-9]{4}$",
114
+ "CRC-16/ARC (CRC-16/IBM or CRC-16/LHA)",
115
+ "Error Detection Checksum",
116
+ POPULARITY_RANKING["High"],
117
+ ),
118
+ Hash(
119
+ r"^[a-fA-F0-9]{4}$",
120
+ "CRC-16/MODBUS",
121
+ "Error Detection Checksum",
122
+ POPULARITY_RANKING["High"],
123
+ ),
124
+ Hash(
125
+ r"^[a-fA-F0-9]{4}$",
126
+ "CRC-16/XMODEM",
127
+ "Error Detection Checksum",
128
+ POPULARITY_RANKING["High"],
129
+ ),
130
+ Hash(
131
+ r"^[a-fA-F0-9]{4}$",
132
+ "CRC-16/USB",
133
+ "Error Detection Checksum",
134
+ POPULARITY_RANKING["Moderate"],
135
+ ),
136
+ Hash(
137
+ r"^[a-fA-F0-9]{4}$",
138
+ "CRC-16/DNP",
139
+ "Error Detection Checksum",
140
+ POPULARITY_RANKING["Moderate"],
141
+ ),
142
+ Hash(
143
+ r"^[a-fA-F0-9]{4}$",
144
+ "CRC-16/KERMIT",
145
+ "Error Detection Checksum",
146
+ POPULARITY_RANKING["Moderate"],
147
+ ),
148
+ Hash(
149
+ r"^[a-fA-F0-9]{4}$",
150
+ "CRC-16/MCRF4XX",
151
+ "Error Detection Checksum",
152
+ POPULARITY_RANKING["Low"],
153
+ ),
154
+ Hash(
155
+ r"^[a-fA-F0-9]{4}$",
156
+ "CRC-16/T10-DIF",
157
+ "Error Detection Checksum",
158
+ POPULARITY_RANKING["Low/Moderate"],
159
+ ),
160
+ Hash(
161
+ r"^[a-fA-F0-9]{4}$",
162
+ "Fletcher-16",
163
+ "Error Detection Checksum",
164
+ POPULARITY_RANKING["Moderate"],
165
+ ),
166
+ Hash(
167
+ r"^[a-fA-F0-9]{6}$",
168
+ "CRC-24 (OpenPGP)",
169
+ "Error Detection Checksum",
170
+ POPULARITY_RANKING["Moderate/High"],
171
+ ),
172
+ Hash(
173
+ r"^[a-fA-F0-9]{6}$",
174
+ "CRC-24/BLE",
175
+ "Error Detection Checksum",
176
+ POPULARITY_RANKING["Moderate"],
177
+ ),
178
+ Hash(
179
+ r"^[a-fA-F0-9]{6}$",
180
+ "CRC-24/FLEXRAY-A",
181
+ "Error Detection Checksum",
182
+ POPULARITY_RANKING["Low/Moderate"],
183
+ ),
184
+ Hash(
185
+ r"^[a-fA-F0-9]{6}$",
186
+ "CRC-24/FLEXRAY-B",
187
+ "Error Detection Checksum",
188
+ POPULARITY_RANKING["Low/Moderate"],
189
+ ),
190
+ Hash(
191
+ r"^[a-fA-F0-9]{6}$",
192
+ "CRC-24/LTE-A",
193
+ "Error Detection Checksum",
194
+ POPULARITY_RANKING["Low/Moderate"],
195
+ ),
196
+ Hash(
197
+ r"^[a-fA-F0-9]{6}$",
198
+ "CRC-24/LTE-B",
199
+ "Error Detection Checksum",
200
+ POPULARITY_RANKING["Low/Moderate"],
201
+ ),
202
+ Hash(
203
+ r"^[a-fA-F0-9]{6}$",
204
+ "CRC-24/INTERLAKEN",
205
+ "Error Detection Checksum",
206
+ POPULARITY_RANKING["Low"],
207
+ ),
208
+ Hash(
209
+ r"^[a-fA-F0-9]{6}$",
210
+ "CRC-24/OS-9",
211
+ "Error Detection Checksum",
212
+ POPULARITY_RANKING["Low"],
213
+ ),
214
+ Hash(
215
+ r"^[a-fA-F0-9]{8}$",
216
+ "CRC-32 (Standard/ISO-HDLC)",
217
+ "Error Detection Checksum",
218
+ POPULARITY_RANKING["Very High"],
219
+ ),
220
+ Hash(
221
+ r"^[a-fA-F0-9]{8}$",
222
+ "CRC-32/BZIP2",
223
+ "Error Detection Checksum",
224
+ POPULARITY_RANKING["Moderate"],
225
+ ),
226
+ Hash(
227
+ r"^[a-fA-F0-9]{8}$",
228
+ "CRC-32C (Castagnoli)",
229
+ "Error Detection Checksum",
230
+ POPULARITY_RANKING["High"],
231
+ ),
232
+ Hash(
233
+ r"^[a-fA-F0-9]{8}$",
234
+ "CRC-32/MPEG-2",
235
+ "Error Detection Checksum",
236
+ POPULARITY_RANKING["Low/Moderate"],
237
+ ),
238
+ Hash(
239
+ r"^[a-fA-F0-9]{8}$",
240
+ "CRC-32/JAMCRC",
241
+ "Error Detection Checksum",
242
+ POPULARITY_RANKING["Low"],
243
+ ),
244
+ Hash(
245
+ r"^[a-fA-F0-9]{8}$",
246
+ "Adler-32",
247
+ "Error Detection Checksum",
248
+ POPULARITY_RANKING["Moderate"],
249
+ ),
250
+ Hash(
251
+ r"^[a-fA-F0-9]{8}$",
252
+ "Fletcher-32",
253
+ "Error Detection Checksum",
254
+ POPULARITY_RANKING["Moderate"],
255
+ ),
256
+ Hash(
257
+ r"^[a-fA-F0-9]{8}$",
258
+ "MurmurHash3 (32-bit variant)",
259
+ "Non-Cryptographic Hash",
260
+ POPULARITY_RANKING["High"],
261
+ ),
262
+ Hash(
263
+ r"^[a-fA-F0-9]{8}$",
264
+ "FNV-1/FNV-1a (32-bit)",
265
+ "Non-Cryptographic Hash",
266
+ POPULARITY_RANKING["High"],
267
+ ),
268
+ Hash(
269
+ r"^[a-fA-F0-9]{8}$",
270
+ "xxHash32",
271
+ "Non-Cryptographic Hash",
272
+ POPULARITY_RANKING["High"],
273
+ ),
274
+ Hash(
275
+ r"^[a-fA-F0-9]{8}$",
276
+ "CityHash32",
277
+ "Non-Cryptographic Hash",
278
+ POPULARITY_RANKING["Moderate"],
279
+ ),
280
+ Hash(
281
+ r"^[a-fA-F0-9]{8}$",
282
+ "Jenkins hash (one-at-a-time, 32-bit)",
283
+ "Non-Cryptographic Hash",
284
+ POPULARITY_RANKING["Moderate"],
285
+ ),
286
+ Hash(
287
+ r"^[a-fA-F0-9]{16}$",
288
+ "CRC-64/XZ",
289
+ "Error Detection Checksum",
290
+ POPULARITY_RANKING["Moderate"],
291
+ ),
292
+ Hash(
293
+ r"^[a-fA-F0-9]{16}$",
294
+ "CRC-64/ECMA-182",
295
+ "Error Detection Checksum",
296
+ POPULARITY_RANKING["Low/Moderate"],
297
+ ),
298
+ Hash(
299
+ r"^[a-fA-F0-9]{16}$",
300
+ "CRC-64/GO-ISO",
301
+ "Error Detection Checksum",
302
+ POPULARITY_RANKING["Low"],
303
+ ),
304
+ Hash(
305
+ r"^[a-fA-F0-9]{16}$",
306
+ "xxHash64",
307
+ "Non-Cryptographic Hash",
308
+ POPULARITY_RANKING["Very High"],
309
+ ),
310
+ Hash(
311
+ r"^[a-fA-F0-9]{16}$",
312
+ "FNV-1/FNV-1a (64-bit)",
313
+ "Non-Cryptographic Hash",
314
+ POPULARITY_RANKING["High"],
315
+ ),
316
+ Hash(
317
+ r"^[a-fA-F0-9]{16}$",
318
+ "MurmurHash3 (64-bit / x64 variant)",
319
+ "Non-Cryptographic Hash",
320
+ POPULARITY_RANKING["High"],
321
+ ),
322
+ Hash(
323
+ r"^[a-fA-F0-9]{16}$",
324
+ "CityHash64",
325
+ "Non-Cryptographic Hash",
326
+ POPULARITY_RANKING["Moderate"],
327
+ ),
328
+ Hash(
329
+ r"^[a-fA-F0-9]{16}$",
330
+ "SipHash (64-bit output)",
331
+ "Non-Cryptographic Hash",
332
+ POPULARITY_RANKING["High"],
333
+ ),
334
+ Hash(
335
+ r"^[a-fA-F0-9]{16}$",
336
+ "FarmHash64",
337
+ "Non-Cryptographic Hash",
338
+ POPULARITY_RANKING["Low/Moderate"],
339
+ ),
340
+ Hash(
341
+ r"^[a-fA-F0-9]{32}$",
342
+ "MD5",
343
+ "Cryptographic Hash",
344
+ POPULARITY_RANKING["Very High"],
345
+ ),
346
+ Hash(r"^[a-fA-F0-9]{32}$", "MD4", "Cryptographic Hash", POPULARITY_RANKING["Low"]),
347
+ Hash(
348
+ r"^[a-fA-F0-9]{32}$",
349
+ "RIPEMD-128",
350
+ "Cryptographic Hash",
351
+ POPULARITY_RANKING["Low"],
352
+ ),
353
+ Hash(
354
+ r"^[a-fA-F0-9]{32}$",
355
+ "Tiger-128 (truncated)",
356
+ "Cryptographic Hash",
357
+ POPULARITY_RANKING["Very Low"],
358
+ ),
359
+ Hash(
360
+ r"^[a-fA-F0-9]{32}$",
361
+ "Haval-128",
362
+ "Cryptographic Hash",
363
+ POPULARITY_RANKING["Very Low"],
364
+ ),
365
+ Hash(
366
+ r"^[a-fA-F0-9]{32}$",
367
+ "MurmurHash3 (128-bit variant, x64)",
368
+ "Non-Cryptographic Hash",
369
+ POPULARITY_RANKING["Moderate"],
370
+ ),
371
+ Hash(
372
+ r"^[a-fA-F0-9]{32}$",
373
+ "CityHash128",
374
+ "Non-Cryptographic Hash",
375
+ POPULARITY_RANKING["Low/Moderate"],
376
+ ),
377
+ Hash(
378
+ r"^[a-fA-F0-9]{32}$",
379
+ "FarmHash128",
380
+ "Non-Cryptographic Hash",
381
+ POPULARITY_RANKING["Low"],
382
+ ),
383
+ Hash(
384
+ r"^[a-fA-F0-9]{32}$",
385
+ "xxHash128 (XXH3-128)",
386
+ "Non-Cryptographic Hash",
387
+ POPULARITY_RANKING["Growing"],
388
+ ),
389
+ Hash(
390
+ r"^[a-fA-F0-9]{32}$", "NTLM", "Cryptographic Hash", POPULARITY_RANKING["High"]
391
+ ),
392
+ Hash(
393
+ r"^[a-fA-F0-9]{40}$",
394
+ "SHA-1",
395
+ "Cryptographic Hash",
396
+ POPULARITY_RANKING["Very High"],
397
+ ),
398
+ Hash(
399
+ r"^[a-fA-F0-9]{40}$",
400
+ "RIPEMD-160",
401
+ "Cryptographic Hash",
402
+ POPULARITY_RANKING["Moderate"],
403
+ ),
404
+ Hash(
405
+ r"^[a-fA-F0-9]{40}$",
406
+ "Tiger-160 (truncated)",
407
+ "Cryptographic Hash",
408
+ POPULARITY_RANKING["Low"],
409
+ ),
410
+ Hash(
411
+ r"^[a-fA-F0-9]{40}$",
412
+ "HAS-160",
413
+ "Cryptographic Hash",
414
+ POPULARITY_RANKING["Very Low"],
415
+ ),
416
+ Hash(
417
+ r"^[a-fA-F0-9]{56}$",
418
+ "SHA-224",
419
+ "Cryptographic Hash",
420
+ POPULARITY_RANKING["Moderate"],
421
+ ),
422
+ Hash(
423
+ r"^[a-fA-F0-9]{56}$",
424
+ "SHA3-224",
425
+ "Cryptographic Hash",
426
+ POPULARITY_RANKING["Low/Moderate"],
427
+ ),
428
+ Hash(
429
+ r"^[a-fA-F0-9]{56}$",
430
+ "RIPEMD-224",
431
+ "Cryptographic Hash",
432
+ POPULARITY_RANKING["Low"],
433
+ ),
434
+ Hash(
435
+ r"^[a-fA-F0-9]{56}$",
436
+ "HAVAL-224",
437
+ "Cryptographic Hash",
438
+ POPULARITY_RANKING["Very Low"],
439
+ ),
440
+ Hash(
441
+ r"^[a-fA-F0-9]{64}$",
442
+ "SHA-256",
443
+ "Cryptographic Hash",
444
+ POPULARITY_RANKING["Very High"],
445
+ ),
446
+ Hash(
447
+ r"^[a-fA-F0-9]{64}$",
448
+ "SHA3-256",
449
+ "Cryptographic Hash",
450
+ POPULARITY_RANKING["Moderate/High"],
451
+ ),
452
+ Hash(
453
+ r"^[a-fA-F0-9]{64}$",
454
+ "BLAKE2s",
455
+ "Cryptographic Hash",
456
+ POPULARITY_RANKING["Moderate"],
457
+ ),
458
+ Hash(
459
+ r"^[a-fA-F0-9]{64}$",
460
+ "BLAKE3",
461
+ "Cryptographic Hash",
462
+ POPULARITY_RANKING["Growing"],
463
+ ),
464
+ Hash(
465
+ r"^[a-fA-F0-9]{64}$",
466
+ "RIPEMD-256",
467
+ "Cryptographic Hash",
468
+ POPULARITY_RANKING["Low"],
469
+ ),
470
+ Hash(
471
+ r"^[a-fA-F0-9]{64}$",
472
+ "GOST R 34.11-2012 (Streebog-256)",
473
+ "Cryptographic Hash",
474
+ POPULARITY_RANKING["Low"],
475
+ ),
476
+ Hash(
477
+ r"^[a-fA-F0-9]{64}$",
478
+ "Whirlpool (truncated)",
479
+ "Cryptographic Hash",
480
+ POPULARITY_RANKING["Very Low"],
481
+ ),
482
+ Hash(r"^[a-fA-F0-9]{64}$", "SM3", "Cryptographic Hash", POPULARITY_RANKING["Low"]),
483
+ Hash(
484
+ r"^[a-fA-F0-9]{80}$",
485
+ "RIPEMD-320",
486
+ "Cryptographic Hash",
487
+ POPULARITY_RANKING["Low"],
488
+ ),
489
+ Hash(
490
+ r"^[a-fA-F0-9]{96}$",
491
+ "SHA-384",
492
+ "Cryptographic Hash",
493
+ POPULARITY_RANKING["Moderate/High"],
494
+ ),
495
+ Hash(
496
+ r"^[a-fA-F0-9]{96}$",
497
+ "SHA3-384",
498
+ "Cryptographic Hash",
499
+ POPULARITY_RANKING["Low/Moderate"],
500
+ ),
501
+ Hash(
502
+ r"^[a-fA-F0-9]{96}$",
503
+ "Whirlpool (truncated)",
504
+ "Cryptographic Hash",
505
+ POPULARITY_RANKING["Very Low"],
506
+ ),
507
+ Hash(
508
+ r"^[a-fA-F0-9]{96}$",
509
+ "Skein-384",
510
+ "Cryptographic Hash",
511
+ POPULARITY_RANKING["Very Low"],
512
+ ),
513
+ Hash(
514
+ r"^[a-fA-F0-9]{128}$",
515
+ "SHA-512",
516
+ "Cryptographic Hash",
517
+ POPULARITY_RANKING["Very High"],
518
+ ),
519
+ Hash(
520
+ r"^[a-fA-F0-9]{128}$",
521
+ "SHA3-512",
522
+ "Cryptographic Hash",
523
+ POPULARITY_RANKING["Moderate"],
524
+ ),
525
+ Hash(
526
+ r"^[a-fA-F0-9]{128}$",
527
+ "Whirlpool",
528
+ "Cryptographic Hash",
529
+ POPULARITY_RANKING["Low/Moderate"],
530
+ ),
531
+ Hash(
532
+ r"^[a-fA-F0-9]{128}$",
533
+ "BLAKE2b",
534
+ "Cryptographic Hash",
535
+ POPULARITY_RANKING["Moderate"],
536
+ ),
537
+ Hash(
538
+ r"^[a-fA-F0-9]{128}$",
539
+ "Skein-512",
540
+ "Cryptographic Hash",
541
+ POPULARITY_RANKING["Low"],
542
+ ),
543
+ Hash(
544
+ r"^[a-fA-F0-9]{128}$",
545
+ "GOST R 34.11-2012 (Streebog-512)",
546
+ "Cryptographic Hash",
547
+ POPULARITY_RANKING["Low"],
548
+ ),
549
+ )
550
+
551
+ PASSWORD_HASH_DATA = (
552
+ Hash(
553
+ r"^\$2[abxy]?\$(?:0[4-9]|[12][0-9]|3[01])\$[a-zA-Z0-9./]{53}$",
554
+ "bcrypt",
555
+ "Password Hashing Function",
556
+ POPULARITY_RANKING["Very High"],
557
+ ),
558
+ Hash(
559
+ r"^\$argon2(i|d|id)\$v=\d+\$m=\d+,t=\d+,p=\d+\$[a-zA-Z0-9+/]+={0,2}\$[a-zA-Z0-9+/]+={0,2}$",
560
+ "Argon2",
561
+ "Password Hashing Function",
562
+ POPULARITY_RANKING["Very High"],
563
+ ),
564
+ Hash(
565
+ r"^pbkdf2_(?:sha256|sha512)\$\d+\$[a-zA-Z0-9+/=]+\$[a-zA-Z0-9+/=]+$",
566
+ "PBKDF2 (Django-style)",
567
+ "Password Hashing Function",
568
+ POPULARITY_RANKING["High"],
569
+ ),
570
+ Hash(
571
+ r"^\$pbkdf2(-sha1|-sha256|-sha512)?\$\d+\$[a-zA-Z0-9./+]+\$[a-zA-Z0-9./+]+$",
572
+ "PBKDF2 (crypt-style)",
573
+ "Password Hashing Function",
574
+ POPULARITY_RANKING["Low/Moderate"],
575
+ ),
576
+ Hash(
577
+ r"^\$(?:y|7)\$[a-zA-Z0-9./]+\$[a-zA-Z0-9./]+\$[a-zA-Z0-9./]+$",
578
+ "yescrypt",
579
+ "Password Hashing Function",
580
+ POPULARITY_RANKING["Moderate"],
581
+ ),
582
+ Hash(
583
+ r"^\$scrypt\$(?:ln=\d+,r=\d+,p=\d+|N=\d+,r=\d+,p=\d+)\$[a-zA-Z0-9+/=]+\$[a-zA-Z0-9+/=]+$",
584
+ "scrypt",
585
+ "Password Hashing Function",
586
+ POPULARITY_RANKING["Moderate"],
587
+ ),
588
+ Hash(
589
+ r"^\$6\$(rounds=\d+\$)?[./a-zA-Z0-9]{1,16}\$[./a-zA-Z0-9]{86}$",
590
+ "SHA-512-crypt",
591
+ "Password Hashing Function",
592
+ POPULARITY_RANKING["Moderate"],
593
+ ),
594
+ Hash(
595
+ r"^\$5\$(rounds=\d+\$)?[./a-zA-Z0-9]{1,16}\$[./a-zA-Z0-9]{43}$",
596
+ "SHA-256-crypt",
597
+ "Password Hashing Function",
598
+ POPULARITY_RANKING["Low"],
599
+ ),
600
+ Hash(
601
+ r"^\$1\$[./a-zA-Z0-9]{1,8}\$[./a-zA-Z0-9]{22}$",
602
+ "MD5-crypt",
603
+ "Password Hashing Function",
604
+ POPULARITY_RANKING["Low"],
605
+ ),
606
+ Hash(
607
+ r"^[./a-zA-Z0-9]{13}$",
608
+ "Traditional DES crypt",
609
+ "Password Hashing Function",
610
+ POPULARITY_RANKING["Very Low"],
611
+ ),
612
+ Hash(
613
+ r"^\$lyra2\$t=\d+,m=\d+,p=\d+\$[a-zA-Z0-9+/=]+\$[a-zA-Z0-9+/=]+$",
614
+ "Lyra2",
615
+ "Password Hashing Function",
616
+ POPULARITY_RANKING["Very Low"],
617
+ ),
618
+ Hash(
619
+ r"^\$2[ay]\$[a-zA-Z0-9./]{53}$",
620
+ "bcrypt (PHP)",
621
+ "Password Hashing Function (PHP)",
622
+ POPULARITY_RANKING["High"],
623
+ ),
624
+ Hash(
625
+ r"^\$P\$[a-zA-Z0-9./]{31}$",
626
+ "phpass (WordPress)",
627
+ "Password Hashing Function",
628
+ POPULARITY_RANKING["High"],
629
+ ),
630
+ Hash(
631
+ r"^argon2\$[a-zA-Z0-9+/=]+\$[a-zA-Z0-9+/=]+$",
632
+ "Argon2 (Django)",
633
+ "Password Hashing Function",
634
+ POPULARITY_RANKING["Moderate"],
635
+ ),
636
+ Hash(
637
+ r"^\{[A-Z]+\}[a-zA-Z0-9+/=]+$",
638
+ "LDAP Hash",
639
+ "Password Hashing Function",
640
+ POPULARITY_RANKING["Low/Moderate"],
641
+ ),
642
+ Hash(
643
+ r"^\*[a-fA-F0-9]{40}$",
644
+ "MySQL Password",
645
+ "Database Password Hash",
646
+ POPULARITY_RANKING["Low"],
647
+ ),
648
+ Hash(
649
+ r"^md5[a-fA-F0-9]{32}$",
650
+ "PostgreSQL MD5",
651
+ "Database Password Hash",
652
+ POPULARITY_RANKING["Low"],
653
+ ),
654
+ Hash(
655
+ r"^\{bcrypt\}[a-zA-Z0-9./]{60}$",
656
+ "Spring Security BCrypt",
657
+ "Password Hashing Function",
658
+ POPULARITY_RANKING["Moderate"],
659
+ ),
660
+ )
HashPrint/formatter.py ADDED
@@ -0,0 +1,119 @@
1
+ # In the Name of God, the Most Compassionate, Most Merciful
2
+
3
+
4
+ from HashPrint.cli import console, DISCLAIMER_MESSAGE, SUCCESS_MESSAGE
5
+ from HashPrint import __project_name__, __version__
6
+ from datetime import datetime
7
+ import json
8
+ import yaml
9
+
10
+ timestamp = datetime.now()
11
+ formatted_timestamp = timestamp.strftime("%Y-%m-%d %H:%M:%S")
12
+
13
+
14
+ def format_data(results):
15
+ data = [
16
+ {
17
+ "timestamp": f"{formatted_timestamp} by {__project_name__} v{__version__}",
18
+ "success": SUCCESS_MESSAGE,
19
+ "disclaimer": DISCLAIMER_MESSAGE,
20
+ }
21
+ ]
22
+ for result in results:
23
+ info = {
24
+ "Hash": result["Hash"],
25
+ "Source": result["Source"],
26
+ "Possible Algorithms": [
27
+ {
28
+ "Name": algorithm.name,
29
+ "Family": algorithm.family,
30
+ "Popularity Score": algorithm.popularity,
31
+ }
32
+ for algorithm in result["Matches"]
33
+ ],
34
+ "Error": result["Error"],
35
+ }
36
+ data.append(info)
37
+ return data
38
+
39
+
40
+ def write_text(file_path, data):
41
+ try:
42
+ with open(file_path, "w", encoding="utf-8") as text_file:
43
+ text_file.write(f"Generated at: {formatted_timestamp} by {__project_name__} v{__version__}\n")
44
+ text_file.write(f"{SUCCESS_MESSAGE}\n")
45
+ text_file.write(f"DISCLAIMER: {DISCLAIMER_MESSAGE}\n\n")
46
+ for result in data[1:]:
47
+ text_file.write(f"Source: {result["Source"]}\n")
48
+ text_file.write(f"Hash: '{result["Hash"]}'\n")
49
+ if not result["Possible Algorithms"]:
50
+ text_file.write("Possible Algorithms: None\n")
51
+ else:
52
+ text_file.write("Possible Algorithms:\n")
53
+ for algorithm in result["Possible Algorithms"]:
54
+ text_file.write(f"[#] Name: {algorithm["Name"]}\n")
55
+ text_file.write(f"\t[-] Family: {algorithm["Family"]}\n")
56
+ if not result["Error"]:
57
+ text_file.write("Error: None\n\n")
58
+ else:
59
+ text_file.write(f"Error: {result["Error"]}\n\n")
60
+ except FileNotFoundError:
61
+ console.print(
62
+ f"[error]\nError: a directory in the path: '{file_path}' does not exist\n[/error]"
63
+ )
64
+ except PermissionError:
65
+ console.print(
66
+ f"[error]\nError: write permission to the path: '{file_path}' denied\n[/error]"
67
+ )
68
+ except IsADirectoryError:
69
+ console.print(
70
+ f"[error]\nError: the path: '{file_path}' is a directory not a file\n[/error]"
71
+ )
72
+ except OSError as exception:
73
+ console.print(
74
+ f"[error\n]Error: cannot write to the path: '{file_path}' - {exception}\n[/error]"
75
+ )
76
+
77
+
78
+ def write_json(file_path, data):
79
+ try:
80
+ with open(file_path, "w", encoding="utf-8") as json_file:
81
+ json.dump(data, json_file, indent=2)
82
+ except FileNotFoundError:
83
+ console.print(
84
+ f"[error]\nError: a directory in the path: '{file_path}' does not exist\n[/error]"
85
+ )
86
+ except PermissionError:
87
+ console.print(
88
+ f"[error]\nError: write permission to the path: '{file_path}' denied\n[/error]"
89
+ )
90
+ except IsADirectoryError:
91
+ console.print(
92
+ f"[error]\nError: the path: '{file_path}' is a directory not a file\n[/error]"
93
+ )
94
+ except OSError as exception:
95
+ console.print(
96
+ f"[error]\nError: cannot write to the path: '{file_path}' - {exception}\n[/error]"
97
+ )
98
+
99
+
100
+ def write_yaml(file_path, data):
101
+ try:
102
+ with open(file_path, "w", encoding="utf-8") as yaml_file:
103
+ yaml.dump(data, yaml_file, encoding="utf-8", sort_keys=False)
104
+ except FileNotFoundError:
105
+ console.print(
106
+ f"[error]\nError: a directory in the path: '{file_path}' does not exist\n[/error]"
107
+ )
108
+ except PermissionError:
109
+ console.print(
110
+ f"[error]\nError: write permission to the path: '{file_path}' denied\n[/error]"
111
+ )
112
+ except IsADirectoryError:
113
+ console.print(
114
+ f"[error]\nError: the path: '{file_path}' is a directory not a file\n[/error]"
115
+ )
116
+ except OSError as exception:
117
+ console.print(
118
+ f"[error]\nError: cannot write to the path: '{file_path}' - {exception}\n[/error]"
119
+ )
@@ -0,0 +1,27 @@
1
+ # In the Name of God, the Most Compassionate, Most Merciful
2
+
3
+
4
+ from HashPrint.database import HASH_DATA, PASSWORD_HASH_DATA
5
+ import re
6
+
7
+
8
+ def is_hex_encoded(candidate_hash):
9
+ hex_regex = re.compile(r"^[a-fA-F0-9]+$")
10
+ if hex_regex.fullmatch(candidate_hash):
11
+ return True
12
+ return False
13
+
14
+
15
+ def identify(candidate_hash):
16
+ possible_matches = []
17
+ if is_hex_encoded(candidate_hash):
18
+ for HASH_INFO in HASH_DATA:
19
+ if HASH_INFO.pattern.fullmatch(candidate_hash):
20
+ possible_matches.append(HASH_INFO)
21
+ else:
22
+ for PASSWORD_HASH_INFO in PASSWORD_HASH_DATA:
23
+ if PASSWORD_HASH_INFO.pattern.fullmatch(candidate_hash):
24
+ possible_matches.append(PASSWORD_HASH_INFO)
25
+ return sorted(
26
+ possible_matches, key=lambda matches: matches.popularity, reverse=True
27
+ )
HashPrint/main.py ADDED
@@ -0,0 +1,134 @@
1
+ # In the Name of God, the Most Compassionate, Most Merciful
2
+
3
+
4
+ import sys
5
+ from HashPrint.cli import (
6
+ cli_parser,
7
+ console,
8
+ hint_line,
9
+ DISCLAIMER_MESSAGE,
10
+ POSSIBILITY_MESSAGE,
11
+ SUCCESS_MESSAGE,
12
+ print_banner,
13
+ )
14
+ from HashPrint.validator import validate
15
+ from HashPrint.identifier import identify
16
+ from HashPrint.printer import print_results_to_console
17
+ from HashPrint.reader import read_hashes
18
+ from HashPrint.walker import walker
19
+ from HashPrint.formatter import (
20
+ write_text,
21
+ write_json,
22
+ write_yaml,
23
+ format_data,
24
+ )
25
+ from HashPrint import __version__, __project_name__
26
+
27
+ EXIT_SUCCESS = 0
28
+ EXIT_GENERAL_ERROR = 1
29
+ EXIT_USAGE_ERROR = 2
30
+
31
+ WRITERS = {
32
+ "text": write_text,
33
+ "json": write_json,
34
+ "yaml": write_yaml,
35
+ }
36
+
37
+
38
+ def main():
39
+ args = cli_parser()
40
+ candidate_hash_values_data = []
41
+ results = []
42
+
43
+ input_option_count = sum(
44
+ [bool(args.candidate_hash), bool(args.hash_file), bool(args.walk)]
45
+ )
46
+
47
+ if input_option_count > 1:
48
+ console.print(
49
+ f"[error]\nError: only one input option (--hash --hash-file --walk) can be used at a time[/error]"
50
+ )
51
+ console.print(f"[hint]{hint_line}[/hint]")
52
+ return EXIT_USAGE_ERROR
53
+
54
+ print_banner()
55
+
56
+ if args.version:
57
+ console.print(f"[description]{__project_name__} v{__version__}[/description]")
58
+ return EXIT_SUCCESS
59
+
60
+ # args.candidate_hash is a list
61
+ if args.candidate_hash:
62
+ for candidate_hash in args.candidate_hash:
63
+ candidate_hash_values_data.append(
64
+ {"Hash": candidate_hash, "Source": "command-line"}
65
+ )
66
+ # args.hash_file is also a list
67
+ elif args.hash_file:
68
+ for hash_file in args.hash_file:
69
+ file_hashes = read_hashes(hash_file)
70
+ for file_hash in file_hashes:
71
+ candidate_hash_values_data.append(
72
+ {"Hash": file_hash, "Source": hash_file}
73
+ )
74
+ elif args.walk:
75
+ text_files = walker(args.walk)
76
+ if not text_files:
77
+ console.print(
78
+ "[error]Error: no text (.txt) files found in the given directory[/error]"
79
+ )
80
+ return EXIT_GENERAL_ERROR
81
+ for text_file in text_files:
82
+ text_file_hashes = read_hashes(text_file)
83
+ for text_file_hash in text_file_hashes:
84
+ candidate_hash_values_data.append(
85
+ {"Hash": text_file_hash, "Source": text_file}
86
+ )
87
+
88
+ for candidate_hash_info in candidate_hash_values_data:
89
+ is_hash_valid, error_msg = validate(candidate_hash_info["Hash"])
90
+ if not is_hash_valid:
91
+ results.append(
92
+ {
93
+ "Hash": candidate_hash_info["Hash"],
94
+ "Source": candidate_hash_info["Source"],
95
+ "Error": error_msg,
96
+ "Matches": [],
97
+ }
98
+ )
99
+ continue
100
+ matches = identify(candidate_hash_info["Hash"])
101
+ results.append(
102
+ {
103
+ "Hash": candidate_hash_info["Hash"],
104
+ "Source": candidate_hash_info["Source"],
105
+ "Error": error_msg,
106
+ "Matches": matches,
107
+ }
108
+ )
109
+
110
+ if not args.path and args.format:
111
+ console.print("[error]\nError: missing option: '--path'[/error]")
112
+ console.print(f"[hint]{hint_line}[/hint]")
113
+ return EXIT_USAGE_ERROR
114
+ elif not args.format and args.path:
115
+ console.print("[error]\nError: missing option: '--format'[/error]")
116
+ console.print(f"[hint]{hint_line}[/hint]")
117
+ return EXIT_USAGE_ERROR
118
+ elif args.format and args.path:
119
+ data = format_data(results)
120
+ WRITERS[args.format](args.path, data)
121
+ console.print(f"[success]\n{SUCCESS_MESSAGE}[/success]")
122
+ console.print(
123
+ f"[disclaimer]File is overwritten if it was not empty[/disclaimer]"
124
+ )
125
+ console.print(f"\n[disclaimer]DISCLAIMER: {DISCLAIMER_MESSAGE}[/disclaimer]")
126
+ return EXIT_SUCCESS
127
+
128
+ print_results_to_console(results, DISCLAIMER_MESSAGE, POSSIBILITY_MESSAGE)
129
+
130
+ return EXIT_SUCCESS
131
+
132
+
133
+ if __name__ == "__main__":
134
+ sys.exit(main())
HashPrint/printer.py ADDED
@@ -0,0 +1,26 @@
1
+ # In the Name of God, the Most Compassionate, Most Merciful
2
+
3
+
4
+ from HashPrint.cli import console
5
+
6
+
7
+ def print_results_to_console(results, DISCLAIMER_MESSAGE, POSSIBILITY_MESSAGE):
8
+ for result in results:
9
+ if not result["Error"]:
10
+ if not result["Matches"]:
11
+ console.print(f"[hint]\nSource: {result["Source"]}[hint]")
12
+ console.print(f"[fail]Hash: '{result["Hash"]}'[/fail]")
13
+ console.print("[fail]No algorithms produce the given hash[/fail]")
14
+ console.print(f"[hint]Note: {POSSIBILITY_MESSAGE}[/hint]")
15
+ continue
16
+ console.print(f"[hint]\nSource: {result["Source"]}[/hint]")
17
+ console.print(f"[information]Hash: '{result["Hash"]}'[/information]")
18
+ console.print(f"[information]Possible Algorithms:\n[/information]")
19
+ for algorithm in result["Matches"]:
20
+ console.print(f"[success]\\[#] {algorithm.name}[/success]")
21
+ console.print(f"[success]\tFamily: {algorithm.family}[/success]")
22
+ else:
23
+ console.print(f"[hint]\nSource: {result["Source"]}[/hint]")
24
+ console.print(f"[error]Hash: '{result["Hash"]}'[/error]")
25
+ console.print(f"[error]Error: {result["Error"]}[/error]")
26
+ console.print(f"[disclaimer]\nDISCLAIMER: {DISCLAIMER_MESSAGE}[/disclaimer]")
HashPrint/reader.py ADDED
@@ -0,0 +1,29 @@
1
+ # In the Name of God, the Most Compassionate, Most Merciful
2
+
3
+
4
+ from HashPrint.cli import console
5
+
6
+
7
+ def read_hashes(file_path):
8
+ try:
9
+ with open(file_path, "r") as hash_file:
10
+ for line in hash_file:
11
+ stripped_line = line.strip()
12
+ if stripped_line:
13
+ yield stripped_line
14
+ except FileNotFoundError:
15
+ console.print(
16
+ f"[error]Error: file at the path: '{file_path}' not found - please check the path[/error]"
17
+ )
18
+ except PermissionError:
19
+ console.print(
20
+ f"[error]Error: read permission to the path: '{file_path}' denied[/error]"
21
+ )
22
+ except IsADirectoryError:
23
+ console.print(
24
+ f"[error]Error: the path: '{file_path}' is a directory not a file[/error]"
25
+ )
26
+ except OSError as exception:
27
+ console.print(
28
+ f"[error]Error: cannot write to the path: '{file_path}' - {exception}[/error]"
29
+ )
HashPrint/validator.py ADDED
@@ -0,0 +1,33 @@
1
+ # In the Name of God, the Most Compassionate, Most Merciful
2
+
3
+
4
+ def validate(raw_hash):
5
+ INVALID = {
6
+ "!",
7
+ "@",
8
+ "#",
9
+ "%",
10
+ "^",
11
+ "&",
12
+ "(",
13
+ ")",
14
+ "[",
15
+ "]",
16
+ "'",
17
+ '"',
18
+ "<",
19
+ ">",
20
+ ";",
21
+ ":",
22
+ "\\",
23
+ }
24
+ cleaned_hash = raw_hash.strip()
25
+ if not cleaned_hash:
26
+ return False, "hash is empty"
27
+ for char in cleaned_hash:
28
+ if char.isspace():
29
+ return False, "hash contains whitespace(s)"
30
+ for hash_char in cleaned_hash:
31
+ if hash_char in INVALID:
32
+ return False, "hash contains invalid charater(s)"
33
+ return True, ""
HashPrint/walker.py ADDED
@@ -0,0 +1,34 @@
1
+ # In the Name of God, the Most Compassionate, Most Merciful
2
+
3
+
4
+ import os
5
+ from HashPrint.cli import console
6
+
7
+
8
+ def is_probably_text_file(file_path, sample_size=1024):
9
+ try:
10
+ with open(file_path, "rb") as file:
11
+ sample = file.read(sample_size)
12
+ if b"\00" in sample:
13
+ console.print(
14
+ f"[warning]Warning: file at path: '{file_path}' skipped - has .txt extensions but appears to be binary[/warning]"
15
+ )
16
+ return False
17
+ except PermissionError:
18
+ console.print("[error]Error: [/error]")
19
+ return False
20
+ except OSError as exception:
21
+ console.print(f"[error]Error: something went wrong - {exception}[/error]")
22
+ return False
23
+ return True
24
+
25
+
26
+ def walker(start_dir):
27
+ text_files = []
28
+ for directory_root, _, files in os.walk(start_dir):
29
+ for file in files:
30
+ if file.endswith(".txt"):
31
+ file_path = os.path.join(directory_root, file)
32
+ if is_probably_text_file(file_path):
33
+ text_files.append(file_path)
34
+ return text_files
@@ -0,0 +1,135 @@
1
+ Metadata-Version: 2.5
2
+ Name: hashprint-cli
3
+ Version: 1.0.0
4
+ Summary: A forensic CLI tool for hash identification
5
+ License-Expression: MIT
6
+ License-File: LICENSE
7
+ Requires-Python: >=3.14
8
+ Requires-Dist: pyfiglet>=1.0.4
9
+ Requires-Dist: pyyaml>=6.0.3
10
+ Requires-Dist: rich-argparse>=1.8.0
11
+ Requires-Dist: rich>=15.0.0
12
+ Description-Content-Type: text/markdown
13
+
14
+ # HashPrint
15
+
16
+ ## What's this?
17
+
18
+ ***Note: this is a student project***
19
+
20
+ Surely a random string like ***'b318c90ba09d05dfc9ff3991974eee68'*** can look scary.
21
+ Be afraid no more, HashPrint got your back.
22
+
23
+ ## About HashPrint
24
+
25
+ HashPrint is a forensic CLI tool that can identify which algorithm (like MD5, bcrypt, SHA256) produced the scary looking string, called a hash, like the above one.
26
+
27
+ ## Features of HashPrint
28
+
29
+ 1. **Hash Support:** HashPrint can identify 90+ hashes.
30
+ 2. **Export Options:** HashPrint allows you to export results in various formats, like JSON and YAML.
31
+ 3. **Multiple Hashes:** HashPrint can read multiple hashes from a text file. It can recursively scan a directory to locate text files that contain hashes. It can also accept multiple hashes from the command-line.
32
+ 4. **Coloured Output:** HashPrint outputs coloured results, making them easier to read.
33
+ 5. **Easy to use:** if you ever feel stuck, remember help is just a flag away.
34
+
35
+ ## Installation
36
+
37
+ ***Requires Python 3.14+***
38
+
39
+ If installing with pip, then you must have Python 3.14 or higher installed on your system
40
+
41
+ ```console
42
+ $ pip install hashprint
43
+ ```
44
+
45
+ **OR with uv**
46
+
47
+ uv installs the correct Python version if necessary
48
+
49
+ **First setup a virtual environment:**
50
+
51
+ ```console
52
+ $ uv venv
53
+ ```
54
+
55
+ **Then install:**
56
+
57
+ ```console
58
+ $ uv pip install hashprint
59
+ ```
60
+
61
+ **Use without installing**
62
+
63
+ ***Requires uv***
64
+
65
+ ```console
66
+ $ uv tool run hashprint
67
+ ```
68
+
69
+ **OR**
70
+
71
+ ```console
72
+ $ uvx hashprint
73
+ ```
74
+
75
+ ## How to use HashPrint?
76
+
77
+ | Option | Purpose |
78
+ |:------:|:-------------|
79
+ *-h*, *--help* | shows the help message and exits. |
80
+ *-f*, *--hash-file* | this option takes a path to a text file as an argument. With this option, HashPrint will read the file at the specified path for hashes. The file must contain one hash per line. |
81
+ *-c*, *--hash* | this option takes a hash as an argument and identifies it. It should be between single quotes and without the 0x prefix. |
82
+ *-x*, *--format* | this option lets you choose between the export formats. You can export the results in JSON, YAML and text formats. This option cannot work without the *-p*, *--path* option. |
83
+ *-p*, *--path* | this option specifies the path of the export file. If a path to a pre-existing file is specified, it will be overwritten if it already contains content. If the file at the specified path does not exist, HashPrint will create it. |
84
+ *-w*, *--walk* | this option recursively scans a directory at the specified path for text files, containing one hash per line. |
85
+ *-v*, *--version* | this option displays the version number of HashPrint. |
86
+
87
+ ## Usage examples
88
+
89
+ **For help**
90
+
91
+ ```console
92
+ $ hashprint --help
93
+ ```
94
+
95
+ **Display the version number**
96
+
97
+ ```console
98
+ $ hashprint --version
99
+ ```
100
+
101
+ **Identify a single hash**
102
+
103
+ ```console
104
+ $ hashprint -c 'a9993e364706816aba3e25717850c26c9cd0d89d'
105
+ ```
106
+
107
+ OR
108
+
109
+ ```console
110
+ $ hashprint --hash 'a9993e364706816aba3e25717850c26c9cd0d89d'
111
+ ```
112
+
113
+ **Identify multiple hashes**
114
+
115
+ ```console
116
+ $ hashprint -c '0cc175b9c0f1b6a831c399e269772661' -c 'e8b7be43'
117
+ ```
118
+
119
+ **Identify hashes from a file + export results to a file**
120
+
121
+ ```console
122
+ $ hashprint -f path/to/txt/file/containing/hashes -x text -p path/of/export/file
123
+ ```
124
+
125
+ The above command exports results in a human readable text format at a file of your choice. HashPrint will create the file if it does not exist.
126
+
127
+ **Recursive Directory Scanner + exporting results to a file**
128
+
129
+ ```console
130
+ $ hashprint -w path/to/directory -x format -p path/of/export/file
131
+ ```
132
+
133
+ ## LICENSE
134
+
135
+ [MIT](./LICENSE)
@@ -0,0 +1,15 @@
1
+ HashPrint/__init__.py,sha256=OebTeKOAc7DjK3HihaQBe76OVswJ7Al7_ZwQBqfW4ak,207
2
+ HashPrint/cli.py,sha256=nFJF3Cm67TB0xFYF75lxMrCSc8pmVLqz7xhhE7u78yI,3284
3
+ HashPrint/database.py,sha256=7V4NWAjtzyEXIPrnHTgUG1xc8dEmE_F397CH2jM5o1Y,17227
4
+ HashPrint/formatter.py,sha256=Xcye85fzvG0QXTrdFGCxb0GZ6xFtpQJ5toxLqTyAL7k,4532
5
+ HashPrint/identifier.py,sha256=nTTeOR10NiVFOI9a6-ujHQoCLHuL_nzp8XPZDzWPHfU,866
6
+ HashPrint/main.py,sha256=vWpERUhiuETkn-6Ahs6NpSVdoJCDrb0NvLm_ltxTwG4,4320
7
+ HashPrint/printer.py,sha256=jlyHjnufgFSNokpdTptq_rAwwIgYcC13-ExWDoc-tgs,1379
8
+ HashPrint/reader.py,sha256=bdk85Oe6LG80I2yaK3urhdZeFuu4Y17Cl3eWXacQgQs,987
9
+ HashPrint/validator.py,sha256=OoRj1ewSjbalzZfAIIBZpzeeTmFnAi7S9ssGt-WQMGc,725
10
+ HashPrint/walker.py,sha256=ApyyJKXTPQMAfMbF_4tRnU7CO8LOVtwAYU6hyhGyXKo,1123
11
+ hashprint_cli-1.0.0.dist-info/METADATA,sha256=W354Mg-eTH1IzM-qEb1hoXUOkjuY9y0OceCTScNJRhc,3821
12
+ hashprint_cli-1.0.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
13
+ hashprint_cli-1.0.0.dist-info/entry_points.txt,sha256=PhiEyYfr-dqSeQImQdCRudA3UJXtJ8qOdiOWpRrxo4Q,50
14
+ hashprint_cli-1.0.0.dist-info/licenses/LICENSE,sha256=SS-4C6Y9mUNXzoD2fj_MSqX-6mYc3Z9tIMts3FiAWd0,1089
15
+ hashprint_cli-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.4
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ hashprint = HashPrint.main:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Taha Qaisar
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.