Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions docs/plugin_health_dashboard.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Plugin Health & Coverage Dashboard

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
- Plugin category distribution
- 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.
122 changes: 122 additions & 0 deletions scripts/plugin_health_dashboard.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import argparse
import json
from pathlib import Path


ROOT = Path(__file__).resolve().parents[1]
PLUGIN_ROOT = ROOT / "plugins"


def safe_relative_path(path, base):
try:
return str(path.relative_to(base))
except ValueError:
return str(path)


def discover_plugins(plugin_root=PLUGIN_ROOT):
plugins = []
plugin_root = Path(plugin_root)

if not plugin_root.exists():
return plugins

for metadata_file in plugin_root.rglob("metadata.json"):
plugin_dir = metadata_file.parent
parser_file = plugin_dir / "parser.py"

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", ""),
})

return sorted(plugins, key=lambda item: item["name"])


def build_report(plugins):
total = len(plugins)
with_parser = sum(1 for plugin in plugins if plugin["has_parser"])

categories = {}
for plugin in plugins:
category = plugin["category"]
categories[category] = categories.get(category, 0) + 1

return {
"summary": {
"total_plugins": total,
"plugins_with_parser": with_parser,
"plugins_without_parser": total - with_parser,
},
"categories": categories,
"plugins": plugins,
}


def format_markdown(report):
summary = report["summary"]

lines = [
"# Plugin Health & Coverage Report",
"",
"## Summary",
"",
f"- Total plugins: {summary['total_plugins']}",
f"- Plugins with parser.py: {summary['plugins_with_parser']}",
f"- Plugins without parser.py: {summary['plugins_without_parser']}",
"",
"## Categories",
"",
]

for category, count in sorted(report["categories"].items()):
lines.append(f"- {category}: {count}")

lines.extend([
"",
"## Plugin Details",
"",
"| Plugin | Category | Parser | Path |",
"|---|---|---|---|",
])

for plugin in report["plugins"]:
parser_status = "Yes" if plugin["has_parser"] else "No"
lines.append(
f"| {plugin['name']} | {plugin['category']} | {parser_status} | `{plugin['path']}` |"
)

return "\n".join(lines)


def main():
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__":
main()
100 changes: 100 additions & 0 deletions testing/test_plugin_health_dashboard.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import json
from pathlib import Path
import subprocess
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


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
Loading