diff --git a/.github/workflows/app.yaml b/.github/workflows/app.yaml index e3cac58..3896020 100644 --- a/.github/workflows/app.yaml +++ b/.github/workflows/app.yaml @@ -47,16 +47,16 @@ jobs: run: | pip install -r requirements.txt pip install -r dev_requirements.txt - PYTHONPATH=$(pwd) pytest tests --junitxml=junit/test-results-${{ matrix.python-version }}.xml --cov=shadycompass --cov-branch --cov-report=xml:coverage-${{ matrix.python-version }}.xml --cov-report=html:htmlcov/${{ matrix.python-version }} --cov-report=term-missing + RESULT_DIR="test-results/${{ matrix.python-version }}" + mkdir -p "${RESULT_DIR}" + PYTHONPATH=$(pwd) pytest tests --junitxml=${RESULT_DIR}/test-results.xml --cov=shadycompass --cov-branch --cov-report=xml:${RESULT_DIR}/coverage.xml --cov-report=html:${RESULT_DIR}/htmlcov --cov-report=term-missing - name: Upload pytest test results uses: actions/upload-artifact@v4 with: name: pytest-results-${{ matrix.python-version }} path: | - junit/test-results-${{ matrix.python-version }}.xml - coverage-${{ matrix.python-version }}.xml - htmlcov/${{ matrix.python-version }} + test-results/${{ matrix.python-version }} # Use always() to always run this step to publish test results when there are test failures if: ${{ always() }} diff --git a/.gitignore b/.gitignore index 9c31c99..b6910c4 100644 --- a/.gitignore +++ b/.gitignore @@ -133,6 +133,7 @@ pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ junit/ +test-results/ .tox/ .nox/ .coverage diff --git a/README.md b/README.md index dac8c04..cac5900 100644 --- a/README.md +++ b/README.md @@ -129,6 +129,8 @@ users displays the users that have been found emails displays the emails that have been found +reload + reload from all files, only needed if recommendations aren't updated properly ``` ### info @@ -427,9 +429,14 @@ tests/fixtures shadycompass > emails - bin@shadycompass.test ``` +### reload + +Shadycompass is designed to update recommendations based on changes in files. Sometimes the logic isn't quite right. +Run the `reload` command to reload all files. + ### facts -Displays the things shady compass knows about to make recommendations. This is used for debugging purposes. +Displays the things shadycompass knows about to make recommendations. This is used for debugging purposes. ## Configuration @@ -467,3 +474,18 @@ shadycompass> set global production true shadycompass> unset production ``` + +### word lists + +Some tools use word lists for brute forcing such as HTTP busting, sub domain enumeration, etc. The files can be +configured using the `set` command. + +``` +shadycompass> set global wordlists.subdomain /usr/share/seclists/Discovery/DNS/subdomains-top1million-5000.txt + +shadycompass> set wordlists.file /usr/share/seclists/Discovery/Web-Content/raft-medium-files.txt + +shadycompass> set wordlists.username /usr/share/seclists/Usernames/xato-net-10-million-usernames-dup.txt + +shadycompass> set wordlists.password /usr/share/seclists/Passwords/xato-net-10-million-passwords-100000.txt +``` diff --git a/dev_requirements.txt b/dev_requirements.txt index 7b57bb8..8368176 100644 --- a/dev_requirements.txt +++ b/dev_requirements.txt @@ -1,3 +1,4 @@ pytest==8.1.1 pytest-cov==5.0.0 -ruff==0.4.4 +ruff==0.5.1 +parameterized==0.9.0 diff --git a/requirements.txt b/requirements.txt index 68ec997..08e330d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,2 @@ git+https://github.com/openmotics/om-experta@d35d53708a46482e1ee4e3a4bc1a36bc03492913#egg=om-experta -prompt_toolkit==3.0.43 +prompt_toolkit==3.0.47 diff --git a/shadycompass.py b/shadycompass.py index 662b32d..e535286 100644 --- a/shadycompass.py +++ b/shadycompass.py @@ -9,6 +9,8 @@ from prompt_toolkit.history import InMemoryHistory from shadycompass import ShadyCompassOps, get_local_config_path, get_global_config_path, ToolAvailable +from shadycompass.config import OPTION_RATELIMIT, OPTION_PRODUCTION, SECTION_WORDLISTS, OPTION_WORDLIST_FILE, \ + OPTION_WORDLIST_USERNAME, OPTION_WORDLIST_PASSWORD, OPTION_WORDLIST_SUBDOMAIN def shadycompass_cli(args: list[str]) -> int: @@ -23,27 +25,33 @@ def shadycompass_cli(args: list[str]) -> int: history = InMemoryHistory() ops = ShadyCompassOps(parsed.directories) commands = ['exit', 'quit', 'save', 'use', 'option', 'set', 'unset', 'reset', 'info', 'facts', 'tools', 'targets', - 'services', 'products', 'urls', 'users', 'emails'] - config_names = {'ratelimit', 'production'} + 'services', 'products', 'urls', 'users', 'emails', 'reload'] + config_names = { + OPTION_RATELIMIT, OPTION_PRODUCTION, + SECTION_WORDLISTS + '.' + OPTION_WORDLIST_FILE, SECTION_WORDLISTS + '.' + OPTION_WORDLIST_SUBDOMAIN, + SECTION_WORDLISTS + '.' + OPTION_WORDLIST_USERNAME, SECTION_WORDLISTS + '.' + OPTION_WORDLIST_PASSWORD, + } + config_dict = {name: None for name in config_names} tools = set(map(lambda e: e.get_name(), filter(lambda e: isinstance(e, ToolAvailable), ops.engine.facts.values()))) + tools_dict = {tool: None for tool in tools} completer = NestedCompleter.from_nested_dict({ **{command:None for command in commands}, 'use': { 'global': tools, - **{tool: None for tool in tools}, + **tools_dict, }, 'option': { 'global': tools, - **{tool: None for tool in tools}, + **tools_dict, }, 'info': tools, 'set': { 'global': config_names, - **{name: None for name in config_names}, + **config_dict, }, 'unset': { 'global': config_names, - **{name: None for name in config_names}, + **config_dict, }, }) @@ -122,6 +130,9 @@ def shadycompass_cli(args: list[str]) -> int: ops.show_users(user_command) elif user_command[0] == 'emails': ops.show_emails(user_command) + elif user_command[0] == 'reload': + ops.reload() + break else: print(f''' @@ -162,6 +173,8 @@ def shadycompass_cli(args: list[str]) -> int: displays the users that have been found emails displays the emails that have been found +reload + reload from all files, only needed if recommendations aren't updated properly facts show current facts (useful for debugging) ''') diff --git a/shadycompass/__init__.py b/shadycompass/__init__.py index 4e04661..fded5f1 100644 --- a/shadycompass/__init__.py +++ b/shadycompass/__init__.py @@ -78,6 +78,10 @@ def update_facts(self): for fact in retract_queue: self.retract(fact) + def reset(self, **kwargs): + super().reset(**kwargs) + self.file_metadata.reset() + def _save_config(self, facts: list[ConfigFact], config_path: str): config = ConfigParser() for fact in facts: @@ -289,6 +293,18 @@ def print_banner(self): """, file=self.fd_out) def refresh(self): + """ + Update the facts from changed files and run the rules. + """ + self.engine.update_facts() + self.engine.run() + + def reload(self): + """ + Clear the facts and load all from files. This should not be needed if the rules are correct to handle + incremental changes. + """ + self.engine.reset() self.engine.update_facts() self.engine.run() diff --git a/shadycompass/config/__init__.py b/shadycompass/config/__init__.py index 7f6f65d..e29b9e7 100644 --- a/shadycompass/config/__init__.py +++ b/shadycompass/config/__init__.py @@ -15,6 +15,13 @@ OPTION_PRODUCTION = 'production' OPTION_VALUE_ALL = '*' +# wordlist configuration +SECTION_WORDLISTS = 'wordlists' +OPTION_WORDLIST_FILE = 'file' +OPTION_WORDLIST_USERNAME = 'username' +OPTION_WORDLIST_PASSWORD = 'password' +OPTION_WORDLIST_SUBDOMAIN = 'subdomain' + class ToolCategory(object): port_scanner = 'port_scanner' @@ -264,6 +271,22 @@ def get_hostname(self) -> str: return self.get('hostname') +class PreferredWordlist(Fact): + """ + Identifies a preferred wordlist for brute forcing. This fact is expected to be present for all categories whether + the default or configured value. Therefore, rules do not need to check for non-existence. + """ + category = Field(str, mandatory=True) + path = Field(str, mandatory=True) + default = Field(bool, mandatory=True) + + def get_path(self) -> str: + return self.get('path') + + def is_default(self) -> bool: + return bool(self.get('default')) + + class ConfigRules(IRules, ABC): def _get_tools(self, category: str) -> list[ToolAvailable]: tools = [] @@ -321,3 +344,55 @@ def tool_chosen(self, f1): ) def retract_tool(self, f1): self.retract(f1) + + def _declare_preferred_wordlist(self, category: str, path: str, default: bool): + retract_queue = [] + for fact in filter( + lambda f: isinstance(f, PreferredWordlist) and f.get('category') == category, + self.get_facts()): + retract_queue.append(fact) + for fact in retract_queue: + self.retract(fact) + self.declare(PreferredWordlist(category=category, path=path, default=default)) + + @Rule( + NOT(ConfigFact(section=SECTION_WORDLISTS, option=OPTION_WORDLIST_FILE)), + salience=200 + ) + def preferred_wordlist_file_default(self): + self._declare_preferred_wordlist(OPTION_WORDLIST_FILE, "raft-large-files.txt", True) + + @Rule( + NOT(ConfigFact(section=SECTION_WORDLISTS, option=OPTION_WORDLIST_USERNAME)), + salience=200 + ) + def preferred_wordlist_username_default(self): + self._declare_preferred_wordlist(OPTION_WORDLIST_USERNAME, "xato-net-10-million-usernames.txt", True) + + @Rule( + NOT(ConfigFact(section=SECTION_WORDLISTS, option=OPTION_WORDLIST_PASSWORD)), + salience=200 + ) + def preferred_wordlist_password_default(self): + self._declare_preferred_wordlist(OPTION_WORDLIST_PASSWORD, "rockyou.txt", True) + + @Rule( + NOT(ConfigFact(section=SECTION_WORDLISTS, option=OPTION_WORDLIST_SUBDOMAIN)), + salience=200 + ) + def preferred_wordlist_subdomain_default(self): + self._declare_preferred_wordlist(OPTION_WORDLIST_SUBDOMAIN, "subdomains-top1million-110000.txt", True) + + @Rule( + ConfigFact(section=SECTION_WORDLISTS, option=MATCH.category, value=MATCH.path, global0=False), + salience=100 + ) + def preferred_wordlist_local(self, category, path): + self._declare_preferred_wordlist(category, path, False) + + @Rule( + ConfigFact(section=SECTION_WORDLISTS, option=MATCH.category, value=MATCH.path, global0=True), + NOT(PreferredWordlist(category=MATCH.category, default=False)), + ) + def preferred_wordlist_global(self, category, path): + self._declare_preferred_wordlist(category, path, False) diff --git a/shadycompass/facts/filemetadata.py b/shadycompass/facts/filemetadata.py index f863722..5d27a32 100644 --- a/shadycompass/facts/filemetadata.py +++ b/shadycompass/facts/filemetadata.py @@ -48,6 +48,9 @@ def find_changes(self) -> list[str]: self.files.pop(path) return changes + def reset(self): + self.files.clear() + def _check_file_change(self, file_path: str) -> list[str]: if file_path in self.files: file_meta_data = self.files[file_path] diff --git a/shadycompass/rules/dns_scanner/dnsenum.py b/shadycompass/rules/dns_scanner/dnsenum.py index 12c85a9..6339218 100644 --- a/shadycompass/rules/dns_scanner/dnsenum.py +++ b/shadycompass/rules/dns_scanner/dnsenum.py @@ -3,7 +3,7 @@ from experta import DefFacts, Rule, AS, NOT, MATCH from shadycompass import ToolAvailable -from shadycompass.config import ToolCategory +from shadycompass.config import ToolCategory, PreferredWordlist, OPTION_WORDLIST_SUBDOMAIN from shadycompass.facts import ScanNeeded, TargetDomain, RateLimitEnable, PublicTarget from shadycompass.rules.conditions import TOOL_PREF, TOOL_CONF from shadycompass.rules.irules import IRules @@ -26,7 +26,7 @@ def dnsenum_available(self): ) def _declare_dnsenum(self, f1: ScanNeeded, domain: TargetDomain, ratelimit: RateLimitEnable = None, - public: PublicTarget = None): + public: PublicTarget = None, wordlist: PreferredWordlist = None): addr = f1.get_addr() addr_file_name_part = f'-{addr}-{f1.get_port()}' @@ -38,6 +38,8 @@ def _declare_dnsenum(self, f1: ScanNeeded, domain: TargetDomain, ratelimit: Rate more_options = [] if ratelimit: more_options.append(['--threads', '1']) + if wordlist: + more_options.append(['-f', wordlist.get_path()]) command_line = self.resolve_command_line( self.dnsenum_tool_name, @@ -45,7 +47,6 @@ def _declare_dnsenum(self, f1: ScanNeeded, domain: TargetDomain, ratelimit: Rate '--dnsserver', f1.get_addr(), *enum_options, '-o', f'dnsenum{addr_file_name_part}-subdomains-{domain.get_domain()}.xml', - '-f', '/usr/share/seclists/Discovery/DNS/subdomains-top1million-110000.txt' ], *more_options ) command_line.extend([domain.get_domain()]) @@ -61,44 +62,50 @@ def _declare_dnsenum(self, f1: ScanNeeded, domain: TargetDomain, ratelimit: Rate @Rule( AS.f1 << ScanNeeded(category=ToolCategory.dns_scanner, addr=MATCH.addr, port=53), AS.domain << TargetDomain(), + AS.wordlist << PreferredWordlist(category=OPTION_WORDLIST_SUBDOMAIN), TOOL_PREF(ToolCategory.dns_scanner, dnsenum_tool_name), TOOL_CONF(ToolCategory.dns_scanner, dnsenum_tool_name), NOT(RateLimitEnable(addr=MATCH.addr)), NOT(PublicTarget(addr=MATCH.addr)), ) - def run_dnsenum(self, f1: ScanNeeded, domain: TargetDomain): - self._declare_dnsenum(f1, domain) + def run_dnsenum(self, f1: ScanNeeded, domain: TargetDomain, wordlist: PreferredWordlist): + self._declare_dnsenum(f1, domain, wordlist=wordlist) @Rule( AS.f1 << ScanNeeded(category=ToolCategory.dns_scanner, addr=MATCH.addr, port=53), AS.domain << TargetDomain(), AS.public << PublicTarget(addr=MATCH.addr), + AS.wordlist << PreferredWordlist(category=OPTION_WORDLIST_SUBDOMAIN), TOOL_PREF(ToolCategory.dns_scanner, dnsenum_tool_name), TOOL_CONF(ToolCategory.dns_scanner, dnsenum_tool_name), NOT(RateLimitEnable(addr=MATCH.addr)), ) - def run_dnsenum_public(self, f1: ScanNeeded, domain: TargetDomain, public: PublicTarget): - self._declare_dnsenum(f1, domain, public=public) + def run_dnsenum_public(self, f1: ScanNeeded, domain: TargetDomain, public: PublicTarget, + wordlist: PreferredWordlist): + self._declare_dnsenum(f1, domain, public=public, wordlist=wordlist) @Rule( AS.f1 << ScanNeeded(category=ToolCategory.dns_scanner, addr=MATCH.addr, port=53), AS.domain << TargetDomain(), AS.ratelimit << RateLimitEnable(addr=MATCH.addr), + AS.wordlist << PreferredWordlist(category=OPTION_WORDLIST_SUBDOMAIN), TOOL_PREF(ToolCategory.dns_scanner, dnsenum_tool_name), TOOL_CONF(ToolCategory.dns_scanner, dnsenum_tool_name), NOT(PublicTarget(addr=MATCH.addr)), ) - def run_dnsenum_ratelimit(self, f1: ScanNeeded, domain: TargetDomain, ratelimit: RateLimitEnable): - self._declare_dnsenum(f1, domain, ratelimit=ratelimit) + def run_dnsenum_ratelimit(self, f1: ScanNeeded, domain: TargetDomain, ratelimit: RateLimitEnable, + wordlist: PreferredWordlist): + self._declare_dnsenum(f1, domain, ratelimit=ratelimit, wordlist=wordlist) @Rule( AS.f1 << ScanNeeded(category=ToolCategory.dns_scanner, addr=MATCH.addr, port=53), AS.domain << TargetDomain(), AS.ratelimit << RateLimitEnable(addr=MATCH.addr), AS.public << PublicTarget(addr=MATCH.addr), + AS.wordlist << PreferredWordlist(category=OPTION_WORDLIST_SUBDOMAIN), TOOL_PREF(ToolCategory.dns_scanner, dnsenum_tool_name), TOOL_CONF(ToolCategory.dns_scanner, dnsenum_tool_name), ) def run_dnsenum_public_ratelimit(self, f1: ScanNeeded, domain: TargetDomain, ratelimit: RateLimitEnable, - public: PublicTarget): - self._declare_dnsenum(f1, domain, ratelimit=ratelimit, public=public) + public: PublicTarget, wordlist: PreferredWordlist): + self._declare_dnsenum(f1, domain, ratelimit=ratelimit, public=public, wordlist=wordlist) diff --git a/shadycompass/rules/dns_scanner/dnsrecon.py b/shadycompass/rules/dns_scanner/dnsrecon.py index 6a3526d..6794325 100644 --- a/shadycompass/rules/dns_scanner/dnsrecon.py +++ b/shadycompass/rules/dns_scanner/dnsrecon.py @@ -3,7 +3,7 @@ from experta import DefFacts, AS, Rule, NOT, MATCH from shadycompass import ToolAvailable, TargetDomain -from shadycompass.config import ToolCategory +from shadycompass.config import ToolCategory, PreferredWordlist, OPTION_WORDLIST_SUBDOMAIN from shadycompass.facts import ScanNeeded, RateLimitEnable, PublicTarget from shadycompass.rules.conditions import TOOL_PREF, TOOL_CONF from shadycompass.rules.irules import IRules @@ -26,7 +26,7 @@ def dnsrecon_available(self): ) def _declare_dnsrecon(self, f1: ScanNeeded, domain: TargetDomain, ratelimit: RateLimitEnable = None, - public: PublicTarget = None): + public: PublicTarget = None, wordlist: PreferredWordlist = None): addr = f1.get_addr() addr_file_name_part = f'-{addr}-{f1.get_port()}' @@ -38,13 +38,14 @@ def _declare_dnsrecon(self, f1: ScanNeeded, domain: TargetDomain, ratelimit: Rat more_options = [] if ratelimit: more_options.append(['--threads', '1']) + if wordlist: + more_options.append(['-D', wordlist.get_path()]) command_line = self.resolve_command_line( self.dnsrecon_tool_name, [ '-n', f1.get_addr(), *enum_options, - '-D', '/usr/share/seclists/Discovery/DNS/subdomains-top1million-110000.txt', '-j', f'dnsrecon{addr_file_name_part}-{domain.get_domain()}.json', ], *more_options ) @@ -61,44 +62,50 @@ def _declare_dnsrecon(self, f1: ScanNeeded, domain: TargetDomain, ratelimit: Rat @Rule( AS.f1 << ScanNeeded(category=ToolCategory.dns_scanner, addr=MATCH.addr, port=53), AS.domain << TargetDomain(), + AS.wordlist << PreferredWordlist(category=OPTION_WORDLIST_SUBDOMAIN), TOOL_PREF(ToolCategory.dns_scanner, dnsrecon_tool_name), TOOL_CONF(ToolCategory.dns_scanner, dnsrecon_tool_name), NOT(RateLimitEnable(addr=MATCH.addr)), NOT(PublicTarget(addr=MATCH.addr)), ) - def run_dnsrecon(self, f1: ScanNeeded, domain: TargetDomain): - self._declare_dnsrecon(f1, domain) + def run_dnsrecon(self, f1: ScanNeeded, domain: TargetDomain, wordlist: PreferredWordlist): + self._declare_dnsrecon(f1, domain, wordlist=wordlist) @Rule( AS.f1 << ScanNeeded(category=ToolCategory.dns_scanner, addr=MATCH.addr, port=53), AS.domain << TargetDomain(), AS.public << PublicTarget(addr=MATCH.addr), + AS.wordlist << PreferredWordlist(category=OPTION_WORDLIST_SUBDOMAIN), TOOL_PREF(ToolCategory.dns_scanner, dnsrecon_tool_name), TOOL_CONF(ToolCategory.dns_scanner, dnsrecon_tool_name), NOT(RateLimitEnable(addr=MATCH.addr)), ) - def run_dnsrecon_public(self, f1: ScanNeeded, domain: TargetDomain, public: PublicTarget): - self._declare_dnsrecon(f1, domain, public=public) + def run_dnsrecon_public(self, f1: ScanNeeded, domain: TargetDomain, public: PublicTarget, + wordlist: PreferredWordlist): + self._declare_dnsrecon(f1, domain, public=public, wordlist=wordlist) @Rule( AS.f1 << ScanNeeded(category=ToolCategory.dns_scanner, addr=MATCH.addr, port=53), AS.domain << TargetDomain(), AS.ratelimit << RateLimitEnable(addr=MATCH.addr), + AS.wordlist << PreferredWordlist(category=OPTION_WORDLIST_SUBDOMAIN), TOOL_PREF(ToolCategory.dns_scanner, dnsrecon_tool_name), TOOL_CONF(ToolCategory.dns_scanner, dnsrecon_tool_name), NOT(PublicTarget(addr=MATCH.addr)), ) - def run_dnsrecon_ratelimit(self, f1: ScanNeeded, domain: TargetDomain, ratelimit: RateLimitEnable): - self._declare_dnsrecon(f1, domain, ratelimit=ratelimit) + def run_dnsrecon_ratelimit(self, f1: ScanNeeded, domain: TargetDomain, ratelimit: RateLimitEnable, + wordlist: PreferredWordlist): + self._declare_dnsrecon(f1, domain, ratelimit=ratelimit, wordlist=wordlist) @Rule( AS.f1 << ScanNeeded(category=ToolCategory.dns_scanner, addr=MATCH.addr, port=53), AS.domain << TargetDomain(), AS.ratelimit << RateLimitEnable(addr=MATCH.addr), AS.public << PublicTarget(addr=MATCH.addr), + AS.wordlist << PreferredWordlist(category=OPTION_WORDLIST_SUBDOMAIN), TOOL_PREF(ToolCategory.dns_scanner, dnsrecon_tool_name), TOOL_CONF(ToolCategory.dns_scanner, dnsrecon_tool_name), ) def run_dnsrecon_public_ratelimit(self, f1: ScanNeeded, domain: TargetDomain, ratelimit: RateLimitEnable, - public: PublicTarget): - self._declare_dnsrecon(f1, domain, ratelimit=ratelimit, public=public) + public: PublicTarget, wordlist: PreferredWordlist): + self._declare_dnsrecon(f1, domain, ratelimit=ratelimit, public=public, wordlist=wordlist) diff --git a/shadycompass/rules/http_buster/dirb.py b/shadycompass/rules/http_buster/dirb.py index 798a168..1f3967f 100644 --- a/shadycompass/rules/http_buster/dirb.py +++ b/shadycompass/rules/http_buster/dirb.py @@ -3,7 +3,7 @@ from experta import Rule, DefFacts, AS, MATCH, NOT -from shadycompass.config import ToolCategory, ToolAvailable +from shadycompass.config import ToolCategory, ToolAvailable, PreferredWordlist, OPTION_WORDLIST_FILE from shadycompass.facts import HttpBustingNeeded, RateLimitEnable from shadycompass.rules.conditions import TOOL_PREF, TOOL_CONF from shadycompass.rules.irules import IRules @@ -25,13 +25,17 @@ def dirb_available(self): methodology_links=METHOD_HTTP_BRUTE_FORCE, ) - def _declare_dirb(self, f1: HttpBustingNeeded, ratelimit: RateLimitEnable = None): + def _declare_dirb(self, f1: HttpBustingNeeded, ratelimit: RateLimitEnable = None, + wordlist: PreferredWordlist = None): more_options = [] if ratelimit: more_options.append(['-z', str(floor(60000 / ratelimit.get_request_per_second()))]) + wordlist_options = [] + if wordlist and not wordlist.is_default(): + wordlist_options.append(wordlist.get_path()) command_line = self.resolve_command_line( self.dirb_tool_name, - [f1.get_url(), '-o', f"dirb-{f1.get_port()}-{f1.get_vhost()}.txt"], *more_options) + [f1.get_url(), *wordlist_options, '-o', f"dirb-{f1.get_port()}-{f1.get_vhost()}.txt"], *more_options) self.recommend_tool( category=ToolCategory.http_buster, name=self.dirb_tool_name, @@ -44,18 +48,20 @@ def _declare_dirb(self, f1: HttpBustingNeeded, ratelimit: RateLimitEnable = None @Rule( AS.f1 << HttpBustingNeeded(addr=MATCH.addr), + AS.wordlist << PreferredWordlist(category=OPTION_WORDLIST_FILE), TOOL_PREF(ToolCategory.http_buster, dirb_tool_name), TOOL_CONF(ToolCategory.http_buster, dirb_tool_name), NOT(RateLimitEnable(addr=MATCH.addr)) ) - def run_dirb(self, f1: HttpBustingNeeded): - self._declare_dirb(f1) + def run_dirb(self, f1: HttpBustingNeeded, wordlist: PreferredWordlist): + self._declare_dirb(f1, wordlist=wordlist) @Rule( AS.f1 << HttpBustingNeeded(addr=MATCH.addr), AS.ratelimit << RateLimitEnable(addr=MATCH.addr), + AS.wordlist << PreferredWordlist(category=OPTION_WORDLIST_FILE), TOOL_PREF(ToolCategory.http_buster, dirb_tool_name), TOOL_CONF(ToolCategory.http_buster, dirb_tool_name), ) - def run_dirb_ratelimit(self, f1: HttpBustingNeeded, ratelimit: RateLimitEnable): - self._declare_dirb(f1, ratelimit=ratelimit) + def run_dirb_ratelimit(self, f1: HttpBustingNeeded, ratelimit: RateLimitEnable, wordlist: PreferredWordlist): + self._declare_dirb(f1, ratelimit=ratelimit, wordlist=wordlist) diff --git a/shadycompass/rules/http_buster/feroxbuster.py b/shadycompass/rules/http_buster/feroxbuster.py index 6e7db17..1a631a5 100644 --- a/shadycompass/rules/http_buster/feroxbuster.py +++ b/shadycompass/rules/http_buster/feroxbuster.py @@ -2,7 +2,7 @@ from experta import Rule, DefFacts, AS, MATCH, NOT -from shadycompass.config import ToolCategory, ToolAvailable +from shadycompass.config import ToolCategory, ToolAvailable, PreferredWordlist, OPTION_WORDLIST_FILE from shadycompass.facts import HttpBustingNeeded, RateLimitEnable from shadycompass.rules.conditions import TOOL_PREF, TOOL_CONF from shadycompass.rules.irules import IRules @@ -24,10 +24,13 @@ def feroxbuster_available(self): methodology_links=METHOD_HTTP_BRUTE_FORCE, ) - def _declare_feroxbuster(self, f1: HttpBustingNeeded, ratelimit: RateLimitEnable = None): + def _declare_feroxbuster(self, f1: HttpBustingNeeded, ratelimit: RateLimitEnable = None, + wordlist: PreferredWordlist = None): more_options = [] if ratelimit: more_options.append(['--scan-limit', '1', '--rate-limit', str(ratelimit.get_request_per_second())]) + if wordlist and not wordlist.is_default(): + more_options.append(['--wordlist', wordlist.get_path()]) command_line = self.resolve_command_line( self.feroxbuster_tool_name, [ @@ -48,18 +51,20 @@ def _declare_feroxbuster(self, f1: HttpBustingNeeded, ratelimit: RateLimitEnable @Rule( AS.f1 << HttpBustingNeeded(addr=MATCH.addr), + AS.wordlist << PreferredWordlist(category=OPTION_WORDLIST_FILE), TOOL_PREF(ToolCategory.http_buster, feroxbuster_tool_name), TOOL_CONF(ToolCategory.http_buster, feroxbuster_tool_name), NOT(RateLimitEnable(addr=MATCH.addr)), ) - def run_feroxbuster(self, f1: HttpBustingNeeded): - self._declare_feroxbuster(f1) + def run_feroxbuster(self, f1: HttpBustingNeeded, wordlist: PreferredWordlist): + self._declare_feroxbuster(f1, wordlist=wordlist) @Rule( AS.f1 << HttpBustingNeeded(addr=MATCH.addr), AS.ratelimit << RateLimitEnable(addr=MATCH.addr), + AS.wordlist << PreferredWordlist(category=OPTION_WORDLIST_FILE), TOOL_PREF(ToolCategory.http_buster, feroxbuster_tool_name), TOOL_CONF(ToolCategory.http_buster, feroxbuster_tool_name), ) - def run_feroxbuster_ratelimit(self, f1: HttpBustingNeeded, ratelimit: RateLimitEnable): - self._declare_feroxbuster(f1, ratelimit=ratelimit) + def run_feroxbuster_ratelimit(self, f1: HttpBustingNeeded, ratelimit: RateLimitEnable, wordlist: PreferredWordlist): + self._declare_feroxbuster(f1, ratelimit=ratelimit, wordlist=wordlist) diff --git a/shadycompass/rules/http_buster/gobuster.py b/shadycompass/rules/http_buster/gobuster.py index 4abfa2e..9a17f63 100644 --- a/shadycompass/rules/http_buster/gobuster.py +++ b/shadycompass/rules/http_buster/gobuster.py @@ -3,7 +3,8 @@ from experta import Rule, DefFacts, AS, MATCH, NOT -from shadycompass.config import ToolCategory, ToolAvailable +from shadycompass.config import ToolCategory, ToolAvailable, PreferredWordlist, OPTION_WORDLIST_FILE, \ + OPTION_WORDLIST_SUBDOMAIN from shadycompass.facts import HttpBustingNeeded, RateLimitEnable, ScanNeeded from shadycompass.rules.conditions import TOOL_PREF, TOOL_CONF from shadycompass.rules.irules import IRules @@ -37,10 +38,13 @@ def gobuster_available(self): def _gobuster_ratelimit_options(self, ratelimit: RateLimitEnable): return ['--threads', '1', '--delay', str(floor(60000 / ratelimit.get_request_per_second())) + "ms"] - def _declare_gobuster_as_http_buster(self, f1: HttpBustingNeeded, ratelimit: RateLimitEnable = None): + def _declare_gobuster_as_http_buster(self, f1: HttpBustingNeeded, ratelimit: RateLimitEnable = None, + wordlist: PreferredWordlist = None): more_options = [] if ratelimit: more_options.append(self._gobuster_ratelimit_options(ratelimit)) + if wordlist: + more_options.append(['-w', wordlist.get_path()]) command_line = self.resolve_command_line( self.gobuster_tool_name, [ @@ -61,26 +65,32 @@ def _declare_gobuster_as_http_buster(self, f1: HttpBustingNeeded, ratelimit: Rat @Rule( AS.f1 << HttpBustingNeeded(addr=MATCH.addr), + AS.wordlist << PreferredWordlist(category=OPTION_WORDLIST_FILE), TOOL_PREF(ToolCategory.http_buster, gobuster_tool_name), TOOL_CONF(ToolCategory.http_buster, gobuster_tool_name), NOT(RateLimitEnable(addr=MATCH.addr)) ) - def run_gobuster_as_http_buster(self, f1: HttpBustingNeeded): - self._declare_gobuster_as_http_buster(f1) + def run_gobuster_as_http_buster(self, f1: HttpBustingNeeded, wordlist: PreferredWordlist): + self._declare_gobuster_as_http_buster(f1, wordlist=wordlist) @Rule( AS.f1 << HttpBustingNeeded(addr=MATCH.addr), AS.ratelimit << RateLimitEnable(addr=MATCH.addr), + AS.wordlist << PreferredWordlist(category=OPTION_WORDLIST_FILE), TOOL_PREF(ToolCategory.http_buster, gobuster_tool_name), TOOL_CONF(ToolCategory.http_buster, gobuster_tool_name), ) - def run_gobuster_as_http_buster_ratelimit(self, f1: HttpBustingNeeded, ratelimit: RateLimitEnable = None): - self._declare_gobuster_as_http_buster(f1, ratelimit) + def run_gobuster_as_http_buster_ratelimit(self, f1: HttpBustingNeeded, ratelimit: RateLimitEnable, + wordlist: PreferredWordlist): + self._declare_gobuster_as_http_buster(f1, ratelimit, wordlist=wordlist) - def _declare_gobuster_as_virtualhost_scan(self, f1: ScanNeeded, ratelimit: RateLimitEnable = None): + def _declare_gobuster_as_virtualhost_scan(self, f1: ScanNeeded, ratelimit: RateLimitEnable = None, + wordlist: PreferredWordlist = None): more_options = [] if ratelimit: more_options.append(self._gobuster_ratelimit_options(ratelimit)) + if wordlist: + more_options.append(['-w', wordlist.get_path()]) command_line = self.resolve_command_line( self.gobuster_tool_name, [ @@ -102,18 +112,21 @@ def _declare_gobuster_as_virtualhost_scan(self, f1: ScanNeeded, ratelimit: RateL @Rule( AS.f1 << ScanNeeded(category=ToolCategory.virtualhost_scanner, addr=MATCH.addr), + AS.wordlist << PreferredWordlist(category=OPTION_WORDLIST_SUBDOMAIN), TOOL_PREF(ToolCategory.http_buster, gobuster_tool_name), TOOL_CONF(ToolCategory.http_buster, gobuster_tool_name), NOT(RateLimitEnable(addr=MATCH.addr)) ) - def run_gobuster_as_virtualhost_scan(self, f1: ScanNeeded): - self._declare_gobuster_as_virtualhost_scan(f1) + def run_gobuster_as_virtualhost_scan(self, f1: ScanNeeded, wordlist: PreferredWordlist): + self._declare_gobuster_as_virtualhost_scan(f1, wordlist=wordlist) @Rule( AS.f1 << ScanNeeded(category=ToolCategory.virtualhost_scanner, addr=MATCH.addr), AS.ratelimit << RateLimitEnable(addr=MATCH.addr), + AS.wordlist << PreferredWordlist(category=OPTION_WORDLIST_SUBDOMAIN), TOOL_PREF(ToolCategory.http_buster, gobuster_tool_name), TOOL_CONF(ToolCategory.http_buster, gobuster_tool_name), ) - def run_gobuster_as_virtualhost_scan_ratelimit(self, f1: ScanNeeded, ratelimit: RateLimitEnable = None): - self._declare_gobuster_as_virtualhost_scan(f1, ratelimit) + def run_gobuster_as_virtualhost_scan_ratelimit(self, f1: ScanNeeded, ratelimit: RateLimitEnable, + wordlist: PreferredWordlist): + self._declare_gobuster_as_virtualhost_scan(f1, ratelimit, wordlist=wordlist) diff --git a/shadycompass/rules/http_buster/wfuzz.py b/shadycompass/rules/http_buster/wfuzz.py index 0dcd4f7..549e218 100644 --- a/shadycompass/rules/http_buster/wfuzz.py +++ b/shadycompass/rules/http_buster/wfuzz.py @@ -3,7 +3,8 @@ from experta import Rule, DefFacts, AS, NOT, MATCH -from shadycompass.config import ToolCategory, ToolAvailable +from shadycompass.config import ToolCategory, ToolAvailable, PreferredWordlist, OPTION_WORDLIST_FILE, \ + OPTION_WORDLIST_SUBDOMAIN from shadycompass.facts import HttpBustingNeeded, RateLimitEnable, ScanNeeded from shadycompass.rules.conditions import TOOL_PREF, TOOL_CONF from shadycompass.rules.irules import IRules @@ -37,14 +38,15 @@ def wfuzz_available(self): def _wfuzz_ratelimit_options(self, ratelimit: RateLimitEnable): return ['-t', '1', '-s', str(floor(60 / ratelimit.get_request_per_second()))] - def _declare_wfuzz_as_http_buster(self, f1: HttpBustingNeeded, ratelimit: RateLimitEnable = None): + def _declare_wfuzz_as_http_buster(self, f1: HttpBustingNeeded, ratelimit: RateLimitEnable = None, + wordlist: PreferredWordlist = None): more_options = [] if ratelimit: more_options.append(self._wfuzz_ratelimit_options(ratelimit)) command_line = self.resolve_command_line( self.wfuzz_tool_name, [ - '-w', '/usr/share/seclists/Discovery/Web-Content/raft-small-files.txt', + '-w', wordlist.get_path(), '--hc', '404', '-f', f'wfuzz-{f1.get_port()}-{f1.get_vhost()}.json,json', ], *more_options @@ -62,23 +64,27 @@ def _declare_wfuzz_as_http_buster(self, f1: HttpBustingNeeded, ratelimit: RateLi @Rule( AS.f1 << HttpBustingNeeded(addr=MATCH.addr), + AS.wordlist << PreferredWordlist(category=OPTION_WORDLIST_FILE), TOOL_PREF(ToolCategory.http_buster, wfuzz_tool_name), TOOL_CONF(ToolCategory.http_buster, wfuzz_tool_name), NOT(RateLimitEnable(addr=MATCH.addr)) ) - def run_wfuzz_as_http_buster(self, f1: HttpBustingNeeded): - self._declare_wfuzz_as_http_buster(f1) + def run_wfuzz_as_http_buster(self, f1: HttpBustingNeeded, wordlist: PreferredWordlist): + self._declare_wfuzz_as_http_buster(f1, wordlist=wordlist) @Rule( AS.f1 << HttpBustingNeeded(addr=MATCH.addr), AS.ratelimit << RateLimitEnable(addr=MATCH.addr), + AS.wordlist << PreferredWordlist(category=OPTION_WORDLIST_FILE), TOOL_PREF(ToolCategory.http_buster, wfuzz_tool_name), TOOL_CONF(ToolCategory.http_buster, wfuzz_tool_name), ) - def run_wfuzz_as_http_buster_ratelimit(self, f1: HttpBustingNeeded, ratelimit: RateLimitEnable): - self._declare_wfuzz_as_http_buster(f1, ratelimit) + def run_wfuzz_as_http_buster_ratelimit(self, f1: HttpBustingNeeded, ratelimit: RateLimitEnable, + wordlist: PreferredWordlist): + self._declare_wfuzz_as_http_buster(f1, ratelimit, wordlist=wordlist) - def _declare_wfuzz_as_virtualhost_scan(self, f1: ScanNeeded, ratelimit: RateLimitEnable = None): + def _declare_wfuzz_as_virtualhost_scan(self, f1: ScanNeeded, ratelimit: RateLimitEnable = None, + wordlist: PreferredWordlist = None): protocol = 'https' if f1.is_secure() else 'http' url = f"{protocol}://FUZZ.{f1.get_hostname()}:{f1.get_port()}/" @@ -88,7 +94,7 @@ def _declare_wfuzz_as_virtualhost_scan(self, f1: ScanNeeded, ratelimit: RateLimi command_line = self.resolve_command_line( self.wfuzz_tool_name, [ - '-w', '/usr/share/seclists/Discovery/DNS/subdomains-top1million-110000.txt', + '-w', wordlist.get_path(), '--hc', '404', '-f', f'wfuzz-vhost-{f1.get_port()}-{f1.get_hostname()}.json,json', ], *more_options @@ -106,18 +112,21 @@ def _declare_wfuzz_as_virtualhost_scan(self, f1: ScanNeeded, ratelimit: RateLimi @Rule( AS.f1 << ScanNeeded(category=ToolCategory.virtualhost_scanner, addr=MATCH.addr), + AS.wordlist << PreferredWordlist(category=OPTION_WORDLIST_SUBDOMAIN), TOOL_PREF(ToolCategory.http_buster, wfuzz_tool_name), TOOL_CONF(ToolCategory.http_buster, wfuzz_tool_name), NOT(RateLimitEnable(addr=MATCH.addr)) ) - def run_wfuzz_as_virtualhost_scan(self, f1: ScanNeeded): - self._declare_wfuzz_as_virtualhost_scan(f1) + def run_wfuzz_as_virtualhost_scan(self, f1: ScanNeeded, wordlist: PreferredWordlist): + self._declare_wfuzz_as_virtualhost_scan(f1, wordlist=wordlist) @Rule( AS.f1 << ScanNeeded(category=ToolCategory.virtualhost_scanner, addr=MATCH.addr), AS.ratelimit << RateLimitEnable(addr=MATCH.addr), + AS.wordlist << PreferredWordlist(category=OPTION_WORDLIST_SUBDOMAIN), TOOL_PREF(ToolCategory.http_buster, wfuzz_tool_name), TOOL_CONF(ToolCategory.http_buster, wfuzz_tool_name), ) - def run_wfuzz_as_virtualhost_scan_ratelimit(self, f1: ScanNeeded, ratelimit: RateLimitEnable): - self._declare_wfuzz_as_virtualhost_scan(f1, ratelimit) + def run_wfuzz_as_virtualhost_scan_ratelimit(self, f1: ScanNeeded, ratelimit: RateLimitEnable, + wordlist: PreferredWordlist): + self._declare_wfuzz_as_virtualhost_scan(f1, ratelimit, wordlist=wordlist) diff --git a/shadycompass/rules/kerberos/kerbrute.py b/shadycompass/rules/kerberos/kerbrute.py index 1d99cce..2c63c85 100644 --- a/shadycompass/rules/kerberos/kerbrute.py +++ b/shadycompass/rules/kerberos/kerbrute.py @@ -1,10 +1,10 @@ from abc import ABC +from math import floor from experta import DefFacts, Rule, AS, NOT, MATCH -from math import floor from shadycompass import ToolAvailable -from shadycompass.config import ToolCategory +from shadycompass.config import ToolCategory, PreferredWordlist, OPTION_WORDLIST_USERNAME from shadycompass.facts import ScanNeeded, RateLimitEnable, WindowsDomain from shadycompass.rules.conditions import TOOL_PREF, TOOL_CONF from shadycompass.rules.irules import IRules @@ -27,7 +27,8 @@ def kerbrute_available(self): # kerbrute --safe passwordspray -d shadycompass.test --dc dc.shadycompass.test users 'Passw0rd!' >kerbrute-passwordspray-shadycompass.test.txt - def _declare_kerbrute_asrep_roast(self, f1: ScanNeeded, domain: WindowsDomain, ratelimit: RateLimitEnable = None): + def _declare_kerbrute_asrep_roast(self, f1: ScanNeeded, domain: WindowsDomain, ratelimit: RateLimitEnable = None, + wordlist: PreferredWordlist = None): addr = f1.get_addr() addr_file_name_part = f'-{addr}' if domain: @@ -49,7 +50,7 @@ def _declare_kerbrute_asrep_roast(self, f1: ScanNeeded, domain: WindowsDomain, r ], ) command_line.extend([ - '/usr/share/seclists/Usernames/xato-net-10-million-usernames.txt', + wordlist.get_path(), f'>{self.kerbrute_tool_name}-userenum{addr_file_name_part}.txt', ]) self.recommend_tool( @@ -64,20 +65,22 @@ def _declare_kerbrute_asrep_roast(self, f1: ScanNeeded, domain: WindowsDomain, r @Rule( AS.f1 << ScanNeeded(category=ToolCategory.asrep_roaster, addr=MATCH.addr), AS.domain << WindowsDomain(netbios_domain_name=MATCH.netbios_domain_name), + AS.wordlist << PreferredWordlist(category=OPTION_WORDLIST_USERNAME), TOOL_PREF(ToolCategory.asrep_roaster, kerbrute_tool_name), TOOL_CONF(ToolCategory.asrep_roaster, kerbrute_tool_name), NOT(RateLimitEnable(addr=MATCH.addr)) ) - def run_kerbrute_asrep_roast_domain(self, f1: ScanNeeded, domain: WindowsDomain): - self._declare_kerbrute_asrep_roast(f1, domain=domain) + def run_kerbrute_asrep_roast_domain(self, f1: ScanNeeded, domain: WindowsDomain, wordlist: PreferredWordlist): + self._declare_kerbrute_asrep_roast(f1, domain=domain, wordlist=wordlist) @Rule( AS.f1 << ScanNeeded(category=ToolCategory.asrep_roaster, addr=MATCH.addr), AS.domain << WindowsDomain(netbios_domain_name=MATCH.netbios_domain_name), AS.ratelimit << RateLimitEnable(addr=MATCH.addr), + AS.wordlist << PreferredWordlist(category=OPTION_WORDLIST_USERNAME), TOOL_PREF(ToolCategory.asrep_roaster, kerbrute_tool_name), TOOL_CONF(ToolCategory.asrep_roaster, kerbrute_tool_name), ) def run_kerbrute_asrep_roast_domain_ratelimit(self, f1: ScanNeeded, domain: WindowsDomain, - ratelimit: RateLimitEnable): - self._declare_kerbrute_asrep_roast(f1, domain=domain, ratelimit=ratelimit) + ratelimit: RateLimitEnable, wordlist: PreferredWordlist): + self._declare_kerbrute_asrep_roast(f1, domain=domain, ratelimit=ratelimit, wordlist=wordlist) diff --git a/shadycompass/rules/smtp_scanner/smtp_user_enum.py b/shadycompass/rules/smtp_scanner/smtp_user_enum.py index 3b3c738..b3001d5 100644 --- a/shadycompass/rules/smtp_scanner/smtp_user_enum.py +++ b/shadycompass/rules/smtp_scanner/smtp_user_enum.py @@ -1,10 +1,11 @@ from abc import ABC -from experta import DefFacts, Rule, AS, MATCH, OR, NOT +from experta import DefFacts, Rule, AS, MATCH, NOT from shadycompass import ToolAvailable -from shadycompass.config import ToolCategory, PreferredTool, OPTION_VALUE_ALL, SECTION_OPTIONS, ConfigFact +from shadycompass.config import ToolCategory, PreferredWordlist, OPTION_WORDLIST_USERNAME from shadycompass.facts import ScanNeeded, RateLimitEnable, TargetDomain +from shadycompass.rules.conditions import TOOL_PREF, TOOL_CONF from shadycompass.rules.irules import IRules from shadycompass.rules.library import METHOD_SMTP @@ -24,7 +25,8 @@ def smtp_user_enum_available(self): methodology_links=METHOD_SMTP, ) - def _declare_smtp_user_enum(self, f1: ScanNeeded, ratelimit: RateLimitEnable = None, domain: TargetDomain = None): + def _declare_smtp_user_enum(self, f1: ScanNeeded, ratelimit: RateLimitEnable = None, domain: TargetDomain = None, + wordlist: PreferredWordlist = None): addr = f1.get_addr() addr_file_name_part = f'-{addr}-{f1.get_port()}' if domain: @@ -41,7 +43,7 @@ def _declare_smtp_user_enum(self, f1: ScanNeeded, ratelimit: RateLimitEnable = N self.smtp_user_enum_name, [ '-M', 'VRFY', - '-U', '/usr/share/seclists/Usernames/xato-net-10-million-usernames.txt', + '-U', wordlist.get_path(), ], *more_options ) command_line.extend( @@ -57,60 +59,45 @@ def _declare_smtp_user_enum(self, f1: ScanNeeded, ratelimit: RateLimitEnable = N @Rule( AS.f1 << ScanNeeded(category=ToolCategory.smtp_scanner, addr=MATCH.addr), - OR( - PreferredTool(category=ToolCategory.smtp_scanner, name=smtp_user_enum_name), - PreferredTool(category=ToolCategory.smtp_scanner, name=OPTION_VALUE_ALL), - NOT(PreferredTool(category=ToolCategory.smtp_scanner)), - ), - OR(ConfigFact(section=SECTION_OPTIONS, option=smtp_user_enum_name), - NOT(ConfigFact(section=SECTION_OPTIONS, option=smtp_user_enum_name))), + AS.wordlist << PreferredWordlist(category=OPTION_WORDLIST_USERNAME), + TOOL_PREF(ToolCategory.smtp_scanner, smtp_user_enum_name), + TOOL_CONF(ToolCategory.smtp_scanner, smtp_user_enum_name), NOT(RateLimitEnable(addr=MATCH.addr)), NOT(TargetDomain()) ) - def run_smtp_user_enum(self, f1: ScanNeeded): - self._declare_smtp_user_enum(f1) + def run_smtp_user_enum(self, f1: ScanNeeded, wordlist: PreferredWordlist): + self._declare_smtp_user_enum(f1, wordlist=wordlist) @Rule( AS.f1 << ScanNeeded(category=ToolCategory.smtp_scanner, addr=MATCH.addr), AS.ratelimit << RateLimitEnable(addr=MATCH.addr), - OR( - PreferredTool(category=ToolCategory.smtp_scanner, name=smtp_user_enum_name), - PreferredTool(category=ToolCategory.smtp_scanner, name=OPTION_VALUE_ALL), - NOT(PreferredTool(category=ToolCategory.smtp_scanner)), - ), - OR(ConfigFact(section=SECTION_OPTIONS, option=smtp_user_enum_name), - NOT(ConfigFact(section=SECTION_OPTIONS, option=smtp_user_enum_name))), + AS.wordlist << PreferredWordlist(category=OPTION_WORDLIST_USERNAME), + TOOL_PREF(ToolCategory.smtp_scanner, smtp_user_enum_name), + TOOL_CONF(ToolCategory.smtp_scanner, smtp_user_enum_name), NOT(TargetDomain()) ) - def run_smtp_user_enum_ratelimit(self, f1: ScanNeeded, ratelimit: RateLimitEnable): - self._declare_smtp_user_enum(f1, ratelimit=ratelimit) + def run_smtp_user_enum_ratelimit(self, f1: ScanNeeded, ratelimit: RateLimitEnable, wordlist: PreferredWordlist): + self._declare_smtp_user_enum(f1, ratelimit=ratelimit, wordlist=wordlist) @Rule( AS.f1 << ScanNeeded(category=ToolCategory.smtp_scanner, addr=MATCH.addr), AS.domain << TargetDomain(), - OR( - PreferredTool(category=ToolCategory.smtp_scanner, name=smtp_user_enum_name), - PreferredTool(category=ToolCategory.smtp_scanner, name=OPTION_VALUE_ALL), - NOT(PreferredTool(category=ToolCategory.smtp_scanner)), - ), - OR(ConfigFact(section=SECTION_OPTIONS, option=smtp_user_enum_name), - NOT(ConfigFact(section=SECTION_OPTIONS, option=smtp_user_enum_name))), + AS.wordlist << PreferredWordlist(category=OPTION_WORDLIST_USERNAME), + TOOL_PREF(ToolCategory.smtp_scanner, smtp_user_enum_name), + TOOL_CONF(ToolCategory.smtp_scanner, smtp_user_enum_name), NOT(RateLimitEnable(addr=MATCH.addr)) ) - def run_smtp_user_enum_domain(self, f1: ScanNeeded, domain: TargetDomain): - self._declare_smtp_user_enum(f1, domain=domain) + def run_smtp_user_enum_domain(self, f1: ScanNeeded, domain: TargetDomain, wordlist: PreferredWordlist): + self._declare_smtp_user_enum(f1, domain=domain, wordlist=wordlist) @Rule( AS.f1 << ScanNeeded(category=ToolCategory.smtp_scanner, addr=MATCH.addr), AS.domain << TargetDomain(), AS.ratelimit << RateLimitEnable(addr=MATCH.addr), - OR( - PreferredTool(category=ToolCategory.smtp_scanner, name=smtp_user_enum_name), - PreferredTool(category=ToolCategory.smtp_scanner, name=OPTION_VALUE_ALL), - NOT(PreferredTool(category=ToolCategory.smtp_scanner)), - ), - OR(ConfigFact(section=SECTION_OPTIONS, option=smtp_user_enum_name), - NOT(ConfigFact(section=SECTION_OPTIONS, option=smtp_user_enum_name))), + AS.wordlist << PreferredWordlist(category=OPTION_WORDLIST_USERNAME), + TOOL_PREF(ToolCategory.smtp_scanner, smtp_user_enum_name), + TOOL_CONF(ToolCategory.smtp_scanner, smtp_user_enum_name), ) - def run_smtp_user_enum_domain_ratelimit(self, f1: ScanNeeded, domain: TargetDomain, ratelimit: RateLimitEnable): - self._declare_smtp_user_enum(f1, ratelimit=ratelimit, domain=domain) + def run_smtp_user_enum_domain_ratelimit(self, f1: ScanNeeded, domain: TargetDomain, ratelimit: RateLimitEnable, + wordlist: PreferredWordlist): + self._declare_smtp_user_enum(f1, ratelimit=ratelimit, domain=domain, wordlist=wordlist) diff --git a/tests/config/test_config.py b/tests/config/test_config.py index e0102c2..9be7745 100644 --- a/tests/config/test_config.py +++ b/tests/config/test_config.py @@ -2,12 +2,23 @@ import tempfile import unittest +from parameterized import parameterized + from shadycompass import ShadyCompassEngine from shadycompass.config import ConfigFactReader, ConfigFact, SECTION_TOOLS, ToolCategory, set_global_config_path, \ - ToolChoiceNeeded, PreferredTool, ToolAvailable, OPTION_VALUE_ALL, ToolRecommended, tool_category_priority + ToolChoiceNeeded, PreferredTool, ToolAvailable, OPTION_VALUE_ALL, ToolRecommended, tool_category_priority, \ + SECTION_WORDLISTS, OPTION_WORDLIST_FILE, PreferredWordlist, OPTION_WORDLIST_USERNAME, OPTION_WORDLIST_PASSWORD, \ + OPTION_WORDLIST_SUBDOMAIN from shadycompass.facts import HttpBustingNeeded from tests.tests import assertFactIn, assertFactNotIn +WORDLIST_CATEGORIES = [ + OPTION_WORDLIST_FILE, + OPTION_WORDLIST_USERNAME, + OPTION_WORDLIST_PASSWORD, + OPTION_WORDLIST_SUBDOMAIN, +] + class ConfigFactReaderTest(unittest.TestCase): @@ -136,10 +147,8 @@ def test_preferred_tool_retracts_other_tool_recommendations(self): t2 = ToolRecommended(category=ToolCategory.http_buster, name='wfuzz') t3 = ToolRecommended(category=ToolCategory.http_buster, name='gobuster') t4 = ToolRecommended(category=ToolCategory.http_buster, name='feroxbuster') - cAll = ConfigFact(section=SECTION_TOOLS, option=ToolCategory.http_buster, value=OPTION_VALUE_ALL, global0=False) - cFerox = ConfigFact(section=SECTION_TOOLS, option=ToolCategory.http_buster, value='feroxbuster', global0=False) - self.engine.declare(cAll) + self.engine.config_set(SECTION_TOOLS, ToolCategory.http_buster, OPTION_VALUE_ALL, False) self.engine.declare(HttpBustingNeeded(secure=True, addr='10.129.229.189', port=443, vhost='shadycompass.test')) self.engine.run() assertFactIn(t1, self.engine) @@ -147,8 +156,7 @@ def test_preferred_tool_retracts_other_tool_recommendations(self): assertFactIn(t3, self.engine) assertFactIn(t4, self.engine) - self.engine.retract(cAll) - self.engine.declare(cFerox) + self.engine.config_set(SECTION_TOOLS, ToolCategory.http_buster, 'feroxbuster', False) self.engine.run() assertFactIn(PreferredTool(category=ToolCategory.http_buster, name='feroxbuster'), self.engine) assertFactNotIn(PreferredTool(category=ToolCategory.http_buster, name=OPTION_VALUE_ALL), self.engine) @@ -157,6 +165,49 @@ def test_preferred_tool_retracts_other_tool_recommendations(self): assertFactNotIn(t3, self.engine) assertFactIn(t4, self.engine) + def test_default_wordllist_file(self): + self.engine.run() + assertFactIn(PreferredWordlist(category=OPTION_WORDLIST_FILE, path='raft-large-files.txt', default=True), + self.engine) + + def test_default_wordllist_username(self): + self.engine.run() + assertFactIn(PreferredWordlist(category=OPTION_WORDLIST_USERNAME, path='xato-net-10-million-usernames.txt', + default=True), self.engine) + + def test_default_wordllist_password(self): + self.engine.run() + assertFactIn(PreferredWordlist(category=OPTION_WORDLIST_PASSWORD, path='rockyou.txt', default=True), + self.engine) + + def test_default_wordllist_subdomain(self): + self.engine.run() + assertFactIn(PreferredWordlist(category=OPTION_WORDLIST_SUBDOMAIN, path='subdomains-top1million-110000.txt', + default=True), self.engine) + + @parameterized.expand(WORDLIST_CATEGORIES) + def test_preferred_wordlist_global(self, category: str): + self.engine.declare( + ConfigFact(section=SECTION_WORDLISTS, option=category, value='wordlist1.txt', global0=True)) + self.engine.run() + assertFactIn(PreferredWordlist(category=category, path='wordlist1.txt', default=False), self.engine) + + @parameterized.expand(WORDLIST_CATEGORIES) + def test_preferred_wordlist_local(self, category: str): + self.engine.declare( + ConfigFact(section=SECTION_WORDLISTS, option=category, value='wordlist2.txt', global0=False)) + self.engine.run() + assertFactIn(PreferredWordlist(category=category, path='wordlist2.txt', default=False), self.engine) + + @parameterized.expand(WORDLIST_CATEGORIES) + def test_preferred_wordlist_local_over_global(self, category: str): + self.engine.declare( + ConfigFact(section=SECTION_WORDLISTS, option=category, value='wordlist1.txt', global0=True)) + self.engine.declare( + ConfigFact(section=SECTION_WORDLISTS, option=category, value='wordlist2.txt', global0=False)) + self.engine.run() + assertFactIn(PreferredWordlist(category=category, path='wordlist2.txt', default=False), self.engine) + class ToolCategoryTest(unittest.TestCase): diff --git a/tests/rules/dns_scanner/test_rules_dnsenum.py b/tests/rules/dns_scanner/test_rules_dnsenum.py index 73c7108..595e567 100644 --- a/tests/rules/dns_scanner/test_rules_dnsenum.py +++ b/tests/rules/dns_scanner/test_rules_dnsenum.py @@ -1,5 +1,5 @@ from shadycompass.config import SECTION_TOOLS, ToolCategory, ToolRecommended, SECTION_OPTIONS, SECTION_DEFAULT, \ - OPTION_RATELIMIT, OPTION_PRODUCTION + OPTION_RATELIMIT, OPTION_PRODUCTION, SECTION_WORDLISTS, OPTION_WORDLIST_SUBDOMAIN from shadycompass.facts import ScanNeeded, ScanPresent, TargetDomain, TargetIPv4Address, DomainUdpIpService from shadycompass.rules.dns_scanner.dnsenum import DnsEnumRules from tests.rules.base import RulesBase @@ -22,7 +22,7 @@ def test_dnsenum_private(self): '--dnsserver', '10.129.229.189', '-p', '0', '-s', '0', '-o', 'dnsenum-10.129.229.189-53-subdomains-shadycompass.test.xml', - '-f', '/usr/share/seclists/Discovery/DNS/subdomains-top1million-110000.txt', 'shadycompass.test', + '-f', 'subdomains-top1million-110000.txt', 'shadycompass.test', ], ), self.engine) self.engine.declare(ScanPresent(category=ToolCategory.dns_scanner, addr='10.129.229.189', port=53, @@ -45,8 +45,8 @@ def test_dnsenum_private_ratelimit(self): '--dnsserver', '10.129.229.189', '-p', '0', '-s', '0', '-o', 'dnsenum-10.129.229.189-53-subdomains-shadycompass.test.xml', - '-f', '/usr/share/seclists/Discovery/DNS/subdomains-top1million-110000.txt', '--threads', '1', + '-f', 'subdomains-top1million-110000.txt', 'shadycompass.test', ], ), self.engine) @@ -65,7 +65,7 @@ def test_dnsenum_public(self): '--dnsserver', '8.8.8.8', '--enum', '-o', 'dnsenum-8.8.8.8-53-subdomains-shadycompass.test.xml', - '-f', '/usr/share/seclists/Discovery/DNS/subdomains-top1million-110000.txt', 'shadycompass.test', + '-f', 'subdomains-top1million-110000.txt', 'shadycompass.test', ], ), self.engine) @@ -84,8 +84,8 @@ def test_dnsenum_public_ratelimit(self): '--dnsserver', '8.8.8.8', '--enum', '-o', 'dnsenum-8.8.8.8-53-subdomains-shadycompass.test.xml', - '-f', '/usr/share/seclists/Discovery/DNS/subdomains-top1million-110000.txt', '--threads', '1', + '-f', 'subdomains-top1million-110000.txt', 'shadycompass.test', ], ), self.engine) @@ -102,7 +102,7 @@ def test_dnsenum_two_domains(self): '--dnsserver', '10.129.229.189', '-p', '0', '-s', '0', '-o', 'dnsenum-10.129.229.189-53-subdomains-shadycompass.test.xml', - '-f', '/usr/share/seclists/Discovery/DNS/subdomains-top1million-110000.txt', 'shadycompass.test', + '-f', 'subdomains-top1million-110000.txt', 'shadycompass.test', ], ), self.engine) assertFactIn(ToolRecommended( @@ -112,7 +112,7 @@ def test_dnsenum_two_domains(self): '--dnsserver', '10.129.229.189', '-p', '0', '-s', '0', '-o', 'dnsenum-10.129.229.189-53-subdomains-shadycompass2.test.xml', - '-f', '/usr/share/seclists/Discovery/DNS/subdomains-top1million-110000.txt', 'shadycompass2.test', + '-f', 'subdomains-top1million-110000.txt', 'shadycompass2.test', ], ), self.engine) @@ -127,8 +127,8 @@ def test_dnsenum_options_local(self): '--dnsserver', '10.129.229.189', '-p', '0', '-s', '0', '-o', 'dnsenum-10.129.229.189-53-subdomains-shadycompass.test.xml', - '-f', '/usr/share/seclists/Discovery/DNS/subdomains-top1million-110000.txt', '--nocolor', + '-f', 'subdomains-top1million-110000.txt', 'shadycompass.test', ], ), self.engine) @@ -144,8 +144,8 @@ def test_dnsenum_options_global(self): '--dnsserver', '10.129.229.189', '-p', '0', '-s', '0', '-o', 'dnsenum-10.129.229.189-53-subdomains-shadycompass.test.xml', - '-f', '/usr/share/seclists/Discovery/DNS/subdomains-top1million-110000.txt', '--nocolor', + '-f', 'subdomains-top1million-110000.txt', 'shadycompass.test', ], ), self.engine) @@ -162,3 +162,25 @@ def test_dnsenum_retract(self): category=ToolCategory.dns_scanner, name=DnsEnumRules.dnsenum_tool_name, ), self.engine) + + def test_dnsenum_private_wordlist(self): + self.engine.config_set(SECTION_TOOLS, ToolCategory.dns_scanner, DnsEnumRules.dnsenum_tool_name, True) + self.engine.config_set(SECTION_WORDLISTS, OPTION_WORDLIST_SUBDOMAIN, 'subdomains-top1million-5000.txt', True) + self.engine.run() + assertFactIn(ScanNeeded(category=ToolCategory.dns_scanner), self.engine) + assertFactIn(ToolRecommended( + category=ToolCategory.dns_scanner, + name=DnsEnumRules.dnsenum_tool_name, + command_line=[ + '--dnsserver', '10.129.229.189', + '-p', '0', '-s', '0', + '-o', 'dnsenum-10.129.229.189-53-subdomains-shadycompass.test.xml', + '-f', 'subdomains-top1million-5000.txt', 'shadycompass.test', + ], + ), self.engine) + self.engine.declare(ScanPresent(category=ToolCategory.dns_scanner, addr='10.129.229.189', port=53, + name=DnsEnumRules.dnsenum_tool_name)) + self.engine.run() + assertFactNotIn(ScanNeeded(category=ToolCategory.dns_scanner, addr='10.129.229.189'), self.engine) + assertFactNotIn(ToolRecommended(category=ToolCategory.dns_scanner, name=DnsEnumRules.dnsenum_tool_name), + self.engine) diff --git a/tests/rules/dns_scanner/test_rules_dnsrecon.py b/tests/rules/dns_scanner/test_rules_dnsrecon.py index 3a0069e..637e69d 100644 --- a/tests/rules/dns_scanner/test_rules_dnsrecon.py +++ b/tests/rules/dns_scanner/test_rules_dnsrecon.py @@ -1,5 +1,5 @@ from shadycompass.config import SECTION_TOOLS, ToolCategory, ToolRecommended, SECTION_OPTIONS, SECTION_DEFAULT, \ - OPTION_RATELIMIT, OPTION_PRODUCTION + OPTION_RATELIMIT, OPTION_PRODUCTION, SECTION_WORDLISTS, OPTION_WORDLIST_SUBDOMAIN from shadycompass.facts import ScanNeeded, ScanPresent, TargetDomain, TargetIPv4Address, DomainUdpIpService from shadycompass.rules.dns_scanner.dnsrecon import DnsReconRules from tests.rules.base import RulesBase @@ -20,8 +20,8 @@ def test_dnsrecon_private(self): name=DnsReconRules.dnsrecon_tool_name, command_line=[ '-n', '10.129.229.189', - '-D', '/usr/share/seclists/Discovery/DNS/subdomains-top1million-110000.txt', '-j', 'dnsrecon-10.129.229.189-53-shadycompass.test.json', + '-D', 'subdomains-top1million-110000.txt', '-d', 'shadycompass.test', ], ), self.engine) @@ -43,9 +43,9 @@ def test_dnsrecon_private_ratelimit(self): name=DnsReconRules.dnsrecon_tool_name, command_line=[ '-n', '10.129.229.189', - '-D', '/usr/share/seclists/Discovery/DNS/subdomains-top1million-110000.txt', '-j', 'dnsrecon-10.129.229.189-53-shadycompass.test.json', '--threads', '1', + '-D', 'subdomains-top1million-110000.txt', '-d', 'shadycompass.test', ], ), self.engine) @@ -63,8 +63,8 @@ def test_dnsrecon_public(self): command_line=[ '-n', '8.8.8.8', '-a', '-s', '-b', '-y', '-k', '-w', '-z', - '-D', '/usr/share/seclists/Discovery/DNS/subdomains-top1million-110000.txt', '-j', 'dnsrecon-8.8.8.8-53-shadycompass.test.json', + '-D', 'subdomains-top1million-110000.txt', '-d', 'shadycompass.test', ], ), self.engine) @@ -83,9 +83,9 @@ def test_dnsrecon_public_ratelimit(self): command_line=[ '-n', '8.8.8.8', '-a', '-s', '-b', '-y', '-k', '-w', '-z', - '-D', '/usr/share/seclists/Discovery/DNS/subdomains-top1million-110000.txt', '-j', 'dnsrecon-8.8.8.8-53-shadycompass.test.json', '--threads', '1', + '-D', 'subdomains-top1million-110000.txt', '-d', 'shadycompass.test', ], ), self.engine) @@ -100,8 +100,8 @@ def test_dnsrecon_two_domains(self): name=DnsReconRules.dnsrecon_tool_name, command_line=[ '-n', '10.129.229.189', - '-D', '/usr/share/seclists/Discovery/DNS/subdomains-top1million-110000.txt', '-j', 'dnsrecon-10.129.229.189-53-shadycompass.test.json', + '-D', 'subdomains-top1million-110000.txt', '-d', 'shadycompass.test', ], ), self.engine) @@ -110,8 +110,8 @@ def test_dnsrecon_two_domains(self): name=DnsReconRules.dnsrecon_tool_name, command_line=[ '-n', '10.129.229.189', - '-D', '/usr/share/seclists/Discovery/DNS/subdomains-top1million-110000.txt', '-j', 'dnsrecon-10.129.229.189-53-shadycompass2.test.json', + '-D', 'subdomains-top1million-110000.txt', '-d', 'shadycompass2.test', ], ), self.engine) @@ -125,9 +125,9 @@ def test_dnsrecon_options_local(self): name=DnsReconRules.dnsrecon_tool_name, command_line=[ '-n', '10.129.229.189', - '-D', '/usr/share/seclists/Discovery/DNS/subdomains-top1million-110000.txt', '-j', 'dnsrecon-10.129.229.189-53-shadycompass.test.json', '-f', + '-D', 'subdomains-top1million-110000.txt', '-d', 'shadycompass.test', ], ), self.engine) @@ -141,9 +141,9 @@ def test_dnsrecon_options_global(self): name=DnsReconRules.dnsrecon_tool_name, command_line=[ '-n', '10.129.229.189', - '-D', '/usr/share/seclists/Discovery/DNS/subdomains-top1million-110000.txt', '-j', 'dnsrecon-10.129.229.189-53-shadycompass.test.json', '-f', + '-D', 'subdomains-top1million-110000.txt', '-d', 'shadycompass.test', ], ), self.engine) @@ -160,3 +160,25 @@ def test_dnsrecon_retract(self): category=ToolCategory.dns_scanner, name=DnsReconRules.dnsrecon_tool_name, ), self.engine) + + def test_dnsrecon_private_wordlist(self): + self.engine.config_set(SECTION_TOOLS, ToolCategory.dns_scanner, DnsReconRules.dnsrecon_tool_name, True) + self.engine.config_set(SECTION_WORDLISTS, OPTION_WORDLIST_SUBDOMAIN, 'subdomains-top1million-5000.txt', True) + self.engine.run() + assertFactIn(ScanNeeded(category=ToolCategory.dns_scanner), self.engine) + assertFactIn(ToolRecommended( + category=ToolCategory.dns_scanner, + name=DnsReconRules.dnsrecon_tool_name, + command_line=[ + '-n', '10.129.229.189', + '-j', 'dnsrecon-10.129.229.189-53-shadycompass.test.json', + '-D', 'subdomains-top1million-5000.txt', + '-d', 'shadycompass.test', + ], + ), self.engine) + self.engine.declare(ScanPresent(category=ToolCategory.dns_scanner, addr='10.129.229.189', port=53, + name=DnsReconRules.dnsrecon_tool_name)) + self.engine.run() + assertFactNotIn(ScanNeeded(category=ToolCategory.dns_scanner, addr='10.129.229.189'), self.engine) + assertFactNotIn(ToolRecommended(category=ToolCategory.dns_scanner, name=DnsReconRules.dnsrecon_tool_name), + self.engine) diff --git a/tests/rules/http_buster/test_rules_dirb.py b/tests/rules/http_buster/test_rules_dirb.py index 86a7156..3b4d82a 100644 --- a/tests/rules/http_buster/test_rules_dirb.py +++ b/tests/rules/http_buster/test_rules_dirb.py @@ -1,5 +1,5 @@ from shadycompass.config import SECTION_TOOLS, ToolCategory, ToolRecommended, SECTION_OPTIONS, SECTION_DEFAULT, \ - OPTION_RATELIMIT, OPTION_PRODUCTION + OPTION_RATELIMIT, OPTION_PRODUCTION, SECTION_WORDLISTS, OPTION_WORDLIST_FILE from shadycompass.rules.http_buster.dirb import DirbRules from tests.rules.base import RulesBase from tests.tests import assertFactIn, assertFactNotIn @@ -89,3 +89,20 @@ def test_dirb_ratelimit(self): name=DirbRules.dirb_tool_name, command_line=['https://shadycompass.test:443', '-o', 'dirb-443-shadycompass.test.txt'], ), self.engine) + + def test_dirb_wordlist(self): + self.engine.config_set(SECTION_TOOLS, ToolCategory.http_buster, DirbRules.dirb_tool_name, True) + self.engine.config_set(SECTION_WORDLISTS, OPTION_WORDLIST_FILE, 'raft-large-files.txt', True) + self.engine.run() + assertFactIn(ToolRecommended( + category=ToolCategory.http_buster, + name=DirbRules.dirb_tool_name, + command_line=['http://shadycompass.test:8080', 'raft-large-files.txt', '-o', + 'dirb-8080-shadycompass.test.txt'], + ), self.engine) + assertFactIn(ToolRecommended( + category=ToolCategory.http_buster, + name=DirbRules.dirb_tool_name, + command_line=['https://shadycompass.test:443', 'raft-large-files.txt', '-o', + 'dirb-443-shadycompass.test.txt'], + ), self.engine) diff --git a/tests/rules/http_buster/test_rules_feroxbuster.py b/tests/rules/http_buster/test_rules_feroxbuster.py index 9b6283d..d12a334 100644 --- a/tests/rules/http_buster/test_rules_feroxbuster.py +++ b/tests/rules/http_buster/test_rules_feroxbuster.py @@ -1,5 +1,5 @@ from shadycompass.config import SECTION_TOOLS, ToolCategory, ToolRecommended, SECTION_OPTIONS, SECTION_DEFAULT, \ - OPTION_RATELIMIT, OPTION_PRODUCTION + OPTION_RATELIMIT, OPTION_PRODUCTION, SECTION_WORDLISTS, OPTION_WORDLIST_FILE from shadycompass.rules.http_buster.feroxbuster import FeroxBusterRules from tests.rules.base import RulesBase from tests.tests import assertFactIn, assertFactNotIn @@ -73,3 +73,17 @@ def test_feroxbuster_ratelimit(self): '-o', "feroxbuster-443-shadycompass.test.txt", '--insecure', '--scan-limit', '4'], ), self.engine) + + def test_feroxbuster_wordlist(self): + self.engine.config_set(SECTION_TOOLS, ToolCategory.http_buster, FeroxBusterRules.feroxbuster_tool_name, True) + self.engine.config_set(SECTION_WORDLISTS, OPTION_WORDLIST_FILE, 'raft-large-files.txt', True) + self.engine.run() + assertFactIn(ToolRecommended( + category=ToolCategory.http_buster, + name=FeroxBusterRules.feroxbuster_tool_name, + command_line=['-u', 'http://shadycompass.test:8080', + '-o', "feroxbuster-8080-shadycompass.test.txt", '--insecure', + '--scan-limit', '4', + '--wordlist', 'raft-large-files.txt', + ], + ), self.engine) diff --git a/tests/rules/http_buster/test_rules_gobuster.py b/tests/rules/http_buster/test_rules_gobuster.py index c5c31ae..df4074d 100644 --- a/tests/rules/http_buster/test_rules_gobuster.py +++ b/tests/rules/http_buster/test_rules_gobuster.py @@ -1,5 +1,5 @@ from shadycompass.config import SECTION_TOOLS, ToolCategory, ToolRecommended, SECTION_OPTIONS, SECTION_DEFAULT, \ - OPTION_RATELIMIT, OPTION_PRODUCTION + OPTION_RATELIMIT, OPTION_PRODUCTION, SECTION_WORDLISTS, OPTION_WORDLIST_FILE from shadycompass.rules.http_buster.gobuster import GoBusterRules from tests.rules.base import RulesBase from tests.tests import assertFactIn, assertFactNotIn @@ -19,7 +19,8 @@ def test_gobuster(self): command_line=[ 'dir', '-k', '-o', "gobuster-8080-shadycompass.test.txt", - '-u', 'http://shadycompass.test:8080' + '-u', 'http://shadycompass.test:8080', + '-w', 'raft-large-files.txt', ], ), self.engine) assertFactIn(ToolRecommended( @@ -28,7 +29,8 @@ def test_gobuster(self): command_line=[ 'dir', '-k', '-o', "gobuster-443-shadycompass.test.txt", - '-u', 'https://shadycompass.test:443' + '-u', 'https://shadycompass.test:443', + '-w', 'raft-large-files.txt', ], ), self.engine) @@ -42,7 +44,8 @@ def test_gobuster_options(self): command_line=[ 'dir', '-k', '-o', "gobuster-8080-shadycompass.test.txt", - '-u', 'http://shadycompass.test:8080', '--retry' + '-u', 'http://shadycompass.test:8080', '--retry', + '-w', 'raft-large-files.txt', ], ), self.engine) @@ -58,7 +61,8 @@ def test_gobuster_ratelimit(self): 'dir', '-k', '-o', "gobuster-8080-shadycompass.test.txt", '-u', 'http://shadycompass.test:8080', - '--threads', '1', '--delay', '12000ms' + '--threads', '1', '--delay', '12000ms', + '-w', 'raft-large-files.txt', ], ), self.engine) assertFactIn(ToolRecommended( @@ -68,7 +72,8 @@ def test_gobuster_ratelimit(self): 'dir', '-k', '-o', "gobuster-443-shadycompass.test.txt", '-u', 'https://shadycompass.test:443', - '--threads', '1', '--delay', '12000ms' + '--threads', '1', '--delay', '12000ms', + '-w', 'raft-large-files.txt', ], ), self.engine) assertFactNotIn(ToolRecommended( @@ -77,7 +82,8 @@ def test_gobuster_ratelimit(self): command_line=[ 'dir', '-k', '-o', "gobuster-8080-shadycompass.test.txt", - '-u', 'http://shadycompass.test:8080' + '-u', 'http://shadycompass.test:8080', + '-w', 'raft-large-files.txt', ], ), self.engine) assertFactNotIn(ToolRecommended( @@ -86,6 +92,32 @@ def test_gobuster_ratelimit(self): command_line=[ 'dir', '-k', '-o', "gobuster-443-shadycompass.test.txt", - '-u', 'https://shadycompass.test:443' + '-u', 'https://shadycompass.test:443', + '-w', 'raft-large-files.txt', + ], + ), self.engine) + + def test_gobuster_wordlist(self): + self.engine.config_set(SECTION_TOOLS, ToolCategory.http_buster, GoBusterRules.gobuster_tool_name, True) + self.engine.config_set(SECTION_WORDLISTS, OPTION_WORDLIST_FILE, 'raft-medium-files.txt', True) + self.engine.run() + assertFactIn(ToolRecommended( + category=ToolCategory.http_buster, + name=GoBusterRules.gobuster_tool_name, + command_line=[ + 'dir', '-k', + '-o', "gobuster-8080-shadycompass.test.txt", + '-u', 'http://shadycompass.test:8080', + '-w', 'raft-medium-files.txt', + ], + ), self.engine) + assertFactIn(ToolRecommended( + category=ToolCategory.http_buster, + name=GoBusterRules.gobuster_tool_name, + command_line=[ + 'dir', '-k', + '-o', "gobuster-443-shadycompass.test.txt", + '-u', 'https://shadycompass.test:443', + '-w', 'raft-medium-files.txt', ], ), self.engine) diff --git a/tests/rules/http_buster/test_rules_wfuzz.py b/tests/rules/http_buster/test_rules_wfuzz.py index ba6bcb9..ea2739c 100644 --- a/tests/rules/http_buster/test_rules_wfuzz.py +++ b/tests/rules/http_buster/test_rules_wfuzz.py @@ -1,5 +1,5 @@ from shadycompass.config import SECTION_TOOLS, ToolCategory, ToolRecommended, SECTION_OPTIONS, SECTION_DEFAULT, \ - OPTION_RATELIMIT, OPTION_PRODUCTION + OPTION_RATELIMIT, OPTION_PRODUCTION, SECTION_WORDLISTS, OPTION_WORDLIST_FILE from shadycompass.rules.http_buster.wfuzz import WfuzzRules from tests.rules.base import RulesBase from tests.tests import assertFactIn, assertFactNotIn @@ -17,7 +17,7 @@ def test_wfuzz(self): category=ToolCategory.http_buster, name=WfuzzRules.wfuzz_tool_name, command_line=[ - '-w', '/usr/share/seclists/Discovery/Web-Content/raft-small-files.txt', + '-w', 'raft-large-files.txt', '--hc', '404', '-f', 'wfuzz-8080-shadycompass.test.json,json', 'http://shadycompass.test:8080/FUZZ', @@ -27,7 +27,7 @@ def test_wfuzz(self): category=ToolCategory.http_buster, name=WfuzzRules.wfuzz_tool_name, command_line=[ - '-w', '/usr/share/seclists/Discovery/Web-Content/raft-small-files.txt', + '-w', 'raft-large-files.txt', '--hc', '404', '-f', 'wfuzz-443-shadycompass.test.json,json', 'https://shadycompass.test:443/FUZZ', @@ -42,7 +42,7 @@ def test_wfuzz_options(self): category=ToolCategory.http_buster, name=WfuzzRules.wfuzz_tool_name, command_line=[ - '-w', '/usr/share/seclists/Discovery/Web-Content/raft-small-files.txt', + '-w', 'raft-large-files.txt', '--hc', '404,500', '-f', 'wfuzz-8080-shadycompass.test.json,json', 'http://shadycompass.test:8080/FUZZ', @@ -58,7 +58,7 @@ def test_wfuzz_ratelimit(self): category=ToolCategory.http_buster, name=WfuzzRules.wfuzz_tool_name, command_line=[ - '-w', '/usr/share/seclists/Discovery/Web-Content/raft-small-files.txt', + '-w', 'raft-large-files.txt', '--hc', '404', '-f', 'wfuzz-8080-shadycompass.test.json,json', '-t', '1', '-s', '12', @@ -69,7 +69,7 @@ def test_wfuzz_ratelimit(self): category=ToolCategory.http_buster, name=WfuzzRules.wfuzz_tool_name, command_line=[ - '-w', '/usr/share/seclists/Discovery/Web-Content/raft-small-files.txt', + '-w', 'raft-large-files.txt', '--hc', '404', '-f', 'wfuzz-443-shadycompass.test.json,json', '-t', '1', '-s', '12', @@ -80,7 +80,7 @@ def test_wfuzz_ratelimit(self): category=ToolCategory.http_buster, name=WfuzzRules.wfuzz_tool_name, command_line=[ - '-w', '/usr/share/seclists/Discovery/Web-Content/raft-small-files.txt', + '-w', 'raft-large-files.txt', '--hc', '404', '-f', 'wfuzz-8080-shadycompass.test.json,json', 'http://shadycompass.test:8080/FUZZ' @@ -90,9 +90,34 @@ def test_wfuzz_ratelimit(self): category=ToolCategory.http_buster, name=WfuzzRules.wfuzz_tool_name, command_line=[ - '-w', '/usr/share/seclists/Discovery/Web-Content/raft-small-files.txt', + '-w', 'raft-large-files.txt', '--hc', '404', '-f', 'wfuzz-443-shadycompass.test.json,json', 'https://shadycompass.test:443/FUZZ' ], ), self.engine) + + def test_wfuzz_wordlist(self): + self.engine.config_set(SECTION_TOOLS, ToolCategory.http_buster, WfuzzRules.wfuzz_tool_name, True) + self.engine.config_set(SECTION_WORDLISTS, OPTION_WORDLIST_FILE, 'raft-large-files.txt', True) + self.engine.run() + assertFactIn(ToolRecommended( + category=ToolCategory.http_buster, + name=WfuzzRules.wfuzz_tool_name, + command_line=[ + '-w', 'raft-large-files.txt', + '--hc', '404', + '-f', 'wfuzz-8080-shadycompass.test.json,json', + 'http://shadycompass.test:8080/FUZZ', + ], + ), self.engine) + assertFactIn(ToolRecommended( + category=ToolCategory.http_buster, + name=WfuzzRules.wfuzz_tool_name, + command_line=[ + '-w', 'raft-large-files.txt', + '--hc', '404', + '-f', 'wfuzz-443-shadycompass.test.json,json', + 'https://shadycompass.test:443/FUZZ', + ], + ), self.engine) diff --git a/tests/rules/kerberos/test_rules_kerbrute.py b/tests/rules/kerberos/test_rules_kerbrute.py index 5e240d7..6f3a45b 100644 --- a/tests/rules/kerberos/test_rules_kerbrute.py +++ b/tests/rules/kerberos/test_rules_kerbrute.py @@ -1,5 +1,6 @@ from shadycompass import SECTION_TOOLS -from shadycompass.config import ToolCategory, ToolRecommended, SECTION_OPTIONS, SECTION_DEFAULT, OPTION_RATELIMIT +from shadycompass.config import ToolCategory, ToolRecommended, SECTION_OPTIONS, SECTION_DEFAULT, OPTION_RATELIMIT, \ + SECTION_WORDLISTS, OPTION_WORDLIST_USERNAME from shadycompass.facts import ScanNeeded, WindowsDomain, TargetIPv4Address, Kerberos5SecTcpService, ScanPresent from shadycompass.rules.kerberos.kerbrute import KerbruteRules from tests.rules.base import RulesBase @@ -20,7 +21,7 @@ def test_asrep_roaster(self): command_line=[ '--safe', '--dc', '10.129.229.189', '-d', 'SHADYCOMPASS', 'userenum', - '/usr/share/seclists/Usernames/xato-net-10-million-usernames.txt', + 'xato-net-10-million-usernames.txt', '>kerbrute-userenum-10.129.229.189-SHADYCOMPASS.txt' ], addr='10.129.229.189', @@ -45,7 +46,7 @@ def test_asrep_roaster_options_local(self): '--safe', '--dc', '10.129.229.189', '-d', 'SHADYCOMPASS', 'userenum', '-v', - '/usr/share/seclists/Usernames/xato-net-10-million-usernames.txt', + 'xato-net-10-million-usernames.txt', '>kerbrute-userenum-10.129.229.189-SHADYCOMPASS.txt' ], addr='10.129.229.189', port=88, @@ -64,7 +65,7 @@ def test_asrep_roaster_options_global(self): '--safe', '--dc', '10.129.229.189', '-d', 'SHADYCOMPASS', 'userenum', '-v', - '/usr/share/seclists/Usernames/xato-net-10-million-usernames.txt', + 'xato-net-10-million-usernames.txt', '>kerbrute-userenum-10.129.229.189-SHADYCOMPASS.txt' ], addr='10.129.229.189', port=88, @@ -82,7 +83,7 @@ def test_asrep_roaster_domains(self): command_line=[ '--safe', '--dc', '10.129.229.189', '-d', 'SHADYCOMPASS', 'userenum', - '/usr/share/seclists/Usernames/xato-net-10-million-usernames.txt', + 'xato-net-10-million-usernames.txt', '>kerbrute-userenum-10.129.229.189-SHADYCOMPASS.txt' ], addr='10.129.229.189', port=88, @@ -93,7 +94,7 @@ def test_asrep_roaster_domains(self): command_line=[ '--safe', '--dc', '10.129.229.189', '-d', 'SHADYCOMPASS2', 'userenum', - '/usr/share/seclists/Usernames/xato-net-10-million-usernames.txt', + 'xato-net-10-million-usernames.txt', '>kerbrute-userenum-10.129.229.189-SHADYCOMPASS2.txt' ], addr='10.129.229.189', port=88, @@ -112,7 +113,7 @@ def test_asrep_roaster_ratelimit(self): command_line=[ '--safe', '--dc', '10.129.229.189', '--delay', '12000', '-d', 'SHADYCOMPASS', 'userenum', - '/usr/share/seclists/Usernames/xato-net-10-million-usernames.txt', + 'xato-net-10-million-usernames.txt', '>kerbrute-userenum-10.129.229.189-SHADYCOMPASS.txt' ], addr='10.129.229.189', port=88, @@ -131,12 +132,35 @@ def test_asrep_roaster_domains_ratelimit(self): command_line=[ '--safe', '--dc', '10.129.229.189', '--delay', '12000', '-d', 'SHADYCOMPASS', 'userenum', - '/usr/share/seclists/Usernames/xato-net-10-million-usernames.txt', + 'xato-net-10-million-usernames.txt', '>kerbrute-userenum-10.129.229.189-SHADYCOMPASS.txt' ], addr='10.129.229.189', port=88, ), self.engine) + def test_asrep_roaster_wordlist(self): + self.engine.config_set(SECTION_TOOLS, ToolCategory.asrep_roaster, KerbruteRules.kerbrute_tool_name, True) + self.engine.config_set(SECTION_WORDLISTS, OPTION_WORDLIST_USERNAME, 'top-usernames-shortlist.txt', True) + self.engine.run() + assertFactIn(ScanNeeded(category=ToolCategory.asrep_roaster, addr='10.129.229.189'), self.engine) + assertFactIn(ToolRecommended( + category=ToolCategory.asrep_roaster, + name=KerbruteRules.kerbrute_tool_name, + command_line=[ + '--safe', '--dc', '10.129.229.189', '-d', 'SHADYCOMPASS', + 'userenum', + 'top-usernames-shortlist.txt', + '>kerbrute-userenum-10.129.229.189-SHADYCOMPASS.txt' + ], + addr='10.129.229.189', + ), self.engine) + self.engine.declare(ScanPresent(category=ToolCategory.asrep_roaster, addr='10.129.229.189', + name=KerbruteRules.kerbrute_tool_name)) + self.engine.run() + assertFactNotIn(ScanNeeded(category=ToolCategory.asrep_roaster, addr='10.129.229.189'), self.engine) + assertFactNotIn(ToolRecommended(category=ToolCategory.asrep_roaster, name=KerbruteRules.kerbrute_tool_name), + self.engine) + class KerbruteRulesNATest(RulesBase): def __init__(self, methodName: str = ...): diff --git a/tests/rules/smtp_scanner/test_rules_smtp_user_enum.py b/tests/rules/smtp_scanner/test_rules_smtp_user_enum.py index 21b64c3..9fa4a21 100644 --- a/tests/rules/smtp_scanner/test_rules_smtp_user_enum.py +++ b/tests/rules/smtp_scanner/test_rules_smtp_user_enum.py @@ -1,5 +1,6 @@ from shadycompass import ToolRecommended -from shadycompass.config import ToolCategory, SECTION_TOOLS, SECTION_OPTIONS, SECTION_DEFAULT, OPTION_RATELIMIT +from shadycompass.config import ToolCategory, SECTION_TOOLS, SECTION_OPTIONS, SECTION_DEFAULT, OPTION_RATELIMIT, \ + SECTION_WORDLISTS, OPTION_WORDLIST_USERNAME from shadycompass.facts import ScanNeeded, TargetDomain, TargetIPv4Address, ScanPresent from shadycompass.rules.smtp_scanner.smtp_user_enum import SmtpUserEnumRules from tests.rules.base import RulesBase @@ -20,7 +21,7 @@ def test_smtp_user_enum(self): name=SmtpUserEnumRules.smtp_user_enum_name, command_line=[ '-M', 'VRFY', - '-U', '/usr/share/seclists/Usernames/xato-net-10-million-usernames.txt', + '-U', 'xato-net-10-million-usernames.txt', '-t', '10.129.229.189', '-p', '25', '>smtp-user-enum-10.129.229.189-25.txt', ], @@ -44,7 +45,7 @@ def test_smtp_user_enum_options_local(self): name=SmtpUserEnumRules.smtp_user_enum_name, command_line=[ '-M', 'VRFY', - '-U', '/usr/share/seclists/Usernames/xato-net-10-million-usernames.txt', + '-U', 'xato-net-10-million-usernames.txt', '-v', '-t', '10.129.229.189', '-p', '25', '>smtp-user-enum-10.129.229.189-25.txt', @@ -63,7 +64,7 @@ def test_smtp_user_enum_options_global(self): name=SmtpUserEnumRules.smtp_user_enum_name, command_line=[ '-M', 'VRFY', - '-U', '/usr/share/seclists/Usernames/xato-net-10-million-usernames.txt', + '-U', 'xato-net-10-million-usernames.txt', '-v', '-t', '10.129.229.189', '-p', '25', '>smtp-user-enum-10.129.229.189-25.txt', @@ -83,7 +84,7 @@ def test_smtp_user_enum_domains(self): name=SmtpUserEnumRules.smtp_user_enum_name, command_line=[ '-M', 'VRFY', - '-U', '/usr/share/seclists/Usernames/xato-net-10-million-usernames.txt', + '-U', 'xato-net-10-million-usernames.txt', '-D', 'shadycompass.test', '-f', 'user@shadycompass.test', '-t', '10.129.229.189', '-p', '25', @@ -96,7 +97,7 @@ def test_smtp_user_enum_domains(self): name=SmtpUserEnumRules.smtp_user_enum_name, command_line=[ '-M', 'VRFY', - '-U', '/usr/share/seclists/Usernames/xato-net-10-million-usernames.txt', + '-U', 'xato-net-10-million-usernames.txt', '-D', 'shadycompass2.test', '-f', 'user@shadycompass2.test', '-t', '10.129.229.189', '-p', '25', @@ -117,7 +118,7 @@ def test_smtp_user_enum_ratelimit(self): name=SmtpUserEnumRules.smtp_user_enum_name, command_line=[ '-M', 'VRFY', - '-U', '/usr/share/seclists/Usernames/xato-net-10-million-usernames.txt', + '-U', 'xato-net-10-million-usernames.txt', '-m', '1', '-t', '10.129.229.189', '-p', '25', '>smtp-user-enum-10.129.229.189-25.txt', @@ -138,7 +139,7 @@ def test_smtp_user_enum_domains_ratelimit(self): name=SmtpUserEnumRules.smtp_user_enum_name, command_line=[ '-M', 'VRFY', - '-U', '/usr/share/seclists/Usernames/xato-net-10-million-usernames.txt', + '-U', 'xato-net-10-million-usernames.txt', '-m', '1', '-D', 'shadycompass.test', '-f', 'user@shadycompass.test', @@ -148,6 +149,30 @@ def test_smtp_user_enum_domains_ratelimit(self): addr='10.129.229.189', port=25, ), self.engine) + def test_smtp_user_enum_wordlist(self): + self.engine.config_set(SECTION_TOOLS, ToolCategory.smtp_scanner, SmtpUserEnumRules.smtp_user_enum_name, True) + self.engine.config_set(SECTION_WORDLISTS, OPTION_WORDLIST_USERNAME, 'top-usernames-shortlist.txt', True) + self.engine.declare( + ScanNeeded(category=ToolCategory.smtp_scanner, addr='10.129.229.189', port=25, secure=False)) + self.engine.run() + assertFactIn(ToolRecommended( + category=ToolCategory.smtp_scanner, + name=SmtpUserEnumRules.smtp_user_enum_name, + command_line=[ + '-M', 'VRFY', + '-U', 'top-usernames-shortlist.txt', + '-t', '10.129.229.189', '-p', '25', + '>smtp-user-enum-10.129.229.189-25.txt', + ], + addr='10.129.229.189', port=25, + ), self.engine) + self.engine.declare(ScanPresent(category=ToolCategory.smtp_scanner, addr='10.129.229.189', port=25, + name=SmtpUserEnumRules.smtp_user_enum_name)) + self.engine.run() + assertFactNotIn(ScanNeeded(category=ToolCategory.smtp_scanner, addr='10.129.229.189'), self.engine) + assertFactNotIn(ToolRecommended(category=ToolCategory.smtp_scanner, name=SmtpUserEnumRules.smtp_user_enum_name), + self.engine) + class SmtpUserEnumRulesNATest(RulesBase): def __init__(self, methodName: str = ...): diff --git a/tests/rules/virtualhost_scanner/test_rules_gobuster_virtualhost.py b/tests/rules/virtualhost_scanner/test_rules_gobuster_virtualhost.py index d5f926b..051a01c 100644 --- a/tests/rules/virtualhost_scanner/test_rules_gobuster_virtualhost.py +++ b/tests/rules/virtualhost_scanner/test_rules_gobuster_virtualhost.py @@ -1,6 +1,6 @@ from shadycompass import SECTION_TOOLS from shadycompass.config import ToolCategory, ToolRecommended, SECTION_OPTIONS, SECTION_DEFAULT, OPTION_RATELIMIT, \ - OPTION_PRODUCTION + OPTION_PRODUCTION, SECTION_WORDLISTS, OPTION_WORDLIST_SUBDOMAIN from shadycompass.rules.http_buster.gobuster import GoBusterRules from tests.rules.base import RulesBase from tests.tests import assertFactIn, assertFactNotIn @@ -21,7 +21,8 @@ def test_gobuster(self): 'vhost', '-k', '-o', "gobuster-vhost-8080-shadycompass.test.txt", '--append-domain', - '-u', 'http://shadycompass.test:8080' + '-u', 'http://shadycompass.test:8080', + '-w', 'subdomains-top1million-110000.txt', ], ), self.engine) assertFactIn(ToolRecommended( @@ -31,7 +32,8 @@ def test_gobuster(self): 'vhost', '-k', '-o', "gobuster-vhost-443-shadycompass.test.txt", '--append-domain', - '-u', 'https://shadycompass.test:443' + '-u', 'https://shadycompass.test:443', + '-w', 'subdomains-top1million-110000.txt', ], ), self.engine) @@ -46,7 +48,8 @@ def test_gobuster_options(self): 'vhost', '-k', '-o', "gobuster-vhost-8080-shadycompass.test.txt", '--append-domain', - '-u', 'http://shadycompass.test:8080', '--retry' + '-u', 'http://shadycompass.test:8080', '--retry', + '-w', 'subdomains-top1million-110000.txt', ], ), self.engine) @@ -63,7 +66,8 @@ def test_gobuster_ratelimit(self): '-o', "gobuster-vhost-8080-shadycompass.test.txt", '--append-domain', '-u', 'http://shadycompass.test:8080', - '--threads', '1', '--delay', '12000ms' + '--threads', '1', '--delay', '12000ms', + '-w', 'subdomains-top1million-110000.txt', ], ), self.engine) assertFactIn(ToolRecommended( @@ -74,7 +78,8 @@ def test_gobuster_ratelimit(self): '-o', "gobuster-vhost-443-shadycompass.test.txt", '--append-domain', '-u', 'https://shadycompass.test:443', - '--threads', '1', '--delay', '12000ms' + '--threads', '1', '--delay', '12000ms', + '-w', 'subdomains-top1million-110000.txt', ], ), self.engine) assertFactNotIn(ToolRecommended( @@ -84,7 +89,8 @@ def test_gobuster_ratelimit(self): 'vhost', '-k', '-o', "gobuster-vhost-8080-shadycompass.test.txt", '--append-domain', - '-u', 'http://shadycompass.test:8080' + '-u', 'http://shadycompass.test:8080', + '-w', 'subdomains-top1million-110000.txt', ], ), self.engine) assertFactNotIn(ToolRecommended( @@ -94,6 +100,34 @@ def test_gobuster_ratelimit(self): 'vhost', '-k', '-o', "gobuster-vhost-443-shadycompass.test.txt", '--append-domain', - '-u', 'https://shadycompass.test:443' + '-u', 'https://shadycompass.test:443', + '-w', 'subdomains-top1million-110000.txt', + ], + ), self.engine) + + def test_gobuster_wordlist(self): + self.engine.config_set(SECTION_TOOLS, ToolCategory.virtualhost_scanner, GoBusterRules.gobuster_tool_name, True) + self.engine.config_set(SECTION_WORDLISTS, OPTION_WORDLIST_SUBDOMAIN, 'subdomains-top1million-5000.txt', True) + self.engine.run() + assertFactIn(ToolRecommended( + category=ToolCategory.virtualhost_scanner, + name=GoBusterRules.gobuster_tool_name, + command_line=[ + 'vhost', '-k', + '-o', "gobuster-vhost-8080-shadycompass.test.txt", + '--append-domain', + '-u', 'http://shadycompass.test:8080', + '-w', 'subdomains-top1million-5000.txt', + ], + ), self.engine) + assertFactIn(ToolRecommended( + category=ToolCategory.virtualhost_scanner, + name=GoBusterRules.gobuster_tool_name, + command_line=[ + 'vhost', '-k', + '-o', "gobuster-vhost-443-shadycompass.test.txt", + '--append-domain', + '-u', 'https://shadycompass.test:443', + '-w', 'subdomains-top1million-5000.txt', ], ), self.engine) diff --git a/tests/rules/virtualhost_scanner/test_rules_wfuzz_virtualhost.py b/tests/rules/virtualhost_scanner/test_rules_wfuzz_virtualhost.py index f8a97e7..b578c1b 100644 --- a/tests/rules/virtualhost_scanner/test_rules_wfuzz_virtualhost.py +++ b/tests/rules/virtualhost_scanner/test_rules_wfuzz_virtualhost.py @@ -1,5 +1,5 @@ from shadycompass.config import SECTION_TOOLS, ToolCategory, ToolRecommended, SECTION_OPTIONS, SECTION_DEFAULT, \ - OPTION_RATELIMIT, OPTION_PRODUCTION + OPTION_RATELIMIT, OPTION_PRODUCTION, SECTION_WORDLISTS, OPTION_WORDLIST_SUBDOMAIN from shadycompass.rules.http_buster.wfuzz import WfuzzRules from tests.rules.base import RulesBase from tests.tests import assertFactIn, assertFactNotIn @@ -17,7 +17,7 @@ def test_wfuzz(self): category=ToolCategory.virtualhost_scanner, name=WfuzzRules.wfuzz_tool_name, command_line=[ - '-w', '/usr/share/seclists/Discovery/DNS/subdomains-top1million-110000.txt', + '-w', 'subdomains-top1million-110000.txt', '--hc', '404', '-f', 'wfuzz-vhost-8080-shadycompass.test.json,json', 'http://FUZZ.shadycompass.test:8080/', @@ -27,7 +27,7 @@ def test_wfuzz(self): category=ToolCategory.virtualhost_scanner, name=WfuzzRules.wfuzz_tool_name, command_line=[ - '-w', '/usr/share/seclists/Discovery/DNS/subdomains-top1million-110000.txt', + '-w', 'subdomains-top1million-110000.txt', '--hc', '404', '-f', 'wfuzz-vhost-443-shadycompass.test.json,json', 'https://FUZZ.shadycompass.test:443/', @@ -42,7 +42,7 @@ def test_wfuzz_options(self): category=ToolCategory.virtualhost_scanner, name=WfuzzRules.wfuzz_tool_name, command_line=[ - '-w', '/usr/share/seclists/Discovery/DNS/subdomains-top1million-110000.txt', + '-w', 'subdomains-top1million-110000.txt', '--hc', '404,500', '-f', 'wfuzz-vhost-8080-shadycompass.test.json,json', 'http://FUZZ.shadycompass.test:8080/', @@ -58,7 +58,7 @@ def test_wfuzz_ratelimit(self): category=ToolCategory.virtualhost_scanner, name=WfuzzRules.wfuzz_tool_name, command_line=[ - '-w', '/usr/share/seclists/Discovery/DNS/subdomains-top1million-110000.txt', + '-w', 'subdomains-top1million-110000.txt', '--hc', '404', '-f', 'wfuzz-vhost-8080-shadycompass.test.json,json', '-t', '1', '-s', '12', @@ -69,7 +69,7 @@ def test_wfuzz_ratelimit(self): category=ToolCategory.virtualhost_scanner, name=WfuzzRules.wfuzz_tool_name, command_line=[ - '-w', '/usr/share/seclists/Discovery/DNS/subdomains-top1million-110000.txt', + '-w', 'subdomains-top1million-110000.txt', '--hc', '404', '-f', 'wfuzz-vhost-443-shadycompass.test.json,json', '-t', '1', '-s', '12', @@ -80,7 +80,7 @@ def test_wfuzz_ratelimit(self): category=ToolCategory.virtualhost_scanner, name=WfuzzRules.wfuzz_tool_name, command_line=[ - '-w', '/usr/share/seclists/Discovery/DNS/subdomains-top1million-110000.txt', + '-w', 'subdomains-top1million-110000.txt', '--hc', '404', '-f', 'wfuzz-vhost-8080-shadycompass.test.json,json', 'http://FUZZ.shadycompass.test:8080/' @@ -90,9 +90,34 @@ def test_wfuzz_ratelimit(self): category=ToolCategory.virtualhost_scanner, name=WfuzzRules.wfuzz_tool_name, command_line=[ - '-w', '/usr/share/seclists/Discovery/DNS/subdomains-top1million-110000.txt', + '-w', 'subdomains-top1million-110000.txt', '--hc', '404', '-f', 'wfuzz-vhost-443-shadycompass.test.json,json', 'https://FUZZ.shadycompass.test:443/' ], ), self.engine) + + def test_wfuzz_wordlist(self): + self.engine.config_set(SECTION_TOOLS, ToolCategory.virtualhost_scanner, WfuzzRules.wfuzz_tool_name, True) + self.engine.config_set(SECTION_WORDLISTS, OPTION_WORDLIST_SUBDOMAIN, 'subdomains-top1million-5000.txt', True) + self.engine.run() + assertFactIn(ToolRecommended( + category=ToolCategory.virtualhost_scanner, + name=WfuzzRules.wfuzz_tool_name, + command_line=[ + '-w', 'subdomains-top1million-5000.txt', + '--hc', '404', + '-f', 'wfuzz-vhost-8080-shadycompass.test.json,json', + 'http://FUZZ.shadycompass.test:8080/', + ], + ), self.engine) + assertFactIn(ToolRecommended( + category=ToolCategory.virtualhost_scanner, + name=WfuzzRules.wfuzz_tool_name, + command_line=[ + '-w', 'subdomains-top1million-5000.txt', + '--hc', '404', + '-f', 'wfuzz-vhost-443-shadycompass.test.json,json', + 'https://FUZZ.shadycompass.test:443/', + ], + ), self.engine) diff --git a/tests/test_shadycompassops.py b/tests/test_shadycompassops.py index f85681b..8c5f8c5 100644 --- a/tests/test_shadycompassops.py +++ b/tests/test_shadycompassops.py @@ -11,7 +11,7 @@ NetbiosSessionService, DomainUdpIpService, Product, OSTYPE_WINDOWS, HttpUrl, ImapService, TargetDomain, Username, \ EmailAddress, HostnameIPv6Resolution, VirtualHostname from shadycompass.rules.port_scanner.nmap import NmapRules -from tests.tests import assertFactIn, assertFactNotIn +from tests.tests import assertFactIn, assertFactNotIn, assertFactsEqual class OutputCapture: @@ -351,3 +351,9 @@ def test_config_ratelimit(self): command_line=['--top-ports=100', '-sV', '-sC', '-oN', 'nmap-tcp-100.txt', '-oX', 'nmap-tcp-100.xml', '--max-rate', '5', '$IP'], ), self.ops.engine) + + def test_reload(self): + self.ops.refresh() + facts_copy = list(self.ops.engine.facts.values()) + self.ops.reload() + assertFactsEqual(facts_copy, self.ops.engine) diff --git a/tests/tests.py b/tests/tests.py index e766e17..f1e5949 100644 --- a/tests/tests.py +++ b/tests/tests.py @@ -55,3 +55,22 @@ def assertFactNotIn(fact: Fact, facts_source): for f1 in facts: if _is_match(fact, f1): raise AssertionError(f"{repr(fact)} found in:\n{facts_str(_get_facts_by_type(fact, facts))}") + + +def assertFactsEqual(expected: list[Fact], facts_source): + if isinstance(facts_source, KnowledgeEngine): + actual = list(facts_source.facts.values()) + else: + actual = list(facts_source) + left = expected.copy() + right = actual.copy() + for left_fact in left.copy(): + for right_idx, right_fact in enumerate(right): + if left_fact.as_dict() == right_fact.as_dict(): + right.pop(right_idx) + left.remove(left_fact) + break + + if left or right: + raise AssertionError( + f"expected facts != actual facts\nExpected, not in actual:\n{facts_str(left)}\nActual, not in expected:\n{facts_str(right)}")