diff --git a/src/attackmate/executors/common/regexexecutor.py b/src/attackmate/executors/common/regexexecutor.py index 29c5a0f02..664303d20 100644 --- a/src/attackmate/executors/common/regexexecutor.py +++ b/src/attackmate/executors/common/regexexecutor.py @@ -43,6 +43,11 @@ def register_outputvars(self, outputvars: dict, matches): if not matches: self.logger.debug('no match!') self.varstore.set_variable('REGEX_MATCHES_LIST', []) + # Clear the output variables so a stale value from a previous + # loop iteration doesn't produce a false positive when the + # current input has no match. + for k in outputvars: + self.varstore.set_variable(k, '') return for k, v in outputvars.items(): @@ -73,7 +78,7 @@ async def _exec_cmd(self, command: RegExCommand) -> Result: if m3 is not None and isinstance(m3, Match): self.forge_and_register_variables(command.output, m3.group()) else: - self.varstore.set_variable('REGEX_MATCHES_LIST', []) + self.forge_and_register_variables(command.output, None) if command.mode == 'sub': if command.replace: replaced = re.sub(command.cmd, command.replace, self.varstore.get_str(command.input)) diff --git a/test/units/test_regexexecutor.py b/test/units/test_regexexecutor.py index 085d5fc0d..1a91d44e6 100644 --- a/test/units/test_regexexecutor.py +++ b/test/units/test_regexexecutor.py @@ -129,7 +129,7 @@ async def test_exec_cmd_findall_no_match(self): output={'output_var': 'Found: $MATCH_0'}, ) await self.executor._exec_cmd(command) - assert 'output_var' not in self.varstore.variables + assert self.varstore.get_variable('output_var') == '' assert self.varstore.get_variable('REGEX_MATCHES_LIST') == [] @pytest.mark.asyncio @@ -156,7 +156,7 @@ async def test_exec_cmd_search_no_match(self): output={'output_var': 'Found: $MATCH_0'}, ) await self.executor._exec_cmd(command) - assert 'output_var' not in self.varstore.variables + assert self.varstore.get_variable('output_var') == '' assert self.varstore.get_variable('REGEX_MATCHES_LIST') == [] @pytest.mark.asyncio @@ -175,3 +175,20 @@ async def test_exec_cmd_sub_no_match(self): await self.executor._exec_cmd(command) assert self.varstore.get_variable('output_var') == 'Replaced: no matches here' assert self.varstore.get_variable('REGEX_MATCHES_LIST') == ['no matches here'] + + @pytest.mark.asyncio + async def test_exec_cmd_search_no_match_clears_stale_value(self): + # Regression: in a loop a previous iteration might set output + # variable. A following no-match must clear it, not leave the old value. + self.varstore.set_variable('PORT_STATUS', 'open') + self.varstore.set_variable('input_var', 'closed') + command = RegExCommand( + type='regex', + cmd='open', + mode='search', + input='input_var', + output={'PORT_STATUS': '$MATCH_0'}, + ) + await self.executor._exec_cmd(command) + assert self.varstore.get_variable('PORT_STATUS') == '' + assert self.varstore.get_variable('REGEX_MATCHES_LIST') == []