Skip to content

Commit ee6bc31

Browse files
committed
Custom feeds
Signed-off-by: U039b <esther@pts-project.org>
1 parent 8144575 commit ee6bc31

9 files changed

Lines changed: 239 additions & 39 deletions

File tree

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
# Generated by Django 4.2.20 on 2025-11-18 13:13
2+
3+
from django.db import migrations, models
4+
import django.db.models.deletion
5+
6+
7+
class Migration(migrations.Migration):
8+
9+
dependencies = [
10+
("core", "0065_rename_in_errors_feedtemplate_in_error_and_more"),
11+
]
12+
13+
operations = [
14+
migrations.AlterField(
15+
model_name="feedtemplate",
16+
name="case",
17+
field=models.ForeignKey(
18+
on_delete=django.db.models.deletion.CASCADE,
19+
related_name="%(app_label)s_%(class)s_related",
20+
related_query_name="%(app_label)s_%(class)ss",
21+
to="core.case",
22+
),
23+
),
24+
migrations.AlterField(
25+
model_name="feedtemplate",
26+
name="visibility",
27+
field=models.CharField(
28+
choices=[("Cases", "Cases"), ("Public", "Public")],
29+
default="Cases",
30+
help_text="The visibility of the template, either limited to the case, to the teams or public",
31+
max_length=6,
32+
verbose_name="Visibility",
33+
),
34+
),
35+
migrations.CreateModel(
36+
name="CustomExportFeed",
37+
fields=[
38+
(
39+
"outgoingfeed_ptr",
40+
models.OneToOneField(
41+
auto_created=True,
42+
on_delete=django.db.models.deletion.CASCADE,
43+
parent_link=True,
44+
primary_key=True,
45+
serialize=False,
46+
to="core.outgoingfeed",
47+
),
48+
),
49+
("template", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="core.feedtemplate")),
50+
],
51+
bases=("core.outgoingfeed",),
52+
),
53+
]

colander/core/models.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2051,6 +2051,9 @@ def get_cases_templates_qs(cls, cases):
20512051
filter(case__in=cases).
20522052
order_by('name'))
20532053

