├── Screenshots
├── ms-example.png
├── psudohash.png
├── micro-example.png
└── multiple-words.png
├── common_padding_values.txt
├── LICENSE.md
├── README.md
└── psudohash.py
/Screenshots/ms-example.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/t3l3machus/psudohash/HEAD/Screenshots/ms-example.png
--------------------------------------------------------------------------------
/Screenshots/psudohash.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/t3l3machus/psudohash/HEAD/Screenshots/psudohash.png
--------------------------------------------------------------------------------
/Screenshots/micro-example.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/t3l3machus/psudohash/HEAD/Screenshots/micro-example.png
--------------------------------------------------------------------------------
/Screenshots/multiple-words.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/t3l3machus/psudohash/HEAD/Screenshots/multiple-words.png
--------------------------------------------------------------------------------
/common_padding_values.txt:
--------------------------------------------------------------------------------
1 | 123
2 | 1234
3 | 12345
4 | 123456
5 | 234
6 | 345
7 | 456
8 | 567
9 | 678
10 | 789
11 | 890
12 | !@#
13 | !@!
14 | @#@
15 | !@#$%
16 | 123!@#
17 | !@#$%
18 | $%^&
19 | !
20 | !!
21 | !!!
22 | @
23 | @@
24 | @@@
25 | #
26 | ##
27 | ###
28 | $
29 | $$
30 | $$$
31 | %
32 | ^
33 | &
34 | &&
35 | &&&
36 | *
37 | **
38 | ***
39 | .
40 | ..
41 | ...
42 | ?
43 | ??
44 | ???
45 | -
46 | --
47 | ---
48 | !@
49 | @#
50 | #$
51 | $%
52 | %^
53 | ^&
54 | &*
55 | *(
56 | ()
57 | @#$
58 | #$%
59 | $%^
60 | %^&
61 | ^&*
62 | &*(
63 | *()
64 | )_+
65 | 1!1
66 | 2@2
67 | 3#3
68 | 4$4
69 | 5%5
70 | 6^6
71 | 7&7
72 | 8*8
73 | 9(9
74 | 0)0
75 | @2@
76 | #3#
77 | $4$
78 | %5%
79 | ^6^
80 | &7&
81 | *8*
82 | (9(
83 | +++
84 | ,./
85 | abc123
86 | 1111
87 | 2222
88 | 3333
89 | 4444
90 | 5555
91 | 6666
92 | 7777
93 | 8888
94 | 9999
95 | asd
96 | asdf
97 | zxc
98 | zxcv
99 | qwe
100 | qwer
101 | qwerty
102 | q1
103 | w2
104 | e3
105 | r4
106 | t5
107 | q1w2
108 | q1w2e3
109 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 Panagiotis Chartas (t3l3machus)
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.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # psudohash
2 | [](https://www.python.org/) [](https://github.com/t3l3machus/psudohash/blob/main/LICENSE)
3 |
4 |
5 |
6 | ## Cool New Features of v1.1.0
7 | Special thanks to [DavidAngelos](https://github.com/DavidAngelos):
8 | ▶️ Added a progress bar in every step to track execution.
9 | ▶️ Added options:
10 | - **In-order joins** (`-i` / `--inorder`): join keywords only in the original order (e.g. `foo,bar,baz` → `foo, bar, baz, foobar, foobaz, barbaz, foobarbaz`).
11 | - **All-order combinations** (`-c` / `--combinations`): generate every ordering of each subset (e.g. `foo,bar,baz` → `foo, bar, baz, foobar, foobaz, barfoo, …, bazbarfoo`).
12 | - **Custom separator** (`--sep `): when joining words, insert this string between tokens (defaults to no separator).
13 | - **Max combine size** (`--max-combine `): limit how many raw keywords get joined together (default: 2).
14 | - **Min/Max length filtering of final words** (`--minlen/--maxlen `): filter the final wordlist only with word with the desired length.
15 |
16 | ## Purpose
17 | Psudohash is a password list generator for orchestrating brute force attacks and cracking hashes. It imitates certain password creation patterns commonly used by humans, like substituting a word's letters with symbols or numbers (leet), using char-case variations, adding a common padding before or after the main passphrase and more. It is keyword-based and highly customizable. 🎥 -> [Video Presentation](https://www.youtube.com/watch?v=oj3zjApOOGc)
18 |
19 | ## Pentesting Corporate Environments
20 | System administrators and other employees often use a mutated version of the Company's name to set passwords (e.g. Am@z0n_2022). This is commonly the case for network devices (Wi-Fi access points, switches, routers, etc), application or even domain accounts. With the most basic options, psudohash can generate a wordlist with all possible mutations of one or multiple keywords, based on common character substitution patterns (customizable), case variations, strings commonly used as padding and more. Take a look at the following example:
21 |
22 | 
23 |
24 |
25 | ## Customization
26 | ### Leet Character Substitution
27 | The script implements the following character substitution schema. You can add/modify character substitution mappings by editing the `transformations` list in `psudohash.py` and following the data structure presented below (default):
28 | ```
29 | transformations = [
30 | {'a' : ['@', '4']},
31 | {'b' : '8'},
32 | {'e' : '3'},
33 | {'g' : ['9', '6']},
34 | {'i' : ['1', '!']},
35 | {'o' : '0'},
36 | {'s' : ['$', '5']},
37 | {'t' : '7'}
38 | ]
39 | ```
40 | ### Common Padding Values
41 | When setting passwords, I believe it's pretty standard to add a sequence of characters before and/or after the main passphrase to make it "stronger". For example, one may set a password "dragon" and add a value like "!!!" or "!@#" at the end, resulting in "dragon!!!", "dragon!@#", etc. Psudohash reads such values from `common_padding_values.txt` and uses them to mutate the provided keywords by appending them before (`-cpb`) or after (`-cpa`) each generated keyword variation. You can modify it as you see fit.
42 |
43 | ### Year Values
44 | When appending a year value to a mutated keyword, psudohash will do so by utilizing various seperators. by default, it will use the following seperators which you can modify by editing the `year_seperators` list:
45 | ```
46 | year_seperators = ['', '_', '-', '@']
47 | ```
48 | For example, if the given keyword is "amazon" and option `-y 2023` was used, the output will include "amazon2023", "amazon_2023", "amazon-2023", "amazon@2023", "amazon23", "amazon_23", "amazon-23", "amazon@23".
49 |
50 | ## Installation
51 | Install Python 3.x and `tqdm` first:
52 |
53 | ```bash
54 | pip3 install tqdm
55 | ```
56 | Then clone the repo and make the script executable:
57 | ```
58 | git clone https://github.com/t3l3machus/psudohash.git
59 | cd ./psudohash
60 | chmod +x psudohash.py
61 | ```
62 | ## Usage
63 | ```
64 | ./psudohash.py [-h] -w WORDS [-i] [-c] [--sep SEP] [--max-combine N] [-an LEVEL] [-nl LIMIT] [-y YEARS] [-ap VALUES] [-cpb] [-cpa] [-cpo] [-o FILENAME] [-q]
65 | ```
66 | The help dialog [ -h, --help ] includes usage details and examples.
67 |
68 | ## Options
69 |
70 | - **`-w, --words `**
71 | Comma‐separated raw keywords (required).
72 |
73 | - **`-i, --inorder`**
74 | Join up to `--max-combine` keywords in the given order (e.g. `foo,bar,baz` → `foo, bar, baz, foobar, foobaz, barbaz, foobarbaz`).
75 |
76 | - **`-c, --combinations`**
77 | Generate every permutation of each subset (up to `--max-combine`) (e.g. `foo,bar,baz` → `foo, bar, baz, foobar, foobaz, barfoo, …`).
78 |
79 | - **`--max-combine `** (default: 2)
80 | Maximum number of raw keywords to join into one base string.
81 |
82 | - **`--sep `**
83 | When joining words (`-i` or `-c`), place this string between tokens. Defaults to an empty string.
84 |
85 | - **`--minlen `**
86 | Discard any final password shorter than N characters.
87 |
88 | - **`--maxlen `**
89 | Discard any final password longer than N characters.
90 |
91 | - **`-an, --append-numbering `**
92 | Append numbered suffixes (zero‐padded to `` digits) to each word mutation.
93 |
94 | - **`-nl, --numbering-limit `**
95 | Maximum number to count up to when appending numbers (default: 50).
96 |
97 | - **`-y, --years `**
98 | Append one or more years to each mutation (e.g. `1990-2000`, or `2022,2023`).
99 |
100 | - **`-ap, --append-padding `**
101 | Append custom padding values (comma‐separated). Must be used with `-cpb` or `-cpa`.
102 |
103 | - **`-cpb, --common-paddings-before`**
104 | Prepend values from `common_padding_values.txt` before each mutation.
105 |
106 | - **`-cpa, --common-paddings-after`**
107 | Append values from `common_padding_values.txt` after each mutation.
108 |
109 | - **`-cpo, --custom-paddings-only`**
110 | Use only user‐provided paddings (no defaults). Must be used with `-ap`.
111 |
112 | - **`-o, --output `**
113 | Write the results to `` (default: `output.txt`).
114 |
115 | - **`-q, --quiet`**
116 | Suppress the ASCII art banner on startup.
117 |
118 |
119 | ### Usage Examples
120 |
121 | 1. **No multi‐word (singletons only)**
122 | ```bash
123 | ./psudohash.py -w foo,bar,baz -cpa
124 | # → foo, bar, baz
125 | ```
126 |
127 | 2. **In‐order joins (-i, up to 2 words by default)**
128 | ```bash
129 | ./psudohash.py -w foo,bar,baz -i
130 | # → foo, bar, baz, foobar, foobaz, barbaz
131 | ```
132 |
133 | 3. **All‐order combinations (-c, up to 2 words by default)**
134 | ```bash
135 | ./psudohash.py -w foo,bar,baz -c
136 | # → foo, bar, baz, foobar, foobaz, barfoo, barbaz, bazfoo, bazbar
137 | ```
138 |
139 | 4. **Change separator between joined words**
140 | ```bash
141 | ./psudohash.py -w foo,bar,baz -i --sep "_"
142 | # → foo, bar, baz, foo_bar, foo_baz, bar_baz
143 | ```
144 |
145 | 5. **Length Filtering (`--minlen`/`--maxlen`)**
146 | ```bash
147 | ./psudohash.py -w apple,banana -i --minlen 10
148 | # Warning: exact size cannot be determined because of length filters.
149 | # Example final outputs might include “applebanana” (11 chars), “bananaapple” (11 chars).
150 | ```
151 |
152 | 6. **Combine up to 3 words (instead of default 2)**
153 |
154 | ## Usage Tips
155 | 1. Combining options `--years` and `--append-numbering` with a `--numbering-limit` ≥ last two digits of any year input, will most likely produce duplicate words because of the mutation patterns implemented by the tool.
156 | 2. If you add custom padding values and/or modify the predefined common padding values in the source code, in combination with multiple optional parameters, there is a small chance of duplicate words occurring. psudohash includes word filtering controls but for speed's sake, those are limited.
157 | 3. When using `--minlen` or `--maxlen`, the script cannot pre-calculate the exact word count; you’ll see a “exact size cannot be determined” warning and the size without this filter will be calculated, the final size will be smaller.
158 |
159 | ## Individuals
160 | When it comes to people, i think we all have (more or less) set passwords using a mutation of one or more words that mean something to us e.g., our name or wife/kid/pet/band names, sticking the year we were born at the end or maybe a super secure padding like "!@#". Well, guess what?
161 |
162 | 
163 |
164 |
165 | ## Future
166 | I'm gathering information regarding commonly used password creation patterns to enhance the tool's capabilities.
167 |
--------------------------------------------------------------------------------
/psudohash.py:
--------------------------------------------------------------------------------
1 | #!/bin/python3
2 | #
3 | # Author: Panagiotis Chartas (t3l3machus)
4 | # https://github.com/t3l3machus
5 |
6 | import argparse, sys, itertools
7 | from tqdm import tqdm
8 |
9 | # Colors
10 | MAIN = '\033[38;5;50m'
11 | LOGO = '\033[38;5;41m'
12 | LOGO2 = '\033[38;5;42m'
13 | GREEN = '\033[38;5;82m'
14 | ORANGE = '\033[0;38;5;214m'
15 | PRPL = '\033[0;38;5;26m'
16 | PRPL2 = '\033[0;38;5;25m'
17 | RED = '\033[1;31m'
18 | END = '\033[0m'
19 | BOLD = '\033[1m'
20 |
21 | # -------------- Arguments & Usage -------------- #
22 | parser = argparse.ArgumentParser(
23 | formatter_class=argparse.RawTextHelpFormatter,
24 | epilog='''
25 | Usage examples:
26 |
27 | # 1) No multi-word: treat each keyword separately
28 | python3 psudohash.py -w foo,bar,baz -cpa
29 | # → foo, bar, baz
30 |
31 | # 2) In-order joins (-i): up to 2 words by default
32 | python3 psudohash.py -w foo,bar,baz -i
33 | # → foo, bar, baz, foobar, foobaz, barbaz
34 |
35 | # 3) All-order combinations (-c): up to 2 words by default
36 | python3 psudohash.py -w foo,bar,baz -c
37 | # → foo, bar, baz, foobar, foobaz, barfoo, barbaz, bazfoo, bazbar
38 |
39 | # 4) Change separator between joined words
40 | python3 psudohash.py -w foo,bar,baz -i --sep "_"
41 | # → foo, bar, baz, foo_bar, foo_baz, bar_baz, foo_bar_baz
42 |
43 | # 5) Combine up to 3 words (instead of default 2)
44 | python3 psudohash.py -w foo,bar,baz -i --max-combine 3
45 | # → foo, bar, baz, foobar, foobaz, barbaz, foobarbaz, ...
46 | '''
47 | )
48 |
49 | parser.add_argument("-w", "--words", action="store", help = "Comma seperated keywords to mutate", required = True)
50 | parser.add_argument("-i", "--inorder", action="store_true", help="Join keywords only in the order given: for each 1≤r≤max_combine, concatenate each r-subset in original sequence.")
51 | parser.add_argument("-c", "--combinations", action="store_true", help="Generate every ordering of every subset (up to max_combine) of the provided keywords.")
52 | parser.add_argument("--max-combine", type=int, default=2, help="Maximum number of raw keywords to join into one base string (default: 2). Applies when using -i or -c.")
53 | parser.add_argument("--minlen", type=int, help="Minimum length (inclusive) of any resulting password. Mutations shorter than this are skipped.")
54 | parser.add_argument("--maxlen", type=int, help="Maximum length (inclusive) of any resulting password. Mutations longer than this are skipped.")
55 | parser.add_argument("--sep", type=str, default="", help="Separator to insert between joined keywords (default: no separator).")
56 | parser.add_argument("-an", "--append-numbering", action="store", help = "Append numbering range at the end of each word mutation (before appending year or common paddings).\nThe LEVEL value represents the minimum number of digits. LEVEL must be >= 1. \nSet to 1 will append range: 1,2,3..100\nSet to 2 will append range: 01,02,03..100 + previous\nSet to 3 will append range: 001,002,003..100 + previous.\n\n", type = int, metavar='LEVEL')
57 | parser.add_argument("-nl", "--numbering-limit", action="store", help = "Change max numbering limit value of option -an. Default is 50. Must be used with -an.", type = int, metavar='LIMIT')
58 | parser.add_argument("-y", "--years", action="store", help = "Singe OR comma seperated OR range of years to be appended to each word mutation (Example: 2022 OR 1990,2017,2022 OR 1990-2000)")
59 | parser.add_argument("-ap", "--append-padding", action="store", help = "Add comma seperated values to common paddings (must be used with -cpb OR -cpa)", metavar='VALUES')
60 | parser.add_argument("-cpb", "--common-paddings-before", action="store_true", help = "Append common paddings before each mutated word")
61 | parser.add_argument("-cpa", "--common-paddings-after", action="store_true", help = "Append common paddings after each mutated word")
62 | parser.add_argument("-cpo", "--custom-paddings-only", action="store_true", help = "Use only user provided paddings for word mutations (must be used with -ap AND (-cpb OR -cpa))")
63 | parser.add_argument("-o", "--output", action="store", help = "Output filename (default: output.txt)", metavar='FILENAME')
64 | parser.add_argument("-q", "--quiet", action="store_true", help = "Do not print the banner on startup")
65 |
66 | args = parser.parse_args()
67 |
68 | def exit_with_msg(msg):
69 | parser.print_help()
70 | print(f'\n[{RED}Debug{END}] {msg}\n')
71 | sys.exit(1)
72 |
73 |
74 |
75 | def unique(l):
76 |
77 | unique_list = []
78 |
79 | for i in l:
80 | if i not in unique_list:
81 | unique_list.append(i)
82 |
83 | return unique_list
84 |
85 |
86 | # Append numbering
87 | if args.numbering_limit and not args.append_numbering:
88 | exit_with_msg('Option -nl must be used with -an.')
89 |
90 | if args.append_numbering:
91 | if args.append_numbering <= 0:
92 | exit_with_msg('Numbering level must be > 0.')
93 |
94 | _max = args.numbering_limit + 1 if args.numbering_limit and isinstance(args.numbering_limit, int) else 51
95 |
96 |
97 | # Create years list
98 | if args.years:
99 |
100 | years = []
101 |
102 | if args.years.count(',') == 0 and args.years.count('-') == 0 and args.years.isdecimal() and int(args.years) >= 1000 and int(args.years) <= 3200:
103 | years.append(str(args.years))
104 |
105 | elif args.years.count(',') > 0:
106 | for year in args.years.split(','):
107 | if year.strip() != '' and year.isdecimal() and int(year) >= 1000 and int(year) <= 3200:
108 | years.append(year)
109 | else:
110 | exit_with_msg('Illegal year(s) input. Acceptable years range: 1000 - 3200.')
111 |
112 | elif args.years.count('-') == 1:
113 | years_range = args.years.split('-')
114 | start_year = years_range[0]
115 | end_year = years_range[1]
116 |
117 | if (start_year.isdecimal() and int(start_year) < int(end_year) and int(start_year) >= 1000) and (end_year.isdecimal() and int(end_year) <= 3200):
118 | for y in range(int(years_range[0]), int(years_range[1])+1):
119 | years.append(str(y))
120 | else:
121 | exit_with_msg('Illegal year(s) input. Acceptable years range: 1000 - 3200.')
122 | else:
123 | exit_with_msg('Illegal year(s) input. Acceptable years range: 1000 - 3200.')
124 |
125 |
126 | def banner():
127 | padding = ' '
128 |
129 | P = [[' ', '┌', '─', '┐'], [' ', '├','─','┘'], [' ', '┴',' ',' ']]
130 | S = [[' ', '┌','─','┐'], [' ', '└','─','┐'], [' ', '└','─','┘']]
131 | U = [[' ', '┬',' ','┬'], [' ', '│',' ','│'], [' ', '└','─','┘']]
132 | D = [[' ', '┌','┬','┐'], [' ', ' ','│','│'], [' ', '─','┴','┘']]
133 | O = [[' ', '┌','─','┐'], [' ', '│',' ','│'], [' ', '└','─','┘']]
134 | H = [[' ', '┐', ' ', '┌'], [' ', '├','╫','┤'], [' ', '┘',' ','└']]
135 | A = [[' ', '┌','─','┐'], [' ', '├','─','┤'], [' ', '┴',' ','┴']]
136 | S = [[' ', '┌','─','┐'], [' ', '└','─','┐'], [' ', '└','─','┘']]
137 | H = [[' ', '┬',' ','┬'], [' ', '├','─','┤'], [' ', '┴',' ','┴']]
138 |
139 | banner = [P,S,U,D,O,H,A,S,H]
140 | final = []
141 | print('\r')
142 | init_color = 37
143 | txt_color = init_color
144 | cl = 0
145 |
146 | for charset in range(0, 3):
147 | for pos in range(0, len(banner)):
148 | for i in range(0, len(banner[pos][charset])):
149 | clr = f'\033[38;5;{txt_color}m'
150 | char = f'{clr}{banner[pos][charset][i]}'
151 | final.append(char)
152 | cl += 1
153 | txt_color = txt_color + 36 if cl <= 3 else txt_color
154 |
155 | cl = 0
156 |
157 | txt_color = init_color
158 | init_color += 31
159 |
160 | if charset < 2: final.append('\n ')
161 |
162 | print(f" {''.join(final)}")
163 | print(f'{END}{padding} by t3l3machus\n')
164 |
165 |
166 | # ----------------( Base Settings )---------------- #
167 | mutations_cage = []
168 | basic_mutations = []
169 | outfile = args.output if args.output else 'output.txt'
170 | trans_keys = []
171 |
172 | transformations = [
173 | {'a' : ['@', '4']},
174 | {'b' : '8'},
175 | {'e' : '3'},
176 | {'g' : ['9', '6']},
177 | {'i' : ['1', '!']},
178 | {'o' : '0'},
179 | {'s' : ['$', '5']},
180 | {'t' : '7'}
181 | ]
182 |
183 | for t in transformations:
184 | for key in t.keys():
185 | trans_keys.append(key)
186 |
187 | # Common Padding Values
188 | if (args.custom_paddings_only or args.append_padding) and not (args.common_paddings_before or args.common_paddings_after):
189 | exit_with_msg('Options -ap and -cpo must be used with -cpa or -cpb.')
190 |
191 |
192 | elif (args.common_paddings_before or args.common_paddings_after) and not args.custom_paddings_only:
193 |
194 | try:
195 | f = open('common_padding_values.txt', 'r')
196 | content = f.readlines()
197 | common_paddings = [val.strip() for val in content]
198 | f.close()
199 |
200 | except:
201 | exit_with_msg('File "common_padding_values.txt" not found.')
202 |
203 | elif (args.common_paddings_before or args.common_paddings_after) and (args.custom_paddings_only and args.append_padding):
204 | common_paddings = []
205 |
206 | elif not (args.common_paddings_before or args.common_paddings_after):
207 | common_paddings = []
208 |
209 | else:
210 | exit_with_msg('\nIllegal padding settings.\n')
211 |
212 | if args.append_padding:
213 | for val in args.append_padding.split(','):
214 | if val.strip() != '' and val not in common_paddings:
215 | common_paddings.append(val)
216 |
217 |
218 | if (args.common_paddings_before or args.common_paddings_after):
219 | common_paddings = list(set(common_paddings))
220 |
221 |
222 | # ----------------( Functions )---------------- #
223 | # The following list is used to create variations of password values and appended years.
224 | # For example, a passwd value {passwd} will be mutated to "{passwd}{seperator}{year}"
225 | # for each of the symbols included in the list below.
226 | year_seperators = ['', '_', '-', '@']
227 |
228 |
229 |
230 | # ----------------( Functions )---------------- #
231 | def evalTransformations(w):
232 |
233 | trans_chars = []
234 | total = 1
235 | c = 0
236 | w = list(w)
237 |
238 | for char in w:
239 | for t in transformations:
240 | if char in t.keys():
241 | trans_chars.append(c)
242 | if isinstance(t[char], list):
243 | total *= 3
244 | else:
245 | total *= 2
246 | c += 1
247 |
248 | return [trans_chars, total]
249 |
250 |
251 |
252 | def mutate(tc, word):
253 |
254 | global trans_keys, mutations_cage, basic_mutations
255 |
256 | i = trans_keys.index(word[tc].lower())
257 | trans = transformations[i][word[tc].lower()]
258 | limit = len(trans) * len(mutations_cage)
259 | c = 0
260 |
261 | for m in mutations_cage:
262 | w = list(m)
263 |
264 | if isinstance(trans, list):
265 | for tt in trans:
266 | w[tc] = tt
267 | transformed = ''.join(w)
268 | mutations_cage.append(transformed)
269 | c += 1
270 | else:
271 | w[tc] = trans
272 | transformed = ''.join(w)
273 | mutations_cage.append(transformed)
274 | c += 1
275 |
276 | if limit == c: break
277 |
278 | return mutations_cage
279 |
280 |
281 |
282 | def mutations_handler(kword, trans_chars, total):
283 | """
284 | Perform character→symbol/number substitutions and write each new mutation.
285 | """
286 | global mutations_cage, basic_mutations
287 |
288 | container = []
289 | for word in basic_mutations:
290 | mutations_cage = [word.strip()]
291 | for tc in trans_chars:
292 | results = mutate(tc, kword)
293 | container.append(results)
294 |
295 | for m_set in container:
296 | for m in m_set:
297 | basic_mutations.append(m)
298 |
299 | basic_mutations = list(set(basic_mutations))
300 |
301 | desc = " ├─ Mutating word based on commonly used char‐to‐symbol/number substitutions"
302 | with open(outfile, 'a') as wordlist, \
303 | tqdm(total=len(basic_mutations), desc=desc, leave=False) as pbar:
304 |
305 | for m in basic_mutations:
306 | # Only final‐filter if no numbering/years/padding follow
307 | if not args.append_numbering and not args.years and not (args.common_paddings_after or args.common_paddings_before):
308 | if not (args.minlen and len(m) < args.minlen) \
309 | and not (args.maxlen and len(m) > args.maxlen):
310 | wordlist.write(m + '\n')
311 | else:
312 | wordlist.write(m + '\n')
313 | pbar.update(1)
314 |
315 | print(f"{desc}... [100.0%]")
316 |
317 |
318 | def mutateCase(word):
319 | trans = list(map(''.join, itertools.product(*zip(word.upper(), word.lower()))))
320 | return trans
321 |
322 |
323 |
324 | def caseMutationsHandler(word, mutability):
325 | """
326 | Generate all upper/lower combos, add to basic_mutations,
327 | and write to file immediately if mutability is False.
328 | """
329 | global basic_mutations
330 | case_mutations = mutateCase(word)
331 |
332 | desc = " ├─ Producing character case‐based transformations"
333 | with open(outfile, 'a') as wordlist, \
334 | tqdm(total=len(case_mutations), desc=desc, leave=False) as pbar:
335 |
336 | for m in case_mutations:
337 | basic_mutations.append(m)
338 | if not mutability:
339 | # Only final‐filter if no substitutions/numbering/years/padding follow
340 | if not args.combinations and not args.inorder \
341 | and not args.append_numbering and not args.years \
342 | and not (args.common_paddings_after or args.common_paddings_before):
343 | if not (args.minlen and len(m) < args.minlen) \
344 | and not (args.maxlen and len(m) > args.maxlen):
345 | wordlist.write(m + '\n')
346 | else:
347 | wordlist.write(m + '\n')
348 | pbar.update(1)
349 |
350 | print(f"{desc}... [100.0%]")
351 |
352 |
353 | def append_numbering():
354 | """
355 | For each word in basic_mutations, append numbering variants (zfilled up to LEVEL).
356 | """
357 | global _max, basic_mutations
358 |
359 | lvl = args.append_numbering
360 | first_cycle = True
361 | previous_list = []
362 |
363 | # total lines = len(basic_mutations) * lvl * (_max - 1) * 2
364 | total_lines = len(basic_mutations) * lvl * (_max - 1) * 2
365 | desc = " ├─ Appending numbering to each word mutation"
366 |
367 | with open(outfile, 'a') as wordlist, \
368 | tqdm(total=total_lines, desc=desc, leave=False) as pbar:
369 |
370 | for word in basic_mutations:
371 | for i in range(1, lvl + 1):
372 | for k in range(1, _max):
373 | num_z = str(k).zfill(i)
374 | line1 = f"{word}{num_z}\n"
375 | line2 = f"{word}_{num_z}\n"
376 |
377 | if first_cycle:
378 | # Only final‐filter if no years or padding follow
379 | if not args.years and not (args.common_paddings_after or args.common_paddings_before):
380 | if not (args.minlen and len(line1) < args.minlen) \
381 | and not (args.maxlen and len(line1) > args.maxlen):
382 | wordlist.write(line1)
383 | pbar.update(1)
384 | else:
385 | pbar.update(1)
386 | if not (args.minlen and len(line2) < args.minlen) \
387 | and not (args.maxlen and len(line2) > args.maxlen):
388 | wordlist.write(line2)
389 | pbar.update(1)
390 | else:
391 | pbar.update(1)
392 | else:
393 | if not args.years and not (args.common_paddings_after or args.common_paddings_before):
394 | if not (args.minlen and len(line1) < args.minlen) \
395 | and not (args.maxlen and len(line1) > args.maxlen):
396 | wordlist.write(line1)
397 | pbar.update(1)
398 | else:
399 | pbar.update(1)
400 | if not (args.minlen and len(line2) < args.minlen) \
401 | and not (args.maxlen and len(line2) > args.maxlen):
402 | wordlist.write(line2)
403 | pbar.update(1)
404 | else:
405 | pbar.update(1)
406 | else:
407 | wordlist.write(line1)
408 | wordlist.write(line2)
409 | pbar.update(2)
410 | previous_list.append(f"{word}{num_z}")
411 | else:
412 | if previous_list[k - 1] != f"{word}{num_z}":
413 | wordlist.write(line1)
414 | wordlist.write(line2)
415 | previous_list[k - 1] = f"{word}{num_z}"
416 | pbar.update(2)
417 |
418 | first_cycle = False
419 |
420 | print(f"{desc}... [100.0%]")
421 |
422 |
423 | def mutate_years():
424 | """
425 | For each entry in basic_mutations, append year variants (full YYYY + short YY).
426 | """
427 | global basic_mutations
428 | current_mutations = basic_mutations.copy()
429 |
430 | # total lines = len(current_mutations) * len(years) * len(year_seperators) * 2
431 | total_lines = len(current_mutations) * len(years) * len(year_seperators) * 2
432 | desc = " ├─ Appending year patterns after each word mutation"
433 |
434 | with open(outfile, 'a') as wordlist, \
435 | tqdm(total=total_lines, desc=desc, leave=False) as pbar:
436 |
437 | for word in current_mutations:
438 | for y in years:
439 | for sep in year_seperators:
440 | full = f"{word}{sep}{y}\n"
441 | short = f"{word}{sep}{y[2:]}\n"
442 | # Only final-filter if no padding follows
443 | if not (args.common_paddings_after or args.common_paddings_before):
444 | if not (args.minlen and len(full) < args.minlen) \
445 | and not (args.maxlen and len(full) > args.maxlen):
446 | wordlist.write(full)
447 | basic_mutations.append(full.strip())
448 | # count one for pbar regardless
449 | pbar.update(1)
450 | if not (args.minlen and len(short) < args.minlen) \
451 | and not (args.maxlen and len(short) > args.maxlen):
452 | wordlist.write(short)
453 | basic_mutations.append(short.strip())
454 | pbar.update(1)
455 | else:
456 | wordlist.write(full)
457 | basic_mutations.append(full.strip())
458 | wordlist.write(short)
459 | basic_mutations.append(short.strip())
460 | pbar.update(2)
461 |
462 | print(f"{desc}... [100.0%]")
463 |
464 |
465 | def check_underscore(word, pos):
466 | if word[pos] == '_':
467 | return True
468 | else:
469 | return False
470 |
471 |
472 | def append_paddings_before():
473 | """
474 | Prepend each common_paddings value before each word in basic_mutations.
475 | """
476 | global basic_mutations
477 | current_mutations = basic_mutations.copy()
478 |
479 | # total lines = sum(len(common_paddings)*2 for each word)
480 | total_lines = sum(len(common_paddings) * 2 for _ in current_mutations)
481 | desc = " ├─ Appending common paddings before each word mutation"
482 |
483 | with open(outfile, 'a') as wordlist, \
484 | tqdm(total=total_lines, desc=desc, leave=False) as pbar:
485 |
486 | for word in current_mutations:
487 | for val in common_paddings:
488 | line1 = f"{val}{word}\n"
489 | wordlist.write(line1)
490 | pbar.update(1)
491 |
492 | if not check_underscore(val, -1):
493 | line2 = f"{val}_{word}\n"
494 | wordlist.write(line2)
495 | pbar.update(1)
496 |
497 | print(f"{desc}... [100.0%]")
498 |
499 |
500 | def append_paddings_after():
501 | """
502 | Append each common_paddings value after each word in basic_mutations.
503 | """
504 | global basic_mutations
505 | current_mutations = basic_mutations.copy()
506 |
507 | # total lines = sum(len(common_paddings)*2 for each word)
508 | total_lines = sum(len(common_paddings) * 2 for _ in current_mutations)
509 | desc = " ├─ Appending common paddings after each word mutation"
510 |
511 | with open(outfile, 'a') as wordlist, \
512 | tqdm(total=total_lines, desc=desc, leave=False) as pbar:
513 |
514 | for word in current_mutations:
515 | for val in common_paddings:
516 | line1 = f"{word}{val}\n"
517 | # Only final-filter if no before-padding
518 | if not args.common_paddings_before:
519 | if not (args.minlen and len(line1) < args.minlen) \
520 | and not (args.maxlen and len(line1) > args.maxlen):
521 | wordlist.write(line1)
522 | pbar.update(1)
523 | else:
524 | pbar.update(1)
525 | else:
526 | wordlist.write(line1)
527 | pbar.update(1)
528 |
529 | if not check_underscore(val, 0):
530 | line2 = f"{word}_{val}\n"
531 | if not args.common_paddings_before:
532 | if not (args.minlen and len(line2) < args.minlen) \
533 | and not (args.maxlen and len(line2) > args.maxlen):
534 | wordlist.write(line2)
535 | pbar.update(1)
536 | else:
537 | pbar.update(1)
538 | else:
539 | wordlist.write(line2)
540 | pbar.update(1)
541 |
542 | print(f"{desc}... [100.0%]")
543 |
544 |
545 | def calculate_output(keyw):
546 |
547 | global trans_keys
548 |
549 | c = 0
550 | total = 1
551 | basic_total = 1
552 | basic_size = 0
553 | size = 0
554 | numbering_count = 0
555 | numbering_size = 0
556 |
557 | # Basic mutations calc
558 | for char in keyw:
559 | if char in trans_keys:
560 | i = trans_keys.index(keyw[c].lower())
561 | trans = transformations[i][keyw[c].lower()]
562 | basic_total *= (len(trans) + 2)
563 | else:
564 | basic_total = basic_total * 2 if char.isalpha() else basic_total
565 |
566 | c += 1
567 |
568 | total = basic_total
569 | basic_size = total * (len(keyw) + 1)
570 | size = basic_size
571 |
572 | # Words numbering mutations calc
573 | if args.append_numbering:
574 | global _max
575 | word_len = len(keyw) + 1
576 | first_cycle = True
577 | previous_list = []
578 | lvl = args.append_numbering
579 |
580 | for w in range(0, total):
581 | for i in range(1, lvl+1):
582 | for k in range(1, _max):
583 | n = str(k).zfill(i)
584 | if first_cycle:
585 | numbering_count += 2
586 | numbering_size += (word_len * 2) + (len(n) * 2) + 1
587 | previous_list.append(f'{w}{n}')
588 |
589 | else:
590 | if previous_list[k - 1] != f'{w}{n}':
591 | numbering_size += (word_len * 2) + (len(n) * 2) + 1
592 | numbering_count += 2
593 | previous_list[k - 1] = f'{w}{n}'
594 |
595 | first_cycle = False
596 |
597 | del previous_list
598 |
599 | # Adding years mutations calc
600 | if args.years:
601 | patterns = len(year_seperators) * 2
602 | year_chars = 4
603 | year_short = 2
604 | years_len = len(years)
605 | size += (basic_size * patterns * years_len)
606 |
607 | for sep in year_seperators:
608 | size += (basic_total * (year_chars + len(sep)) * years_len)
609 | size += (basic_total * (year_short + len(sep)) * years_len)
610 |
611 | total += total * len(years) * patterns
612 | basic_total = total
613 | basic_size = size
614 |
615 | # Common paddings mutations calc
616 | patterns = 2
617 |
618 | if args.common_paddings_after or args.common_paddings_before:
619 | paddings_len = len(common_paddings)
620 | pads_wlen_sum = sum([basic_total*len(w) for w in common_paddings])
621 | _pads_wlen_sum = sum([basic_total*(len(w)+1) for w in common_paddings])
622 |
623 | if args.common_paddings_after and args.common_paddings_before:
624 | size += ((basic_size * patterns * paddings_len) + pads_wlen_sum + _pads_wlen_sum) * 2
625 | total += (total * len(common_paddings) * 2) * 2
626 |
627 | elif args.common_paddings_after or args.common_paddings_before:
628 | size += (basic_size * patterns * paddings_len) + pads_wlen_sum + _pads_wlen_sum
629 | total += total * len(common_paddings) * 2
630 |
631 | return [total + numbering_count, size + numbering_size]
632 |
633 |
634 |
635 | def check_mutability(word):
636 |
637 | global trans_keys
638 | m = 0
639 |
640 | for char in word:
641 | if char in trans_keys:
642 | m += 1
643 |
644 | return m
645 |
646 |
647 |
648 | def chill():
649 | pass
650 |
651 |
652 |
653 | def main():
654 |
655 | banner() if not args.quiet else chill()
656 |
657 | global basic_mutations, mutations_cage
658 |
659 | # 1) Read raw keywords (ignore empty or digit-only)
660 | raw = []
661 | for w in args.words.split(','):
662 | w = w.strip()
663 | if not w:
664 | continue
665 | if w.isdecimal():
666 | exit_with_msg('Unable to mutate digit-only keywords.')
667 | raw.append(w)
668 |
669 | # 2) Build "base" keywords according to flags:
670 | # - If --inorder: join each r-subset in the given order, for r=1..max_combine.
671 | # - Elif --combinations: for each r-subset (1..max_combine), generate every permutation.
672 | # - Else: treat each raw word on its own.
673 | from itertools import combinations, permutations
674 |
675 | keywords = []
676 | limit = min(len(raw), args.max_combine)
677 |
678 | if args.inorder:
679 | for r in range(1, limit + 1):
680 | for combo in combinations(raw, r):
681 | keywords.append(args.sep.join(combo))
682 | elif args.combinations:
683 | for r in range(1, limit + 1):
684 | for combo in combinations(raw, r):
685 | for perm in permutations(combo):
686 | keywords.append(args.sep.join(perm))
687 | else:
688 | keywords = list(raw)
689 |
690 | # Calculate total words and size of output
691 | total_size = [0, 0]
692 |
693 | for keyw in keywords:
694 | count_size = calculate_output(keyw.strip().lower())
695 | total_size[0] += count_size[0]
696 | total_size[1] += count_size[1]
697 |
698 | size = round(((total_size[1]/1000)/1000), 1) if total_size[1] > 100000 else total_size[1]
699 | prefix = 'bytes' if total_size[1] <= 100000 else 'MB'
700 | fsize = f'{size} {prefix}'
701 |
702 | print(f'[{MAIN}Info{END}] Calculating output length and size...')
703 |
704 | # Inform user about the output size
705 | if args.minlen or args.maxlen:
706 | prompt = (f'[{ORANGE}Warning{END}] Exact final size cannot be determined because min/max-length filtering is active. Without filtering, this would produce {BOLD}{total_size[0]}{END} words, {BOLD}{fsize}{END}. Continue? [y/n]: ')
707 | else:
708 | prompt = (f'[{ORANGE}Warning{END}] This operation will produce {BOLD}{total_size[0]}{END} words, {BOLD}{fsize}{END}. Are you sure you want to proceed? [y/n]: ')
709 |
710 | try:
711 | concent = input(prompt)
712 | except KeyboardInterrupt:
713 | exit('\n')
714 |
715 | if concent.lower() not in ['y', 'yes']:
716 | sys.exit(f'\n[{RED}X{END}] Aborting.')
717 |
718 | else:
719 |
720 | open(outfile, "w").close()
721 |
722 | for word in keywords:
723 | print(f'[{GREEN}*{END}] Mutating keyword: {GREEN}{word}{END} ')
724 | mutability = check_mutability(word.lower())
725 |
726 | # Stage 1: Case mutations
727 | caseMutationsHandler(word.lower(), mutability)
728 | # Stage 2: Substitution mutations
729 | if mutability:
730 | trans = evalTransformations(word.lower())
731 | mutations_handler(word, trans[0], trans[1])
732 | else:
733 | print(f" ├─ {ORANGE}No character substitution instructions match this word.{END}")
734 | # Stage 3: Numbering
735 | if args.append_numbering:
736 | append_numbering()
737 | # Stage 4: Years
738 | if args.years:
739 | mutate_years()
740 | # Stage 5: Common paddings after
741 | if args.common_paddings_after or args.custom_paddings_only:
742 | append_paddings_after()
743 | # Stage 6: Common paddings before
744 | if args.common_paddings_before:
745 | append_paddings_before()
746 | basic_mutations = []
747 | mutations_cage = []
748 | print(f' └─ Done!')
749 |
750 | print(f'\n[{MAIN}Info{END}] Completed! List saved in {outfile}\n')
751 |
752 |
753 | if __name__ == '__main__':
754 | main()
--------------------------------------------------------------------------------