├── .gitignore
├── .pre-commit-config.yaml
├── LICENSE
├── README.md
├── docs
└── images
│ ├── webhook_receiver.png
│ ├── webhook_receiver_group.png
│ └── webhook_receivers.png
├── netbox_webhook_receiver
├── __init__.py
├── api
│ ├── __init__.py
│ ├── serializers.py
│ ├── urls.py
│ └── views.py
├── choices.py
├── filtersets.py
├── forms.py
├── migrations
│ ├── 0001_initial.py
│ ├── 0002_webhookreceiver_hash_algorithm.py
│ └── __init__.py
├── models.py
├── navigation.py
├── tables.py
├── templates
│ └── netbox_webhook_receiver
│ │ ├── webhookreceiver.html
│ │ └── webhookreceivergroup.html
├── urls.py
└── views
│ ├── __init__.py
│ ├── views.py
│ ├── webhookreceiver.py
│ └── webhookreceivergroup.py
└── pyproject.toml
/.gitignore:
--------------------------------------------------------------------------------
1 | __pycache__
2 | .env
3 | .python-version
4 | poetry.lock
5 | dist/
6 |
--------------------------------------------------------------------------------
/.pre-commit-config.yaml:
--------------------------------------------------------------------------------
1 | repos:
2 | - repo: https://github.com/astral-sh/ruff-pre-commit
3 | rev: v0.0.275
4 | hooks:
5 | - id: ruff
6 | args: [--fix, --exit-non-zero-on-fix]
7 | exclude_types: []
8 | - repo: https://github.com/pre-commit/pre-commit-hooks
9 | rev: v4.4.0
10 | hooks:
11 | - id: debug-statements
12 | - id: end-of-file-fixer
13 | - id: trailing-whitespace
14 | - repo: https://github.com/psf/black
15 | rev: 23.3.0
16 | hooks:
17 | - id: black
18 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Aurora Research Lab
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
NetBox Webhook Receiver
2 |
3 | NetBox Webhook Receiver is a NetBox plugin for managing webhook receiver endpoints and executing assigned actions.
4 |
5 | This plugin aims mainly to streamline the deployment process of scripts and export templates to Netbox by triggering the synchronisation of its Data Sources through incoming webhooks. Adding the receiver capability to Netbox opens other possible use cases, which might be worth exploring further. One example could be running Netbox Scripts triggered by remote events where it is not easy to execute a direct API call.
6 |
7 | ## Features
8 | - [x] Per endpoint authentication (signature validation or custom header) on receiving webhooks
9 | - [x] Webhook url enriched with automaticaly generated uuid value for additional security
10 | - [x] Dedicated view to configure each webhook receiver url
11 | - [x] Logical grouping of the receivers
12 | - [x] Optionally store incoming webhook payload
13 | - [x] Custom actions execution on successfully receving authenticated webhook message. Currently only one action available: Synchronize Netbox git 'Data Source'
14 | - [x] Use of standard Netbox jobs to queue webhook actions in the workers.
15 | - [ ] Additional actions to be implemented
16 | - [ ] Plugin configuration parameters
17 |
18 | ## Requirements
19 |
20 | * NetBox 4.1 or higher
21 | * Python 3.10 or higher
22 |
23 | ## Installation & Configuration
24 |
25 | For general knowledge about netbox plugin installation please refer to the official guide: [Using Plugins - NetBox Documentation](https://netbox.readthedocs.io/en/stable/plugins/)
26 |
27 | Install NetBox Webhook Receiver:
28 | ```bash
29 | $ source /opt/netbox/venv/bin/activate
30 | (venv) $ pip install netbox-plugin-webhook-receiver
31 | ```
32 |
33 | To register this plugin in NetBox, add _netbox_webhook_receiver_ to the config file. `~/netbox/configuration.py`
34 |
35 | ```python
36 | PLUGINS = [
37 | "netbox_webhook_receiver",
38 | ]
39 | ```
40 |
41 | Please remember that this plugin introduces new database models, therefore you must run the provided database schema migrations:
42 | ```bash
43 | $ source /opt/netbox/venv/bin/activate
44 | (venv) $ cd /opt/netbox/netbox/
45 | (venv) $ python3 manage.py migrate
46 | ```
47 |
48 | ## Screenshots
49 |
50 | 
51 | 
52 | 
53 |
--------------------------------------------------------------------------------
/docs/images/webhook_receiver.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LuPo/netbox-plugin-webhook-receiver/619a6630e04239715f33051dcd4ad6b62bd25e33/docs/images/webhook_receiver.png
--------------------------------------------------------------------------------
/docs/images/webhook_receiver_group.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LuPo/netbox-plugin-webhook-receiver/619a6630e04239715f33051dcd4ad6b62bd25e33/docs/images/webhook_receiver_group.png
--------------------------------------------------------------------------------
/docs/images/webhook_receivers.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LuPo/netbox-plugin-webhook-receiver/619a6630e04239715f33051dcd4ad6b62bd25e33/docs/images/webhook_receivers.png
--------------------------------------------------------------------------------
/netbox_webhook_receiver/__init__.py:
--------------------------------------------------------------------------------
1 | from netbox.plugins import PluginConfig
2 |
3 | __version__ = "0.2.0"
4 |
5 |
6 | class NetBoxWebhookReceiverConfig(PluginConfig):
7 | name = "netbox_webhook_receiver"
8 | verbose_name = "NetBox Webhook Receiver"
9 | description = "Manage webhook receivers and queues related actions in NetBox"
10 | version = __version__
11 | author = "Łukasz Polański"
12 | author_email = "wookasz@gmail.com"
13 | required_settings = []
14 | default_settings = {}
15 | base_url = "webhook-receiver"
16 |
17 |
18 | config = NetBoxWebhookReceiverConfig
19 |
--------------------------------------------------------------------------------
/netbox_webhook_receiver/api/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LuPo/netbox-plugin-webhook-receiver/619a6630e04239715f33051dcd4ad6b62bd25e33/netbox_webhook_receiver/api/__init__.py
--------------------------------------------------------------------------------
/netbox_webhook_receiver/api/serializers.py:
--------------------------------------------------------------------------------
1 | from rest_framework import serializers
2 |
3 | from netbox.api.serializers import NetBoxModelSerializer
4 | from ..models import WebhookReceiver, WebhookReceiverGroup
5 |
6 |
7 | class WebhookReceiverSerializer(NetBoxModelSerializer):
8 | url = serializers.HyperlinkedIdentityField(
9 | view_name="plugins-api:netbox_webhook_receiver-api:webhookreceiver-detail"
10 | )
11 |
12 | class Meta:
13 | model = WebhookReceiver
14 | fields = (
15 | "custom_fields",
16 | "created",
17 | "datasource",
18 | "last_updated",
19 | "id",
20 | "display",
21 | "name",
22 | "receiver_group",
23 | "store_payload",
24 | "tags",
25 | "auth_header",
26 | "auth_method",
27 | "hash_algorithm",
28 | "url",
29 | "uuid",
30 | )
31 |
32 |
33 | class WebhookReceiverGroupSerializer(NetBoxModelSerializer):
34 | url = serializers.HyperlinkedIdentityField(
35 | view_name="plugins-api:netbox_webhook_receiver-api:webhookreceivergroup-detail"
36 | )
37 | receivers_count = serializers.IntegerField(read_only=True)
38 |
39 | class Meta:
40 | model = WebhookReceiverGroup
41 | fields = (
42 | "created",
43 | "last_updated",
44 | "id",
45 | "display",
46 | "name",
47 | "tags",
48 | "description",
49 | "receivers_count",
50 | "url",
51 | )
52 |
--------------------------------------------------------------------------------
/netbox_webhook_receiver/api/urls.py:
--------------------------------------------------------------------------------
1 | from netbox.api.routers import NetBoxRouter
2 | from . import views
3 |
4 | app_name = "netbox_webhook_receiver"
5 |
6 | router = NetBoxRouter()
7 | router.register("receivers", views.WebhookReceiverViewSet)
8 | router.register("groups", views.WebhookReceiverGroupViewSet)
9 |
10 | urlpatterns = router.urls
11 |
--------------------------------------------------------------------------------
/netbox_webhook_receiver/api/views.py:
--------------------------------------------------------------------------------
1 | from netbox.api.viewsets import NetBoxModelViewSet
2 | from django.db.models import Count
3 |
4 | from .. import models # , filtersets
5 | from .serializers import WebhookReceiverSerializer, WebhookReceiverGroupSerializer
6 |
7 |
8 | class WebhookReceiverViewSet(NetBoxModelViewSet):
9 | queryset = models.WebhookReceiver.objects.prefetch_related("tags")
10 | serializer_class = WebhookReceiverSerializer
11 |
12 |
13 | class WebhookReceiverGroupViewSet(NetBoxModelViewSet):
14 | queryset = models.WebhookReceiverGroup.objects.prefetch_related("tags").annotate(
15 | rule_count=Count("receivers")
16 | )
17 | serializer_class = WebhookReceiverGroupSerializer
18 |
--------------------------------------------------------------------------------
/netbox_webhook_receiver/choices.py:
--------------------------------------------------------------------------------
1 | from utilities.choices import ChoiceSet
2 |
3 |
4 | class WebhookAuthMethodChoices(ChoiceSet):
5 | TOKEN = "token"
6 | SIGNATURE_VERIFICATION = "signature_verification"
7 |
8 | CHOICES = [
9 | (TOKEN, "Token"),
10 | (SIGNATURE_VERIFICATION, "Signature Verification"),
11 | ]
12 |
13 |
14 | class HashingAlgorithmChoices(ChoiceSet):
15 | SHA_256 = "sha256"
16 | SHA_512 = "sha512"
17 |
18 | CHOICES = (
19 | (SHA_256, "SHA-256"),
20 | (SHA_512, "SHA-512"),
21 | )
22 |
--------------------------------------------------------------------------------
/netbox_webhook_receiver/filtersets.py:
--------------------------------------------------------------------------------
1 | from netbox.filtersets import NetBoxModelFilterSet
2 | from .models import WebhookReceiver, WebhookReceiverGroup
3 |
4 |
5 | class WebhookReceiverFilterSet(NetBoxModelFilterSet):
6 | class Meta:
7 | model = WebhookReceiver
8 | fields = (
9 | "uuid",
10 | "store_payload",
11 | "receiver_group",
12 | "hash_algorithm",
13 | "auth_method",
14 | )
15 |
16 | def search(self, queryset, name, value):
17 | return queryset.filter(description__icontains=value)
18 |
19 |
20 | class WebhookReceiverGroupFilterSet(NetBoxModelFilterSet):
21 | class Meta:
22 | model = WebhookReceiverGroup
23 | fields = ("name",)
24 |
25 | def search(self, queryset, name, value):
26 | return queryset.filter(description__icontains=value)
27 |
--------------------------------------------------------------------------------
/netbox_webhook_receiver/forms.py:
--------------------------------------------------------------------------------
1 | from django import forms
2 | from netbox.forms import NetBoxModelForm, NetBoxModelFilterSetForm
3 | from utilities.forms.fields import CommentField
4 | from utilities.forms.rendering import FieldSet
5 | from utilities.forms.utils import get_field_value
6 | from utilities.forms.widgets import HTMXSelect
7 | from .choices import WebhookAuthMethodChoices
8 | from .models import WebhookReceiver, WebhookReceiverGroup
9 |
10 |
11 | class WebhookReceiverForm(NetBoxModelForm):
12 | comments = CommentField()
13 |
14 | fieldsets = (
15 | FieldSet(
16 | "name",
17 | "receiver_group",
18 | "description",
19 | "uuid",
20 | "store_payload",
21 | name="Webhook definition",
22 | ),
23 | FieldSet(
24 | "auth_method",
25 | "auth_header",
26 | "secret_key",
27 | "token",
28 | "hash_algorithm",
29 | name="Authentication",
30 | ),
31 | FieldSet("datasource", name="Trigger action"),
32 | FieldSet("tags", name="Extra"),
33 | )
34 |
35 | # Thanks to Dillon Henschen for his netbox pull #12675
36 | def __init__(self, *args, **kwargs):
37 | super().__init__(*args, **kwargs)
38 |
39 | auth_method = get_field_value(self, "auth_method")
40 |
41 | if auth_method != WebhookAuthMethodChoices.TOKEN:
42 | del self.fields["token"]
43 | self.fields[
44 | "auth_header"
45 | ].help_text = "Custom Header option carying \
46 | payload signature (eg. 'X-Hub-Signature')."
47 | if auth_method != WebhookAuthMethodChoices.SIGNATURE_VERIFICATION:
48 | del self.fields["hash_algorithm"]
49 | del self.fields["secret_key"]
50 |
51 | class Meta:
52 | model = WebhookReceiver
53 | fields = (
54 | "name",
55 | "receiver_group",
56 | "description",
57 | "store_payload",
58 | "auth_method",
59 | "auth_header",
60 | "hash_algorithm",
61 | "secret_key",
62 | "token",
63 | "uuid",
64 | "datasource",
65 | "tags",
66 | "comments",
67 | )
68 | widgets = {
69 | "auth_method": HTMXSelect(),
70 | }
71 |
72 | def clean(self):
73 | super().clean()
74 |
75 | auth_method = self.cleaned_data.get("auth_method")
76 |
77 | if auth_method != WebhookAuthMethodChoices.TOKEN:
78 | self.cleaned_data["token"] = None
79 |
80 | if auth_method != WebhookAuthMethodChoices.SIGNATURE_VERIFICATION:
81 | self.cleaned_data["secret_key"] = None
82 |
83 |
84 | class WebhookReceiverFilterForm(NetBoxModelFilterSetForm):
85 | model = WebhookReceiver
86 | # tags = TagFilterField(
87 | # required=False
88 | # )
89 | uuid = forms.CharField(required=False)
90 | receiver_group = forms.ModelMultipleChoiceField(
91 | queryset=WebhookReceiverGroup.objects.all(), required=False
92 | )
93 |
94 |
95 | class WebhookReceiverGroupForm(NetBoxModelForm):
96 | comments = CommentField()
97 |
98 | class Meta:
99 | model = WebhookReceiverGroup
100 | fields = (
101 | "name",
102 | "description",
103 | "tags",
104 | "comments",
105 | )
106 |
107 |
108 | class WebhookReceiverGroupFilterForm(NetBoxModelFilterSetForm):
109 | model = WebhookReceiverGroup
110 | # tags = TagFilterField(
111 | # required=False
112 | # )
113 |
--------------------------------------------------------------------------------
/netbox_webhook_receiver/migrations/0001_initial.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.1.9 on 2023-07-03 12:00
2 |
3 | from django.db import migrations, models
4 | import django.db.models.deletion
5 | import taggit.managers
6 | import utilities.json
7 | import uuid
8 |
9 |
10 | class Migration(migrations.Migration):
11 | initial = True
12 |
13 | dependencies = [
14 | ("core", "0005_job_created_auto_now"),
15 | ("extras", "0092_delete_jobresult"),
16 | ]
17 |
18 | operations = [
19 | migrations.CreateModel(
20 | name="WebhookMessage",
21 | fields=[
22 | (
23 | "id",
24 | models.BigAutoField(
25 | auto_created=True, primary_key=True, serialize=False
26 | ),
27 | ),
28 | ("payload", models.JSONField(default=None, null=True)),
29 | ("received_at", models.DateTimeField()),
30 | ],
31 | options={
32 | "ordering": ["received_at"],
33 | },
34 | ),
35 | migrations.CreateModel(
36 | name="WebhookReceiverGroup",
37 | fields=[
38 | (
39 | "id",
40 | models.BigAutoField(
41 | auto_created=True, primary_key=True, serialize=False
42 | ),
43 | ),
44 | ("created", models.DateTimeField(auto_now_add=True, null=True)),
45 | ("last_updated", models.DateTimeField(auto_now=True, null=True)),
46 | (
47 | "custom_field_data",
48 | models.JSONField(
49 | blank=True,
50 | default=dict,
51 | encoder=utilities.json.CustomFieldJSONEncoder,
52 | ),
53 | ),
54 | ("comments", models.TextField(blank=True)),
55 | ("description", models.CharField(blank=True, max_length=500)),
56 | ("name", models.CharField(max_length=50)),
57 | (
58 | "tags",
59 | taggit.managers.TaggableManager(
60 | through="extras.TaggedItem", to="extras.Tag"
61 | ),
62 | ),
63 | ],
64 | options={
65 | "ordering": ("name",),
66 | },
67 | ),
68 | migrations.CreateModel(
69 | name="WebhookReceiver",
70 | fields=[
71 | (
72 | "id",
73 | models.BigAutoField(
74 | auto_created=True, primary_key=True, serialize=False
75 | ),
76 | ),
77 | ("created", models.DateTimeField(auto_now_add=True, null=True)),
78 | ("last_updated", models.DateTimeField(auto_now=True, null=True)),
79 | (
80 | "custom_field_data",
81 | models.JSONField(
82 | blank=True,
83 | default=dict,
84 | encoder=utilities.json.CustomFieldJSONEncoder,
85 | ),
86 | ),
87 | ("comments", models.TextField(blank=True)),
88 | ("description", models.CharField(blank=True, max_length=500)),
89 | ("name", models.CharField(max_length=50)),
90 | ("token", models.CharField(blank=True, max_length=50, null=True)),
91 | (
92 | "auth_header",
93 | models.CharField(default="X-Gitlab-Token", max_length=50),
94 | ),
95 | ("auth_method", models.CharField(default="token", max_length=50)),
96 | ("secret_key", models.CharField(blank=True, max_length=50, null=True)),
97 | ("store_payload", models.BooleanField(default=True)),
98 | ("uuid", models.UUIDField(default=uuid.uuid4)),
99 | (
100 | "datasource",
101 | models.ForeignKey(
102 | blank=True,
103 | null=True,
104 | on_delete=django.db.models.deletion.CASCADE,
105 | to="core.datasource",
106 | ),
107 | ),
108 | (
109 | "receiver_group",
110 | models.ForeignKey(
111 | blank=True,
112 | null=True,
113 | on_delete=django.db.models.deletion.PROTECT,
114 | related_name="receivers",
115 | to="netbox_webhook_receiver.webhookreceivergroup",
116 | ),
117 | ),
118 | (
119 | "tags",
120 | taggit.managers.TaggableManager(
121 | through="extras.TaggedItem", to="extras.Tag"
122 | ),
123 | ),
124 | ],
125 | options={
126 | "ordering": ("name", "receiver_group"),
127 | },
128 | ),
129 | migrations.AddIndex(
130 | model_name="webhookmessage",
131 | index=models.Index(
132 | fields=["received_at"], name="netbox_webh_receive_6a2187_idx"
133 | ),
134 | ),
135 | ]
136 |
--------------------------------------------------------------------------------
/netbox_webhook_receiver/migrations/0002_webhookreceiver_hash_algorithm.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.1.9 on 2023-07-04 11:28
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 | dependencies = [
8 | ("netbox_webhook_receiver", "0001_initial"),
9 | ]
10 |
11 | operations = [
12 | migrations.AddField(
13 | model_name="webhookreceiver",
14 | name="hash_algorithm",
15 | field=models.CharField(blank=True, max_length=50, null=True),
16 | ),
17 | ]
18 |
--------------------------------------------------------------------------------
/netbox_webhook_receiver/migrations/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LuPo/netbox-plugin-webhook-receiver/619a6630e04239715f33051dcd4ad6b62bd25e33/netbox_webhook_receiver/migrations/__init__.py
--------------------------------------------------------------------------------
/netbox_webhook_receiver/models.py:
--------------------------------------------------------------------------------
1 | from .choices import WebhookAuthMethodChoices, HashingAlgorithmChoices
2 | from core.models import DataSource
3 | from django.db import models
4 | from django.urls import reverse
5 | from netbox.models import NetBoxModel
6 | import uuid
7 |
8 |
9 | class WebhookMessage(models.Model):
10 | payload = models.JSONField(default=None, null=True)
11 | received_at = models.DateTimeField(help_text="When we received the event.")
12 |
13 | class Meta:
14 | ordering = ["received_at"]
15 | indexes = [
16 | models.Index(fields=["received_at"]),
17 | ]
18 |
19 | def __str__(self):
20 | return self.received_at
21 |
22 |
23 | class WebhookReceiverGroup(NetBoxModel):
24 | comments = models.TextField(blank=True)
25 | description = models.CharField(max_length=500, blank=True)
26 | name = models.CharField(
27 | help_text="Webhook receiver group", max_length=50, null=False
28 | )
29 |
30 | class Meta:
31 | ordering = ("name",)
32 |
33 | def __str__(self):
34 | return self.name
35 |
36 | def get_absolute_url(self):
37 | return reverse(
38 | "plugins:netbox_webhook_receiver:webhookreceivergroup", args=[self.pk]
39 | )
40 |
41 |
42 | class WebhookReceiver(NetBoxModel):
43 | comments = models.TextField(blank=True)
44 | datasource = models.ForeignKey(
45 | help_text="Incomming webhook triggers update of selected datasource",
46 | to=DataSource,
47 | on_delete=models.CASCADE,
48 | blank=True,
49 | null=True,
50 | )
51 | description = models.CharField(max_length=500, blank=True)
52 | name = models.CharField(max_length=50, null=False)
53 | receiver_group = models.ForeignKey(
54 | to=WebhookReceiverGroup,
55 | on_delete=models.PROTECT,
56 | related_name="receivers",
57 | blank=True,
58 | null=True,
59 | )
60 | token = models.CharField(
61 | help_text="Authentication is mandatory. Custom field Token",
62 | max_length=50,
63 | null=True,
64 | blank=True,
65 | )
66 |
67 | auth_header = models.CharField(
68 | help_text="Custom Header option carying authentication token",
69 | max_length=50,
70 | null=False,
71 | default="X-Gitlab-Token",
72 | )
73 | auth_method = models.CharField(
74 | help_text="Webhook authentication method",
75 | max_length=50,
76 | choices=WebhookAuthMethodChoices,
77 | default=WebhookAuthMethodChoices.TOKEN,
78 | null=False,
79 | )
80 | secret_key = models.CharField(
81 | help_text="Authentication is mandatory. \
82 | Secret key for HMAC hex digest of the payload body",
83 | max_length=50,
84 | null=True,
85 | blank=True,
86 | )
87 | hash_algorithm = models.CharField(
88 | help_text="Hashing algorithm for message authentication signature. \
89 | If not provided sha512 will be used",
90 | max_length=50,
91 | choices=HashingAlgorithmChoices,
92 | null=True,
93 | blank=True,
94 | )
95 | store_payload = models.BooleanField(
96 | help_text="Store payload of incomming webhooks", default=True
97 | )
98 | uuid = models.UUIDField(default=uuid.uuid4)
99 |
100 | class Meta:
101 | ordering = (
102 | "name",
103 | "receiver_group",
104 | )
105 |
106 | def __str__(self):
107 | return self.name
108 |
109 | def get_absolute_url(self):
110 | return reverse(
111 | "plugins:netbox_webhook_receiver:webhookreceiver", args=[self.pk]
112 | )
113 |
--------------------------------------------------------------------------------
/netbox_webhook_receiver/navigation.py:
--------------------------------------------------------------------------------
1 | from netbox.plugins import PluginMenuButton, PluginMenuItem
2 |
3 | webhook_receiver_buttons = [
4 | PluginMenuButton(
5 | link="plugins:netbox_webhook_receiver:webhookreceiver_add",
6 | title="Add",
7 | icon_class="mdi mdi-plus-thick",
8 | )
9 | ]
10 |
11 | webhook_receiver_group_buttons = [
12 | PluginMenuButton(
13 | link="plugins:netbox_webhook_receiver:webhookreceivergroup_add",
14 | title="Add",
15 | icon_class="mdi mdi-plus-thick",
16 | )
17 | ]
18 |
19 | menu_items = (
20 | PluginMenuItem(
21 | link="plugins:netbox_webhook_receiver:webhookreceivergroup_list",
22 | link_text="Webhook Receiver Groups",
23 | buttons=webhook_receiver_group_buttons,
24 | ),
25 | PluginMenuItem(
26 | link="plugins:netbox_webhook_receiver:webhookreceiver_list",
27 | link_text="Webhook Receivers",
28 | buttons=webhook_receiver_buttons,
29 | ),
30 | )
31 |
--------------------------------------------------------------------------------
/netbox_webhook_receiver/tables.py:
--------------------------------------------------------------------------------
1 | import django_tables2 as tables
2 |
3 | from netbox.tables import NetBoxTable
4 | from .models import WebhookReceiver, WebhookReceiverGroup
5 |
6 |
7 | class WebhookReceiverTable(NetBoxTable):
8 | name = tables.Column(linkify=True)
9 | receiver_group = tables.Column(linkify=True)
10 |
11 | class Meta(NetBoxTable.Meta):
12 | model = WebhookReceiver
13 | fields = (
14 | "pk",
15 | "comments",
16 | "name",
17 | "datasource",
18 | "description",
19 | "receiver_group",
20 | "store_payload",
21 | "auth_header",
22 | "auth_method",
23 | "hash_algorithm",
24 | "secret_key",
25 | "token",
26 | "uuid",
27 | )
28 | default_columns = ("name", "receiver_group", "description", "uuid")
29 |
30 |
31 | class WebhookReceiverGroupTable(NetBoxTable):
32 | name = tables.Column(linkify=True)
33 | receivers_count = tables.Column()
34 |
35 | class Meta(NetBoxTable.Meta):
36 | model = WebhookReceiverGroup
37 | fields = (
38 | "pk",
39 | "comments",
40 | "name",
41 | "description",
42 | "receivers_count",
43 | )
44 | default_columns = ("name", "description", "receivers_count")
45 |
--------------------------------------------------------------------------------
/netbox_webhook_receiver/templates/netbox_webhook_receiver/webhookreceiver.html:
--------------------------------------------------------------------------------
1 | {% extends 'generic/object.html' %}
2 | {% load render_table from django_tables2 %}
3 |
4 | {% block content %}
5 |
6 |
7 |
8 |
9 |
10 |
11 | Name |
12 | {{ object.name }} |
13 |
14 |
15 | Description |
16 | {{ object.description }} |
17 |
18 |
19 | Receiver group |
20 |
21 | {{ object.receiver_group }}
22 | |
23 |
24 |
25 | UUID |
26 | {{ object.uuid }} |
27 |
28 |
29 | Auth method |
30 | {{ object.auth_method }} |
31 |
32 |
33 | Auth header |
34 | {{ object.auth_header }} |
35 |
36 |
37 | Token |
38 | {{ object.token|placeholder }} |
39 |
40 |
41 | Hash algotithm |
42 | {{ object.hash_algorithm|placeholder }} |
43 |
44 |
45 | Secret Key |
46 | {{ object.secret_key|placeholder }} |
47 |
48 |
49 |
50 | {% include 'inc/panels/custom_fields.html' %}
51 |
52 |
53 |
54 |
55 |
56 |
57 | Store payload |
58 | {{ object.store_payload|placeholder }} |
59 |
60 |
61 | Update datasource |
62 |
63 | {{ object.datasource|placeholder }}
64 | |
65 |
66 |
67 | Run Custom Script |
68 | {{ object.customscript|placeholder }} |
69 |
70 |
71 |
72 |
73 |
74 | {% include 'inc/panels/tags.html' %}
75 | {% include 'inc/panels/comments.html' %}
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 | {{request.headers.referer | split:"/" | slice:"5" | join:"/" }}/webhooks/{{ object.uuid}}/ |
85 |
86 |
87 | {% copy_content "webhook_endpoint" %}
88 |
89 | |
90 |
91 |
92 |
93 |
94 |
95 | {% endblock content %}
96 |
--------------------------------------------------------------------------------
/netbox_webhook_receiver/templates/netbox_webhook_receiver/webhookreceivergroup.html:
--------------------------------------------------------------------------------
1 | {% extends 'generic/object.html' %}
2 | {% load render_table from django_tables2 %}
3 |
4 | {% block content %}
5 |
6 |
7 |
8 |
9 |
10 |
11 | Name |
12 | {{ object.name }} |
13 |
14 |
15 | Description |
16 | {{ object.description }} |
17 |
18 |
19 | Tenant |
20 | {{ object.tenant }} |
21 |
22 |
23 | Related receivers |
24 | {{ object.receivers.count }} |
25 |
26 |
27 |
28 | {% include 'inc/panels/custom_fields.html' %}
29 |
30 |
31 | {% include 'inc/panels/tags.html' %}
32 | {% include 'inc/panels/comments.html' %}
33 |
34 |
35 |
36 |
37 |
38 |
39 | {% render_table receivers_table %}
40 |
41 |
42 |
43 | {% endblock content %}
44 |
--------------------------------------------------------------------------------
/netbox_webhook_receiver/urls.py:
--------------------------------------------------------------------------------
1 | # ruff: noqa: E501
2 | # fmt: off
3 | from netbox.views.generic import ObjectChangeLogView
4 | from django.urls import path
5 | from . import models, views
6 | from netbox_webhook_receiver.views import incomming_webhook_request
7 |
8 | urlpatterns = (
9 | # Receive Webhook Messages
10 | path("webhooks//", incomming_webhook_request),
11 | # Webhook Receiver
12 | path("receivers/", views.WebhookReceiverListView.as_view(), name="webhookreceiver_list",),
13 | path("receivers/add/", views.WebhookReceiverEditView.as_view(), name="webhookreceiver_add",),
14 | path("receivers//", views.WebhookReceiverView.as_view(), name="webhookreceiver",),
15 | path("receivers//edit/", views.WebhookReceiverEditView.as_view(), name="webhookreceiver_edit",),
16 | path("receivers//delete/", views.WebhookReceiverDeleteView.as_view(), name="webhookreceiver_delete",),
17 | path("receivers//changelog/", ObjectChangeLogView.as_view(),
18 | name="webhookreceiver_changelog",
19 | kwargs={"model": models.WebhookReceiver},
20 | ),
21 | # Webhook Receiver Group
22 | path("groups/", views.WebhookReceiverGroupListView.as_view(), name="webhookreceivergroup_list",),
23 | path("groups/add/", views.WebhookReceiverGroupEditView.as_view(), name="webhookreceivergroup_add",),
24 | path("groups//", views.WebhookReceiverGroupView.as_view(), name="webhookreceivergroup",),
25 | path("groups//edit/", views.WebhookReceiverGroupEditView.as_view(), name="webhookreceivergroup_edit",),
26 | path("groups//delete/", views.WebhookReceiverGroupDeleteView.as_view(), name="webhookreceivergroup_delete",),
27 | path("groups//changelog/", ObjectChangeLogView.as_view(),
28 | name="webhookreceivergroup_changelog",
29 | kwargs={"model": models.WebhookReceiverGroup},
30 | ),
31 | )
32 |
--------------------------------------------------------------------------------
/netbox_webhook_receiver/views/__init__.py:
--------------------------------------------------------------------------------
1 | from .views import * # noqa : F403
2 | from .webhookreceiver import * # noqa : F403
3 | from .webhookreceivergroup import * # noqa : F403
4 |
--------------------------------------------------------------------------------
/netbox_webhook_receiver/views/views.py:
--------------------------------------------------------------------------------
1 | import datetime as dt
2 | import json
3 | import logging
4 |
5 | from django.db.transaction import atomic, non_atomic_requests
6 | from django.http import (
7 | HttpResponse,
8 | HttpResponseForbidden,
9 | HttpResponseNotFound,
10 | )
11 | from django.utils import timezone
12 | from django.views.decorators.csrf import csrf_exempt
13 | from django.views.decorators.http import require_POST
14 | from netbox_webhook_receiver.models import WebhookMessage, WebhookReceiver
15 | from secrets import compare_digest
16 | from core.jobs import SyncDataSourceJob
17 | from users.models import User
18 |
19 | logger = logging.getLogger(__name__)
20 |
21 |
22 | @csrf_exempt
23 | @require_POST
24 | @non_atomic_requests
25 | def incomming_webhook_request(request, **kwargs):
26 | """
27 | Validates incomming webhook path and custom header authentication.
28 | Calls the webhook processing function.
29 | """
30 | try:
31 | receiver = WebhookReceiver.objects.get(uuid=kwargs["random_path"])
32 | except WebhookReceiver.DoesNotExist:
33 | return HttpResponseNotFound(
34 | f"Provided path selector ({kwargs['random_path']}) is not correct"
35 | )
36 |
37 | # Check if defined Authorisation header is present in the request
38 | if receiver.auth_header not in request.headers:
39 | return HttpResponseForbidden(
40 | f"Did not get authentication header {receiver.auth_header} in the request"
41 | )
42 |
43 | # Webhook authentication
44 | verified = authenticate_request(request, receiver)
45 | if not verified:
46 | return HttpResponseForbidden(
47 | f"Incorrect {'token' if receiver.token else 'signature'} \
48 | in {receiver.auth_header} header.",
49 | content_type="text/plain",
50 | )
51 |
52 | # Retain webhook payload for any future use case.
53 | if receiver.store_payload:
54 | WebhookMessage.objects.filter(
55 | received_at__lte=timezone.now() - dt.timedelta(days=2)
56 | ).delete()
57 | payload = json.loads(request.body)
58 | WebhookMessage.objects.create(
59 | received_at=timezone.now(),
60 | payload=payload,
61 | )
62 |
63 | result = "No action has been taken by webhook receiver"
64 | # So far datasource sync is the only action of the incomming webhook
65 | if receiver.datasource:
66 | # Prevent queuing multiple syncronizzation requestes
67 | if job := receiver.datasource.jobs.first():
68 | if job.status == "pending":
69 | return HttpResponseForbidden(
70 | "There are already pending jobs in the queue",
71 | content_type="text/plain",
72 | )
73 |
74 | result = process_webhook_sync_datasource(receiver)
75 |
76 | logger.info(result)
77 | return HttpResponse(
78 | result,
79 | content_type="text/plain",
80 | )
81 |
82 |
83 | @atomic
84 | def process_webhook_sync_datasource(receiver):
85 | SyncDataSourceJob.enqueue(
86 | instance=receiver.datasource, user=DefaultUserRequest().user
87 | )
88 |
89 | return f"Synchronizing Data Source: {receiver.datasource.name}"
90 |
91 |
92 | def authenticate_request(request, receiver) -> bool:
93 | """
94 | Auth token/signature header name is defined on individual webhook receiver.
95 | """
96 | # Custom header auth method
97 | if receiver.token:
98 | request_token = ascii(request.headers.get(receiver.auth_header, ""))
99 | configured_token = ascii(receiver.token)
100 | return compare_digest(request_token, configured_token)
101 |
102 | # Signature verification auth method
103 | elif receiver.secret_key:
104 | import hashlib
105 | import hmac
106 |
107 | hmac_header = request.headers.get(receiver.auth_header, "").removeprefix(
108 | "sha256="
109 | )
110 | hash_algorithm = receiver.hash_algorithm or "sha512"
111 |
112 | # Calculate hexadecimal HMAC digest
113 | hmac_digest = hmac.new(
114 | key=receiver.secret_key.encode("utf-8"),
115 | msg=request.body,
116 | digestmod=getattr(hashlib, hash_algorithm),
117 | ).hexdigest()
118 |
119 | return hmac.compare_digest(
120 | hmac_digest.encode("utf-8"), hmac_header.encode("utf-8")
121 | )
122 |
123 | else:
124 | return False
125 |
126 |
127 | class DefaultUserRequest:
128 | """
129 | Netbox Job model requres valid user object when queueing datasource sync.
130 | Hardcoding default admin user
131 | """
132 |
133 | def __init__(self, userid=1):
134 | self.user = User.objects.get(id=userid)
135 |
--------------------------------------------------------------------------------
/netbox_webhook_receiver/views/webhookreceiver.py:
--------------------------------------------------------------------------------
1 | from netbox.views import generic
2 | from .. import filtersets, forms, models, tables
3 |
4 |
5 | class WebhookReceiverView(generic.ObjectView):
6 | queryset = models.WebhookReceiver.objects.all()
7 | # template_name = "netbox_webhook_receiver/webhookreceiver.html"
8 |
9 |
10 | class WebhookReceiverListView(generic.ObjectListView):
11 | queryset = models.WebhookReceiver.objects.all()
12 | table = tables.WebhookReceiverTable
13 | filterset = filtersets.WebhookReceiverFilterSet
14 | filterset_form = forms.WebhookReceiverFilterForm
15 |
16 |
17 | class WebhookReceiverEditView(generic.ObjectEditView):
18 | queryset = models.WebhookReceiver.objects.all()
19 | form = forms.WebhookReceiverForm
20 |
21 |
22 | class WebhookReceiverDeleteView(generic.ObjectDeleteView):
23 | queryset = models.WebhookReceiver.objects.all()
24 |
--------------------------------------------------------------------------------
/netbox_webhook_receiver/views/webhookreceivergroup.py:
--------------------------------------------------------------------------------
1 | from django.db.models import Count
2 | from netbox.views import generic
3 | from .. import filtersets, forms, models, tables
4 |
5 |
6 | class WebhookReceiverGroupView(generic.ObjectView):
7 | queryset = models.WebhookReceiverGroup.objects.all()
8 |
9 | def get_extra_context(self, request, instance):
10 | table = tables.WebhookReceiverTable(instance.receivers.all())
11 | table.configure(request)
12 | return {
13 | "receivers_table": table,
14 | }
15 |
16 |
17 | class WebhookReceiverGroupListView(generic.ObjectListView):
18 | queryset = models.WebhookReceiverGroup.objects.all()
19 | table = tables.WebhookReceiverGroupTable
20 | filterset = filtersets.WebhookReceiverGroupFilterSet
21 | filterset_form = forms.WebhookReceiverGroupFilterForm
22 | queryset = models.WebhookReceiverGroup.objects.annotate(
23 | receivers_count=Count("receivers")
24 | )
25 |
26 |
27 | class WebhookReceiverGroupEditView(generic.ObjectEditView):
28 | queryset = models.WebhookReceiverGroup.objects.all()
29 | form = forms.WebhookReceiverGroupForm
30 |
31 |
32 | class WebhookReceiverGroupDeleteView(generic.ObjectDeleteView):
33 | queryset = models.WebhookReceiverGroup.objects.all()
34 |
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [tool.poetry]
2 | name = "netbox-plugin-webhook-receiver"
3 | version = "0.5.0"
4 | description = "NetBox Webhook is a plugin for managing webhook receivers in NetBox."
5 | authors = ["Łukasz Polański "]
6 | homepage = "https://github.com/LuPo/netbox-plugin-webhook-receiver"
7 | repository = "https://github.com/LuPo/netbox-plugin-webhook-receiver"
8 | license = "MIT"
9 | readme = "README.md"
10 | packages = [{include = "netbox_webhook_receiver"}]
11 | exclude = ["netbox_webhook_receiver/tests/*"]
12 | keywords = ["netbox", "netbox-plugin", "webhook"]
13 |
14 | [tool.poetry.dependencies]
15 | python = "^3.10"
16 |
17 | [tool.poetry.group.dev.dependencies]
18 | pytest = ">=8.1.0"
19 | black = "^24.0.0"
20 |
21 | [build-system]
22 | requires = ["poetry-core"]
23 | build-backend = "poetry.core.masonry.api"
24 |
--------------------------------------------------------------------------------