Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
ae6a01c
removed organization sponsor link
mlodic Apr 1, 2026
0a6560e
build(deps): bump gunicorn from 25.2.0 to 25.3.0 (#1163)
dependabot[bot] Apr 1, 2026
3b207a7
build(deps): bump requests from 2.33.0 to 2.33.1 (#1164)
dependabot[bot] Apr 1, 2026
c454518
build(deps): bump pandas from 3.0.1 to 3.0.2 (#1165)
dependabot[bot] Apr 1, 2026
ed0d621
build(deps): bump numpy from 2.4.3 to 2.4.4 (#1166)
dependabot[bot] Apr 1, 2026
786ff60
All instances don't hit enrichment sources at the same time. Closes #…
rootp1 Apr 1, 2026
037f16b
fix: reject promise on requestPasswordReset failure and add test. Clo…
Deepanshu1230 Apr 1, 2026
2bc0367
perf: cache ASN aggregation results with version-based invalidation. …
opbot-xd Apr 1, 2026
a798c90
fix: replace hardcoded GID 82 with www-data group name . Closes #1162…
drona-gyawali Apr 1, 2026
2489f79
Fix/search help text Closes #1161 (#1172)
Demiserular Apr 1, 2026
ede95fc
feat: extract attacker country code in IOC model. Closes #1160 (#1173)
manik3160 Apr 2, 2026
df13e8e
Refactor: Rename usage of GeneralHoneypot to Honeypot across the code…
TEMHITHORPHE Apr 2, 2026
cc94c28
fix: scan all hits for GeoIP enrichment in iocs_from_hits(). Closes #…
tanmayjoddar Apr 2, 2026
010f3e9
updated readme
mlodic Apr 3, 2026
1cdc0d4
build(deps-dev): bump ruff from 0.15.8 to 0.15.9 (#1187)
dependabot[bot] Apr 7, 2026
b34b83f
build(deps): bump sass from 1.98.0 to 1.99.0 in /frontend (#1186)
dependabot[bot] Apr 7, 2026
84374a2
Refactor: Dashboard Country Normalization. Closes #1175 (#1180)
chauhan-varun Apr 7, 2026
3badb0f
Accessibility improvements using pa11y. Closes #1179 (#1182)
armoredvortex Apr 7, 2026
b0cf561
fix(docker): fix qcluster permission error on mlmodels volume (#1177)
manik3160 Apr 7, 2026
8ec367c
build(deps-dev): bump vite from 8.0.3 to 8.0.7 in /frontend (#1200)
dependabot[bot] Apr 8, 2026
75ad85e
build(deps-dev): bump @vitest/coverage-v8 in /frontend (#1199)
dependabot[bot] Apr 8, 2026
e366ade
build(deps-dev): bump jsdom from 29.0.1 to 29.0.2 in /frontend (#1198)
dependabot[bot] Apr 8, 2026
c535c6e
Revert "build(deps-dev): bump vite from 8.0.3 to 8.0.7 in /frontend (…
regulartim Apr 8, 2026
a3d5bff
replace psycopg2-binary with psycopg[c] (psycopg v3) (#1202)
opbot-xd Apr 8, 2026
c1ed5c8
use os.sched_getaffinity for cgroup-aware CPU count in gunicorn confi…
manik3160 Apr 8, 2026
ccee952
bump 3.3.1
regulartim Apr 8, 2026
7d9ac5a
update uv lock file
regulartim Apr 8, 2026
971c43d
Merge pull request #1203 from GreedyBear-Project/develop
regulartim Apr 9, 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
1 change: 0 additions & 1 deletion .github/FUNDING.yml
Original file line number Diff line number Diff line change
@@ -1,2 +1 @@
open_collective: intelowl-project
github: intelowlproject
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,10 +54,12 @@ To install it locally, Please refer to our [installation guide](https://intelowl

Thanks to [The Honeynet Project](https://www.honeynet.org) we are providing free public feeds available [here](https://greedybear.honeynet.org).

#### DigitalOcean
#### Google Summer of Code
<a href="https://summerofcode.withgoogle.com/"> <img style="border: 0.2px solid black" width=150 height=89 src="static/gsoc_logo.png" alt="GSoC logo"> </a>

In 2022 we joined the official [DigitalOcean Open Source Program](https://www.digitalocean.com/open-source?utm_medium=opensource&utm_source=IntelOwl).
In 2026 we started participating to the [Google Summer of Code](https://summerofcode.withgoogle.com/) (GSoC)!

If you are interested in participating in the next Google Summer of Code, check all the info available in the [dedicated repository](https://github.com/intelowlproject/gsoc)!

## Maintainers and Key Contributors

Expand Down
8 changes: 4 additions & 4 deletions api/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,15 @@
from rest_framework import serializers

from greedybear.consts import REGEX_DOMAIN
from greedybear.models import IOC, GeneralHoneypot, Sensor, Tag
from greedybear.models import IOC, Honeypot, Sensor, Tag
from greedybear.utils import is_ip_address

logger = logging.getLogger(__name__)


class GeneralHoneypotSerializer(serializers.ModelSerializer):
class HoneypotSerializer(serializers.ModelSerializer):
class Meta:
model = GeneralHoneypot
model = Honeypot

def to_representation(self, value):
return value.name
Expand All @@ -33,7 +33,7 @@ class Meta:


class IOCSerializer(serializers.ModelSerializer):
general_honeypot = GeneralHoneypotSerializer(many=True, read_only=True)
general_honeypot = HoneypotSerializer(many=True, read_only=True, source="honeypots")
tags = TagSerializer(many=True, read_only=True)
sensors = SensorSerializer(many=True, read_only=True)

Expand Down
2 changes: 1 addition & 1 deletion api/views/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from api.views.cowrie_session import *
from api.views.enrichment import *
from api.views.feeds import *
from api.views.general_honeypot import *
from api.views.health import *
from api.views.honeypots import *
from api.views.news import *
from api.views.statistics import *
6 changes: 3 additions & 3 deletions api/views/health.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
IOC,
CowrieSession,
FireHolList,
GeneralHoneypot,
Honeypot,
MassScanner,
TorExitNode,
)
Expand Down Expand Up @@ -71,8 +71,8 @@ def get_observables_overview(last_24h):
)

honeypot_stats = {
"total": GeneralHoneypot.objects.count(),
"active": GeneralHoneypot.objects.filter(active=True).count(),
"total": Honeypot.objects.count(),
"active": Honeypot.objects.filter(active=True).count(),
}

threat_list_stats = {
Expand Down
14 changes: 7 additions & 7 deletions api/views/general_honeypot.py → api/views/honeypots.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from rest_framework.response import Response

from greedybear.consts import GET
from greedybear.models import GeneralHoneypot
from greedybear.models import Honeypot

logger = logging.getLogger(__name__)

Expand All @@ -23,14 +23,14 @@ def general_honeypot_list(request):
Response: A JSON response containing the list of general honeypots.
"""

logger.info(f"Requested general honeypots list from {request.user}.")
logger.info(f"Requested honeypots list from {request.user}.")
active = request.query_params.get("onlyActive")
honeypots = []
general_honeypots = GeneralHoneypot.objects.all()
honeypot_objs = Honeypot.objects.all()
if active == "true":
general_honeypots = general_honeypots.filter(active=True)
logger.info(f"Requested only active general honeypots from {request.user}")
honeypots.extend([hp.name for hp in general_honeypots])
honeypot_objs = honeypot_objs.filter(active=True)
logger.info(f"Requested only active honeypots from {request.user}")
honeypots.extend([hp.name for hp in honeypot_objs])

logger.info(f"General honeypots: {honeypots} given back to user {request.user}")
logger.info(f"Honeypots: {honeypots} given back to user {request.user}")
return Response(honeypots)
14 changes: 7 additions & 7 deletions api/views/statistics.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from rest_framework.decorators import action
from rest_framework.response import Response

from greedybear.models import IOC, GeneralHoneypot, Statistics, ViewType
from greedybear.models import IOC, Honeypot, Statistics, ViewType

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -92,7 +92,7 @@ def countries(self, request):
qs = (
IOC.objects.filter(last_seen__gte=delta)
.exclude(attacker_country="")
.filter(general_honeypot__active=True)
.filter(honeypots__active=True)
.values("attacker_country")
.annotate(count=Count("id", distinct=True))
.order_by("-count")
Expand All @@ -103,7 +103,7 @@ def countries(self, request):
@action(detail=False, methods=["get"])
def feeds_types(self, request):
"""
Retrieve statistics for different types of feeds using GeneralHoneypot M2M relationship.
Retrieve statistics for different types of feeds using Honeypot M2M relationship.

Args:
request: The incoming request object.
Expand All @@ -113,10 +113,10 @@ def feeds_types(self, request):
"""
# Build annotations for each active general honeypot
annotations = {}
general_honeypots = GeneralHoneypot.objects.all().filter(active=True)
for hp in general_honeypots:
honeypots = Honeypot.objects.all().filter(active=True)
for hp in honeypots:
# Use M2M relationship instead of boolean fields
annotations[hp.name] = Count("name", distinct=True, filter=Q(general_honeypot__name__iexact=hp.name))
annotations[hp.name] = Count("name", distinct=True, filter=Q(honeypots__name__iexact=hp.name))
return self.__aggregation_response_static_ioc(annotations)

def __aggregation_response_static_statistics(self, annotations: dict) -> Response:
Expand Down Expand Up @@ -147,7 +147,7 @@ def __aggregation_response_static_ioc(self, annotations: dict) -> Response:

qs = (
IOC.objects.filter(last_seen__gte=delta)
.exclude(general_honeypot__active=False)
.exclude(honeypots__active=False)
.annotate(date=Trunc("last_seen", basis))
.values("date")
.annotate(**annotations)
Expand Down
64 changes: 44 additions & 20 deletions api/views/utils.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
# This file is a part of GreedyBear https://github.com/honeynet/GreedyBear
# See the file 'LICENSE' for copying permission.
import csv
import hashlib
import logging
import urllib.parse
from datetime import datetime, timedelta

import feedparser
import requests
from django.conf import settings
from django.contrib.postgres.aggregates import ArrayAgg
from django.core.cache import cache
from django.core.cache import cache, caches
from django.db.models import Count, F, Max, Min, Q, Sum, Value
from django.db.models.functions import JSONObject
from django.http import HttpResponse, HttpResponseBadRequest, StreamingHttpResponse
Expand All @@ -19,7 +21,7 @@
from api.serializers import FeedsRequestSerializer, parse_feed_types
from greedybear.consts import CACHE_KEY_GREEDYBEAR_NEWS, CACHE_TIMEOUT_SECONDS, RSS_FEED_URL
from greedybear.enums import IpReputation
from greedybear.models import IOC, GeneralHoneypot, Statistics
from greedybear.models import IOC, Honeypot, Statistics
from greedybear.utils import is_ip_address, is_valid_domain

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -144,8 +146,8 @@ def get_valid_feed_types() -> frozenset[str]:
Returns:
frozenset[str]: An immutable set of valid feed type strings
"""
general_honeypots = GeneralHoneypot.objects.filter(active=True)
feed_types = ["all"] + [hp.name.lower() for hp in general_honeypots]
honeypots = Honeypot.objects.filter(active=True)
feed_types = ["all"] + [hp.name.lower() for hp in honeypots]
return frozenset(feed_types)


Expand Down Expand Up @@ -226,13 +228,13 @@ def get_queryset(request, feed_params, valid_feed_types, is_aggregated=False, se
if "all" not in feed_params.feed_types:
type_filter = Q()
for ft in feed_params.feed_types:
type_filter |= Q(general_honeypot__name__iexact=ft)
type_filter |= Q(honeypots__name__iexact=ft)
iocs = iocs.filter(type_filter)

# aggregated feeds calculate metrics differently and need all rows to be accurate.
if not is_aggregated:
iocs = iocs.filter(general_honeypot__active=True)
iocs = iocs.annotate(honeypots=ArrayAgg("general_honeypot__name", distinct=True))
iocs = iocs.filter(honeypots__active=True)
iocs = iocs.annotate(honeypot_names=ArrayAgg("honeypots__name", distinct=True))
# Only annotate tags metadata when the response format needs it (e.g. JSON),
# to avoid unnecessary joins and aggregation work for txt/csv feeds.
if getattr(feed_params, "format", "").lower() == "json":
Expand Down Expand Up @@ -315,7 +317,7 @@ def feeds_response(request=None, iocs=None, feed_params=None, valid_feed_types=N
"login_attempts",
"recurrence_probability",
"expected_interactions",
"honeypots", # used to build feed_type; removed from response
"honeypot_names", # used to build feed_type; removed from response
"destination_ports", # used to calculate destination_port_count
"attacker_country",
"autonomous_system",
Expand Down Expand Up @@ -344,7 +346,7 @@ def feeds_response(request=None, iocs=None, feed_params=None, valid_feed_types=N
else:
iocs_iter = iocs.values(*required_fields).iterator(chunk_size=2000)
for ioc in iocs_iter:
ioc_feed_type = [hp.lower() for hp in ioc.get("honeypots", []) if hp]
ioc_feed_type = [hp.lower() for hp in ioc.get("honeypot_names", []) if hp]

data_ = ioc | {
"first_seen": ioc["first_seen"].strftime("%Y-%m-%d"),
Expand All @@ -358,7 +360,7 @@ def feeds_response(request=None, iocs=None, feed_params=None, valid_feed_types=N
if not verbose:
data_.pop("destination_ports", None)
data_.pop("autonomous_system", None)
data_.pop("honeypots", None)
data_.pop("honeypot_names", None)
data_.pop("id", None)

json_list.append(data_)
Expand Down Expand Up @@ -386,7 +388,7 @@ def feeds_response(request=None, iocs=None, feed_params=None, valid_feed_types=N
"first_seen",
"last_seen",
"recurrence_probability",
"honeypots",
"honeypot_names",
"ip_reputation",
}
# Fetch fields from database
Expand Down Expand Up @@ -416,7 +418,7 @@ def feeds_response(request=None, iocs=None, feed_params=None, valid_feed_types=N
confidence = 90

# Labels
labels = [hp.lower() for hp in ioc.get("honeypots", []) if hp]
labels = [hp.lower() for hp in ioc.get("honeypot_names", []) if hp]
if ioc.get("ip_reputation"):
labels.append(ioc["ip_reputation"])

Expand Down Expand Up @@ -446,15 +448,33 @@ def feeds_response(request=None, iocs=None, feed_params=None, valid_feed_types=N

def asn_aggregated_queryset(iocs_qs, request, feed_params):
"""
Perform DB-level aggregation grouped by ASN.
Retrieve ASN aggregation data. Caches the heavy aggregation query
since the data only updates during the extraction cronjob.

Args
iocs_qs (QuerySet): Filtered IOC queryset from get_queryset;
request (Request): The API request object;
feed_params (FeedRequestParams): Validated parameter object

Returns: A values-grouped queryset with annotated metrics and honeypot arrays.
Returns: A list of dicts with aggregated metrics and honeypot arrays per ASN.
"""

# Build reliable cache key from query params
sorted_params = sorted(request.query_params.lists())
params_string = urllib.parse.urlencode(sorted_params, doseq=True)
param_hash = hashlib.sha256(params_string.encode("utf-8")).hexdigest()

# To prevent per-worker continuous RAM bloat, use the shared DB-backed cache
# instead of the default LocMemCache, since the JSON response size can be large.
# The extraction pipeline invalidates this cache by bumping the version counter.
shared_cache = caches["django-q"]
version = shared_cache.get("asn_feeds_version", 1)
cache_key = f"asn_feeds_v{version}_{param_hash}"

cached_result = shared_cache.get(cache_key)
if cached_result is not None:
return cached_result

asn_filter = request.query_params.get("asn")
if asn_filter:
iocs_qs = iocs_qs.filter(autonomous_system__asn=asn_filter)
Expand All @@ -480,31 +500,35 @@ def asn_aggregated_queryset(iocs_qs, request, feed_params):
first_seen=Min("first_seen"),
last_seen=Max("last_seen"),
)
.order_by(ordering)
)
numeric_agg = numeric_agg.order_by(ordering)

# Honeypot names still require a lightweight aggregation because
# they depend on the active flag which can change independently.
honeypot_agg = (
iocs_qs.exclude(autonomous_system__isnull=True)
.filter(general_honeypot__active=True)
.filter(honeypots__active=True)
.values(asn=F("autonomous_system__asn"))
.annotate(
honeypots=ArrayAgg(
"general_honeypot__name",
honeypot_names=ArrayAgg(
"honeypots__name",
distinct=True,
)
)
)

hp_lookup = {row["asn"]: row["honeypots"] or [] for row in honeypot_agg}
hp_lookup = {row["asn"]: row["honeypot_names"] or [] for row in honeypot_agg}

# merging numeric aggregate with honeypot names for each asn
result = []
for row in numeric_agg:
asn = row["asn"]
row_dict = dict(row)
row_dict["honeypots"] = sorted(hp_lookup.get(asn, []))
result.append(row_dict)

# Set cache with a 60-minute timeout (max extraction interval length) to prevent memory bloat
shared_cache.set(cache_key, result, timeout=3600)

return result


Expand Down
4 changes: 2 additions & 2 deletions configuration/gunicorn/config.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import multiprocessing
import os

# Server socket
bind = "unix:/run/gunicorn/main.sock"

# Worker processes
workers = 2 * multiprocessing.cpu_count() + 1
workers = 2 * len(os.sched_getaffinity(0)) + 1
max_requests = 1000
max_requests_jitter = 50

Expand Down
25 changes: 16 additions & 9 deletions docker/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -37,30 +37,37 @@ ENV UV_PROJECT_ENVIRONMENT=/usr/local

WORKDIR $APP_ROOT

# Install runtime dependencies
# - libgomp1 is required for model training
# - curl is used for healthcheck
RUN apt-get update && apt-get install -y --no-install-recommends \
libgomp1 curl gosu \
# Layer 1: stable runtime OS deps — cached across pyproject.toml/uv.lock changes.
# libgomp1: model training; curl: healthcheck; gosu: entrypoint privilege drop
# libpq5: runtime shared library required by the psycopg[c] C extension
RUN apt-get update \
&& apt-get install -y --no-install-recommends libgomp1 curl gosu libpq5 \
&& rm -rf /var/lib/apt/lists/*

# Install python packages
# Layer 2: Python packages — only re-runs when pyproject.toml/uv.lock change.
# Build-only deps (gcc, python3-dev, libpq-dev) compile the psycopg[c] C
# extension and are purged in the same layer to keep the final image lean.
COPY pyproject.toml uv.lock ./
RUN uv sync --no-dev --locked
RUN apt-get update \
&& apt-get install -y --no-install-recommends gcc python3-dev libpq-dev \
&& uv sync --no-dev --locked \
&& uv cache clean \
&& apt-get purge -y gcc python3-dev libpq-dev \
&& apt-get autoremove -y \
&& rm -rf /var/lib/apt/lists/*

# Copy files
COPY . $APP_ROOT
COPY --from=frontend-build /app/build /var/www/reactapp

# separation is required to avoid to re-execute os installation in case of change of python requirements
# Set up log directories, fix permissions, and remove frontend source (served from /var/www/reactapp)
RUN mkdir -p ${LOG_PATH}/django ${LOG_PATH}/gunicorn \
&& touch ${LOG_PATH}/django/api.log ${LOG_PATH}/django/api_errors.log \
&& touch ${LOG_PATH}/django/greedybear.log ${LOG_PATH}/django/greedybear_errors.log \
&& touch ${LOG_PATH}/django/django_q.log ${LOG_PATH}/django/django_q_errors.log \
&& touch ${LOG_PATH}/django/django_errors.log ${LOG_PATH}/django/elasticsearch.log \
&& touch ${LOG_PATH}/django/authentication.log ${LOG_PATH}/django/authentication_errors.log \
&& mkdir -p ${APP_ROOT}/mlmodels \
&& usermod -u 2000 www-data \
&& chown -R www-data:www-data ${LOG_PATH} /opt/deploy/ ${APP_ROOT}/mlmodels/ \
&& rm -rf frontend/

Expand Down
Loading
Loading