From 8c6112f9a143021053e8742cfefdecc1d2937901 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 10 Oct 2025 17:34:51 +0200 Subject: [PATCH 01/15] Bump slack-sdk from 3.36.0 to 3.37.0 in /requirements (#588) Bumps [slack-sdk](https://github.com/slackapi/python-slack-sdk) from 3.36.0 to 3.37.0. - [Release notes](https://github.com/slackapi/python-slack-sdk/releases) - [Commits](https://github.com/slackapi/python-slack-sdk/compare/v3.36.0...v3.37.0) --- updated-dependencies: - dependency-name: slack-sdk dependency-version: 3.37.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements/project-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/project-requirements.txt b/requirements/project-requirements.txt index 252cfc3e..b2e0ca29 100644 --- a/requirements/project-requirements.txt +++ b/requirements/project-requirements.txt @@ -11,7 +11,7 @@ django-ses==4.4.0 psycopg2-binary==2.9.10 certego-saas==0.7.11 -slack-sdk==3.36.0 +slack-sdk==3.37.0 uwsgitop==0.12 uwsgi==2.0.30 From feade6e30b600e7c614af395f15bcf6ca95c330f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 16 Oct 2025 08:35:56 +0200 Subject: [PATCH 02/15] Bump numpy from 2.2.4 to 2.3.3 in /requirements (#586) Bumps [numpy](https://github.com/numpy/numpy) from 2.2.4 to 2.3.3. - [Release notes](https://github.com/numpy/numpy/releases) - [Changelog](https://github.com/numpy/numpy/blob/main/doc/RELEASE_WALKTHROUGH.rst) - [Commits](https://github.com/numpy/numpy/compare/v2.2.4...v2.3.3) --- updated-dependencies: - dependency-name: numpy dependency-version: 2.3.3 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements/project-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/project-requirements.txt b/requirements/project-requirements.txt index b2e0ca29..fd847b32 100644 --- a/requirements/project-requirements.txt +++ b/requirements/project-requirements.txt @@ -19,5 +19,5 @@ uwsgi==2.0.30 joblib==1.5.2 pandas==2.3.3 scikit-learn==1.6.1 -numpy==2.2.4 +numpy==2.3.3 datasketch==1.6.5 From 9dbaf0ba86e4ae36c62b78c646a06c2107715660 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 16 Oct 2025 08:40:45 +0200 Subject: [PATCH 03/15] Bump scikit-learn from 1.6.1 to 1.7.2 in /requirements (#587) Bumps [scikit-learn](https://github.com/scikit-learn/scikit-learn) from 1.6.1 to 1.7.2. - [Release notes](https://github.com/scikit-learn/scikit-learn/releases) - [Commits](https://github.com/scikit-learn/scikit-learn/compare/1.6.1...1.7.2) --- updated-dependencies: - dependency-name: scikit-learn dependency-version: 1.7.2 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements/project-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/project-requirements.txt b/requirements/project-requirements.txt index fd847b32..adb0ef2d 100644 --- a/requirements/project-requirements.txt +++ b/requirements/project-requirements.txt @@ -18,6 +18,6 @@ uwsgi==2.0.30 joblib==1.5.2 pandas==2.3.3 -scikit-learn==1.6.1 +scikit-learn==1.7.2 numpy==2.3.3 datasketch==1.6.5 From 849835e0c661ab1a41a004256c77910326d8b335 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 25 Nov 2025 09:46:13 +0100 Subject: [PATCH 04/15] Bump numpy from 2.3.3 to 2.3.5 in /requirements (#597) Bumps [numpy](https://github.com/numpy/numpy) from 2.3.3 to 2.3.5. - [Release notes](https://github.com/numpy/numpy/releases) - [Changelog](https://github.com/numpy/numpy/blob/main/doc/RELEASE_WALKTHROUGH.rst) - [Commits](https://github.com/numpy/numpy/compare/v2.3.3...v2.3.5) --- updated-dependencies: - dependency-name: numpy dependency-version: 2.3.5 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements/project-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/project-requirements.txt b/requirements/project-requirements.txt index adb0ef2d..aa924a23 100644 --- a/requirements/project-requirements.txt +++ b/requirements/project-requirements.txt @@ -19,5 +19,5 @@ uwsgi==2.0.30 joblib==1.5.2 pandas==2.3.3 scikit-learn==1.7.2 -numpy==2.3.3 +numpy==2.3.5 datasketch==1.6.5 From a4c54f992a1641fb2722be023a76c50ba6847b08 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 25 Nov 2025 09:47:37 +0100 Subject: [PATCH 05/15] Bump library/nginx from 1.29.1-alpine to 1.29.3-alpine in /docker (#594) Bumps library/nginx from 1.29.1-alpine to 1.29.3-alpine. --- updated-dependencies: - dependency-name: library/nginx dependency-version: 1.29.3-alpine dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- docker/Dockerfile_nginx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/Dockerfile_nginx b/docker/Dockerfile_nginx index 55d4b221..c1a53bdf 100644 --- a/docker/Dockerfile_nginx +++ b/docker/Dockerfile_nginx @@ -1,4 +1,4 @@ -FROM library/nginx:1.29.1-alpine +FROM library/nginx:1.29.3-alpine RUN mkdir -p /var/cache/nginx /var/cache/nginx/feeds RUN apk update && apk upgrade && apk add bash ENV NGINX_LOG_DIR=/var/log/nginx From fff7820055906cb83290b778a98f8cb709eb3d53 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 25 Nov 2025 09:48:07 +0100 Subject: [PATCH 06/15] Bump psycopg2-binary from 2.9.10 to 2.9.11 in /requirements (#590) Bumps [psycopg2-binary](https://github.com/psycopg/psycopg2) from 2.9.10 to 2.9.11. - [Changelog](https://github.com/psycopg/psycopg2/blob/master/NEWS) - [Commits](https://github.com/psycopg/psycopg2/compare/2.9.10...2.9.11) --- updated-dependencies: - dependency-name: psycopg2-binary dependency-version: 2.9.11 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements/project-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/project-requirements.txt b/requirements/project-requirements.txt index aa924a23..903b5b44 100644 --- a/requirements/project-requirements.txt +++ b/requirements/project-requirements.txt @@ -8,7 +8,7 @@ djangorestframework==3.16.1 django-rest-email-auth==5.0.0 django-ses==4.4.0 -psycopg2-binary==2.9.10 +psycopg2-binary==2.9.11 certego-saas==0.7.11 slack-sdk==3.37.0 From e84158b8c174eef409481b53805bf1cfafaf1198 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 25 Nov 2025 09:48:33 +0100 Subject: [PATCH 07/15] Bump uwsgi from 2.0.30 to 2.0.31 in /requirements (#589) Bumps [uwsgi](https://uwsgi-docs.readthedocs.io/en/latest/) from 2.0.30 to 2.0.31. --- updated-dependencies: - dependency-name: uwsgi dependency-version: 2.0.31 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements/project-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/project-requirements.txt b/requirements/project-requirements.txt index 903b5b44..ed68c4d9 100644 --- a/requirements/project-requirements.txt +++ b/requirements/project-requirements.txt @@ -14,7 +14,7 @@ certego-saas==0.7.11 slack-sdk==3.37.0 uwsgitop==0.12 -uwsgi==2.0.30 +uwsgi==2.0.31 joblib==1.5.2 pandas==2.3.3 From b2cf30966f9f1124c303dfaa0178e8a8f44e63cb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 25 Nov 2025 09:49:29 +0100 Subject: [PATCH 08/15] Bump datasketch from 1.6.5 to 1.7.0 in /requirements (#596) Bumps [datasketch](https://github.com/ekzhu/datasketch) from 1.6.5 to 1.7.0. - [Release notes](https://github.com/ekzhu/datasketch/releases) - [Commits](https://github.com/ekzhu/datasketch/compare/v1.6.5...v1.7.0) --- updated-dependencies: - dependency-name: datasketch dependency-version: 1.7.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements/project-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/project-requirements.txt b/requirements/project-requirements.txt index ed68c4d9..f1a852a0 100644 --- a/requirements/project-requirements.txt +++ b/requirements/project-requirements.txt @@ -20,4 +20,4 @@ joblib==1.5.2 pandas==2.3.3 scikit-learn==1.7.2 numpy==2.3.5 -datasketch==1.6.5 +datasketch==1.7.0 From 0e319884d8674a4aeaa77f7de7f9cb8c0e709076 Mon Sep 17 00:00:00 2001 From: tim Date: Thu, 27 Nov 2025 12:01:28 +0100 Subject: [PATCH 09/15] change python and ubuntu versions in CI --- .github/workflows/pull_request_automation.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pull_request_automation.yml b/.github/workflows/pull_request_automation.yml index 03b7c81c..c779dd01 100644 --- a/.github/workflows/pull_request_automation.yml +++ b/.github/workflows/pull_request_automation.yml @@ -100,6 +100,6 @@ jobs: "BROKER_URL": "amqp://guest:guest@rabbitmq:5672", } python_versions: >- - ["3.10"] + ["3.11", "3.12", "3.13", "3.14"] max_timeout: 15 - ubuntu_version: 22.04 + ubuntu_version: 24.04 From 52439d7648f337b9f6c82d8f9abef47e3e48ae05 Mon Sep 17 00:00:00 2001 From: tim Date: Thu, 27 Nov 2025 13:21:13 +0100 Subject: [PATCH 10/15] change python version in CI to only run tests with version 3.13 (same as in the dockerfile) --- .github/workflows/pull_request_automation.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pull_request_automation.yml b/.github/workflows/pull_request_automation.yml index c779dd01..1ff60d09 100644 --- a/.github/workflows/pull_request_automation.yml +++ b/.github/workflows/pull_request_automation.yml @@ -100,6 +100,6 @@ jobs: "BROKER_URL": "amqp://guest:guest@rabbitmq:5672", } python_versions: >- - ["3.11", "3.12", "3.13", "3.14"] + ["3.13"] max_timeout: 15 ubuntu_version: 24.04 From 4e42262994c849e84b75af98d018318461338d73 Mon Sep 17 00:00:00 2001 From: tim <46972822+regulartim@users.noreply.github.com> Date: Mon, 1 Dec 2025 19:35:06 +0100 Subject: [PATCH 11/15] CowrieSession API. Closes #446 (#600) * add API to access Cowrie session data * add test cases * add exception calls behind except keyword * change block comment style of test separators --- api/urls.py | 12 ++- api/views/__init__.py | 1 + api/views/cowrie_session.py | 119 +++++++++++++++++++++++ greedybear/models.py | 1 + tests/__init__.py | 27 ++++++ tests/test_views.py | 189 ++++++++++++++++++++++++++++++++++++ 6 files changed, 348 insertions(+), 1 deletion(-) create mode 100644 api/views/cowrie_session.py diff --git a/api/urls.py b/api/urls.py index 4dfffb59..ec341bcd 100644 --- a/api/urls.py +++ b/api/urls.py @@ -1,6 +1,15 @@ # This file is a part of GreedyBear https://github.com/honeynet/GreedyBear # See the file 'LICENSE' for copying permission. -from api.views import StatisticsViewSet, command_sequence_view, enrichment_view, feeds, feeds_advanced, feeds_pagination, general_honeypot_list +from api.views import ( + StatisticsViewSet, + command_sequence_view, + cowrie_session_view, + enrichment_view, + feeds, + feeds_advanced, + feeds_pagination, + general_honeypot_list, +) from django.urls import include, path from rest_framework import routers @@ -14,6 +23,7 @@ path("feeds/advanced/", feeds_advanced), path("feeds///.", feeds), path("enrichment", enrichment_view), + path("cowrie_session", cowrie_session_view), path("command_sequence", command_sequence_view), path("general_honeypot", general_honeypot_list), # router viewsets diff --git a/api/views/__init__.py b/api/views/__init__.py index 2f5a7307..d76a0ef3 100644 --- a/api/views/__init__.py +++ b/api/views/__init__.py @@ -1,4 +1,5 @@ from api.views.command_sequence import * +from api.views.cowrie_session import * from api.views.enrichment import * from api.views.feeds import * from api.views.general_honeypot import * diff --git a/api/views/cowrie_session.py b/api/views/cowrie_session.py new file mode 100644 index 00000000..6fe9e76e --- /dev/null +++ b/api/views/cowrie_session.py @@ -0,0 +1,119 @@ +# This file is a part of GreedyBear https://github.com/honeynet/GreedyBear +# See the file 'LICENSE' for copying permission. +import itertools +import logging +import socket + +from api.views.utils import is_ip_address, is_sha256hash +from certego_saas.apps.auth.backend import CookieTokenAuthentication +from django.http import Http404, HttpResponseBadRequest +from greedybear.consts import FEEDS_LICENSE, GET +from greedybear.models import IOC, CommandSequence, CowrieSession, Statistics, viewType +from rest_framework import status +from rest_framework.decorators import api_view, authentication_classes, permission_classes +from rest_framework.permissions import IsAuthenticated +from rest_framework.response import Response + +logger = logging.getLogger(__name__) + + +@api_view([GET]) +@authentication_classes([CookieTokenAuthentication]) +@permission_classes([IsAuthenticated]) +def cowrie_session_view(request): + """ + Retrieve Cowrie honeypot session data including command sequences, credentials, and session details. + Queries can be performed using either an IP address to find all sessions from that source, + or a SHA-256 hash to find sessions containing a specific command sequence. + + Args: + request: The HTTP request object containing query parameters + query (str, required): The search term, can be either an IP address or the SHA-256 hash of a command sequence. + SHA-256 hashes should match command sequences generated using Python's "\\n".join(sequence) format. + include_similar (bool, optional): When "true", expands the result to include all sessions that executed + command sequences belonging to the same cluster(s) as command sequences found in the initial query result. + Requires CLUSTER_COWRIE_COMMAND_SEQUENCES enabled in configuration. Default: false + include_credentials (bool, optional): When "true", includes all credentials used across matching Cowrie sessions. + Default: false + include_session_data (bool, optional): When "true", includes detailed information about matching Cowrie sessions. + Default: false + + Returns: + Response (200): JSON object containing: + - query (str): The original query parameter + - commands (list[str]): Unique command sequences (newline-delimited strings) + - sources (list[str]): Unique source IP addresses + - credentials (list[str], optional): Unique credentials if include_credentials=true + - sessions (list[dict], optional): Session details if include_session_data=true + - time (datetime): Session start time + - duration (int): Session duration in seconds + - source (str): Source IP address + - interactions (int): Number of interactions in session + - credentials (list[str]): Credentials used in this session + - commands (str): Command sequence executed (newline-delimited) + Response (400): Bad Request - Missing or invalid query parameter + Response (404): Not Found - No matching sessions found + Response (500): Internal Server Error - Unexpected error occurred + + Example Queries: + /api/cowrie?query=1.2.3.4 + /api/cowrie?query=5120e94e366ec83a79ee80454e4d1c76c06499ab19032bcdc7f0b4523bdb37a6 + /api/cowrie?query=1.2.3.4&include_credentials=true&include_session_data=true&include_similar=true + """ + observable = request.query_params.get("query") + include_similar = request.query_params.get("include_similar", "false").lower() == "true" + include_credentials = request.query_params.get("include_credentials", "false").lower() == "true" + include_session_data = request.query_params.get("include_session_data", "false").lower() == "true" + + logger.info(f"Cowrie view requested by {request.user} for {observable}") + source_ip = str(request.META["REMOTE_ADDR"]) + request_source = Statistics(source=source_ip, view=viewType.COWRIE_SESSION_VIEW.value) + request_source.save() + + if not observable: + return HttpResponseBadRequest("Missing required 'query' parameter") + + if is_ip_address(observable): + sessions = CowrieSession.objects.filter(source__name=observable, duration__gt=0).prefetch_related("source", "commands") + if not sessions.exists(): + raise Http404(f"No information found for IP: {observable}") + + elif is_sha256hash(observable): + try: + commands = CommandSequence.objects.get(commands_hash=observable.lower()) + except CommandSequence.DoesNotExist as exc: + raise Http404(f"No command sequences found with hash: {observable}") from exc + sessions = CowrieSession.objects.filter(commands=commands, duration__gt=0).prefetch_related("source", "commands") + else: + return HttpResponseBadRequest("Query must be a valid IP address or SHA-256 hash") + + if include_similar: + commands = set(s.commands for s in sessions if s.commands) + clusters = set(cmd.cluster for cmd in commands if cmd.cluster is not None) + related_sessions = CowrieSession.objects.filter(commands__cluster__in=clusters).prefetch_related("source", "commands") + sessions = sessions.union(related_sessions) + + response_data = { + "license": FEEDS_LICENSE, + "query": observable, + } + + unique_commands = set(s.commands for s in sessions if s.commands) + response_data["commands"] = sorted("\n".join(cmd.commands) for cmd in unique_commands) + response_data["sources"] = sorted(set(s.source.name for s in sessions), key=socket.inet_aton) + if include_credentials: + response_data["credentials"] = sorted(set(itertools.chain(*[s.credentials for s in sessions]))) + if include_session_data: + response_data["sessions"] = [ + { + "time": s.start_time, + "duration": s.duration, + "source": s.source.name, + "interactions": s.interaction_count, + "credentials": s.credentials if s.credentials else [], + "commands": "\n".join(s.commands.commands) if s.commands else "", + } + for s in sessions + ] + + return Response(response_data, status=status.HTTP_200_OK) diff --git a/greedybear/models.py b/greedybear/models.py index 393a27a9..18e6c4f9 100644 --- a/greedybear/models.py +++ b/greedybear/models.py @@ -10,6 +10,7 @@ class viewType(models.TextChoices): FEEDS_VIEW = "feeds" ENRICHMENT_VIEW = "enrichment" COMMAND_SEQUENCE_VIEW = "command sequence" + COWRIE_SESSION_VIEW = "cowrie session" class iocType(models.TextChoices): diff --git a/tests/__init__.py b/tests/__init__.py index a84a7b98..31329e0d 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -112,10 +112,37 @@ def setUpTestData(cls): ) cls.cowrie_session.save() + cls.cmd_seq_2 = ["cd bar", "ls -la"] + cls.command_sequence_2 = CommandSequence.objects.create( + first_seen=cls.current_time, + last_seen=cls.current_time, + commands=cls.cmd_seq_2, + commands_hash=sha256("\n".join(cls.cmd_seq_2).encode()).hexdigest(), + cluster=11, + ) + cls.command_sequence_2.save() + + cls.cowrie_session_2 = CowrieSession.objects.create( + session_id=int("eeeeeeeeeeee", 16), + start_time=cls.current_time, + duration=2.234, + login_attempt=True, + credentials=["user | user"], + command_execution=True, + interaction_count=5, + source=cls.ioc_2, + commands=cls.command_sequence_2, + ) + cls.cowrie_session_2.save() + try: cls.superuser = User.objects.get(is_superuser=True) except User.DoesNotExist: cls.superuser = User.objects.create_superuser(username="test", email="test@greedybear.com", password="test") + try: + cls.regular_user = User.objects.get(is_superuser=False) + except User.DoesNotExist: + cls.regular_user = User.objects.create_user(username="regular", email="regular@greedybear.com", password="regular") @classmethod def tearDownClass(self): diff --git a/tests/test_views.py b/tests/test_views.py index 6cabad7d..e6d9c587 100644 --- a/tests/test_views.py +++ b/tests/test_views.py @@ -298,6 +298,195 @@ def test_nonexistent_hash(self): self.assertEqual(response.status_code, 404) +class CowrieSessionViewTestCase(CustomTestCase): + """Test cases for the cowrie_session_view.""" + + def setUp(self): + # setup client + self.client = APIClient() + self.client.force_authenticate(user=self.superuser) + + # # # # # Basic IP Query Test # # # # # + def test_ip_address_query(self): + """Test view with a valid IP address query.""" + response = self.client.get("/api/cowrie_session?query=140.246.171.141") + self.assertEqual(response.status_code, 200) + self.assertIn("query", response.data) + self.assertIn("commands", response.data) + self.assertIn("sources", response.data) + self.assertNotIn("credentials", response.data) + self.assertNotIn("sessions", response.data) + + def test_ip_address_query_with_similar(self): + """Test view with a valid IP address query including similar sequences.""" + response = self.client.get("/api/cowrie_session?query=140.246.171.141&include_similar=true") + self.assertEqual(response.status_code, 200) + self.assertIn("query", response.data) + self.assertIn("commands", response.data) + self.assertIn("sources", response.data) + self.assertNotIn("credentials", response.data) + self.assertNotIn("sessions", response.data) + self.assertEqual(len(response.data["sources"]), 2) + + def test_ip_address_query_with_credentials(self): + """Test view with a valid IP address query including credentials.""" + response = self.client.get("/api/cowrie_session?query=140.246.171.141&include_credentials=true") + self.assertEqual(response.status_code, 200) + self.assertIn("query", response.data) + self.assertIn("commands", response.data) + self.assertIn("sources", response.data) + self.assertIn("credentials", response.data) + self.assertNotIn("sessions", response.data) + self.assertEqual(len(response.data["credentials"]), 1) + self.assertEqual(response.data["credentials"][0], "root | root") + + def test_ip_address_query_with_sessions(self): + """Test view with a valid IP address query including session data.""" + response = self.client.get("/api/cowrie_session?query=140.246.171.141&include_session_data=true") + self.assertEqual(response.status_code, 200) + self.assertIn("query", response.data) + self.assertIn("commands", response.data) + self.assertIn("sources", response.data) + self.assertNotIn("credentials", response.data) + self.assertIn("sessions", response.data) + self.assertEqual(len(response.data["sessions"]), 1) + self.assertIn("time", response.data["sessions"][0]) + self.assertEqual(response.data["sessions"][0]["duration"], 1.234) + self.assertEqual(response.data["sessions"][0]["source"], "140.246.171.141") + self.assertEqual(response.data["sessions"][0]["interactions"], 5) + self.assertEqual(response.data["sessions"][0]["credentials"][0], "root | root") + self.assertEqual(response.data["sessions"][0]["commands"], "cd foo\nls -la") + + def test_ip_address_query_with_all(self): + """Test view with a valid IP address query including everything.""" + response = self.client.get("/api/cowrie_session?query=140.246.171.141&include_similar=true&include_credentials=true&include_session_data=true") + self.assertEqual(response.status_code, 200) + self.assertIn("query", response.data) + self.assertIn("commands", response.data) + self.assertIn("sources", response.data) + self.assertIn("credentials", response.data) + self.assertIn("sessions", response.data) + + # # # # # Basic Hash Query Test # # # # # + def test_hash_query(self): + """Test view with a valid hash query.""" + response = self.client.get(f"/api/cowrie_session?query={self.hash}") + self.assertEqual(response.status_code, 200) + self.assertIn("query", response.data) + self.assertIn("commands", response.data) + self.assertIn("sources", response.data) + self.assertNotIn("credentials", response.data) + self.assertNotIn("sessions", response.data) + + def test_hash_query_with_all(self): + """Test view with a valid hash query including everything.""" + response = self.client.get(f"/api/cowrie_session?query={self.hash}&include_similar=true&include_credentials=true&include_session_data=true") + self.assertEqual(response.status_code, 200) + self.assertIn("query", response.data) + self.assertIn("commands", response.data) + self.assertIn("sources", response.data) + self.assertIn("credentials", response.data) + self.assertIn("sessions", response.data) + self.assertEqual(len(response.data["sources"]), 2) + + # # # # # IP Address Validation Tests # # # # # + def test_nonexistent_ip_address(self): + """Test that view returns 404 for IP with no sequences.""" + response = self.client.get("/api/cowrie_session?query=10.0.0.1") + self.assertEqual(response.status_code, 404) + + def test_ipv6_address_query(self): + """Test view with a valid IPv6 address query.""" + response = self.client.get("/api/cowrie_session?query=2001:db8::1") + self.assertEqual(response.status_code, 404) + + def test_invalid_ip_format(self): + """Test that malformed IP addresses are rejected.""" + response = self.client.get("/api/cowrie_session?query=999.999.999.999") + self.assertEqual(response.status_code, 400) + + def test_ip_with_cidr_notation(self): + """Test that CIDR notation is rejected.""" + response = self.client.get("/api/cowrie_session?query=192.168.1.0/24") + self.assertEqual(response.status_code, 400) + + # # # # # Parameter Validation Tests # # # # # + def test_missing_query_parameter(self): + """Test that view returns BadRequest when query parameter is missing.""" + response = self.client.get("/api/cowrie_session") + self.assertEqual(response.status_code, 400) + + def test_invalid_query_parameter(self): + """Test that view returns BadRequest when query parameter is invalid.""" + response = self.client.get("/api/cowrie_session?query=invalid-input}") + self.assertEqual(response.status_code, 400) + + def test_include_credentials_invalid_value(self): + """Test that invalid boolean values default to false.""" + response = self.client.get("/api/cowrie_session?query=140.246.171.141&include_credentials=maybe") + self.assertEqual(response.status_code, 200) + self.assertNotIn("credentials", response.data) + + def test_case_insensitive_boolean_parameters(self): + """Test that boolean parameters accept various case formats.""" + response = self.client.get("/api/cowrie_session?query=140.246.171.141&include_credentials=TRUE") + self.assertEqual(response.status_code, 200) + self.assertIn("credentials", response.data) + + response = self.client.get("/api/cowrie_session?query=140.246.171.141&include_credentials=True") + self.assertEqual(response.status_code, 200) + self.assertIn("credentials", response.data) + + # # # # # Hash Validation Tests # # # # # + def test_nonexistent_hash(self): + """Test that view returns 404 for nonexistent hash.""" + response = self.client.get(f"/api/cowrie_session?query={'f' * 64}") + self.assertEqual(response.status_code, 404) + + def test_hash_wrong_length(self): + """Test that hashes with incorrect length are rejected.""" + response = self.client.get("/api/cowrie_session?query=" + "a" * 32) # 32 chars instead of 64 + self.assertEqual(response.status_code, 400) + + def test_hash_invalid_characters(self): + """Test that hashes with invalid characters are rejected.""" + invalid_hash = "g" * 64 # 'g' is not a valid hex character + response = self.client.get(f"/api/cowrie_session?query={invalid_hash}") + self.assertEqual(response.status_code, 400) + + def test_hash_case_insensitive(self): + """Test that hash queries are case-insensitive.""" + response_lower = self.client.get(f"/api/cowrie_session?query={self.hash.lower()}") + response_upper = self.client.get(f"/api/cowrie_session?query={self.hash.upper()}") + self.assertEqual(response_lower.status_code, response_upper.status_code) + + # # # # # Special Characters & Encoding Tests # # # # # + def test_query_with_url_encoding(self): + """Test that URL-encoded queries work correctly.""" + response = self.client.get("/api/cowrie_session?query=140.246.171.141%20") + # Should either work or return 400, not crash + self.assertIn(response.status_code, [200, 400, 404]) + + def test_query_with_special_characters(self): + """Test handling of queries with special characters.""" + response = self.client.get("/api/cowrie_session?query=") + self.assertEqual(response.status_code, 400) + + # # # # # Authentication & Authorization Tests # # # # # + def test_unauthenticated_request(self): + """Test that unauthenticated requests are rejected.""" + client = APIClient() # No authentication + response = client.get("/api/cowrie_session?query=140.246.171.141") + self.assertEqual(response.status_code, 401) + + def test_regular_user_access(self): + """Test that regular (non-superuser) authenticated users can access.""" + client = APIClient() + client.force_authenticate(user=self.regular_user) + response = client.get("/api/cowrie_session?query=140.246.171.141") + self.assertEqual(response.status_code, 200) + + class ValidationHelpersTestCase(CustomTestCase): """Test cases for the validation helper functions.""" From 791de4fdb478356b364dde2b7bad37b55c28b0d6 Mon Sep 17 00:00:00 2001 From: tim <46972822+regulartim@users.noreply.github.com> Date: Mon, 1 Dec 2025 19:38:17 +0100 Subject: [PATCH 12/15] Migrate to elasticsearch. Closes #601. (#602) * replace elasticsearch_dsl with elasticsearch8 --- greedybear/cronjobs/base.py | 2 +- greedybear/settings.py | 2 +- requirements/project-requirements.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/greedybear/cronjobs/base.py b/greedybear/cronjobs/base.py index 9d83fa6d..c1928169 100644 --- a/greedybear/cronjobs/base.py +++ b/greedybear/cronjobs/base.py @@ -5,7 +5,7 @@ from datetime import datetime, timedelta from django.conf import settings -from elasticsearch_dsl import Q, Search +from elasticsearch8.dsl import Q, Search from greedybear.settings import EXTRACTION_INTERVAL, LEGACY_EXTRACTION diff --git a/greedybear/settings.py b/greedybear/settings.py index dcaa931a..cb986fbd 100644 --- a/greedybear/settings.py +++ b/greedybear/settings.py @@ -6,7 +6,7 @@ from datetime import timedelta from django.core.management.utils import get_random_secret_key -from elasticsearch import Elasticsearch +from elasticsearch8 import Elasticsearch BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) BASE_STATIC_PATH = os.path.join(BASE_DIR, "static/") diff --git a/requirements/project-requirements.txt b/requirements/project-requirements.txt index f1a852a0..a129727c 100644 --- a/requirements/project-requirements.txt +++ b/requirements/project-requirements.txt @@ -1,7 +1,7 @@ celery==5.5.3 # if you change this, update the documentation -elasticsearch-dsl==8.18.0 +elasticsearch8==8.19.2 Django==5.2.7 djangorestframework==3.16.1 From 86c67cd051d8505a729b59ac3ed7df6bbffeb455 Mon Sep 17 00:00:00 2001 From: tim Date: Tue, 2 Dec 2025 09:27:14 +0100 Subject: [PATCH 13/15] fix docstring --- api/views/cowrie_session.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/api/views/cowrie_session.py b/api/views/cowrie_session.py index 6fe9e76e..9ddb0b4c 100644 --- a/api/views/cowrie_session.py +++ b/api/views/cowrie_session.py @@ -46,7 +46,7 @@ def cowrie_session_view(request): - credentials (list[str], optional): Unique credentials if include_credentials=true - sessions (list[dict], optional): Session details if include_session_data=true - time (datetime): Session start time - - duration (int): Session duration in seconds + - duration (float): Session duration in seconds - source (str): Source IP address - interactions (int): Number of interactions in session - credentials (list[str]): Credentials used in this session @@ -56,9 +56,9 @@ def cowrie_session_view(request): Response (500): Internal Server Error - Unexpected error occurred Example Queries: - /api/cowrie?query=1.2.3.4 - /api/cowrie?query=5120e94e366ec83a79ee80454e4d1c76c06499ab19032bcdc7f0b4523bdb37a6 - /api/cowrie?query=1.2.3.4&include_credentials=true&include_session_data=true&include_similar=true + /api/cowrie_session?query=1.2.3.4 + /api/cowrie_session?query=5120e94e366ec83a79ee80454e4d1c76c06499ab19032bcdc7f0b4523bdb37a6 + /api/cowrie_session?query=1.2.3.4&include_credentials=true&include_session_data=true&include_similar=true """ observable = request.query_params.get("query") include_similar = request.query_params.get("include_similar", "false").lower() == "true" From 2b3defe399aa574a3b057db3d48fae450f3a1fce Mon Sep 17 00:00:00 2001 From: tim Date: Tue, 2 Dec 2025 10:20:07 +0100 Subject: [PATCH 14/15] Bump Django from 5.2.7 to 5.2.8 in /requirements --- requirements/project-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/project-requirements.txt b/requirements/project-requirements.txt index a129727c..f87af4a1 100644 --- a/requirements/project-requirements.txt +++ b/requirements/project-requirements.txt @@ -3,7 +3,7 @@ celery==5.5.3 # if you change this, update the documentation elasticsearch8==8.19.2 -Django==5.2.7 +Django==5.2.8 djangorestframework==3.16.1 django-rest-email-auth==5.0.0 django-ses==4.4.0 From 0de05ea312ed5eeeb63a80a5a9423a320c34e048 Mon Sep 17 00:00:00 2001 From: tim Date: Tue, 2 Dec 2025 10:20:56 +0100 Subject: [PATCH 15/15] bump 2.1.0 --- .env_template | 2 +- docker/.version | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.env_template b/.env_template index 5af30fd8..85c172c3 100644 --- a/.env_template +++ b/.env_template @@ -13,4 +13,4 @@ COMPOSE_FILE=docker/default.yml:docker/local.override.yml #COMPOSE_FILE=docker/default.yml:docker/local.override.yml:docker/elasticsearch.yml # If you want to run a specific version, populate this -# REACT_APP_INTELOWL_VERSION="2.0.1" +# REACT_APP_INTELOWL_VERSION="2.1.0" diff --git a/docker/.version b/docker/.version index 45a727fb..f32f3526 100644 --- a/docker/.version +++ b/docker/.version @@ -1 +1 @@ -REACT_APP_GREEDYBEAR_VERSION="2.0.1" \ No newline at end of file +REACT_APP_GREEDYBEAR_VERSION="2.1.0" \ No newline at end of file