Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 2 additions & 2 deletions .github/workflows/pull_request_automation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,6 @@ jobs:
"BROKER_URL": "amqp://guest:guest@rabbitmq:5672",
}
python_versions: >-
["3.10"]
["3.13"]
max_timeout: 15
ubuntu_version: 22.04
ubuntu_version: 24.04
12 changes: 11 additions & 1 deletion api/urls.py
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -14,6 +23,7 @@
path("feeds/advanced/", feeds_advanced),
path("feeds/<str:feed_type>/<str:attack_type>/<str:prioritize>.<str:format_>", 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
Expand Down
1 change: 1 addition & 0 deletions api/views/__init__.py
Original file line number Diff line number Diff line change
@@ -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 *
Expand Down
119 changes: 119 additions & 0 deletions api/views/cowrie_session.py
Original file line number Diff line number Diff line change
@@ -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)
1 change: 1 addition & 0 deletions greedybear/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
27 changes: 27 additions & 0 deletions tests/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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="[email protected]", 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="[email protected]", password="regular")

@classmethod
def tearDownClass(self):
Expand Down
Loading
Loading