2054+
def __str__(self):
2055+
return self.name
2056+
20542057
def render(self, feed: dict) -> str:
20552058
"""
20562059
Renders the content of an InternalFeed object using a specified template.

colander/core/views/export_feeds_views.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,57 @@ def delete_detection_rule_export_feed_view(request, pk):
8080
return redirect("feeds_detection_rule_out_feed_create_view", case_id=request.contextual_case.id)
8181

8282

83+
class CustomExportFeedCreateView(LoginRequiredMixin, CaseContextMixin, CreateView):
84+
model = CustomExportFeed
85+
template_name = 'pages/feeds/custom_out_feeds.html'
86+
contextual_success_url = 'feeds_custom_out_feed_create_view'
87+
fields = [
88+
'name',
89+
'description',
90+
'template',
91+
'secret',
92+
]
93+
case_required_message_action = "create detection rule outgoing feed"
94+
95+
def get_form(self, form_class=None):
96+
form = super(CustomExportFeedCreateView, self).get_form(form_class)
97+
form.fields['description'].widget = Textarea(attrs={'rows': 2, 'cols': 20})
98+
return form
99+
100+
def form_valid(self, form):
101+
if form.is_valid() and self.active_case:
102+
feed = form.save(commit=False)
103+
if not hasattr(feed, 'owner'):
104+
feed.owner = self.request.user
105+
feed.case = self.active_case
106+
feed.save()
107+
form.save_m2m()
108+
return super().form_valid(form)
109+
110+
def get_context_data(self, **kwargs):
111+
ctx = super().get_context_data(**kwargs)
112+
ctx['feeds'] = CustomExportFeed.get_user_custom_out_feeds(self.request.user, self.active_case)
113+
ctx['is_editing'] = False
114+
return ctx
115+
116+
117+
class CustomExportFeedUpdateView(CustomExportFeedCreateView, UpdateView):
118+
case_required_message_action = "update feed"
119+
120+
def get_context_data(self, **kwargs):
121+
ctx = super().get_context_data(**kwargs)
122+
ctx['feeds'] = CustomExportFeed.get_user_custom_out_feeds(self.request.user, self.active_case)
123+
ctx['is_editing'] = True
124+
return ctx
125+
126+
127+
@login_required
128+
def delete_custom_export_feed_view(request, pk):
129+
obj = CustomExportFeed.objects.get(id=pk)
130+
obj.delete()
131+
return redirect("feeds_custom_out_feed_create_view", case_id=request.contextual_case.id)
132+
133+
83134
class FeedTemplateCreateView(LoginRequiredMixin, CaseContextMixin, CreateView):
84135
model = FeedTemplate
85136
template_name = 'pages/feeds/template.html'

colander/core/views/views.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
colander_models,
2828
color_scheme,
2929
icons, Observable, Actor, Artifact, DataFragment, DetectionRule, Device, Event,
30-
Threat,
30+
Threat, CustomExportFeed,
3131
)
3232
from colander.core.templatetags.colander_tags import model_name
3333

@@ -489,6 +489,7 @@ def feeds_view(request):
489489
feeds = []
490490
feeds.extend(DetectionRuleExportFeed.get_user_detection_rule_out_feeds(request.user, request.contextual_case))
491491
feeds.extend(EntityExportFeed.get_user_entity_out_feeds(request.user, request.contextual_case))
492+
feeds.extend(CustomExportFeed.get_user_custom_out_feeds(request.user, request.contextual_case))
492493
ctx = {
493494
'feeds': feeds
494495
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{% load i18n %}
2+
3+
4+
<div class="btn-group" role="group">
5+
<a href="{% url "feeds_custom_out_feed_update_view" case_id=feed.case.id pk=feed.id %}"
6+
class="btn btn-sm btn-primary {{ btn_class }}">
7+
<i class="nf nf-fa-edit"></i>
8+
{% translate "Edit" %}
9+
</a>
10+
<a href="{% url "feeds_custom_out_feed_delete_view" case_id=feed.case.id pk=feed.id %}"
11+
class="delete-entity-btn btn-sm btn btn-danger text-white {{ btn_class }}"
12+
is="vue:ConfirmButton">
13+
<i class="nf nf-fa-trash"></i>
14+
Delete
15+
</a>
16+
</div>
Lines changed: 47 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
{% load i18n %}
1+
{% load i18n colander_tags %}
22

33

44
<div class="card m-1 shadow-sm bg-secondary-light border-secondary">
@@ -7,18 +7,17 @@
77
<div class="col-md-10 text-truncate border-end border-secondary">
88
<div class="border-bottom border-secondary">
99
<h4>
10-
{% if feed.feed_type == "detection_rules" %}
11-
<i class="nf {{ feed.content_type.nf_icon }} h4 text-primary"></i>
12-
{% else %}
13-
<i class="nf nf-md-code_json h4 text-primary"></i>
14-
{% endif %}
10+
<i class="nf nf-fae-file_export h4 text-primary"></i>
1511
{{ feed.name }}
1612
</h4>
1713
<span class="font-monospace">
1814
{% if feed.feed_type == "detection_rules" %}
1915
{{ request.scheme }}://{{ request.META.HTTP_HOST }}/feed/{{ feed.feed_type }}/{{ feed.id }}
20-
{% else %}
21-
{{ request.scheme }}://{{ request.META.HTTP_HOST }}/feed/{{ feed.feed_type }}/{{ feed.id }}?format=[json|misp|stix2|csv|mermaid|dot]
16+
{% elif feed.feed_type == "entities" %}
17+
{{ request.scheme }}://{{ request.META.HTTP_HOST }}/feed/{{ feed.feed_type }}/{{ feed.id }}
18+
?format=[json|misp|stix2|csv|mermaid|dot]
19+
{% elif feed.feed_type == "custom" %}
20+
{{ request.scheme }}://{{ request.META.HTTP_HOST }}/feed/{{ feed.feed_type }}/{{ feed.id }}
2221
{% endif %}
2322
</span>
2423
</div>
@@ -41,24 +40,34 @@ <h4>
4140
</div>
4241
</div>
4342
<div class="col-md-9">
44-
Get feed information:
45-
<p class="font-monospace small bg-dark text-white p-2 mt-1 mb-1 rounded-2 shadow text-wrap">
46-
curl {{ request.scheme }}://{{ request.META.HTTP_HOST }}/feed/{{ feed.feed_type }}/{{ feed.id }}?secret={{ feed.secret }}&info
47-
</p>
43+
{% if feed.feed_type == "custom" %}
44+
Get feed content:
45+
<p class="font-monospace small bg-dark text-white p-2 mt-1 mb-1 rounded-2 shadow text-wrap">
46+
curl {{ request.scheme }}://{{ request.META.HTTP_HOST }}/feed/{{ feed.feed_type }}/{{ feed.id }}?secret={{ feed.secret }}
47+
</p>
48+
{% else %}
49+
Get feed information:
50+
<p class="font-monospace small bg-dark text-white p-2 mt-1 mb-1 rounded-2 shadow text-wrap">
51+
curl {{ request.scheme }}://{{ request.META.HTTP_HOST }}/feed/{{ feed.feed_type }}/{{ feed.id }}?secret={{ feed.secret }}&info
52+
</p>
53+
{% endif %}
4854
{% if feed.content_type.short_name == "YARA" %}
49-
Download <a href="https://yara.readthedocs.io/en/stable/index.html" title="Yara documentation" target="_blank">Yara</a> rules:
55+
Download<a href="https://yara.readthedocs.io/en/stable/index.html" title="Yara documentation"
56+
target="_blank">Yara</a> rules:
5057
<p class="font-monospace small bg-dark text-white p-2 mt-1 mb-1 rounded-2 shadow text-wrap">
5158
curl -H "X-Colander-Feed: Secret {{ feed.secret }}" \ <br>
5259
-o Colander_{{ feed.name|slugify }}_{{ feed.id }}.yar \ <br>
5360
{{ request.scheme }}://{{ request.META.HTTP_HOST }}/feed/{{ feed.feed_type }}/{{ feed.id }}
5461
</p>
5562
{% endif %}
5663
{% if feed.content_type.short_name == "SURICATA" %}
57-
{% url "collaborate_detection_rule_out_feed_view-rules" pk=feed.id as feed_url %}
64+
{% url "detection_rule_out_feed_view-rules" pk=feed.id as feed_url %}
5865

59-
Download rules and use with <a href="https://suricata.io/" title="Suricata documentation" target="_blank">Suricata</a>:
66+
Download rules and use with
67+
<a href="https://suricata.io/" title="Suricata documentation" target="_blank">Suricata</a>:
6068
<p class="font-monospace small bg-dark text-white p-2 mt-1 mb-1 rounded-2 shadow text-wrap">
61-
suricata-update add-source --no-checksum colander/{{ feed.name|slugify }} {{ request.scheme }}://{{ request.META.HTTP_HOST }}{{ feed_url }}?secret={{ feed.secret }}<br>
69+
suricata-update add-source --no-checksum
70+
colander/{{ feed.name|slugify }} {{ request.scheme }}://{{ request.META.HTTP_HOST }}{{ feed_url }}?secret={{ feed.secret }}<br>
6271
suricata-update<br>
6372
suricatasc -c reload-rules
6473
</p>
@@ -70,11 +79,13 @@ <h4>
7079
-o entities-{{ feed.id }}.json \ <br>
7180
{{ request.scheme }}://{{ request.META.HTTP_HOST }}/feed/{{ feed.feed_type }}/{{ feed.id }}
7281
</p>
73-
Download as a STIX2 file and use with <a href="https://docs.mvt.re" title="MVT documentation" target="_blank">mvt</a>:
82+
Download as a STIX2 file and use with
83+
<a href="https://docs.mvt.re" title="MVT documentation" target="_blank">mvt</a>:
7484
<p class="font-monospace small bg-dark text-white p-2 mt-1 mb-1 rounded-2 shadow text-wrap">
7585
curl -H "X-Colander-Feed: Secret {{ feed.secret }}" \ <br>
7686
-o ~/entities-{{ feed.id }}.stix2 \ <br>
77-
{{ request.scheme }}://{{ request.META.HTTP_HOST }}/feed/{{ feed.feed_type }}/{{ feed.id }}?format=stix2 <br>
87+
{{ request.scheme }}://{{ request.META.HTTP_HOST }}/feed/{{ feed.feed_type }}/{{ feed.id }}?format=stix2
88+
<br>
7889
mvt-[ios|android] check-backup --iocs ~/entities-{{ feed.id }}.stix2 [...]
7990
</p>
8091
{% endif %}
@@ -84,18 +95,21 @@ <h4>
8495
<div class="col-md-2 text-truncate ">
8596
<div class="row">
8697
<div class="col">
87-
{% include "tlp/badge_tlp.html" with tlp=feed.max_tlp c="small" %}
88-
{% include "tlp/badge_pap.html" with pap=feed.max_pap c="small" %}
89-
{% if feed.feed_type == "detection_rules" %}
90-
<div class="small">
91-
<i class="nf nf-fa-filter"></i> {{ feed.content_type.name }}
92-
</div>
93-
{% else %}
94-
{% for t in feed.content_type.all %}
95-
<div class="small">
96-
<i class="nf nf-fa-filter"></i> {{ t.name }}
97-
</div>
98-
{% endfor %}
98+
<div class="text-muted">{{ feed.feed_type|to_title }} export</div>
99+
{% if feed.feed_type != "custom" %}
100+
{% include "tlp/badge_tlp.html" with tlp=feed.max_tlp c="small" %}
101+
{% include "tlp/badge_pap.html" with pap=feed.max_pap c="small" %}
102+
{% if feed.feed_type == "detection_rules" %}
103+
<div class="small">
104+
<i class="nf nf-fa-filter"></i> {{ feed.content_type.name }}
105+
</div>
106+
{% else %}
107+
{% for t in feed.content_type.all %}
108+
<div class="small">
109+
<i class="nf nf-fa-filter"></i> {{ t.name }}
110+
</div>
111+
{% endfor %}
112+
{% endif %}
99113
{% endif %}
100114
<div class="small">
101115
<i class="nf nf-fa-lock"></i> <code>{{ feed.secret }}</code>
@@ -108,8 +122,10 @@ <h4>
108122
<div class="card-footer bg-transparent border-secondary m-0 p-1 text-end">
109123
{% if feed.feed_type == "detection_rules" %}
110124
{% include "feed/detection_rule_out_feed_controls.html" with exclude="" btn_class="btn-sm" %}
111-
{% else %}
125+
{% elif feed.feed_type == "entities" %}
112126
{% include "feed/entity_out_feed_controls.html" with exclude="" btn_class="btn-sm" %}
127+
{% elif feed.feed_type == "custom" %}
128+
{% include "feed/custom_out_feed_controls.html" with exclude="" btn_class="btn-sm" %}
113129
{% endif %}
114130
</div>
115131
</div>

colander/templates/pages/feeds/base.html

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,24 +3,32 @@
33

44
{% block feeds-sidebar-items %}
55
{% url 'feeds_detection_rule_out_feed_create_view' case_id=contextual_case.id as feeds_detection_rule_out_feed_create_view_url %}
6+
{% url 'feeds_custom_out_feed_create_view' case_id=contextual_case.id as feeds_custom_out_feed_create_view_url %}
67
{% url 'feeds_entity_out_feed_create_view' case_id=contextual_case.id as feeds_entity_out_feed_create_view_url %}
78
{% url 'feeds_template_create_view' case_id=contextual_case.id as feeds_template_create_view_url %}
89
<a
910
class="list-group-item list-group-item-action list-group-item-light{% if feeds_entity_out_feed_create_view_url in request.path %} active {% endif %}"
1011
href="{{ feeds_entity_out_feed_create_view_url }}" title="Entities export feeds">
11-
<i class="nf nf-md-code_json"></i>
12+
<i class="nf nf-fae-file_export"></i>
1213
<span class="list-group-item-label">Entities</span>
1314
</a>
1415
<a
1516
class="list-group-item list-group-item-action list-group-item-light{% if feeds_detection_rule_out_feed_create_view_url in request.path %} active {% endif %}"
1617
href="{{ feeds_detection_rule_out_feed_create_view_url }}" title="Detection rules export feeds">
17-
{% include "icons/out_feed_icon.html" %}
18+
<i class="nf nf-fae-file_export"></i>
1819
<span class="list-group-item-label">Detection rules</span>
1920
</a>
21+
<a
22+
class="list-group-item list-group-item-action list-group-item-light{% if feeds_custom_out_feed_create_view_url in request.path %} active {% endif %}"
23+
href="{{ feeds_custom_out_feed_create_view_url }}" title="Custom export feeds">
24+
<i class="nf nf-fae-file_export"></i>
25+
<span class="list-group-item-label">Custom</span>
26+
</a>
2027
<a
2128
class="list-group-item list-group-item-action list-group-item-light{% if feeds_template_create_view_url in request.path %} active {% endif %}"
2229
href="{{ feeds_template_create_view_url }}" title="Templates">
23-
<span class="list-group-item-label fst-italic">Edit feed templates</span>
30+
<i class="nf nf-fa-code"></i>
31+
<span class="list-group-item-label">Feed templates</span>
2432
</a>
2533
{% endblock feeds-sidebar-items %}
2634

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
{% extends "pages/feeds/base.html" %}
2+
{% load crispy_forms_tags %}
3+
{% load crispy_forms_field %}
4+
{% load i18n %}
5+
6+
7+
{% block inner-content %}
8+
<div class="row justify-content-center">
9+
<div class="col-md-12 mt-2">
10+
{% if is_editing %}
11+
<h2>{% translate "Edit this feed" %}</h2>
12+
{% else %}
13+
<h2>{% translate "New feed" %}</h2>
14+
{% endif %}
15+
<div class="card mb-4 bg-secondary-light">
16+
<div class="card-body">
17+
<form method="post">
18+
{% csrf_token %}
19+
<div class="row justify-content-center">
20+
<div class="col">
21+
{{ form.name|as_crispy_field }}
22+
{{ form.description|as_crispy_field }}
23+
{{ form.template|as_crispy_field }}
24+
{{ form.secret|as_crispy_field }}
25+
{% if is_editing %}
26+
<button class="btn btn-primary" type="submit" name="save_feed">{% translate "Update" %}</button>
27+
{% else %}
28+
<button class="btn btn-primary" type="submit" name="save_feed">{% translate "Create" %}</button>
29+
{% endif %}
30+
</div>
31+
</form>
32+
</div>
33+
</div>
34+
</div>
35+
</div>
36+
<div class="row justify-content-center mt-4">
37+
<div class="col-md-12">
38+
<h2>{% translate "Latest feeds" %}</h2>
39+
{% if feeds %}
40+
{% for feed in feeds %}
41+
{% include "feed/list_item.html" %}
42+
{% endfor %}
43+
{% endif %}
44+
</div>
45+
</div>
46+
{% endblock inner-content %}

0 commit comments

Comments
 (0)