diff --git a/extensions/business/cybersec/red_mesh/constants.py b/extensions/business/cybersec/red_mesh/constants.py index 5086b460..adb16a81 100644 --- a/extensions/business/cybersec/red_mesh/constants.py +++ b/extensions/business/cybersec/red_mesh/constants.py @@ -342,7 +342,7 @@ def resolve_target_response_timeout(timeout_profile, standard_timeout): # The backend release, recorded in every JobConfig at launch so a report names # the scanner that produced it. `pentester_api_01.__VER__` must equal it (a test # guards the pair; the plugin loader reads the literal there). -REDMESH_BACKEND_VERSION = "0.13.0" +REDMESH_BACKEND_VERSION = "0.13.1" # ===================================================================== # Risk score computation diff --git a/extensions/business/cybersec/red_mesh/pentester_api_01.py b/extensions/business/cybersec/red_mesh/pentester_api_01.py index 132043f8..600ed348 100644 --- a/extensions/business/cybersec/red_mesh/pentester_api_01.py +++ b/extensions/business/cybersec/red_mesh/pentester_api_01.py @@ -572,7 +572,7 @@ def _run_manual_analysis_worker(work: _ManualAnalysisWork) -> _ManualAnalysisOut return _ManualAnalysisWorker(work).run() -__VER__ = '0.13.0' +__VER__ = '0.13.1' _CONFIG = { diff --git a/extensions/business/cybersec/red_mesh/tests/test_api.py b/extensions/business/cybersec/red_mesh/tests/test_api.py index 09b7ee89..e16a275e 100644 --- a/extensions/business/cybersec/red_mesh/tests/test_api.py +++ b/extensions/business/cybersec/red_mesh/tests/test_api.py @@ -390,9 +390,9 @@ def test_launch_records_the_backend_release_that_ran_the_scan(self): self._launch(plugin) from extensions.business.cybersec.red_mesh.models import JobConfig config_dict = plugin.r1fs.add_json.call_args_list[0][0][0] - self.assertEqual(config_dict["redmesh_release"], {"backend": "0.13.0"}) + self.assertEqual(config_dict["redmesh_release"], {"backend": "0.13.1"}) restored = JobConfig.from_dict(config_dict).to_dict() - self.assertEqual(restored["redmesh_release"], {"backend": "0.13.0"}) + self.assertEqual(restored["redmesh_release"], {"backend": "0.13.1"}) def test_launch_records_the_console_and_report_pipeline_it_was_sent(self): """RM-103 item 12: the release names all three deployables. Only the diff --git a/extensions/business/cybersec/red_mesh/tests/test_csrf_finding_identity.py b/extensions/business/cybersec/red_mesh/tests/test_csrf_finding_identity.py index 0155e012..fad9b798 100644 --- a/extensions/business/cybersec/red_mesh/tests/test_csrf_finding_identity.py +++ b/extensions/business/cybersec/red_mesh/tests/test_csrf_finding_identity.py @@ -92,6 +92,23 @@ def test_forms_posting_to_one_place_are_one_finding(self): sharing an id.""" self.assertEqual(len(self._run({"/": _page(_form(), _form())})), 1) + def test_path_parameters_are_dropped_per_segment_not_at_the_first_semicolon(self): + findings = self._run({"/": _page( + _form("/account;jsessionid=ABC/delete"), _form("/account;jsessionid=ABC/update"), + )}) + self.assertEqual(len({f["finding_id"] for f in findings}), 2) + self.assertEqual( + sorted(f["affected_assets"][0]["parameter"] for f in findings), + ["/account/delete", "/account/update"], + ) + + def test_a_session_id_in_a_middle_segment_does_not_move_the_id(self): + [clean] = self._run({"/": _page(_form("/a/b"))}) + for volatile in ("/a;jsessionid=X/b", "/a;jsessionid=Y/b;v=2"): + with self.subTest(action=volatile): + [finding] = self._run({"/": _page(_form(volatile))}) + self.assertEqual(finding["finding_id"], clean["finding_id"]) + def test_actions_differing_only_in_a_query_are_one_finding(self): findings = self._run({"/": _page(_form("/search?t=1"), _form("/search?t=2"))}) self.assertEqual(len(findings), 1) diff --git a/extensions/business/cybersec/red_mesh/worker/web/hardening.py b/extensions/business/cybersec/red_mesh/worker/web/hardening.py index 00b0d203..1b9a2d69 100644 --- a/extensions/business/cybersec/red_mesh/worker/web/hardening.py +++ b/extensions/business/cybersec/red_mesh/worker/web/hardening.py @@ -540,11 +540,14 @@ def _csrf_form_action(cls, form_html, path): @staticmethod def _csrf_identity_action(action, path): """The action as an identity input: entities decoded, then the query, - fragment and `;param` path parameters dropped, since a cache-buster or a - session id there would hand the finding a new id on every run. The raw - action stays in `evidence`.""" + fragment and each segment's `;param` path parameters dropped, since a + cache-buster or a session id there would hand the finding a new id on + every run. The segments after a `;param` stay: `/a;jsessionid=X/delete` + and `/a;jsessionid=X/update` are two endpoints. The raw action stays in + `evidence`.""" decoded = _html_unescape(action) - stable = _re.split(r"[?#;]", decoded, maxsplit=1)[0].strip() + stable = _re.split(r"[?#]", decoded, maxsplit=1)[0] + stable = _re.sub(r";[^/]*", "", stable).strip() return stable or path diff --git a/ver.py b/ver.py index 7096a618..67f9136e 100644 --- a/ver.py +++ b/ver.py @@ -1 +1 @@ -__VER__ = '2.10.480' +__VER__ = '2.10.481'