Contributing to VulnRadar

February 27, 2026 · View on GitHub

Thanks for your interest in contributing! VulnRadar is designed to be easy to extend.

Development Setup

# Clone your fork
git clone https://github.com/YOUR_USERNAME/VulnRadar.git
cd VulnRadar

# Install everything (runtime + dev deps + pre-commit hooks) in one command
make dev

# Or manually:
pip install -e ".[dev]"
pre-commit install

# Run the test suite
make test

# Run linting & formatting
make lint
make fmt

Project Structure

vulnradar/                 # Core package
├── cli.py                 # argparse entry points (main_etl, main_notify)
├── config.py              # Pydantic models (WatchlistConfig, etc.)
├── downloaders.py         # Sequential HTTP fetchers (requests + tenacity)
├── async_downloaders.py   # Parallel fetchers (aiohttp, used with --parallel)
├── parsers.py             # CVE JSON parsing, CVSS extraction, matching
├── enrichment.py          # KEV/EPSS/PatchThis/NVD merge → radar items
├── report.py              # Jinja2 Markdown report writer
├── state.py               # StateManager for alert deduplication
├── notifications/         # Strategy-pattern notification providers
│   ├── base.py            # Abstract NotificationProvider
│   ├── discord.py         # DiscordProvider
│   ├── slack.py           # SlackProvider
│   ├── teams.py           # TeamsProvider
│   └── github_issues.py   # GitHubIssueProvider (+ Projects v2)
└── templates/
    └── report.md.j2       # Customizable Jinja2 report template

Adding a New Notification Provider

The notification system uses a strategy pattern. Adding a provider (e.g., PagerDuty, email, Matrix) requires just one file.

1. Create the provider class

Create vulnradar/notifications/pagerduty.py:

"""PagerDuty notification provider."""

import requests
from typing import Any

from .base import NotificationProvider
from ..state import Change


class PagerDutyProvider(NotificationProvider):
    """Send alerts to PagerDuty Events API."""

    name = "pagerduty"

    def __init__(self, routing_key: str, max_alerts: int = 10):
        self.routing_key = routing_key
        self.max_alerts = max_alerts

    def send_alert(self, item: dict[str, Any], changes: list[Change] | None = None) -> None:
        """Send an individual CVE alert."""
        payload = {
            "routing_key": self.routing_key,
            "event_action": "trigger",
            "payload": {
                "summary": f"[VulnRadar] {item['cve_id']}{item.get('description', '')[:120]}",
                "severity": "critical" if item.get("is_critical") else "warning",
                "source": "vulnradar",
            },
        }
        requests.post("https://events.pagerduty.com/v2/enqueue", json=payload, timeout=30)

    def send_summary(self, items: list[dict[str, Any]], repo: str,
                     changes_by_cve: dict[str, tuple] | None = None) -> None:
        """Send a summary (optional for PagerDuty — could be a no-op)."""
        pass

    def send_baseline(self, items: list[dict[str, Any]],
                      critical_items: list[dict[str, Any]], repo: str) -> None:
        """Send a baseline (optional — could be a no-op)."""
        pass

2. Register it in the provider loader

Add loading logic to vulnradar/notifications/__init__.py:

from .pagerduty import PagerDutyProvider

# In load_providers():
if args.pagerduty_key:
    providers.append(PagerDutyProvider(routing_key=args.pagerduty_key))

3. Add the CLI flag

In vulnradar/cli.py, add the argparse argument for the notify subcommand:

parser.add_argument("--pagerduty-key", help="PagerDuty routing key")

4. Add tests

Add tests/test_pagerduty.py with mocked requests.post calls to verify payload format. See tests/test_notify.py for examples.

5. Document it

Add a docs/pagerduty.md setup guide.

Customizing the Report Template

The Markdown report is rendered from vulnradar/templates/report.md.j2 using Jinja2.

Available template variables

VariableTypeDescription
generated_atstrISO 8601 timestamp
totalintTotal radar items
watch_hitsintItems matching watchlist
kev_countintItems in CISA KEV
patch_countintItems with PatchThis exploit intel
critical_patch_watchintCritical items (exploit intel + watchlist)
critical_toplist[dict]Top 5 critical items
itemslist[dict]All radar items, sorted by risk
recent_changeslist[dict]CVEs that changed recently

Each item dict contains

KeyTypeExample
cve_idstr"CVE-2024-12345"
descriptionstr"Remote code execution in..."
cvss_scorefloat | None9.8
cvss_severitystr | None"CRITICAL"
probability_scorefloat | None0.85
is_criticalbooltrue
priority_labelstr"CRITICAL (CVSS ≥ 9.0)"
active_threatbooltrue
watchlist_hitbooltrue
kevdict | NoneKEV metadata
affectedlist[dict]Vendor/product entries

Overriding the template

Copy the template and modify it:

cp vulnradar/templates/report.md.j2 my_report.md.j2

Then point to it in your workflow or pass a custom template path.

Running Tests

# All tests
python -m pytest tests/ -v

# With coverage
python -m pytest tests/ --cov=vulnradar --cov-report=term-missing

# Single test file
python -m pytest tests/test_features.py -v

# Tests matching a pattern
python -m pytest tests/ -k "test_severity"

Test organisation

FileCovers
test_etl.pyCLI entry points, download orchestration
test_notify.pyNotification providers, GitHub Issues, Projects
test_parsers.pyCVE JSON parsing, CVSS extraction, risk scoring
test_config.pyPydantic config validation
test_enrichment.pyKEV/EPSS/NVD enrichment merge
test_downloaders.pyHTTP fetchers with mocked responses
test_report.pyJinja2 report rendering
test_notifications.pyProvider loading, payload formatting
test_features.pySeverity thresholds, notification routing
test_async_downloaders.pyParallel download orchestrator

Code Style

  • Formatter: ruff (line length 120, Python 3.11+)
  • Docstrings: Google style with Args:, Returns:, Raises: sections
  • Type hints: Required on all public function signatures
  • Imports: stdlib → third-party → local, separated by blank lines

Pull Request Checklist

  • Tests pass: python -m pytest tests/ -v
  • Linting passes: ruff check vulnradar/ tests/
  • New public functions have Google-style docstrings
  • New features include tests
  • Documentation updated if user-facing behaviour changes