diff --git a/nettacker/api/core.py b/nettacker/api/core.py index b86905bd3..6e5bf5e95 100644 --- a/nettacker/api/core.py +++ b/nettacker/api/core.py @@ -213,22 +213,83 @@ def profiles(): Returns: HTML content or available profiles """ - res = "" - for profile in sorted(Nettacker.load_profiles().keys()): - label = ( - "success" - if (profile == "scan") - else "warning" - if (profile == "brute") - else "danger" - if (profile == "vulnerability") - else "default" - ) + all_profiles = Nettacker.load_profiles() + if "all" in all_profiles: + del all_profiles["all"] + if "..." in all_profiles: + del all_profiles["..."] + + categories = { + "scan": { + "title": _("scan_modules_title"), + "desc": _("scan_modules_desc"), + "label": "success", + "profiles": [], + }, + "brute": { + "title": _("brute_modules_title"), + "desc": _("brute_modules_desc"), + "label": "warning", + "profiles": [], + }, + "vuln": { + "title": _("vuln_modules_title"), + "desc": _("vuln_modules_desc"), + "label": "danger", + "profiles": [], + }, + } + + for profile in sorted(all_profiles.keys()): + modules = all_profiles[profile] + cats = set(m.split("_")[-1] for m in modules) + + for cat in cats: + if cat in categories: + categories[cat]["profiles"].append(profile) + elif cat == "vulnerability" or cat == "vuln": + categories["vuln"]["profiles"].append(profile) + + # Dedup and sort + for cat in categories: + categories[cat]["profiles"] = sorted(list(set(categories[cat]["profiles"]))) + + res = """ +
+ """ + for cat_name, cat_info in categories.items(): res += """ -      """.format( - profile, label +
+
+

+ + {2} + {3} + +

+
+
+
+ """.format( + cat_name, cat_info["label"], cat_info["title"], cat_info["desc"], _("select_all") ) + + for profile in cat_info["profiles"]: + label_type = cat_info["label"] + res += """ +       + """.format(profile, cat_name, label_type, ",".join(all_profiles[profile])) + + res += """ +
+
+
+ """ + res += "
" return res @@ -240,29 +301,71 @@ def scan_methods(): HTML content or available modules """ methods = Nettacker.load_modules() - methods.pop("all") - res = "" - for sm in methods.keys(): - label = ( - "success" - if sm.endswith("_scan") - else "warning" - if sm.endswith("_brute") - else "danger" - if sm.endswith("_vuln") - else "default" - ) - profile = ( - "scan" - if sm.endswith("_scan") - else "brute" - if sm.endswith("_brute") - else "vuln" - if sm.endswith("_vuln") - else "default" - ) - res += """     """.format( - sm, label, profile + if "all" in methods: + methods.pop("all") + + categories = { + "scan": { + "title": _("scan_modules_title"), + "desc": _("scan_modules_desc"), + "label": "success", + "modules": [], + }, + "brute": { + "title": _("brute_modules_title"), + "desc": _("brute_modules_desc"), + "label": "warning", + "modules": [], + }, + "vuln": { + "title": _("vuln_modules_title"), + "desc": _("vuln_modules_desc"), + "label": "danger", + "modules": [], + }, + } + + for sm in sorted(methods.keys()): + cat = sm.split("_")[-1] + if cat in categories: + categories[cat]["modules"].append(sm) + elif cat == "vulnerability": + categories["vuln"]["modules"].append(sm) + + res = """ +
+ """ + for cat_name, cat_info in categories.items(): + res += """ +
+
+

+ + {2} + {3} + +

+
+
+
+ """.format( + cat_name, cat_info["label"], cat_info["title"], cat_info["desc"], _("select_all") ) + + for module in cat_info["modules"]: + label_type = cat_info["label"] + res += """ +       + """.format(module, cat_name, label_type) + + res += """ +
+
+
+ """ + res += "
" return res diff --git a/nettacker/locale/en.yaml b/nettacker/locale/en.yaml index c3acde483..8782971e2 100644 --- a/nettacker/locale/en.yaml +++ b/nettacker/locale/en.yaml @@ -9,6 +9,13 @@ API_invalid: invalid API key API_key: " * API is accessible from https://nettacker-api.z3r0d4y.com:{0}/ via API Key: {1}" API_options: API options API_port: API port number +scan_modules_title: Scan Modules +scan_modules_desc: (Information Gathering and Reconnaissance) +brute_modules_title: Brute Force Modules +brute_modules_desc: (Authentication Attacks) +vuln_modules_title: Vulnerability Modules +vuln_modules_desc: (Exploit and Vulnerability Detection) +select_all: Select all Method: Method skip_service_discovery: skip service discovery before scan and enforce all modules to scan anyway no_live_service_found: no any live service found to scan. diff --git a/nettacker/web/static/js/main.js b/nettacker/web/static/js/main.js index 0ee398b36..fde1b1d7e 100644 --- a/nettacker/web/static/js/main.js +++ b/nettacker/web/static/js/main.js @@ -350,20 +350,30 @@ $(document).ready(function () { // profiles var p = []; var n = 0; - $("#profiles input:checked").each(function () { - if (this.id !== "all_profiles") { + $("#profiles input[type='checkbox']:checked").each(function () { + if ( + this.id !== "all_profiles" && + !$(this).hasClass("check-all-category") + ) { p[n] = this.id; n += 1; } }); + // Deduplicate profiles as they might appear in multiple categories + p = Array.from(new Set(p)); var profiles = p.join(","); // scan_methods n = 0; sm = []; - $("#selected_modules input:checked").each(function () { - sm[n] = this.id; - n += 1; + $("#selected_modules input[type='checkbox']:checked").each(function () { + if ( + this.id !== "all" && + !$(this).hasClass("check-all-sm-category") + ) { + sm[n] = this.id; + n += 1; + } }); var selected_modules = sm.join(","); // language @@ -666,48 +676,137 @@ $(document).ready(function () { $(".checkbox").prop("checked", $(this).prop("checked")); }); - $(".checkbox-brute").click(function () { - $(".checkbox-brute-module").prop("checked", $(this).prop("checked")); + $(document).on("change", "#profiles input[type='checkbox']", function () { + if ($(this).hasClass("check-all-category") || this.id === "all_profiles") { + return; + } + var modules = $(this).data("modules").split(","); + var isChecked = $(this).prop("checked"); + for (var i = 0; i < modules.length; i++) { + var moduleId = modules[i]; + if (isChecked) { + $("#" + moduleId).prop("checked", true); + } else { + // Only uncheck if no other checked profile includes this module + var stillNeeded = false; + $("#profiles input[type='checkbox']:checked").each(function () { + if ( + this.id !== "all_profiles" && + !$(this).hasClass("check-all-category") + ) { + var otherModules = $(this).data("modules").split(","); + if (otherModules.indexOf(moduleId) !== -1) { + stillNeeded = true; + return false; + } + } + }); + if (!stillNeeded) { + $("#" + moduleId).prop("checked", false); + } + } + } }); - $(".checkbox-scan").click(function () { - $(".checkbox-scan-module").prop("checked", $(this).prop("checked")); + $(".checkbox-brute-profile").click(function () { + if (this.id === "brute") { + $(".checkbox-sm-brute-module").prop("checked", $(this).prop("checked")); + } }); - $(".checkbox-vulnerability").click(function () { - $(".checkbox-vuln-module").prop("checked", $(this).prop("checked")); + $(".checkbox-scan-profile").click(function () { + if (this.id === "scan") { + $(".checkbox-sm-scan-module").prop("checked", $(this).prop("checked")); + } + }); + + $(".checkbox-vuln-profile").click(function () { + if (this.id === "vulnerability" || this.id === "vuln") { + $(".checkbox-sm-vuln-module").prop("checked", $(this).prop("checked")); + } }); $(".check-all-profiles").click(function () { - $("#profiles input[type='checkbox']").not(this).prop("checked", $(this).prop("checked")); + var isChecked = $(this).prop("checked"); + $("#profiles input[type='checkbox']") + .not(this) + .not(".check-all-category") + .prop("checked", isChecked) + .trigger("change"); + $(".check-all-category").prop("checked", isChecked); + }); + + $(document).on("change", ".check-all-category", function () { + var category = $(this).data("category"); + var isChecked = $(this).prop("checked"); + $(".checkbox-" + category + "-profile") + .prop("checked", isChecked) + .trigger("change"); + }); + + $(document).on("show.bs.collapse", "#profile_accordion", function (e) { + $(e.target) + .prev(".panel-heading") + .find(".fa") + .removeClass("fa-chevron-right") + .addClass("fa-chevron-down"); + }); + + $(document).on("hide.bs.collapse", "#profile_accordion", function (e) { + $(e.target) + .prev(".panel-heading") + .find(".fa") + .removeClass("fa-chevron-down") + .addClass("fa-chevron-right"); }); $(".check-all-scans").click(function () { - $(".checkbox-brute-module").prop("checked", $(this).prop("checked")); - $(".checkbox-scan-module").prop("checked", $(this).prop("checked")); - $(".checkbox-vuln-module").prop("checked", $(this).prop("checked")); + $("#selected_modules input[type='checkbox']").not(this).prop("checked", $(this).prop("checked")); + $(".check-all-sm-category").prop("checked", $(this).prop("checked")); + }); + + $(document).on("change", ".check-all-sm-category", function () { + var category = $(this).data("category"); + $(".checkbox-sm-" + category + "-module").prop("checked", $(this).prop("checked")); + }); + + $(document).on("show.bs.collapse", "#scan_methods_accordion", function (e) { + $(e.target) + .prev(".panel-heading") + .find(".fa") + .removeClass("fa-chevron-right") + .addClass("fa-chevron-down"); + }); + + $(document).on("hide.bs.collapse", "#scan_methods_accordion", function (e) { + $(e.target) + .prev(".panel-heading") + .find(".fa") + .removeClass("fa-chevron-down") + .addClass("fa-chevron-right"); }); - $(".checkbox-vuln-module").click(function () { + $(document).on("click", ".checkbox-sm-vuln-module", function () { if (!$(this).is(":checked")) { $(".checkAll").prop("checked", false); - $(".checkbox-vulnerability").prop("checked", false); + $("#vulnerability").prop("checked", false); + $("#vuln").prop("checked", false); $(".check-all-scans").prop("checked", false); } }); - $(".checkbox-scan-module").click(function () { + $(document).on("click", ".checkbox-sm-scan-module", function () { if (!$(this).is(":checked")) { $(".checkAll").prop("checked", false); - $(".checkbox-scan").prop("checked", false); + $("#scan").prop("checked", false); $(".check-all-scans").prop("checked", false); } }); - $(".checkbox-brute-module").click(function () { + $(document).on("click", ".checkbox-sm-brute-module", function () { if (!$(this).is(":checked")) { $(".checkAll").prop("checked", false); - $(".checkbox-brute").prop("checked", false); + $("#brute").prop("checked", false); $(".check-all-scans").prop("checked", false); } }); diff --git a/tests/api/test_core.py b/tests/api/test_core.py index 3f6d7183f..7d4ce533b 100644 --- a/tests/api/test_core.py +++ b/tests/api/test_core.py @@ -103,14 +103,16 @@ def test_graphs_empty(mock_graphs): @patch( "nettacker.core.app.Nettacker.load_profiles", - return_value={"scan": {}, "brute": {}, "custom": {}}, + return_value={"scan": ["a_scan"], "brute": ["b_brute"], "custom": ["c_vuln"]}, ) def test_profiles(mock_profiles): result = profiles() - assert "checkbox-scan" in result + assert "checkbox-scan-profile" in result assert 'label-success">scan' in result + assert "checkbox-brute-profile" in result assert 'label-warning">brute' in result - assert 'label-default">custom' in result + assert "checkbox-vuln-profile" in result + assert 'label-danger">custom' in result @patch( @@ -119,13 +121,13 @@ def test_profiles(mock_profiles): ) def test_scan_methods(mock_methods): result = scan_methods() - assert "checkbox-scan-module" in result + assert "checkbox-sm-scan-module" in result assert 'label-success">tcp_scan' in result - assert "checkbox-brute-module" in result + assert "checkbox-sm-brute-module" in result assert 'label-warning">ssh_brute' in result - assert "checkbox-vuln-module" in result + assert "checkbox-sm-vuln-module" in result assert 'label-danger">http_vuln' in result - assert "all" not in result + assert 'id="all"' not in result @patch("nettacker.core.messages.get_languages", return_value=["en", "fr", "es", "de"]) diff --git a/tests/api/test_reorganized_ui.py b/tests/api/test_reorganized_ui.py new file mode 100644 index 000000000..90baa7866 --- /dev/null +++ b/tests/api/test_reorganized_ui.py @@ -0,0 +1,64 @@ + +import pytest +from unittest.mock import patch +from nettacker.api.core import profiles, scan_methods + +@patch("nettacker.core.app.Nettacker.load_profiles") +def test_profiles_deduplication(mock_load_profiles): + # Setup: a profile that appears in both scan and brute categories + # Profiles are categorized by the suffix of the modules they contain. + # 'mixed_profile' has 'a_scan' and 'b_brute', so it should appear in both categories. + mock_load_profiles.return_value = { + "mixed_profile": ["mod1_scan", "mod2_brute"], + "only_scan": ["mod3_scan"] + } + + result = profiles() + + # Check if 'mixed_profile' appears in the scan category + assert 'id="mixed_profile"' in result + # Check if 'mixed_profile' is listed with its modules + assert 'data-modules="mod1_scan,mod2_brute"' in result + + # The HTML structure should have panels for scan, brute, and vuln + assert 'id="collapse_scan"' in result + assert 'id="collapse_brute"' in result + assert 'id="collapse_vuln"' in result + +@patch("nettacker.core.app.Nettacker.load_modules") +def test_scan_methods_categorization(mock_load_modules): + mock_load_modules.return_value = { + "ssh_brute": {}, + "ftp_scan": {}, + "heartbleed_vuln": {}, + "custom_vulnerability": {} + } + + result = scan_methods() + + # Check if modules are in their respective categories + assert 'id="ssh_brute"' in result + assert 'checkbox-sm-brute-module' in result + + assert 'id="ftp_scan"' in result + assert 'checkbox-sm-scan-module' in result + + assert 'id="heartbleed_vuln"' in result + assert 'checkbox-sm-vuln-module' in result + + # 'custom_vulnerability' should be mapped to 'vuln' category + assert 'id="custom_vulnerability"' in result + assert 'checkbox-sm-vuln-module' in result + +@patch("nettacker.core.app.Nettacker.load_profiles") +def test_profiles_removes_all_and_dots(mock_load_profiles): + mock_load_profiles.return_value = { + "all": ["mod1_scan"], + "...": ["mod2_scan"], + "valid": ["mod3_scan"] + } + + result = profiles() + assert 'id="all"' not in result + assert 'id="..."' not in result + assert 'id="valid"' in result