Skip to content

Commit 54cd62d

Browse files
authored
Merge pull request GreedyBear-Project#1271 from GreedyBear-Project/develop
3.4.0
2 parents 4462228 + 6207581 commit 54cd62d

62 files changed

Lines changed: 1942 additions & 651 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/.pre-commit-config.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
repos:
22
# Python linting with Ruff
33
- repo: https://github.com/astral-sh/ruff-pre-commit
4-
rev: v0.12.7
4+
rev: v0.15.11
55
hooks:
66
- id: ruff
77
name: ruff-lint

.github/CONTRIBUTING.md

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1 @@
1-
GreedyBear is handled by the same maintainers of [IntelOwl](https://github.com/intelowlproject/IntelOwl/).
2-
3-
So, please refer to the [Contribute guide](https://github.com/GreedyBear-Project/GreedyBear/wiki/Contribute)
1+
Please refer to our [Contribution guidelines](https://github.com/GreedyBear-Project/GreedyBear/wiki/Contribute).

README.md

Lines changed: 28 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,48 +1,25 @@
11
<p align="center"><img src="static/greedybear.png" width=350 height=404 alt="GreedyBear"/></p>
22

33
# GreedyBear
4-
[![GitHub release (latest by date)](https://img.shields.io/github/v/release/intelowlproject/Greedybear)](https://github.com/intelowlproject/Greedybear/releases)
5-
[![GitHub Repo stars](https://img.shields.io/github/stars/intelowlproject/Greedybear?style=social)](https://github.com/intelowlproject/Greedybear/stargazers)
6-
[![Twitter Follow](https://img.shields.io/twitter/follow/intel_owl?style=social)](https://twitter.com/intel_owl)
7-
[![Linkedin](https://img.shields.io/badge/LinkedIn-0077B5?style=flat&logo=linkedin&logoColor=white)](https://www.linkedin.com/company/intelowl/)
4+
[![GitHub release (latest by date)](https://img.shields.io/github/v/release/GreedyBear-Project/Greedybear)](https://github.com/GreedyBear-Project/Greedybear/releases)
5+
[![GitHub Repo stars](https://img.shields.io/github/stars/GreedyBear-Project/Greedybear?style=social)](https://github.com/GreedyBear-Project/Greedybear/stargazers)
6+
![GitHub License](https://img.shields.io/github/license/GreedyBear-Project/GreedyBear)
87

8+
[![uv](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/uv/main/assets/badge/v0.json)](https://github.com/astral-sh/uv)
99
[![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
10-
[![CodeQL](https://github.com/intelowlproject/GreedyBear/actions/workflows/codeql-analysis.yml/badge.svg)](https://github.com/intelowlproject/GreedyBear/actions/workflows/codeql-analysis.yml)
11-
[![Dependency Review](https://github.com/intelowlproject/GreedyBear/actions/workflows/dependency_review.yml/badge.svg)](https://github.com/intelowlproject/GreedyBear/actions/workflows/dependency_review.yml)
12-
[![Pull request automation](https://github.com/intelowlproject/GreedyBear/actions/workflows/pull_request_automation.yml/badge.svg)](https://github.com/intelowlproject/GreedyBear/actions/workflows/pull_request_automation.yml)
10+
[![CodeQL](https://github.com/GreedyBear-Project/GreedyBear/actions/workflows/codeql-analysis.yml/badge.svg)](https://github.com/GreedyBear-Project/GreedyBear/actions/workflows/codeql-analysis.yml)
11+
[![Dependency Review](https://github.com/GreedyBear-Project/GreedyBear/actions/workflows/dependency_review.yml/badge.svg)](https://github.com/GreedyBear-Project/GreedyBear/actions/workflows/dependency_review.yml)
12+
[![Pull request automation](https://github.com/GreedyBear-Project/GreedyBear/actions/workflows/pull_request_automation.yml/badge.svg)](https://github.com/GreedyBear-Project/GreedyBear/actions/workflows/pull_request_automation.yml)
1313

14-
The project goal is to extract data of the attacks detected by a [TPOT](https://github.com/telekom-security/tpotce) or a cluster of them and to generate some feeds that can be used to prevent and detect attacks.
14+
The project goal is to extract attack data detected by a [T-Pot](https://github.com/telekom-security/tpotce) or a cluster of them and to generate some feeds that can be used to prevent and detect attacks. You can read the [official announcement here](https://www.honeynet.org/2021/12/27/new-project-available-greedybear/).
1515

16-
[Official announcement here](https://www.honeynet.org/2021/12/27/new-project-available-greedybear/).
17-
18-
## Documentation
19-
20-
Documentation about GreedyBear installation, usage, configuration and contribution can be found at [this link](https://github.com/GreedyBear-Project/GreedyBear/wiki)
21-
22-
## Public feeds
23-
24-
There are public feeds provided by [The Honeynet Project](https://www.honeynet.org) in this [site](https://greedybear.honeynet.org). [Example](https://greedybear.honeynet.org/api/feeds/cowrie/all/recent.txt)
25-
26-
Please do not perform too many requests to extract feeds or you will be banned.
27-
28-
If you want to be updated regularly, please download the feeds only once every 10 minutes (this is the time between each internal update).
29-
30-
To check all the available feeds, Please refer to our [usage guide](https://github.com/GreedyBear-Project/GreedyBear/wiki/Usage)
31-
32-
33-
## Enrichment Service
34-
35-
GreedyBear provides an easy-to-query API to get the information available in GB regarding the queried observable (domain or IP address).
36-
37-
To understand more, Please refer to our [usage guide](https://github.com/GreedyBear-Project/GreedyBear/wiki/Usage)
38-
39-
## Run Greedybear on your environment
40-
The tool has been created not only to provide the feeds from The Honeynet Project's cluster of TPOTs.
41-
42-
If you manage one or more T-POTs of your own, you can get the code of this application and run Greedybear on your environment.
43-
In this way, you are able to provide new feeds of your own.
44-
45-
To install it locally, Please refer to our [installation guide](https://github.com/GreedyBear-Project/GreedyBear/wiki/Installation)
16+
## How to ...
17+
- **... try it out**: visit the [public instance](https://greedybear.honeynet.org) provided by [The Honeynet Project](https://www.honeynet.org) and take a look at a [threat intelligence live feed example](https://greedybear.honeynet.org/api/feeds/cowrie/all/recent.txt)
18+
- **... dive in**: read through our documentation in the [Wiki](https://github.com/GreedyBear-Project/GreedyBear/wiki) and explore GreedyBear's features
19+
- **... run your own instance**: to leverage everything GreedyBear has to offer, you might want to [install](https://github.com/GreedyBear-Project/GreedyBear/wiki/Installation) it and connect it to your own T-Pot
20+
- **... stay up to date**: [read](https://greedybear-project.github.io/) and [subscribe](https://greedybear-project.github.io/feed.xml) to our blog, where we regularly write about the most recent changes and new features
21+
- **... contact us**: using a Github [issue](https://github.com/GreedyBear-Project/GreedyBear/issues) or start a [discussion](https://github.com/GreedyBear-Project/GreedyBear/discussions)
22+
- **... contribute**: read through our [contribution guidelines](https://github.com/GreedyBear-Project/GreedyBear/wiki/Contribute), open an [issue](https://github.com/GreedyBear-Project/GreedyBear/issues), get assigned and raise a [pull request](https://github.com/GreedyBear-Project/GreedyBear/pulls)
4623

4724
## Sponsors and Acknowledgements
4825

@@ -57,15 +34,23 @@ Thanks to [The Honeynet Project](https://www.honeynet.org) we are providing free
5734
#### Google Summer of Code
5835
<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>
5936

60-
In 2026 we started participating to the [Google Summer of Code](https://summerofcode.withgoogle.com/) (GSoC)!
37+
In 2026 we started participating in the [Google Summer of Code](https://summerofcode.withgoogle.com/) (GSoC)!
6138

6239
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)!
6340

64-
## Maintainers and Key Contributors
41+
## Maintainers and Contributors
6542

6643
This project was started as a personal Christmas project by [Matteo Lodi](https://twitter.com/matte_lodi) in 2021.
6744

6845
Special thanks to:
69-
* [Tim Leonhard](https://github.com/regulartim) for having greatly improved the project and added Machine Learning Models during his master thesis. He's the actual Principal Mantainer.
70-
* [Martina Carella](https://github.com/carellamartina) for having created the GUI during her master thesis.
71-
* [Daniele Rosetti](https://github.com/drosetti) for helping maintaining the Frontend.
46+
- [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.
47+
- [Martina Carella](https://github.com/carellamartina) for having created the GUI during her master thesis.
48+
- [Daniele Rosetti](https://github.com/drosetti) for helping maintaining the Frontend.
49+
- and everyone who has contributed to GreedyBear!
50+
51+
<a href="https://github.com/GreedyBear-Project/GreedyBear/graphs/contributors">
52+
<img src="https://contrib.rocks/image?repo=GreedyBear-Project/GreedyBear" alt="GreedyBear contributors" />
53+
</a>
54+
55+
## License
56+
Distributed under the MIT license. See [`LICENSE`](LICENSE) for the full text.

api/serializers.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,7 @@ class FeedsResponseSerializer(serializers.Serializer):
237237
attacker_country = serializers.CharField(allow_null=True, allow_blank=True, max_length=120)
238238
attacker_country_code = serializers.CharField(allow_null=True, allow_blank=True, max_length=2)
239239
tags = TagSerializer(many=True, required=False, default=list)
240+
sensors = SensorSerializer(many=True, required=False, default=list)
240241

241242
def validate_feed_type(self, feed_type):
242243
logger.debug(f"FeedsResponseSerializer - validation feed_type: '{feed_type}'")

api/urls.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
feeds_pagination,
1616
feeds_revoke,
1717
feeds_share,
18+
feeds_tokens,
1819
general_honeypot_list,
1920
health_view,
2021
news_view,
@@ -30,6 +31,7 @@
3031
path("feeds/share", feeds_share),
3132
path("feeds/consume/<str:token>", feeds_consume),
3233
path("feeds/revoke/<str:token>", feeds_revoke),
34+
path("feeds/tokens/", feeds_tokens),
3335
path("feeds/advanced/", feeds_advanced),
3436
path("feeds/asn/", feeds_asn),
3537
path("feeds/<str:feed_type>/<str:attack_type>/<str:prioritize>.<str:format_>", feeds),

api/views/cowrie_session.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ def cowrie_session_view(request):
101101
if include_similar:
102102
commands = {s.commands for s in sessions if s.commands}
103103
clusters = {cmd.cluster for cmd in commands if cmd.cluster is not None}
104-
related_sessions = CowrieSession.objects.filter(commands__cluster__in=clusters).prefetch_related("source", "commands", "credentials")
104+
related_sessions = CowrieSession.objects.filter(commands__cluster__in=clusters, duration__gt=0).prefetch_related("source", "commands", "credentials")
105105
sessions = sessions.union(related_sessions)
106106

107107
response_data = {

api/views/feeds.py

Lines changed: 49 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,14 @@
4141
"prioritize",
4242
]
4343

44+
_TOKEN_LIST_FIELDS = (
45+
"token_hash",
46+
"reason",
47+
"created_at",
48+
"revoked",
49+
"revoked_at",
50+
)
51+
4452

4553
@api_view([GET])
4654
@throttle_classes([FeedsThrottle])
@@ -145,13 +153,14 @@ def feeds_advanced(request):
145153
valid_feed_types,
146154
tag_key=request.query_params.get("tag_key", "").strip(),
147155
tag_value=request.query_params.get("tag_value", "").strip(),
156+
include_sensors=True,
148157
)
149158
if paginate:
150159
paginator = CustomPageNumberPagination()
151160
iocs = paginator.paginate_queryset(iocs_queryset, request)
152-
resp_data = feeds_response(request, iocs, feed_params, valid_feed_types, dict_only=True, verbose=verbose)
161+
resp_data = feeds_response(request, iocs, feed_params, valid_feed_types, dict_only=True, verbose=verbose, include_sensors=True)
153162
return paginator.get_paginated_response(resp_data)
154-
return feeds_response(request, iocs_queryset, feed_params, valid_feed_types, verbose=verbose)
163+
return feeds_response(request, iocs_queryset, feed_params, valid_feed_types, verbose=verbose, include_sensors=True)
155164

156165

157166
@api_view(["GET"])
@@ -219,20 +228,27 @@ def feeds_share(request):
219228
port (int): Filter by destination port.
220229
start_date (str): Filter by start date (YYYY-MM-DD).
221230
end_date (str): Filter by end date (YYYY-MM-DD).
231+
reason (str): Optional human-readable label for this share token (max 256 chars).
222232
223233
Returns:
224234
Response: A JSON object containing the signed shareable URL.
225235
"""
226-
logger.info(f"request /api/feeds/share with params: {request.query_params}")
236+
safe_params = {k: v for k, v in request.query_params.items() if k != "reason"}
237+
logger.info(f"request /api/feeds/share with params: {safe_params}")
227238
feed_params = FeedRequestParams(request.query_params)
228239
data = vars(feed_params)
229240
# Remove internal or non-serializable objects if any
230241
data.pop("feed_type_sorting", None)
231242

243+
reason = request.query_params.get("reason", "").strip()[:256]
244+
232245
# Generate signed token and persist a ShareToken record
233246
token = signing.dumps(data, salt="greedybear-feeds")
234247
token_hash = hashlib.sha256(token.encode()).hexdigest()
235-
ShareToken.objects.get_or_create(token_hash=token_hash, defaults={"user": request.user})
248+
ShareToken.objects.get_or_create(
249+
token_hash=token_hash,
250+
defaults={"user": request.user, "reason": reason},
251+
)
236252

237253
host = request.build_absolute_uri("/")
238254
share_url = f"{host}api/feeds/consume/{token}"
@@ -323,3 +339,32 @@ def feeds_revoke(request, token):
323339
share_token.revoked_at = timezone.now()
324340
share_token.save(update_fields=["revoked", "revoked_at"])
325341
return Response({"detail": "Token revoked successfully."}, status=status.HTTP_200_OK)
342+
343+
344+
@api_view([GET])
345+
@authentication_classes([CookieTokenAuthentication])
346+
@permission_classes([IsAuthenticated])
347+
def feeds_tokens(request):
348+
"""
349+
List the calling user's share tokens with safe metadata.
350+
351+
Returns only non-sensitive fields: a truncated hash prefix (first 12 hex
352+
chars), the reason label, creation timestamp, and revocation status.
353+
The raw token is never stored and therefore cannot be returned.
354+
355+
Returns:
356+
Response: A JSON list of token metadata objects.
357+
"""
358+
logger.info("request /api/feeds/tokens/")
359+
tokens = ShareToken.objects.filter(user=request.user).order_by("-created_at").values(*_TOKEN_LIST_FIELDS)
360+
results = [
361+
{
362+
"hash_prefix": t["token_hash"][:12],
363+
"reason": t["reason"],
364+
"created_at": t["created_at"],
365+
"revoked": t["revoked"],
366+
"revoked_at": t["revoked_at"],
367+
}
368+
for t in tokens
369+
]
370+
return Response(results)

api/views/statistics.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -86,18 +86,25 @@ def countries(self, request):
8686
request: The incoming request object.
8787
8888
Returns:
89-
Response: A JSON list of {country, count} objects ordered by count descending.
89+
Response: A JSON list of {country, code, count} objects ordered by count descending.
9090
"""
9191
delta, _ = self.__parse_range(self.request)
9292
qs = (
9393
IOC.objects.filter(last_seen__gte=delta)
9494
.exclude(attacker_country="")
9595
.filter(honeypots__active=True)
96-
.values("attacker_country")
96+
.values("attacker_country", "attacker_country_code")
9797
.annotate(count=Count("id", distinct=True))
9898
.order_by("-count")
9999
)
100-
data = [{"country": item["attacker_country"], "count": item["count"]} for item in qs]
100+
data = [
101+
{
102+
"country": item["attacker_country"],
103+
"code": item["attacker_country_code"],
104+
"count": item["count"],
105+
}
106+
for item in qs
107+
]
101108
return Response(data)
102109

103110
@action(detail=False, methods=["get"])

api/views/utils.py

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,9 @@ def get_valid_feed_types() -> frozenset[str]:
153153
return frozenset(feed_types)
154154

155155

156-
def get_queryset(request, feed_params, valid_feed_types, is_aggregated=False, serializer_class=FeedsRequestSerializer, tag_key="", tag_value=""):
156+
def get_queryset(
157+
request, feed_params, valid_feed_types, is_aggregated=False, serializer_class=FeedsRequestSerializer, tag_key="", tag_value="", include_sensors=False
158+
):
157159
"""
158160
Build a queryset to filter IOC data based on the request parameters.
159161
@@ -172,6 +174,8 @@ def get_queryset(request, feed_params, valid_feed_types, is_aggregated=False, se
172174
- Default: `FeedsRequestSerializer`.
173175
tag_key (str, optional): Filter IOCs by tag key. Only passed from feeds_advanced.
174176
tag_value (str, optional): Filter IOCs by tag value (case-insensitive substring). Only passed from feeds_advanced.
177+
include_sensors (bool, optional): If True, annotates sensors_json for each IOC.
178+
Only passed from authenticated views like feeds_advanced. Default: False.
175179
176180
Returns:
177181
QuerySet: The filtered queryset of IOC data.
@@ -252,6 +256,15 @@ def get_queryset(request, feed_params, valid_feed_types, is_aggregated=False, se
252256
distinct=True,
253257
)
254258
)
259+
if include_sensors:
260+
iocs = iocs.annotate(
261+
sensors_json=ArrayAgg(
262+
JSONObject(address=F("sensors__address"), label=F("sensors__label")),
263+
filter=Q(sensors__isnull=False),
264+
default=Value([]),
265+
distinct=True,
266+
)
267+
)
255268
iocs = iocs.order_by(feed_params.ordering)
256269
iocs = iocs[: int(feed_params.feed_size)]
257270

@@ -276,7 +289,7 @@ def ioc_as_dict(ioc, fields: set) -> dict:
276289
return {k: v for k, v in ioc.__dict__.items() if k in fields}
277290

278291

279-
def feeds_response(request=None, iocs=None, feed_params=None, valid_feed_types=None, dict_only=False, verbose=False):
292+
def feeds_response(request=None, iocs=None, feed_params=None, valid_feed_types=None, dict_only=False, verbose=False, include_sensors=False):
280293
"""
281294
Format the IOC data into the requested format (e.g., JSON, CSV, TXT).
282295
@@ -339,13 +352,19 @@ def feeds_response(request=None, iocs=None, feed_params=None, valid_feed_types=N
339352
required_fields = base_fields + verbose_only_fields if verbose else base_fields
340353

341354
# `tags_json` is annotated in get_queryset (only for JSON format) to avoid conflicting
342-
# with the `tags` reverse FK on IOC. When the queryset comes from a repository method
355+
# with the `tags` reverse FK on IOC. When the queryset comes from a repository method
343356
# that does not annotate `tags_json` (e.g. the ML scoring path), exclude the field.
357+
# `sensors_json` follows the same pattern and is only annotated for authenticated views.
344358
if isinstance(iocs, list):
345359
has_tags_annotation = bool(iocs) and hasattr(iocs[0], "tags_json")
360+
has_sensors_annotation = include_sensors and bool(iocs) and hasattr(iocs[0], "sensors_json")
346361
else:
347362
has_tags_annotation = "tags_json" in getattr(iocs, "query", type("", (), {"annotations": {}})()).annotations
363+
has_sensors_annotation = include_sensors and "sensors_json" in getattr(iocs, "query", type("", (), {"annotations": {}})()).annotations
364+
348365
required_fields = tuple(("tags_json" if f == "tags" else f) for f in required_fields if f != "tags" or has_tags_annotation)
366+
if has_sensors_annotation:
367+
required_fields = required_fields + ("sensors_json",)
349368

350369
iocs_iter: object
351370
if isinstance(iocs, list):
@@ -362,6 +381,7 @@ def feeds_response(request=None, iocs=None, feed_params=None, valid_feed_types=N
362381
"destination_port_count": len(ioc.get("destination_ports", [])),
363382
"asn": ioc.get("autonomous_system", ""),
364383
"tags": ioc.pop("tags_json", []),
384+
**({"sensors": ioc.pop("sensors_json", [])} if has_sensors_annotation else {}),
365385
}
366386

367387
if not verbose:
@@ -557,7 +577,7 @@ def get_greedybear_news() -> list[dict]:
557577
feed = feedparser.parse(response.content)
558578

559579
filtered_entries = sorted(
560-
[entry for entry in feed.entries if "greedybear" in entry.get("title", "").lower() and entry.get("published_parsed")],
580+
[entry for entry in feed.entries if entry.get("published_parsed")],
561581
key=lambda e: e.published_parsed,
562582
reverse=True,
563583
)

0 commit comments

Comments
 (0)