From 31bd522e19816745428e2082eb0d27d59ba6242c Mon Sep 17 00:00:00 2001 From: upasana-2006 Date: Sun, 31 May 2026 05:23:02 +0000 Subject: [PATCH 1/3] feat: add plugin health coverage dashboard --- docs/plugin_health_dashboard.md | 16 +++ scripts/plugin_health_dashboard.py | 151 ++++++++++++++++++++++++++ tests/test_plugin_health_dashboard.py | 38 +++++++ 3 files changed, 205 insertions(+) create mode 100644 docs/plugin_health_dashboard.md create mode 100644 scripts/plugin_health_dashboard.py create mode 100644 tests/test_plugin_health_dashboard.py diff --git a/docs/plugin_health_dashboard.md b/docs/plugin_health_dashboard.md new file mode 100644 index 00000000..88ae7adb --- /dev/null +++ b/docs/plugin_health_dashboard.md @@ -0,0 +1,16 @@ +# Plugin Health & Coverage Dashboard + +The Plugin Health & Coverage Dashboard is a developer utility that analyzes SecuScan plugins and generates coverage reports. + +## What it checks + +- Total plugin count +- Parser availability +- Test coverage presence +- Plugin category distribution +- Plugin file paths + +## How to run + +```bash +python scripts/plugin_health_dashboard.py diff --git a/scripts/plugin_health_dashboard.py b/scripts/plugin_health_dashboard.py new file mode 100644 index 00000000..4d193475 --- /dev/null +++ b/scripts/plugin_health_dashboard.py @@ -0,0 +1,151 @@ +import json +from pathlib import Path +from datetime import datetime + + +ROOT = Path(__file__).resolve().parents[1] +PLUGIN_DIR = ROOT / "plugins" +TEST_DIR = ROOT / "tests" +REPORT_DIR = ROOT / "reports" + + +def discover_plugins(): + plugins = [] + + if not PLUGIN_DIR.exists(): + return plugins + + for file in PLUGIN_DIR.rglob("*.py"): + if file.name.startswith("__"): + continue + + content = file.read_text(encoding="utf-8", errors="ignore") + relative_path = file.relative_to(ROOT) + + plugin_info = { + "name": file.stem, + "path": str(relative_path), + "has_parser": "parse" in content.lower(), + "has_tests": has_test_for_plugin(file.stem), + "category": infer_category(file), + } + + plugins.append(plugin_info) + + return plugins + + +def has_test_for_plugin(plugin_name): + if not TEST_DIR.exists(): + return False + + for test_file in TEST_DIR.rglob("test_*.py"): + content = test_file.read_text(encoding="utf-8", errors="ignore").lower() + if plugin_name.lower() in content: + return True + + return False + + +def infer_category(file_path): + parts = file_path.parts + if "plugins" in parts: + index = parts.index("plugins") + if len(parts) > index + 1: + return parts[index + 1] + return "uncategorized" + + +def build_report(plugins): + total = len(plugins) + with_parsers = sum(1 for plugin in plugins if plugin["has_parser"]) + with_tests = sum(1 for plugin in plugins if plugin["has_tests"]) + + categories = {} + for plugin in plugins: + categories.setdefault(plugin["category"], 0) + categories[plugin["category"]] += 1 + + return { + "generated_at": datetime.utcnow().isoformat() + "Z", + "summary": { + "total_plugins": total, + "plugins_with_parsers": with_parsers, + "plugins_without_parsers": total - with_parsers, + "plugins_with_tests": with_tests, + "plugins_without_tests": total - with_tests, + }, + "categories": categories, + "plugins": plugins, + } + + +def write_json_report(report): + REPORT_DIR.mkdir(exist_ok=True) + output_path = REPORT_DIR / "plugin_health_report.json" + + with output_path.open("w", encoding="utf-8") as file: + json.dump(report, file, indent=2) + + return output_path + + +def write_markdown_report(report): + REPORT_DIR.mkdir(exist_ok=True) + output_path = REPORT_DIR / "plugin_health_report.md" + + summary = report["summary"] + + lines = [ + "# Plugin Health & Coverage Report", + "", + f"Generated at: `{report['generated_at']}`", + "", + "## Summary", + "", + f"- Total plugins: {summary['total_plugins']}", + f"- Plugins with parsers: {summary['plugins_with_parsers']}", + f"- Plugins without parsers: {summary['plugins_without_parsers']}", + f"- Plugins with tests: {summary['plugins_with_tests']}", + f"- Plugins without tests: {summary['plugins_without_tests']}", + "", + "## Category Distribution", + "", + ] + + for category, count in sorted(report["categories"].items()): + lines.append(f"- {category}: {count}") + + lines.extend([ + "", + "## Plugin Details", + "", + "| Plugin | Category | Parser | Tests | Path |", + "|---|---|---|---|---|", + ]) + + for plugin in sorted(report["plugins"], key=lambda item: item["name"]): + parser_status = "Yes" if plugin["has_parser"] else "No" + test_status = "Yes" if plugin["has_tests"] else "No" + + lines.append( + f"| {plugin['name']} | {plugin['category']} | {parser_status} | {test_status} | `{plugin['path']}` |" + ) + + output_path.write_text("\n".join(lines), encoding="utf-8") + return output_path + + +def main(): + plugins = discover_plugins() + report = build_report(plugins) + + json_path = write_json_report(report) + markdown_path = write_markdown_report(report) + + print(f"Plugin health JSON report generated: {json_path}") + print(f"Plugin health Markdown report generated: {markdown_path}") + + +if __name__ == "__main__": + main() diff --git a/tests/test_plugin_health_dashboard.py b/tests/test_plugin_health_dashboard.py new file mode 100644 index 00000000..b5d62beb --- /dev/null +++ b/tests/test_plugin_health_dashboard.py @@ -0,0 +1,38 @@ +import json +from pathlib import Path +import sys + + +ROOT = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(ROOT)) + +from scripts.plugin_health_dashboard import build_report + + +def test_build_report_summary_counts(): + plugins = [ + { + "name": "nmap", + "path": "plugins/network/nmap.py", + "has_parser": True, + "has_tests": True, + "category": "network", + }, + { + "name": "whois", + "path": "plugins/recon/whois.py", + "has_parser": False, + "has_tests": False, + "category": "recon", + }, + ] + + report = build_report(plugins) + + assert report["summary"]["total_plugins"] == 2 + assert report["summary"]["plugins_with_parsers"] == 1 + assert report["summary"]["plugins_without_parsers"] == 1 + assert report["summary"]["plugins_with_tests"] == 1 + assert report["summary"]["plugins_without_tests"] == 1 + assert report["categories"]["network"] == 1 + assert report["categories"]["recon"] == 1 From 67b90ad82d660b4bafc16a5be68c29878d5b1558 Mon Sep 17 00:00:00 2001 From: upasana-2006 Date: Sun, 31 May 2026 14:07:56 +0000 Subject: [PATCH 2/3] fix: align plugin health dashboard with metadata model --- docs/plugin_health_dashboard.md | 34 +++++- scripts/plugin_health_dashboard.py | 147 ++++++++++-------------- testing/test_plugin_health_dashboard.py | 72 ++++++++++++ 3 files changed, 162 insertions(+), 91 deletions(-) create mode 100644 testing/test_plugin_health_dashboard.py diff --git a/docs/plugin_health_dashboard.md b/docs/plugin_health_dashboard.md index 88ae7adb..c8d536ca 100644 --- a/docs/plugin_health_dashboard.md +++ b/docs/plugin_health_dashboard.md @@ -1,16 +1,44 @@ # Plugin Health & Coverage Dashboard -The Plugin Health & Coverage Dashboard is a developer utility that analyzes SecuScan plugins and generates coverage reports. +The Plugin Health & Coverage Dashboard is a developer utility for inspecting SecuScan plugins. + +It follows the repository plugin model by treating plugin directories containing `metadata.json` as the source of truth and checking whether each plugin has a corresponding `parser.py`. ## What it checks - Total plugin count - Parser availability -- Test coverage presence - Plugin category distribution -- Plugin file paths +- Plugin metadata path +- Plugin directory path ## How to run +Print a Markdown report to the terminal: + ```bash python scripts/plugin_health_dashboard.py +``` + +Print JSON output: + +```bash +python scripts/plugin_health_dashboard.py --format json +``` + +Write Markdown output to a file: + +```bash +python scripts/plugin_health_dashboard.py --output plugin_health_report.md +``` + +Write JSON output to a file: + +```bash +python scripts/plugin_health_dashboard.py --format json --output plugin_health_report.json +``` + +## Notes + +The script does not write generated reports by default. Reports are printed to stdout unless an explicit `--output` path is provided. +``` diff --git a/scripts/plugin_health_dashboard.py b/scripts/plugin_health_dashboard.py index 4d193475..fc0db74f 100644 --- a/scripts/plugin_health_dashboard.py +++ b/scripts/plugin_health_dashboard.py @@ -1,115 +1,81 @@ +import argparse import json from pathlib import Path -from datetime import datetime ROOT = Path(__file__).resolve().parents[1] -PLUGIN_DIR = ROOT / "plugins" -TEST_DIR = ROOT / "tests" -REPORT_DIR = ROOT / "reports" +PLUGIN_ROOT = ROOT / "plugins" -def discover_plugins(): - plugins = [] - - if not PLUGIN_DIR.exists(): - return plugins - - for file in PLUGIN_DIR.rglob("*.py"): - if file.name.startswith("__"): - continue - - content = file.read_text(encoding="utf-8", errors="ignore") - relative_path = file.relative_to(ROOT) +def safe_relative_path(path, base): + try: + return str(path.relative_to(base)) + except ValueError: + return str(path) - plugin_info = { - "name": file.stem, - "path": str(relative_path), - "has_parser": "parse" in content.lower(), - "has_tests": has_test_for_plugin(file.stem), - "category": infer_category(file), - } - - plugins.append(plugin_info) - - return plugins +def discover_plugins(plugin_root=PLUGIN_ROOT): + plugins = [] + plugin_root = Path(plugin_root) -def has_test_for_plugin(plugin_name): - if not TEST_DIR.exists(): - return False + if not plugin_root.exists(): + return plugins - for test_file in TEST_DIR.rglob("test_*.py"): - content = test_file.read_text(encoding="utf-8", errors="ignore").lower() - if plugin_name.lower() in content: - return True + for metadata_file in plugin_root.rglob("metadata.json"): + plugin_dir = metadata_file.parent + parser_file = plugin_dir / "parser.py" - return False + try: + metadata = json.loads(metadata_file.read_text(encoding="utf-8")) + except json.JSONDecodeError: + metadata = {} + plugins.append({ + "name": metadata.get("name", plugin_dir.name), + "path": safe_relative_path(plugin_dir, plugin_root.parent), + "metadata_path": safe_relative_path(metadata_file, plugin_root.parent), + "has_metadata": True, + "has_parser": parser_file.exists(), + "category": metadata.get("category", "uncategorized"), + "description": metadata.get("description", ""), + }) -def infer_category(file_path): - parts = file_path.parts - if "plugins" in parts: - index = parts.index("plugins") - if len(parts) > index + 1: - return parts[index + 1] - return "uncategorized" + return sorted(plugins, key=lambda item: item["name"]) def build_report(plugins): total = len(plugins) - with_parsers = sum(1 for plugin in plugins if plugin["has_parser"]) - with_tests = sum(1 for plugin in plugins if plugin["has_tests"]) + with_parser = sum(1 for plugin in plugins if plugin["has_parser"]) categories = {} for plugin in plugins: - categories.setdefault(plugin["category"], 0) - categories[plugin["category"]] += 1 + category = plugin["category"] + categories[category] = categories.get(category, 0) + 1 return { - "generated_at": datetime.utcnow().isoformat() + "Z", "summary": { "total_plugins": total, - "plugins_with_parsers": with_parsers, - "plugins_without_parsers": total - with_parsers, - "plugins_with_tests": with_tests, - "plugins_without_tests": total - with_tests, + "plugins_with_parser": with_parser, + "plugins_without_parser": total - with_parser, }, "categories": categories, "plugins": plugins, } -def write_json_report(report): - REPORT_DIR.mkdir(exist_ok=True) - output_path = REPORT_DIR / "plugin_health_report.json" - - with output_path.open("w", encoding="utf-8") as file: - json.dump(report, file, indent=2) - - return output_path - - -def write_markdown_report(report): - REPORT_DIR.mkdir(exist_ok=True) - output_path = REPORT_DIR / "plugin_health_report.md" - +def format_markdown(report): summary = report["summary"] lines = [ "# Plugin Health & Coverage Report", "", - f"Generated at: `{report['generated_at']}`", - "", "## Summary", "", f"- Total plugins: {summary['total_plugins']}", - f"- Plugins with parsers: {summary['plugins_with_parsers']}", - f"- Plugins without parsers: {summary['plugins_without_parsers']}", - f"- Plugins with tests: {summary['plugins_with_tests']}", - f"- Plugins without tests: {summary['plugins_without_tests']}", + f"- Plugins with parser.py: {summary['plugins_with_parser']}", + f"- Plugins without parser.py: {summary['plugins_without_parser']}", "", - "## Category Distribution", + "## Categories", "", ] @@ -120,31 +86,36 @@ def write_markdown_report(report): "", "## Plugin Details", "", - "| Plugin | Category | Parser | Tests | Path |", - "|---|---|---|---|---|", + "| Plugin | Category | Parser | Path |", + "|---|---|---|---|", ]) - for plugin in sorted(report["plugins"], key=lambda item: item["name"]): + for plugin in report["plugins"]: parser_status = "Yes" if plugin["has_parser"] else "No" - test_status = "Yes" if plugin["has_tests"] else "No" - lines.append( - f"| {plugin['name']} | {plugin['category']} | {parser_status} | {test_status} | `{plugin['path']}` |" + f"| {plugin['name']} | {plugin['category']} | {parser_status} | `{plugin['path']}` |" ) - output_path.write_text("\n".join(lines), encoding="utf-8") - return output_path + return "\n".join(lines) def main(): - plugins = discover_plugins() - report = build_report(plugins) - - json_path = write_json_report(report) - markdown_path = write_markdown_report(report) - - print(f"Plugin health JSON report generated: {json_path}") - print(f"Plugin health Markdown report generated: {markdown_path}") + parser = argparse.ArgumentParser( + description="Generate plugin health report from metadata.json/parser.py plugin directories." + ) + parser.add_argument("--format", choices=["json", "markdown"], default="markdown") + parser.add_argument("--output", type=Path) + + args = parser.parse_args() + + report = build_report(discover_plugins()) + content = json.dumps(report, indent=2) if args.format == "json" else format_markdown(report) + + if args.output: + args.output.parent.mkdir(parents=True, exist_ok=True) + args.output.write_text(content, encoding="utf-8") + else: + print(content) if __name__ == "__main__": diff --git a/testing/test_plugin_health_dashboard.py b/testing/test_plugin_health_dashboard.py new file mode 100644 index 00000000..ffddce8c --- /dev/null +++ b/testing/test_plugin_health_dashboard.py @@ -0,0 +1,72 @@ +import json +from pathlib import Path +import sys + + +ROOT = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(ROOT)) + +from scripts.plugin_health_dashboard import discover_plugins, build_report, format_markdown + + +def test_discover_plugins_uses_metadata_and_parser_directories(tmp_path): + plugin_dir = tmp_path / "plugins" / "network" / "demo_plugin" + plugin_dir.mkdir(parents=True) + + metadata = { + "name": "demo_plugin", + "category": "network", + "description": "Demo plugin", + } + + (plugin_dir / "metadata.json").write_text(json.dumps(metadata), encoding="utf-8") + (plugin_dir / "parser.py").write_text("def parse(): pass", encoding="utf-8") + + plugins = discover_plugins(tmp_path / "plugins") + + assert len(plugins) == 1 + assert plugins[0]["name"] == "demo_plugin" + assert plugins[0]["category"] == "network" + assert plugins[0]["has_parser"] is True + assert plugins[0]["has_metadata"] is True + + +def test_build_report_counts_parser_coverage(): + plugins = [ + {"name": "plugin_one", "category": "network", "has_parser": True}, + {"name": "plugin_two", "category": "web", "has_parser": False}, + ] + + report = build_report(plugins) + + assert report["summary"]["total_plugins"] == 2 + assert report["summary"]["plugins_with_parser"] == 1 + assert report["summary"]["plugins_without_parser"] == 1 + assert report["categories"]["network"] == 1 + assert report["categories"]["web"] == 1 + + +def test_format_markdown_contains_plugin_details(): + report = { + "summary": { + "total_plugins": 1, + "plugins_with_parser": 1, + "plugins_without_parser": 0, + }, + "categories": {"network": 1}, + "plugins": [ + { + "name": "demo_plugin", + "category": "network", + "has_parser": True, + "path": "plugins/network/demo_plugin", + } + ], + } + + markdown = format_markdown(report) + + assert "Plugin Health & Coverage Report" in markdown + assert "demo_plugin" in markdown + assert "network" in markdown + assert "Yes" in markdown From 7d1405c71fe4f71c3fa020fb4de7a278400235af Mon Sep 17 00:00:00 2001 From: upasana-2006 Date: Sun, 31 May 2026 16:40:38 +0000 Subject: [PATCH 3/3] fix: clean up plugin dashboard tests and docs --- docs/plugin_health_dashboard.md | 1 - testing/test_plugin_health_dashboard.py | 28 ++++++++++++++++++ tests/test_plugin_health_dashboard.py | 38 ------------------------- 3 files changed, 28 insertions(+), 39 deletions(-) delete mode 100644 tests/test_plugin_health_dashboard.py diff --git a/docs/plugin_health_dashboard.md b/docs/plugin_health_dashboard.md index c8d536ca..81d2c06c 100644 --- a/docs/plugin_health_dashboard.md +++ b/docs/plugin_health_dashboard.md @@ -41,4 +41,3 @@ python scripts/plugin_health_dashboard.py --format json --output plugin_health_r ## Notes The script does not write generated reports by default. Reports are printed to stdout unless an explicit `--output` path is provided. -``` diff --git a/testing/test_plugin_health_dashboard.py b/testing/test_plugin_health_dashboard.py index ffddce8c..41560d7d 100644 --- a/testing/test_plugin_health_dashboard.py +++ b/testing/test_plugin_health_dashboard.py @@ -1,5 +1,6 @@ import json from pathlib import Path +import subprocess import sys @@ -70,3 +71,30 @@ def test_format_markdown_contains_plugin_details(): assert "demo_plugin" in markdown assert "network" in markdown assert "Yes" in markdown + + +def test_cli_output_path_creates_parent_directories_and_writes_file(tmp_path): + output_path = tmp_path / "nested" / "reports" / "plugin_health_report.json" + + result = subprocess.run( + [ + sys.executable, + "scripts/plugin_health_dashboard.py", + "--format", + "json", + "--output", + str(output_path), + ], + cwd=ROOT, + capture_output=True, + text=True, + check=True, + ) + + assert result.stdout == "" + assert output_path.exists() + + report = json.loads(output_path.read_text(encoding="utf-8")) + assert "summary" in report + assert "plugins" in report + assert "categories" in report diff --git a/tests/test_plugin_health_dashboard.py b/tests/test_plugin_health_dashboard.py deleted file mode 100644 index b5d62beb..00000000 --- a/tests/test_plugin_health_dashboard.py +++ /dev/null @@ -1,38 +0,0 @@ -import json -from pathlib import Path -import sys - - -ROOT = Path(__file__).resolve().parents[1] -sys.path.insert(0, str(ROOT)) - -from scripts.plugin_health_dashboard import build_report - - -def test_build_report_summary_counts(): - plugins = [ - { - "name": "nmap", - "path": "plugins/network/nmap.py", - "has_parser": True, - "has_tests": True, - "category": "network", - }, - { - "name": "whois", - "path": "plugins/recon/whois.py", - "has_parser": False, - "has_tests": False, - "category": "recon", - }, - ] - - report = build_report(plugins) - - assert report["summary"]["total_plugins"] == 2 - assert report["summary"]["plugins_with_parsers"] == 1 - assert report["summary"]["plugins_without_parsers"] == 1 - assert report["summary"]["plugins_with_tests"] == 1 - assert report["summary"]["plugins_without_tests"] == 1 - assert report["categories"]["network"] == 1 - assert report["categories"]["recon"] == 1