Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
e7defe2
build(deps): bump github/codeql-action from 1 to 4 (#1546)
dependabot[bot] Sep 4, 2026
ff98762
build(deps): bump pilosus/action-pip-license-checker from 2 to 3 (#1550)
dependabot[bot] Sep 4, 2026
eeda6b5
build(deps): bump actions/setup-python from 5 to 7 (#1549)
dependabot[bot] Sep 4, 2026
d47f229
build(deps): bump actions/upload-artifact from 4 to 7 (#1548)
dependabot[bot] Sep 4, 2026
d90439a
build(deps): bump library/nginx in /docker (#1545)
dependabot[bot] Sep 4, 2026
0795368
build(deps): bump actions/checkout from 2 to 7 (#1547)
dependabot[bot] Sep 4, 2026
b6e3774
feat: add API docs links to the header Docs menu. Closes #1541 (#1553)
aashu2006 Sep 7, 2026
671942c
Payload quota error notification. Closes #1555 (#1558)
joshuavetos Sep 7, 2026
b6883ab
Document rationale for add_tags vs replace_tags_for_source. Closes #1…
rainnj Sep 8, 2026
a77b019
Add usage guide to add_tags docstring. Closes #1559 (#1564)
rainnj Sep 8, 2026
946c7ad
fix: normalize SHA256 case in payload extraction. Closes #1556 (#1565)
aashu2006 Sep 8, 2026
fb3b61c
build(deps): bump actions/setup-node from 4 to 7 (#1577)
dependabot[bot] Sep 8, 2026
62d859d
build(deps): bump actions/dependency-review-action from 4 to 5 (#1576)
dependabot[bot] Sep 8, 2026
b236b33
build(deps-dev): bump ruff from 0.16.5 to 0.16.6 (#1574)
dependabot[bot] Sep 8, 2026
a6893de
build(deps): bump softprops/action-gh-release from 2 to 3 (#1575)
dependabot[bot] Sep 8, 2026
cf3322e
feat: Link HoneypotPayload to Cowrie sessions and attacker IOCs by SH…
kikiscodedeliveryservice Sep 9, 2026
eb5a219
Move gb-ui components into frontend. Closes #1454 (#1482)
cclts Sep 9, 2026
0d59fac
fix: normalize SHA256 case in payload API lookups. Closes #1579 (#1580)
aashu2006 Sep 9, 2026
8d7472b
fix(cronjobs): keep metadata for payloads skipped at quarantine quota…
Prasad8830 Sep 14, 2026
0daa060
Update README (#1567)
regulartim Sep 14, 2026
b348e4b
perf(admin): use raw_id_fields in HoneypotPayloadAdmin to optimize pa…
suvani-ctrl Sep 14, 2026
471111c
Add BaseEnrichmentJob/HttpEnrichmentJob base classes. Closes #1566 (#…
rainnj Sep 14, 2026
dac586d
Fix stored XSS in admin collapsed_list_display. Closes #1594 (#1595)
AdeshDeshmukh Sep 15, 2026
885ec5f
Fix payload extraction time window. Closes #1554. (#1588)
cclts Sep 15, 2026
004ec00
Allow querying Cowrie sessions by ID. Closes #1578 (#1586)
aashu2006 Sep 15, 2026
1a24a07
Remove imports from deep CommonJS paths. Closes #1597 (#1598)
regulartim Sep 15, 2026
8a5729a
fix: let payload extraction upgrade hash-only stubs instead of skippi…
kikiscodedeliveryservice Sep 16, 2026
fa73dfc
Allow GreedyBear to run without Elasticsearch. Closes #1599 (#1611)
Nisarg0007 Sep 17, 2026
7a19b5d
Disable facet counts in Django admin. Closes #1615 (#1616)
regulartim Sep 17, 2026
3f38870
Add raw ID fields in Django admin. Closes #1617 (#1618)
regulartim Sep 17, 2026
aaf077f
Keep feed cache version from expiring on bump. Closes #1609 (#1610)
dev-aditya-hub Sep 22, 2026
52397c1
Recover event batches stuck in processing after worker crash. Closes …
dev-aditya-hub Sep 22, 2026
270f6e7
fix: Normalize hashes in command sequence API. Closes #1581 (#1626)
SupRaKoshti Sep 22, 2026
317dece
Add payload hashes to the advanced feed responses. Closes #1543 (#1612)
Prasad8830 Sep 22, 2026
360a192
Count IOCs linked to any active honeypot in feeds_types statistics. C…
AdeshDeshmukh Sep 22, 2026
36934b1
Guard scoring jobs against empty IoC data. Closes #1625 (#1627)
AdeshDeshmukh Sep 22, 2026
d3f78d0
build(deps): bump library/nginx in /docker (#1630)
dependabot[bot] Sep 23, 2026
03c1f3f
build(deps-dev): bump ruff from 0.16.6 to 0.16.7 (#1604)
dependabot[bot] Sep 23, 2026
4726bf3
Enforce Tag identity uniqueness and deduplicate existing rows. Closes…
rainnj Sep 23, 2026
82d9479
Link event collector IOCs to their sensor's honeypot. Closes #1624 (#…
dev-aditya-hub Sep 24, 2026
e7e81c4
bump python dependencies
regulartim Sep 24, 2026
45684e9
bump node dependencies
regulartim Sep 24, 2026
2f031e7
bump 3.6.3
regulartim Sep 24, 2026
713e73d
Merge pull request #1560 from GreedyBear-Project/develop
regulartim Sep 24, 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
4 changes: 2 additions & 2 deletions .github/workflows/_detect_changes.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,12 +46,12 @@ jobs:
frontend: ${{steps.diff_check_frontend.outputs.frontend}}
steps:
- name: Check out PR target branch
uses: actions/checkout@v4
uses: actions/checkout@v7
with:
ref: ${{ github.base_ref }}

- name: Check out source branch latest commit
uses: actions/checkout@v4
uses: actions/checkout@v7
with:
clean: false

Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/_node.yml
Original file line number Diff line number Diff line change
Expand Up @@ -97,10 +97,10 @@ jobs:
language: ['javascript']
steps:
- name: Check out latest commit for current branch
uses: actions/checkout@v4
uses: actions/checkout@v7

- name: Set up Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@v7
with:
node-version: ${{ matrix.node_version }}
cache: 'npm'
Expand Down Expand Up @@ -137,7 +137,7 @@ jobs:
if: ${{ inputs.check_packages_licenses }}
id: license_check_report
continue-on-error: true
uses: pilosus/action-pip-license-checker@v2
uses: pilosus/action-pip-license-checker@v3
with:
requirements: ${{ inputs.requirements_path }}
external: ${{ inputs.working_directory }}/licenses.csv
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/_python.yml
Original file line number Diff line number Diff line change
Expand Up @@ -258,11 +258,11 @@ jobs:
env: ${{ fromJson(inputs.env) }}
steps:
- name: Check out latest commit
uses: actions/checkout@v4
uses: actions/checkout@v7

- name: Set up Python
id: setup_python
uses: actions/setup-python@v5
uses: actions/setup-python@v7
with:
python-version-file: "pyproject.toml"

Expand Down Expand Up @@ -460,7 +460,7 @@ jobs:

- name: Upload coverage report as artifact
if: inputs.use_coverage && inputs.upload_coverage
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v7
with:
name: coverage-report-py${{ matrix.python_version }}
path: ${{ inputs.working_directory }}/coverage.xml
Expand Down
8 changes: 4 additions & 4 deletions .github/workflows/codeql-analysis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,15 +37,15 @@ jobs:

steps:
- name: Checkout repository
uses: actions/checkout@v2
uses: actions/checkout@v7
with:
# We must fetch at least the immediate parents so that if this is
# a pull request then we can checkout the head.
fetch-depth: 2

# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
uses: github/codeql-action/init@v4
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
Expand All @@ -56,7 +56,7 @@ jobs:
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v1
uses: github/codeql-action/autobuild@v4

# ℹ️ Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
Expand All @@ -70,4 +70,4 @@ jobs:
# make release

- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1
uses: github/codeql-action/analyze@v4
2 changes: 1 addition & 1 deletion .github/workflows/create_apt_cache.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out latest commit on current branch
uses: actions/checkout@v4
uses: actions/checkout@v7

# Remember to set the same APT requirements file path set before!
- name: Install APT dependencies
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/create_python_cache.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out latest commit
uses: actions/checkout@v4
uses: actions/checkout@v7

# Uncomment only if necessary
#- name: Install system dependencies required by Python packages
Expand All @@ -34,7 +34,7 @@ jobs:

- name: Set up Python
id: setup_python
uses: actions/setup-python@v5
uses: actions/setup-python@v7
with:
python-version: "3.13"

Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/dependency_review.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,6 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: 'Checkout Repository'
uses: actions/checkout@v3
uses: actions/checkout@v7
- name: 'Dependency Review'
uses: actions/dependency-review-action@v4
uses: actions/dependency-review-action@v5
4 changes: 2 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ jobs:
match: ${{ steps.check-tag.outputs.match }}
version: ${{ steps.check-tag.outputs.version }}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v7

- name: Check version tag format
id: check-tag
Expand All @@ -43,7 +43,7 @@ jobs:

- name: Create release
if: steps.check-tag.outputs.match == 'true'
uses: softprops/action-gh-release@v2
uses: softprops/action-gh-release@v3
with:
tag_name: ${{ steps.check-tag.outputs.version }}
name: Version ${{ steps.check-tag.outputs.version }}
Expand Down
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,9 @@ Thanks to [The Honeynet Project](https://www.honeynet.org) we are providing free
#### 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 2026 we started participating in the [Google Summer of Code](https://summerofcode.withgoogle.com/) (GSoC)!
In 2026 we participated in the [Google Summer of Code](https://summerofcode.withgoogle.com/) and three projects were successfully implemented.

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)!
If you are interested in participating in the next Google Summer of Code, check all the info available in the [wiki](https://github.com/GreedyBear-Project/GreedyBear/wiki/Google-Summer-of-Code)!

## Maintainers and Contributors

Expand All @@ -45,8 +45,8 @@ This project was started as a personal Christmas project by [Matteo Lodi](https:
Special thanks to:
- [Tim Leonhard](https://github.com/regulartim) for having greatly improved the project and added Machine Learning Models during his master thesis. He's the current Principal Maintainer.
- [Martina Carella](https://github.com/carellamartina) for having created the GUI during her master thesis.
- [Daniele Rosetti](https://github.com/drosetti) for helping maintaining the Frontend.
- and everyone who has contributed to GreedyBear!
- Our GSoC 2026 participants [Drona Raj Gyawali](https://github.com/drona-gyawali), [Krishna Awasthi](https://github.com/opbot-xd) and [Rachit Kumar Pandey](https://github.com/armoredvortex) for having built the [event collector API](https://www.honeynet.org/2026/08/11/11/), the [payload access feature](https://www.honeynet.org/2026/08/17/17/) and the [dashboard modularization](https://www.honeynet.org/2026/08/14/16/).
- and everyone else who has contributed to GreedyBear!

<a href="https://github.com/GreedyBear-Project/GreedyBear/graphs/contributors">
<img src="https://contrib.rocks/image?repo=GreedyBear-Project/GreedyBear" alt="GreedyBear contributors" />
Expand Down
29 changes: 27 additions & 2 deletions api/serializers/cowrie_session.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,26 @@
import re

from rest_framework import serializers

from greedybear.regex import REGEX_COWRIE_SESSION_ID


class CowrieSessionRequestSerializer(serializers.Serializer):
query = serializers.CharField(
required=False,
max_length=256,
help_text=(
"The search term, can be an IP address, the SHA-256 hash of a command sequence, or a password. "
'SHA-256 hashes should match command sequences generated using Python\'s `"\n".join(sequence)` format.'
'SHA-256 hashes should match command sequences generated using Python\'s `"\n".join(sequence)` format. '
"Mutually exclusive with `id`."
),
)
id = serializers.CharField(
required=False,
max_length=16,
help_text=("Hex session ID, in the same format the payloads API returns. Mutually exclusive with `query`."),
)

include_similar = serializers.BooleanField(
required=False,
default=False,
Expand All @@ -25,6 +37,18 @@ class CowrieSessionRequestSerializer(serializers.Serializer):
required=False, default=False, help_text="When `true`, includes detailed information about matching Cowrie sessions."
)

def validate_id(self, value: str) -> str:
if not re.fullmatch(REGEX_COWRIE_SESSION_ID, value):
raise serializers.ValidationError(f"Not a valid hex session ID: {value}")
return value

def validate(self, data: dict) -> dict:
if data.get("query") and data.get("id"):
raise serializers.ValidationError("Provide either `query` or `id`, not both.")
if not data.get("query") and not data.get("id"):
raise serializers.ValidationError("Provide either `query` or `id`.")
return data


class SessionDetailSerializer(serializers.Serializer):
"""A single matching Cowrie session."""
Expand All @@ -40,7 +64,8 @@ class SessionDetailSerializer(serializers.Serializer):
class CowrieSessionSerializer(serializers.Serializer):
"""Aggregated view of the sessions matching a query."""

query = serializers.CharField(max_length=256, help_text="The query this result was produced for.")
query = serializers.CharField(required=False, max_length=256, help_text="The query this result was produced for. Present when searching by `query`.")
id = serializers.CharField(required=False, max_length=16, help_text="The session ID this result was produced for. Present when searching by `id`.")
license = serializers.CharField(required=False, help_text="Present when a feed license is configured.")
commands = serializers.ListField(child=serializers.CharField(), help_text="Unique command sequences, each newline-delimited.")
sources = serializers.ListField(child=serializers.IPAddressField(), help_text="Unique source IP addresses.")
Expand Down
5 changes: 4 additions & 1 deletion api/serializers/events.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,9 @@ class Meta:
"help_text": "IPv4 or IPv6 address of the sensor.",
},
"honeypot_type": {"help_text": "Type of honeypot."},
"honeypot_software": {"help_text": "Honeypot software name."},
# capped to Honeypot.name's width: the value becomes a Honeypot record
# when the sensor's events are turned into IOCs
"honeypot_software": {"help_text": "Honeypot software name.", "max_length": 15},
"honeypot_description": {"help_text": "Description of the sensor."},
"group_label": {"help_text": "Group classification label."},
"country_code": {"help_text": "2-letter ISO country code."},
Expand Down Expand Up @@ -158,6 +160,7 @@ class Meta:
"status",
"ioc_count",
"last_error",
"started_at",
"processed_at",
"created_at",
]
Expand Down
7 changes: 7 additions & 0 deletions api/serializers/feeds.py
Original file line number Diff line number Diff line change
Expand Up @@ -409,6 +409,13 @@ class SimpleFeedResponseSerializer(serializers.Serializer):
class AdvancedFeedResponseSerializer(SimpleFeedResponseSerializer):
credential_count = serializers.IntegerField(min_value=0)
sensors = SensorSerializer(many=True, required=False, default=list)
payload_hashes = serializers.ListField(
child=serializers.CharField(max_length=64),
allow_empty=True,
required=False,
default=list,
help_text="Lowercase SHA256 hashes of the payloads observed from this IOC. Only included when `verbose=true`.",
)
firehol_categories = serializers.ListField(child=serializers.CharField(max_length=64), allow_empty=True, required=False)
destination_ports = serializers.ListField(child=serializers.IntegerField(min_value=1, max_value=65535), required=False)
days_seen = serializers.ListField(child=serializers.DateField(format="%Y-%m-%d"), required=False)
Expand Down
12 changes: 12 additions & 0 deletions api/serializers/payloads.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,13 @@ class HoneypotPayloadSerializer(serializers.ModelSerializer):
slug_field="name",
help_text="Names of the honeypots that captured this payload.",
)
iocs = serializers.SlugRelatedField(
many=True,
read_only=True,
slug_field="name",
help_text="Attacker IPs (or other IOCs) linked to this payload.",
)
cowrie_sessions = serializers.SerializerMethodField(help_text="IDs of the Cowrie sessions that transferred this payload, as hex strings.")

class Meta:
model = HoneypotPayload
Expand All @@ -23,6 +30,8 @@ class Meta:
"mime_type",
"size",
"source_honeypots",
"iocs",
"cowrie_sessions",
]
extra_kwargs = {
"id": {"help_text": "Unique identifier of the payload."},
Expand All @@ -32,3 +41,6 @@ class Meta:
"mime_type": {"help_text": "MIME type of the payload file."},
"size": {"help_text": "Size of the payload in bytes."},
}

def get_cowrie_sessions(self, obj: HoneypotPayload) -> list[str]:
return [f"{session.session_id:x}" for session in obj.cowrie_sessions.all()]
2 changes: 1 addition & 1 deletion api/views/command_sequence.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ def command_sequence_view(request):

if is_sha256hash(observable):
try:
seq = CommandSequence.objects.get(commands_hash=observable)
seq = CommandSequence.objects.get(commands_hash=observable.lower())
seqs = CommandSequence.objects.filter(cluster=seq.cluster) if include_similar and seq.cluster is not None else [seq]
commands = ["\n".join(seq.commands) for seq in seqs]
sessions = CowrieSession.objects.filter(commands__in=seqs, start_time__isnull=False)
Expand Down
21 changes: 14 additions & 7 deletions api/views/cowrie_session.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,14 @@
"Retrieve Cowrie honeypot session data including command sequences, credentials, and session details. "
"Queries can be performed using an IP address to find all sessions from that source, "
"a SHA-256 hash to find sessions containing a specific command sequence, "
"or a password to find all sessions where that password was used."
"or a password to find all sessions where that password was used. "
"Alternatively, pass `id` to look up one specific session by its hex ID, "
"as returned by the payloads API."
),
parameters=[CowrieSessionRequestSerializer],
responses={
200: CowrieSessionSerializer,
400: OpenApiResponse(description="Missing or invalid `query` parameter."),
400: OpenApiResponse(description="Missing or invalid `query`/`id` parameter, or both were given."),
401: OpenApiResponse(description="Authentication credentials were not provided or are invalid."),
404: OpenApiResponse(description="No matching sessions found."),
},
Expand All @@ -47,8 +49,15 @@ def get(self, request: Request, *args, **kwargs):
request_serializer.is_valid(raise_exception=True)
save_request_source(request, ViewType.COWRIE_SESSION_VIEW.value)

observable = request_serializer.validated_data["query"]
if is_ip_address(observable):
session_id = request_serializer.validated_data.get("id")
observable = request_serializer.validated_data.get("query")

if session_id is not None:
sessions = CowrieSession.objects.filter(session_id=int(session_id, 16), duration__gt=0).prefetch_related("source", "commands", "credentials")
if not sessions.exists():
raise Http404(f"No session found with ID: {session_id}")

elif is_ip_address(observable):
sessions = CowrieSession.objects.filter(source__name=observable, duration__gt=0).prefetch_related("source", "commands", "credentials")
if not sessions.exists():
raise Http404(f"No information found for IP: {observable}")
Expand All @@ -72,9 +81,7 @@ def get(self, request: Request, *args, **kwargs):
)
sessions = sessions.union(related_sessions)

data = {
"query": observable,
}
data = {"id": session_id} if session_id is not None else {"query": observable}
if settings.FEEDS_LICENSE:
data["license"] = settings.FEEDS_LICENSE

Expand Down
20 changes: 15 additions & 5 deletions api/views/feeds.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,10 @@
from certego_saas.apps.auth.backend import CookieTokenAuthentication
from certego_saas.ext.pagination import CustomPageNumberPagination
from django.contrib.postgres.aggregates import ArrayAgg
from django.contrib.postgres.expressions import ArraySubquery
from django.core import signing
from django.db.models import Count, F, Q, QuerySet, Value
from django.db.models.functions import JSONObject
from django.db.models import Count, F, OuterRef, Q, QuerySet, Value
from django.db.models.functions import JSONObject, Lower
from django.http import HttpResponseBase, StreamingHttpResponse
from django.utils import timezone
from drf_spectacular.types import OpenApiTypes
Expand Down Expand Up @@ -50,7 +51,7 @@
from greedybear.consts import SHARE_TOKEN_SALT, TRENDING_FEEDS_DATA_VERSION_KEY
from greedybear.cronjobs.repositories import TrendingBucketRepository
from greedybear.cronjobs.trending import build_ranked_attackers
from greedybear.models import IOC, ShareToken, ViewType
from greedybear.models import IOC, HoneypotPayload, ShareToken, ViewType

RENDERERS_BY_FORMAT = {
"json": FeedJSONRenderer,
Expand Down Expand Up @@ -303,8 +304,8 @@ class AdvancedFeedView(BaseFeedView):
include_sensors = True

def get_queryset(self) -> QuerySet:
"""Overrides base class to include credential count
and sensor information."""
"""Overrides base class to include credential count,
sensor information and, when verbose, the hashes of the payloads seen from each IOC."""
iocs = super().get_queryset()

iocs = iocs.annotate(credential_count=Count("credentials", distinct=True))
Expand All @@ -318,6 +319,15 @@ def get_queryset(self) -> QuerySet:
distinct=True,
)
)
if self.request_params.get("verbose", False):
# Annotate payload hashes viasubquery instead of another ArrayAgg:
# The aggregates above already join honeypots, tags, sensors and credentials into one GROUP BY,
# and every additional multi-valued join multiplies the rows per IOC.
# ARRAY(subquery) also yields an empty array when an IOC has no payloads.
payload_hashes = (
HoneypotPayload.objects.filter(iocs=OuterRef("pk")).annotate(sha256_lower=Lower("sha256")).order_by("sha256_lower").values("sha256_lower")
)
iocs = iocs.annotate(payload_hashes=ArraySubquery(payload_hashes))

return iocs

Expand Down
Loading
Loading