Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
133 commits
Select commit Hold shift + click to select a range
ce4c69f
fix(api): return 400 Bad Request for unknown statistics pk values (#1…
RicardoMacedo-prj May 21, 2026
8443fa6
Fix/1351 improve 404 page (#1361)
suvani-ctrl May 24, 2026
c5cdec1
Deduplicate IOC identities and enforce uniqueness. Closes #1240 (#1344)
cclts May 25, 2026
3be13a3
Make pending migration
regulartim May 25, 2026
a55edc4
feat: add APISource model and extend Sensor model with admin view. Cl…
drona-gyawali May 26, 2026
8784b2d
fix(news-widget): enforce strict UTC date parsing to prevent timezone…
suvani-ctrl May 26, 2026
9325aa4
IOC feed index tuning. Closes #1368 (#1370)
cclts May 27, 2026
9778627
build(deps): bump library/nginx in /docker (#1371)
dependabot[bot] May 27, 2026
005ff08
build(deps-dev): bump @vitest/coverage-v8 in /frontend (#1372)
dependabot[bot] May 27, 2026
c4e33fd
build(deps): bump sass from 1.99.0 to 1.100.0 in /frontend (#1373)
dependabot[bot] May 27, 2026
b79b5ba
build(deps-dev): bump vite from 8.0.13 to 8.0.14 in /frontend (#1374)
dependabot[bot] May 27, 2026
2a5446c
build(deps-dev): bump ruff from 0.15.13 to 0.15.14 (#1378)
dependabot[bot] May 27, 2026
012dc62
build(deps-dev): bump coverage from 7.14.0 to 7.14.1 (#1379)
dependabot[bot] May 27, 2026
2b54ac7
build(deps): bump elasticsearch from 9.4.0 to 9.4.1 (#1377)
dependabot[bot] May 27, 2026
abf5f5f
build(deps-dev): bump stylelint from 17.11.1 to 17.12.0 in /frontend …
dependabot[bot] May 27, 2026
20290e6
feat: Sensor creation API and Testcases. Closes #1359 (#1369)
drona-gyawali May 28, 2026
6a607e2
Move unit tests for is_valid_cidr, is_valid_ipv4, and get_ioc_type to…
suvani-ctrl Jun 1, 2026
19bdd46
feat: add EventStatus and RawEvent models with admin integration. Cl…
drona-gyawali Jun 2, 2026
5601cea
Refactor dashboard: widgetRegistry & react-grid-layout integration. C…
armoredvortex Jun 5, 2026
809b029
build(deps): bump axios from 1.16.1 to 1.17.0 in /frontend (#1390)
dependabot[bot] Jun 5, 2026
d1ed0e8
build(deps-dev): bump @vitest/coverage-v8 in /frontend (#1391)
dependabot[bot] Jun 5, 2026
ecbc5c3
build(deps-dev): bump vitest from 4.1.7 to 4.1.8 in /frontend (#1392)
dependabot[bot] Jun 5, 2026
65032de
build(deps): bump scikit-learn from 1.8.0 to 1.9.0 (#1395)
dependabot[bot] Jun 5, 2026
0f5280f
build(deps-dev): bump ruff from 0.15.14 to 0.15.15 (#1396)
dependabot[bot] Jun 5, 2026
c2f3188
build(deps): bump @vnedyalk0v/react19-simple-maps in /frontend (#1393)
dependabot[bot] Jun 5, 2026
982a786
build(deps-dev): bump vite from 8.0.14 to 8.0.16 in /frontend (#1394)
dependabot[bot] Jun 5, 2026
6d3c34d
build(deps-dev): bump ruff from 0.15.15 to 0.15.16 (#1402)
dependabot[bot] Jun 10, 2026
31d475c
build(deps-dev): bump prettier from 3.8.3 to 3.8.4 in /frontend (#1401)
dependabot[bot] Jun 10, 2026
0840239
build(deps-dev): bump stylelint from 17.12.0 to 17.13.0 in /frontend …
dependabot[bot] Jun 10, 2026
b40af3b
feat(cronjobs): add shared HTTP client wrapper. Refs #1214 (#1253)
Prasad8830 Jun 11, 2026
9bb23aa
Add login rate limiting. Closes #1383 (#1386)
cclts Jun 11, 2026
e9af7d0
Limit NewsWidget posts. Closes #1384 (#1389)
suvani-ctrl Jun 11, 2026
7689f19
feat: event ingestion API with asynchronous IOC processing pipeline .…
drona-gyawali Jun 11, 2026
5cee040
Feeds API Refactoring. Closes #1092 and #1350 (#1388)
regulartim Jun 12, 2026
bd52ae1
Refactor Dashboard: extract shared useWidgetData hook. Closes #1398, …
armoredvortex Jun 15, 2026
56e3b7f
fix: ensure shared secret key across containers for secure task signi…
drona-gyawali Jun 15, 2026
e2c8118
refactor(cronjobs): migrate tor_exit_nodes and whatsmyip to HttpClien…
Prasad8830 Jun 15, 2026
19f09cd
refactor(cronjobs): migrate mass_scanners, firehol, and spamhaus_drop…
Prasad8830 Jun 15, 2026
7e917ca
Refactor update_db to use Django iterator and batch bulk_update. Clos…
manik3160 Jun 16, 2026
ee0364b
build(deps): bump react-use from 17.6.0 to 17.6.1 in /frontend (#1412)
dependabot[bot] Jun 17, 2026
d49aee1
build(deps): bump axios from 1.17.0 to 1.18.0 in /frontend (#1413)
dependabot[bot] Jun 17, 2026
848db75
build(deps-dev): bump @vitest/coverage-v8 in /frontend (#1414)
dependabot[bot] Jun 17, 2026
0514d4b
build(deps-dev): bump ruff from 0.15.16 to 0.15.17 (#1417)
dependabot[bot] Jun 17, 2026
312b10a
build(deps-dev): bump vitest from 4.1.8 to 4.1.9 in /frontend (#1416)
dependabot[bot] Jun 17, 2026
dd21005
build(deps): bump sass from 1.100.0 to 1.101.0 in /frontend (#1415)
dependabot[bot] Jun 17, 2026
d163889
refactor(cronjobs): migrate threatfox_feed, abuseipdb_feed, and extra…
Prasad8830 Jun 22, 2026
8775037
feat: Periodic reset for invalid_event_count . Closes #1420 (#1423)
drona-gyawali Jun 23, 2026
8f4b546
refactor(fetchers): migrate ntfy and get_greedybear_news to HttpClien…
Prasad8830 Jun 23, 2026
753e3bd
build(deps-dev): bump ruff from 0.15.17 to 0.15.18 (#1432)
dependabot[bot] Jun 23, 2026
d00dbcd
build(deps-dev): bump coverage from 7.14.1 to 7.14.3 (#1431)
dependabot[bot] Jun 23, 2026
0ac37b7
build(deps): bump axios from 1.18.0 to 1.18.1 in /frontend (#1429)
dependabot[bot] Jun 23, 2026
85c8e04
build(deps): bump library/nginx in /docker (#1426)
dependabot[bot] Jun 23, 2026
fe83279
build(deps-dev): bump @vitejs/plugin-react in /frontend (#1428)
dependabot[bot] Jun 23, 2026
f2b883e
build(deps): bump numpy from 2.4.6 to 2.5.0 (#1430)
dependabot[bot] Jun 23, 2026
9d46c5e
build(deps-dev): bump vite from 8.0.16 to 8.1.0 in /frontend (#1427)
dependabot[bot] Jun 23, 2026
3efd450
Feat: Add API endpoint for trending attackers. Progresses #1071 (#1338)
rootp1 Jun 25, 2026
7291a02
feat: introduce HoneypotPayload model with custom quarantine storage …
opbot-xd Jun 29, 2026
b81965b
build(deps): bump slack-sdk from 3.42.0 to 3.43.0 (#1441)
dependabot[bot] Jul 2, 2026
0d4a8ee
build(deps-dev): bump ruff from 0.15.18 to 0.15.20 (#1440)
dependabot[bot] Jul 2, 2026
c58b7d4
build(deps-dev): bump vite from 8.1.0 to 8.1.2 in /frontend (#1439)
dependabot[bot] Jul 2, 2026
a4ad059
build(deps-dev): bump prettier from 3.8.4 to 3.9.4 in /frontend (#1438)
dependabot[bot] Jul 2, 2026
e63cb41
build(deps-dev): bump stylelint from 17.13.0 to 17.14.0 in /frontend …
dependabot[bot] Jul 2, 2026
db3cccd
Implement NDJSON renderer for feed endpoints.Closes #1408 (#1419)
suvani-ctrl Jul 2, 2026
eed8997
feat: implement retention-based cleanup for RawEvent and EventStatus …
drona-gyawali Jul 2, 2026
88768ae
Implement Django Q2 Task for Payload Extraction. Closes #1404 (#1435)
opbot-xd Jul 3, 2026
d46922b
Frontend for Trending attackers feed. Closes #1071 (#1433)
rootp1 Jul 13, 2026
4071aa0
build(deps): bump croniter from 6.2.2 to 6.2.3 (#1452)
dependabot[bot] Jul 13, 2026
222dbe1
build(deps): bump drf-spectacular from 0.29.0 to 0.30.0 (#1450)
dependabot[bot] Jul 13, 2026
c4656a2
build(deps): bump numpy from 2.5.0 to 2.5.1 (#1449)
dependabot[bot] Jul 13, 2026
2d51304
build(deps-dev): bump vite from 8.1.2 to 8.1.3 in /frontend (#1446)
dependabot[bot] Jul 13, 2026
6eba08c
build(deps-dev): bump coverage from 7.14.3 to 7.15.0 (#1448)
dependabot[bot] Jul 13, 2026
a36978b
build(deps-dev): bump vitest from 4.1.9 to 4.1.10 in /frontend (#1447)
dependabot[bot] Jul 13, 2026
1aa9757
build(deps-dev): bump @vitest/coverage-v8 in /frontend (#1445)
dependabot[bot] Jul 13, 2026
f48d824
build(deps): bump datasketch from 1.10.0 to 2.0.0 (#1451)
dependabot[bot] Jul 13, 2026
a7c1c4c
Bump gb-ui from 1.0.0 to 1.0.1 (#1453)
regulartim Jul 13, 2026
96cd676
Cache IOC-backed statistics aggregations. Closes #1405 (#1411)
cclts Jul 14, 2026
4d02e41
Bump django from 5.2.14 to 5.2.16
regulartim Jul 14, 2026
3bcb8c4
build(deps): bump croniter from 6.2.3 to 6.2.4 (#1459)
dependabot[bot] Jul 14, 2026
be5641d
build(deps-dev): bump coverage from 7.15.0 to 7.15.1 (#1458)
dependabot[bot] Jul 14, 2026
17e8fad
build(deps): bump django-filter from 25.2 to 26.1 (#1457)
dependabot[bot] Jul 14, 2026
42c9cda
build(deps-dev): bump ruff from 0.15.20 to 0.15.21 (#1456)
dependabot[bot] Jul 14, 2026
f540bf8
build(deps): bump library/nginx in /docker (#1465)
dependabot[bot] Jul 22, 2026
d1bcd51
build(deps-dev): bump prettier from 3.9.5 to 3.9.6 in /frontend (#1466)
dependabot[bot] Jul 22, 2026
0ceb4e7
build(deps): bump sass from 1.101.0 to 1.101.3 in /frontend (#1467)
dependabot[bot] Jul 22, 2026
a3b4a46
build(deps-dev): bump ruff from 0.15.21 to 0.15.22 (#1471)
dependabot[bot] Jul 22, 2026
d135b4e
build(deps-dev): bump coverage from 7.15.1 to 7.15.2 (#1472)
dependabot[bot] Jul 22, 2026
4324259
build(deps-dev): bump vite from 8.1.4 to 8.1.5 in /frontend (#1470)
dependabot[bot] Jul 22, 2026
d121fe2
build(deps-dev): bump stylelint from 17.14.0 to 17.14.1 in /frontend …
dependabot[bot] Jul 22, 2026
3580464
build(deps-dev): bump @testing-library/jest-dom in /frontend (#1468)
dependabot[bot] Jul 22, 2026
9936c8b
feat: Process remaining RawEvent fields into IOC and HoneypotPayload.…
drona-gyawali Jul 22, 2026
a2bd040
bump node dependencies
regulartim Jul 22, 2026
d2b554b
Publish images on GHCR. Progresses #1442 (#1473)
regulartim Jul 22, 2026
a2e13b1
feat: add HoneypotPayloadViewSet with RBAC-gated download. closes #14…
opbot-xd Jul 23, 2026
583d93b
[Dashboard Modularization] Add configEditor enabling drag and drop. C…
armoredvortex Jul 28, 2026
ea17035
build(deps-dev): bump ruff from 0.15.22 to 0.16.0 (#1481)
dependabot[bot] Jul 29, 2026
2de78db
build(deps): bump pandas from 3.0.3 to 3.0.5 (#1480)
dependabot[bot] Jul 29, 2026
b49b5b7
build(deps): bump feedparser from 6.0.12 to 6.0.13 (#1479)
dependabot[bot] Jul 29, 2026
b731c35
build(deps-dev): bump @vitejs/plugin-react in /frontend (#1478)
dependabot[bot] Jul 29, 2026
73164f1
build(deps): bump sass from 1.101.3 to 1.102.0 in /frontend (#1476)
dependabot[bot] Jul 29, 2026
e3d77cf
build(deps-dev): bump jsdom from 29.1.1 to 30.0.1 in /frontend (#1477)
dependabot[bot] Jul 29, 2026
4832696
OpenAPI docs for more endpoints. Progresses #962 (#1488)
regulartim Aug 3, 2026
825ee8c
Improvements to IocModelAdmin
regulartim Aug 4, 2026
7bc1455
Add OpenAPI docs to Event Injection endpoints. Progresses #962 and #1…
regulartim Aug 4, 2026
f5228a1
Add OpenAPI docs to honeypot endpoint. Closes #962 (#1490)
regulartim Aug 4, 2026
e885cde
feat: fill protocols and cve fields in IOC from Tpot-logs . Closes #1…
drona-gyawali Aug 4, 2026
bf54c73
build(deps): bump feedparser from 6.0.13 to 6.0.14 (#1498)
dependabot[bot] Aug 4, 2026
caa27e8
build(deps-dev): bump vite from 8.1.5 to 8.2.0 in /frontend (#1496)
dependabot[bot] Aug 4, 2026
110b0c6
build(deps-dev): bump @vitejs/plugin-react in /frontend (#1495)
dependabot[bot] Aug 4, 2026
16adc6e
build(deps): bump @vnedyalk0v/react19-simple-maps in /frontend (#1494)
dependabot[bot] Aug 4, 2026
aca7cac
build(deps): bump react-grid-layout from 2.2.3 to 2.2.4 in /frontend …
dependabot[bot] Aug 4, 2026
3f0ed1a
build(deps-dev): bump ruff from 0.16.0 to 0.16.1 (#1497)
dependabot[bot] Aug 4, 2026
731c456
build(deps): bump axios from 1.18.1 to 1.19.0 in /frontend (#1492)
dependabot[bot] Aug 5, 2026
27684c6
Bump node dependencies
regulartim Aug 5, 2026
40e344b
feat: integrate MalwareBazaar submission task (#1485)
opbot-xd Aug 5, 2026
0ec0541
refactor: extract stream_ioc_objects generator for lazy NDJSON stream…
suvani-ctrl Aug 6, 2026
5be3d44
[Dashboard modularization] Backend persistence for react-grid-layout.…
armoredvortex Aug 6, 2026
cd5a02d
bump npm dependencies
regulartim Aug 28, 2026
9facb18
build(deps): bump library/nginx in /docker (#1516)
dependabot[bot] Aug 28, 2026
fd2a5a6
build(deps-dev): bump coverage from 7.15.2 to 7.15.4 (#1507)
dependabot[bot] Aug 28, 2026
c409cf2
build(deps): bump djangorestframework from 3.17.1 to 3.18.0 (#1504)
dependabot[bot] Aug 28, 2026
d5c7d3e
build(deps-dev): bump django-test-migrations from 1.5.0 to 1.6.0 (#1505)
dependabot[bot] Aug 28, 2026
871b630
build(deps-dev): bump ruff from 0.16.1 to 0.16.4 (#1506)
dependabot[bot] Aug 28, 2026
1305422
build(deps): bump elasticsearch from 9.4.1 to 9.5.0 (#1503)
dependabot[bot] Aug 28, 2026
e2bd730
fix: update mass scanner URL to new maltrail path. Closes #1521 (#1524)
suvani-ctrl Sep 1, 2026
c653010
bump 3.6.0
regulartim Sep 1, 2026
7db3a37
Merge remote-tracking branch 'origin/main' into develop
regulartim Sep 1, 2026
84b83c3
build(deps): bump gunicorn from 26.0.0 to 26.2.0 (#1525)
dependabot[bot] Sep 1, 2026
87c005d
build(deps): bump numpy from 2.5.1 to 2.5.2 (#1526)
dependabot[bot] Sep 1, 2026
f493ca6
build(deps): bump slack-sdk from 3.43.0 to 3.44.0 (#1527)
dependabot[bot] Sep 1, 2026
4c86341
build(deps-dev): bump coverage from 7.15.4 to 7.16.0 (#1528)
dependabot[bot] Sep 1, 2026
ff3c54f
build(deps): bump django-q2 from 1.10.0 to 1.11.1 (#1529)
dependabot[bot] Sep 1, 2026
17a868b
bump npm dependencies
regulartim Sep 1, 2026
716e452
Merge pull request #1523 from GreedyBear-Project/develop
regulartim Sep 1, 2026
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
12 changes: 12 additions & 0 deletions .github/workflows/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -215,3 +215,15 @@ TODO
## [Reusable release and tag workflow](_release_and_tag.yml)

TODO

## [Build and publish Docker images](docker_publish.yml)

This workflow builds the `greedybear` and `greedybear_nginx` images and publishes them to the **GitHub Container Registry (GHCR)** under `ghcr.io/greedybear-project`, decoupling image hosting from the IntelOwl-owned DockerHub account.

It runs on:

* pushes to **main** — tagged `prod`
* pushes to **develop** — tagged `stag`
* pushes of a **`X.Y.Z` git tag** — tagged with that version

Each image is built via a job matrix and pushed by [**docker/build-push-action**](https://github.com/docker/build-push-action), with tags/labels derived by [**docker/metadata-action**](https://github.com/docker/metadata-action) and layer caching backed by the GitHub Actions cache.
76 changes: 76 additions & 0 deletions .github/workflows/docker_publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
name: Build and publish Docker images

# Publishes the greedybear and greedybear_nginx images to the
# GitHub Container Registry (GHCR) under ghcr.io/greedybear-project.

on:
push:
branches:
- main
- develop
tags:
- "[0-9]+.[0-9]+.[0-9]+"
workflow_dispatch:

# cancel superseded runs for the same ref
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

env:
GHCR_NAMESPACE: ghcr.io/greedybear-project

jobs:
build-and-push:
name: Build and push ${{ matrix.image }}
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
strategy:
fail-fast: false
matrix:
include:
- image: greedybear
dockerfile: docker/Dockerfile
- image: greedybear_nginx
dockerfile: docker/Dockerfile_nginx
steps:
- name: Check out repository
uses: actions/checkout@v7

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4

- name: Log in to GHCR
uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Extract image metadata
id: meta
uses: docker/metadata-action@v6
with:
images: ${{ env.GHCR_NAMESPACE }}/${{ matrix.image }}
# main branch -> prod
# develop branch -> stag
# X.Y.Z git tag -> X.Y.Z
tags: |
type=raw,value=prod,enable=${{ github.ref == 'refs/heads/main' }}
type=raw,value=stag,enable=${{ github.ref == 'refs/heads/develop' }}
type=semver,pattern={{version}}
flavor: |
latest=false

- name: Build and push
uses: docker/build-push-action@v7
with:
context: .
file: ${{ matrix.dockerfile }}
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha,scope=${{ matrix.image }}
cache-to: type=gha,mode=max,scope=${{ matrix.image }}
18 changes: 18 additions & 0 deletions api/apps.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,24 @@
# This file is a part of GreedyBear https://github.com/honeynet/GreedyBear
# See the file 'LICENSE' for copying permission.
from django.apps import AppConfig
from drf_spectacular.extensions import OpenApiAuthenticationExtension


class CookieTokenAuthScheme(OpenApiAuthenticationExtension):
"""Tell drf-spectacular how to represent certego-saas' CookieTokenAuthentication
as an OpenAPI security scheme, so authenticated endpoints document the Token
header (and get an "Authorize" entry in Swagger UI)."""

target_class = "certego_saas.apps.auth.backend.CookieTokenAuthentication"
name = "tokenAuth"

def get_security_definition(self, auto_schema):
return {
"type": "apiKey",
"in": "header",
"name": "Authorization",
"description": "Durin token auth. Use header `Authorization: Token <key>`.",
}


class ApiConfig(AppConfig):
Expand Down
74 changes: 74 additions & 0 deletions api/filters.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
from datetime import timedelta

import django_filters
from django.db.models import Count, QuerySet
from django.utils import timezone

from greedybear.models import IOC


class FeedsFilterSet(django_filters.FilterSet):
asn = django_filters.NumberFilter(field_name="autonomous_system__asn")
min_score = django_filters.NumberFilter(field_name="recurrence_probability", lookup_expr="gte")
min_expected_interactions = django_filters.NumberFilter(field_name="expected_interactions", lookup_expr="gte")
start_date = django_filters.DateFilter(field_name="last_seen", lookup_expr="gte")
end_date = django_filters.DateFilter(field_name="last_seen", lookup_expr="lte")
tag_key = django_filters.CharFilter(field_name="tags__key", lookup_expr="iexact")
tag_value = django_filters.CharFilter(field_name="tags__value", lookup_expr="icontains")

attack_type = django_filters.CharFilter(method="filter_attack_type")
ioc_type = django_filters.CharFilter(method="filter_ioc_type")
port = django_filters.NumberFilter(method="filter_port")
country_code = django_filters.CharFilter(method="filter_country_code")
min_days_seen = django_filters.NumberFilter(method="filter_min_days_seen")
max_age = django_filters.NumberFilter(method="filter_max_age")
include_reputation = django_filters.Filter(method="filter_include_reputation")
min_credential_count = django_filters.NumberFilter(method="filter_min_credential_count")
max_credential_count = django_filters.NumberFilter(method="filter_max_credential_count")

class Meta:
model = IOC
fields = [] # all filters are declared explicitly above

def filter_attack_type(self, queryset: QuerySet, name: str, value: str) -> QuerySet:
if value and value != "all":
return queryset.filter(**{value: True})
return queryset

def filter_ioc_type(self, queryset: QuerySet, name: str, value: str) -> QuerySet:
if value and value != "all":
return queryset.filter(type=value)
return queryset

def filter_port(self, queryset: QuerySet, name: str, value: int) -> QuerySet:
return queryset.filter(destination_ports__contains=[int(value)])

def filter_country_code(self, queryset: QuerySet, name: str, value: str) -> QuerySet:
return queryset.filter(attacker_country_code=value.upper())

def filter_min_days_seen(self, queryset: QuerySet, name: str, value: int) -> QuerySet:
if value and value > 1:
return queryset.filter(number_of_days_seen__gte=value)
return queryset

def filter_include_reputation(self, queryset: QuerySet, name: str, value: list[str]) -> QuerySet:
if value:
return queryset.filter(ip_reputation__in=value)
return queryset

def filter_min_credential_count(self, queryset: QuerySet, name: str, value: int) -> QuerySet:
return self._filter_by_credential_count(queryset, "gte", value)

def filter_max_credential_count(self, queryset: QuerySet, name: str, value: int) -> QuerySet:
return self._filter_by_credential_count(queryset, "lte", value)

def _filter_by_credential_count(self, queryset: QuerySet, lookup: str, value: int) -> QuerySet:
qualifying = IOC.objects.annotate(cc=Count("credentials", distinct=True)).filter(**{f"cc__{lookup}": value}).values("id")
return queryset.filter(id__in=qualifying)

def filter_max_age(self, queryset: QuerySet, name: str, value: int) -> QuerySet:
# drop max_age id an explicit date range replaces is set
if self.data.get("start_date") or self.data.get("end_date"):
return queryset
cutoff = timezone.now() - timedelta(days=int(value))
return queryset.filter(last_seen__gte=cutoff)
89 changes: 89 additions & 0 deletions api/mixins.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import logging
import urllib.parse
from functools import cached_property

from django.http import HttpResponse
from rest_framework.request import Request
from rest_framework.response import Response

from greedybear.cache import Cache, build_versioned_key
from greedybear.consts import API_CACHE_ALIAS, API_CACHE_TIMEOUT_SECONDS, IOC_DATA_VERSION_KEY

logger = logging.getLogger(__name__)


class CachedResponseMixin:
"""Adds versioned response caching to an APIView.
Subclasses opt in by setting cache_namespace."""

cache = Cache(API_CACHE_ALIAS)
cache_namespace: str | None = None
cache_version_key: str = IOC_DATA_VERSION_KEY
cache_timeout: int = API_CACHE_TIMEOUT_SECONDS

@cached_property
def cache_key(self) -> str | None:
"""Versioned cache key for this request, or None when caching is disabled.
Computed once on first access (during the read)
and memoized on the per-request view instance."""
if not self._cache_enabled():
return None
version = self.cache.get_data_version(self.cache_version_key)
sorted_params = sorted(self.request.query_params.lists())
params_string = urllib.parse.urlencode(sorted_params, doseq=True)
key_material = f"{self.__class__.__name__}|{self.request.path}|{params_string}"
return build_versioned_key(self.cache_namespace, version, key_material)

def get_cached_response(self) -> HttpResponse | None:
"""Return a hit, or None when caching is disabled or on a miss."""
if self.cache_key is None:
return None
cached = self.cache.get(self.cache_key)
if cached is None:
return None
return HttpResponse(cached["content"], content_type=cached["content_type"], status=cached["status"])

def finalize_response(self, request, response, *args, **kwargs):
"""Cache the rendered response under the key captured during the read.
Runs after DRF has attached the renderer to the response."""
response = super().finalize_response(request, response, *args, **kwargs)
self._store_api_response(response)
return response

def _cache_enabled(self) -> bool:
"""Determines if cache was enabled
by checking if cache_namespace is set."""
return self.cache_namespace is not None

def _store_api_response(self, response: Response) -> bool:
"""Store a successful (200) and rendered DRF Response under cache_key."""
if self.cache_key is None:
return False
if not isinstance(response, Response):
return False
if response.status_code != 200:
return False
if not response.is_rendered:
response.render()
self.cache.set(
self.cache_key,
{
"content": response.rendered_content,
"content_type": response["content-type"],
"status": response.status_code,
},
timeout=self.cache_timeout,
)
return True


class RequestLoggingMixin:
"""Emit a access-log line per request for any APIView/ViewSet it is mixed into."""

EXCLUDED_LOG_PARAMS = frozenset({"reason"})

def initial(self, request: Request, *args, **kwargs):
route = getattr(getattr(request, "resolver_match", None), "route", None) or request.path.lstrip("/")
params = {k: v for k, v in request.query_params.items() if k not in self.EXCLUDED_LOG_PARAMS}
logger.info(f"request {request.method} /{route} params: {params}")
super().initial(request, *args, **kwargs)
22 changes: 22 additions & 0 deletions api/permissions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# This file is a part of GreedyBear https://github.com/honeynet/GreedyBear
# See the file 'LICENSE' for copying permission.
from rest_framework.permissions import SAFE_METHODS, BasePermission

THREAT_RESEARCHER_GROUP = "threat_researcher"


class IsThreatResearcherOrAdmin(BasePermission):
"""Allow access only to staff users or members of the ``threat_researcher`` group."""

def has_permission(self, request, view):
user = request.user
return user and user.is_authenticated and (user.is_staff or user.groups.filter(name=THREAT_RESEARCHER_GROUP).exists())


class IsSuperuserOrReadOnly(BasePermission):
"""Allow read access to anyone; restrict write access to superusers only."""

def has_permission(self, request, view):
if request.method in SAFE_METHODS:
return True
return request.user and request.user.is_authenticated and request.user.is_superuser
90 changes: 90 additions & 0 deletions api/renderers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import csv
import io

from django.conf import settings
from django.db.models import QuerySet
from rest_framework.renderers import BaseRenderer, JSONRenderer

from api.views.utils import build_feed_dict, build_stix_bundle


class FeedJSONRenderer(JSONRenderer):
"""JSON feed renderer.
When the `build_feed_envelope` context flag is set, it shapes the raw IOC
rows into the {"iocs": [...], "license": ...} envelope.
Any other payload (ASN list, pagination envelope) is rendered as plain JSON.
"""

def render(self, data, accepted_media_type=None, renderer_context=None):
context = renderer_context or {}
if context.get("build_feed_envelope"):
data = build_feed_dict(
data,
verbose=context.get("verbose", False),
include_sensors=context.get("include_sensors", False),
)
return super().render(data, accepted_media_type, renderer_context)


class FeedNDJSONRenderer(BaseRenderer):
media_type = "application/x-ndjson"
format = "ndjson"
charset = "utf-8"

# FeedNDJSONRenderer exists only for DRF content negotiation, actual streaming is handled via StreamingHttpResponse in render_response().
def render(self, data, accepted_media_type=None, renderer_context=None):
return b""


class FeedRendererMixin(BaseRenderer):
"""Shared safety net for the non-JSON feed renderers."""

def render(self, data, accepted_media_type=None, renderer_context=None):
if not isinstance(data, QuerySet):
return JSONRenderer().render(data, accepted_media_type, renderer_context)
return self.render_feed(data, accepted_media_type, renderer_context)

def render_feed(self, data, accepted_media_type=None, renderer_context=None):
raise NotImplementedError


class FeedTextRenderer(FeedRendererMixin):
"""Plain-text feed: one IOC value per line, prefixed by the license comment."""

media_type = "text/plain"
format = "txt"
charset = None

def render_feed(self, data, accepted_media_type=None, renderer_context=None):
lines = [f"# {settings.FEEDS_LICENSE}"] if settings.FEEDS_LICENSE else []
lines += [row[0] for row in data.values_list("name")]
return "\n".join(lines).encode("utf-8")


class FeedCSVRenderer(FeedRendererMixin):
"""CSV feed: one IOC value per row, prefixed by the license comment."""

media_type = "text/csv"
format = "csv"
charset = None

def render_feed(self, data, accepted_media_type=None, renderer_context=None):
buffer = io.StringIO()
writer = csv.writer(buffer, quoting=csv.QUOTE_MINIMAL)
if settings.FEEDS_LICENSE:
buffer.write("# ")
writer.writerow([settings.FEEDS_LICENSE])
writer.writerows([list(row) for row in data.values_list("name")])
return buffer.getvalue().encode("utf-8")


class Stix21Renderer(FeedRendererMixin):
"""STIX 2.1 bundle feed (served as application/json)."""

media_type = "application/json"
format = "stix21"
charset = None

def render_feed(self, data, accepted_media_type=None, renderer_context=None):
request = (renderer_context or {}).get("request")
return build_stix_bundle(data, request=request).encode("utf-8")
Loading
Loading