Skip to content

Commit c7e0c0f

Browse files
mlodicregulartimdependabot[bot]
authoredFeb 24, 2025··
1.5.2 (#477)
* Command sequences. Closes #457 (#468) * add CommandSequence model * add CommandSequence model to admin page * make migration file * add unique constraint to commands hash in CommandSequence model * add extraction of command sequences * add tests * add clustering task for command sequences * limit single command length during extraction * add tests for clustering * add 10 second delay to extraction jobs (will hopefully fix #451) * removed twitter publish cause not working * Deliver scores in Feeds API (#473) * add scores to serializer * fix docstring * add scores to required fields in deeds_response function * adapt tests * fix constant assignments (see #469) * make pending migration * skip empty IP address fields when extracting attacker data fixes #475 * Advanced feeds integration (#476) * Rename "age" to "prioritize" in backend code and add new prioritization mechanisms * Rename "age" to "prioritize" in frontend code * fix tests * adapt frontend tests * Bump numpy from 2.2.2 to 2.2.3 in /requirements (#465) Bumps [numpy](https://github.com/numpy/numpy) from 2.2.2 to 2.2.3. - [Release notes](https://github.com/numpy/numpy/releases) - [Changelog](https://github.com/numpy/numpy/blob/main/doc/RELEASE_WALKTHROUGH.rst) - [Commits](numpy/numpy@v2.2.2...v2.2.3) --- updated-dependencies: - dependency-name: numpy dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <[email protected]> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * bump --------- Signed-off-by: dependabot[bot] <[email protected]> Co-authored-by: tim <[email protected]> Co-authored-by: tim <[email protected]> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2 parents 8f267f0 + 7c13f87 commit c7e0c0f

26 files changed

+545
-84
lines changed
 

‎.env_template

+1-1
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,4 @@ COMPOSE_FILE=docker/default.yml:docker/local.override.yml
1313
#COMPOSE_FILE=docker/default.yml:docker/local.override.yml:docker/elasticsearch.yml
1414

1515
# If you want to run a specific version, populate this
16-
# REACT_APP_INTELOWL_VERSION="1.5.1"
16+
# REACT_APP_INTELOWL_VERSION="1.5.2"

‎.github/workflows/twitter_publish.yml

-17
This file was deleted.

‎api/serializers.py

+2
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,8 @@ class FeedsResponseSerializer(serializers.Serializer):
127127
asn = serializers.IntegerField(allow_null=True, min_value=1)
128128
destination_port_count = serializers.IntegerField(min_value=0)
129129
login_attempts = serializers.IntegerField(min_value=0)
130+
recurrence_probability = serializers.FloatField(min_value=0, max_value=1)
131+
expected_interactions = serializers.FloatField(min_value=0)
130132

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

‎api/urls.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
urlpatterns = [
1313
path("feeds/", feeds_pagination),
1414
path("feeds/advanced/", feeds_advanced),
15-
path("feeds/<str:feed_type>/<str:attack_type>/<str:age>.<str:format_>", feeds),
15+
path("feeds/<str:feed_type>/<str:attack_type>/<str:prioritize>.<str:format_>", feeds),
1616
path("enrichment", enrichment_view),
1717
path("general_honeypot", general_honeypot_list),
1818
# router viewsets

‎api/views/feeds.py

+16-23
Original file line numberDiff line numberDiff line change
@@ -15,25 +15,25 @@
1515

1616

1717
@api_view([GET])
18-
def feeds(request, feed_type, attack_type, age, format_):
18+
def feeds(request, feed_type, attack_type, prioritize, format_):
1919
"""
2020
Handle requests for IOC feeds with specific parameters and format the response accordingly.
2121
2222
Args:
2323
request: The incoming request object.
2424
feed_type (str): Type of feed (e.g., log4j, cowrie, etc.).
2525
attack_type (str): Type of attack (e.g., all, specific attack types).
26-
age (str): Age of the data to filter (e.g., recent, persistent).
26+
prioritize (str): Prioritization mechanism to use (e.g., recent, persistent).
2727
format_ (str): Desired format of the response (e.g., json, csv, txt).
2828
exclude_mass_scanners (bool): query parameter flag to exclude IOCs that are known mass scanners.
2929
3030
Returns:
3131
Response: The HTTP response with formatted IOC data.
3232
"""
33-
logger.info(f"request /api/feeds with params: feed type: {feed_type}, " f"attack_type: {attack_type}, Age: {age}, format: {format_}")
33+
logger.info(f"request /api/feeds with params: feed type: {feed_type}, " f"attack_type: {attack_type}, prioritization: {prioritize}, format: {format_}")
3434

3535
feed_params = FeedRequestParams({"feed_type": feed_type, "attack_type": attack_type, "format_": format_})
36-
feed_params.set_legacy_age(age)
36+
feed_params.set_prioritization(prioritize)
3737
if request.query_params and "exclude_mass_scanners" in request.query_params:
3838
feed_params.exclude_mass_scanners()
3939

@@ -58,7 +58,7 @@ def feeds_pagination(request):
5858

5959
feed_params = FeedRequestParams(request.query_params)
6060
feed_params.format = "json"
61-
feed_params.set_legacy_age(request.query_params.get("age"))
61+
feed_params.set_prioritization(request.query_params.get("prioritize"))
6262
if request.query_params and "exclude_mass_scanners" in request.query_params:
6363
feed_params.exclude_mass_scanners()
6464

@@ -79,24 +79,17 @@ def feeds_advanced(request):
7979
8080
Args:
8181
request: The incoming request object.
82-
83-
Supported query parameters are:
84-
85-
- **feed_type**: Type of feed to retrieve. (supported: `cowrie`, `log4j`, etc.; default: `all`)
86-
- **attack_type**: Type of attack to filter. (supported: `scanner`, `payload_request`, `all`; default: `all`)
87-
- **max_age**: Maximum number of days since last occurrence. \
88-
E.g. an IOC that was last seen 4 days ago is excluded by default. (default: 3)
89-
- **min_days_seen**: Minimum number of days on which an IOC must have been seen. (default: 1)
90-
- **include_reputation**: `;`-separated list of reputation values to include, \
91-
e.g. `known attacker` or `known attacker;` to include IOCs without reputation. (default: include all)
92-
- **exclude_reputation**: `;`-separated list of reputation values to exclude, \
93-
e.g. `mass scanner` or `mass scanner;bot, crawler`. (default: exclude none)
94-
- **feed_size**: Number of IOC items to return. (default: 5000)
95-
- **ordering**: Field to order results by, with optional `-` prefix for descending. (default: `-last_seen`)
96-
- **verbose**: `true` to include IOC properties that contain a lot of data, e.g. the list of days it was seen. (default: `false`)
97-
- **paginate**: `true` to paginate results. This forces the json format. (default: `false`)
98-
- **format_**: Response format type. Besides `json`, `txt` and `csv` are supported \
99-
but the response will only contain IOC values (e.g. IP adresses) without further information. (default: `json`)
82+
feed_type (str): Type of feed to retrieve. (supported: `cowrie`, `log4j`, etc.; default: `all`)
83+
attack_type (str): Type of attack to filter. (supported: `scanner`, `payload_request`, `all`; default: `all`)
84+
max_age (int): Maximum number of days since last occurrence. E.g. an IOC that was last seen 4 days ago is excluded by default. (default: 3)
85+
min_days_seen (int): Minimum number of days on which an IOC must have been seen. (default: 1)
86+
include_reputation (str): `;`-separated list of reputation values to include, e.g. `known attacker` or `known attacker;` to include IOCs without reputation. (default: include all)
87+
exclude_reputation (str): `;`-separated list of reputation values to exclude, e.g. `mass scanner` or `mass scanner;bot, crawler`. (default: exclude none)
88+
feed_size (int): Number of IOC items to return. (default: 5000)
89+
ordering (str): Field to order results by, with optional `-` prefix for descending. (default: `-last_seen`)
90+
verbose (bool): `true` to include IOC properties that contain a lot of data, e.g. the list of days it was seen. (default: `false`)
91+
paginate (bool): `true` to paginate results. This forces the json format. (default: `false`)
92+
format (str): Response format type. Besides `json`, `txt` and `csv` are supported but the response will only contain IOC values (e.g. IP adresses) without further information. (default: `json`)
10093
10194
Returns:
10295
Response: The HTTP response with formatted IOC data.

‎api/views/utils.py

+12-8
Original file line numberDiff line numberDiff line change
@@ -76,14 +76,8 @@ def __init__(self, query_params: dict):
7676
def exclude_mass_scanners(self):
7777
self.exclude_reputation.append("mass scanner")
7878

79-
def set_legacy_age(self, age: str):
80-
"""Translates legacy age specification into max_age and min_days_seen attributes
81-
and sets ordering accordingly.
82-
83-
Parameters:
84-
age (str): Age of the data to filter (recent or persistent).
85-
"""
86-
match age:
79+
def set_prioritization(self, prioritize: str):
80+
match prioritize:
8781
case "recent":
8882
self.max_age = "3"
8983
self.min_days_seen = "1"
@@ -96,6 +90,14 @@ def set_legacy_age(self, age: str):
9690
if "feed_type" in self.ordering:
9791
self.feed_type_sorting = self.ordering
9892
self.ordering = "-attack_count"
93+
case "likely_to_recur":
94+
self.max_age = "30"
95+
self.min_days_seen = "1"
96+
self.ordering = "-recurrence_probability"
97+
case "most_expected_hits":
98+
self.max_age = "30"
99+
self.min_days_seen = "1"
100+
self.ordering = "-expected_interactions"
99101

100102

101103
def get_valid_feed_types() -> frozenset[str]:
@@ -233,6 +235,8 @@ def feeds_response(iocs, feed_params, valid_feed_types, dict_only=False, verbose
233235
"login_attempts",
234236
"honeypots",
235237
"days_seen",
238+
"recurrence_probability",
239+
"expected_interactions",
236240
}
237241
iocs = (ioc_as_dict(ioc, required_fields) for ioc in iocs) if isinstance(iocs, list) else iocs.values(*required_fields)
238242
for ioc in iocs:

‎docker/.version

+1-1
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
REACT_APP_GREEDYBEAR_VERSION="1.5.1"
1+
REACT_APP_GREEDYBEAR_VERSION="1.5.2"

‎docker/env_file_template

+4
Original file line numberDiff line numberDiff line change
@@ -46,3 +46,7 @@ PUBLIC_DEPLOYMENT=False
4646
LEGACY_EXTRACTION=False
4747
# Interval for the honeypot data extraction in minutes (only choose divisors of 60)
4848
EXTRACTION_INTERVAL=10
49+
50+
# Set True to cluster command sequences recorded by Cowrie once a day
51+
# This might be computationaly expensive on large Databases
52+
CLUSTER_COWRIE_COMMAND_SEQUENCES=False

‎frontend/src/components/feeds/Feeds.jsx

+14-12
Original file line numberDiff line numberDiff line change
@@ -26,15 +26,17 @@ const attackTypeChoices = [
2626
{ label: "Payload request", value: "payload_request" },
2727
];
2828

29-
const ageChoices = [
29+
const prioritizationChoices = [
3030
{ label: "Recent", value: "recent" },
3131
{ label: "Persistent", value: "persistent" },
32+
{ label: "Likely to recur", value: "likely_to_recur" },
33+
{ label: "Most expected hits", value: "most_expected_hits" },
3234
];
3335

3436
const initialValues = {
3537
feeds_type: "all",
3638
attack_type: "all",
37-
age: "recent",
39+
prioritize: "recent",
3840
};
3941

4042
const initialState = {
@@ -59,7 +61,7 @@ export default function Feeds() {
5961
console.debug("Feeds-initialValues", initialValues);
6062

6163
const [url, setUrl] = React.useState(
62-
`${FEEDS_BASE_URI}/${initialValues.feeds_type}/${initialValues.attack_type}/${initialValues.age}.json`
64+
`${FEEDS_BASE_URI}/${initialValues.feeds_type}/${initialValues.attack_type}/${initialValues.prioritize}.json`
6365
);
6466

6567
// API to extract general honeypot
@@ -85,7 +87,7 @@ export default function Feeds() {
8587
params: {
8688
feed_type: initialValues.feeds_type,
8789
attack_type: initialValues.attack_type,
88-
age: initialValues.age,
90+
prioritize: initialValues.prioritize,
8991
},
9092
initialParams: {
9193
page: "1",
@@ -100,11 +102,11 @@ export default function Feeds() {
100102
(values) => {
101103
try {
102104
setUrl(
103-
`${FEEDS_BASE_URI}/${values.feeds_type}/${values.attack_type}/${values.age}.json`
105+
`${FEEDS_BASE_URI}/${values.feeds_type}/${values.attack_type}/${values.prioritize}.json`
104106
);
105107
initialValues.feeds_type = values.feeds_type;
106108
initialValues.attack_type = values.attack_type;
107-
initialValues.age = values.age;
109+
initialValues.prioritize = values.prioritize;
108110

109111
const resetPage = {
110112
type: "gotoPage",
@@ -185,15 +187,15 @@ export default function Feeds() {
185187
<Col sm={12} md={4}>
186188
<Label
187189
className="form-control-label"
188-
htmlFor="Feeds__age"
190+
htmlFor="Feeds__prioritize"
189191
>
190-
Age:
192+
Prioritize:
191193
</Label>
192194
<Select
193-
id="Feeds__age"
194-
name="age"
195-
value={initialValues.age}
196-
choices={ageChoices}
195+
id="Feeds__prioritize"
196+
name="prioritize"
197+
value={initialValues.prioritize}
198+
choices={prioritizationChoices}
197199
onChange={(e) => {
198200
formik.handleChange(e);
199201
formik.submitForm();

‎frontend/tests/components/feeds/Feeds.test.jsx

+3-3
Original file line numberDiff line numberDiff line change
@@ -72,8 +72,8 @@ describe("Feeds component", () => {
7272
expect(feedTypeSelectElement).toBeInTheDocument();
7373
const attackTypeSelectElement = screen.getByLabelText("Attack type:");
7474
expect(attackTypeSelectElement).toBeInTheDocument();
75-
const ageSelectElement = screen.getByLabelText("Age:");
76-
expect(ageSelectElement).toBeInTheDocument();
75+
const prioritizationSelectElement = screen.getByLabelText("Prioritize:");
76+
expect(prioritizationSelectElement).toBeInTheDocument();
7777

7878
const buttonRawData = screen.getByRole("link", { name: /Raw data/i });
7979
expect(buttonRawData).toHaveAttribute(
@@ -83,7 +83,7 @@ describe("Feeds component", () => {
8383

8484
await user.selectOptions(feedTypeSelectElement, "log4j");
8585
await user.selectOptions(attackTypeSelectElement, "scanner");
86-
await user.selectOptions(ageSelectElement, "persistent");
86+
await user.selectOptions(prioritizationSelectElement, "persistent");
8787

8888
await waitFor(() => {
8989
// check link has been changed

‎greedybear/admin.py

+18-2
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from django.contrib import admin, messages
66
from django.db.models import Q
77
from django.utils.translation import ngettext
8-
from greedybear.models import IOC, CowrieSession, GeneralHoneypot
8+
from greedybear.models import IOC, CommandSequence, CowrieSession, GeneralHoneypot
99

1010
logger = logging.getLogger(__name__)
1111

@@ -15,11 +15,26 @@
1515
# list_display = [field.name for field in Sensors._meta.get_fields()]
1616

1717

18+
class SessionInline(admin.TabularInline):
19+
model = CowrieSession
20+
fields = ["source", "start_time", "duration", "credentials", "interaction_count", "commands"]
21+
readonly_fields = fields
22+
show_change_link = True
23+
extra = 0
24+
ordering = ["-start_time"]
25+
26+
1827
@admin.register(CowrieSession)
1928
class CowrieSessionModelAdmin(admin.ModelAdmin):
2029
list_display = ["session_id", "start_time", "duration", "login_attempt", "credentials", "command_execution", "interaction_count", "source"]
2130
search_fields = ["source__name"]
22-
raw_id_fields = ["source"]
31+
raw_id_fields = ["source", "commands"]
32+
33+
34+
@admin.register(CommandSequence)
35+
class CommandSequenceModelAdmin(admin.ModelAdmin):
36+
list_display = ["first_seen", "last_seen", "cluster", "commands"]
37+
inlines = [SessionInline]
2338

2439

2540
@admin.register(IOC)
@@ -47,6 +62,7 @@ class IOCModelAdmin(admin.ModelAdmin):
4762
search_fields = ["name", "related_ioc__name"]
4863
raw_id_fields = ["related_ioc"]
4964
filter_horizontal = ["general_honeypot"]
65+
inlines = [SessionInline]
5066

5167
def general_honeypots(self, ioc):
5268
return ", ".join([str(element) for element in ioc.general_honeypot.all()])

‎greedybear/celery.py

+10-3
Original file line numberDiff line numberDiff line change
@@ -62,20 +62,20 @@ def setup_loggers(*args, **kwargs):
6262
"extract_log4pot": {
6363
"task": "greedybear.tasks.extract_log4pot",
6464
"schedule": crontab(minute=f"*/{hp_extraction_interval}"),
65-
"options": {"queue": "default"},
65+
"options": {"queue": "default", "countdown": 10},
6666
},
6767
# every 10 minutes or according to EXTRACTION_INTERVAL
6868
"extract_cowrie": {
6969
"task": "greedybear.tasks.extract_cowrie",
7070
"schedule": crontab(minute=f"*/{hp_extraction_interval}"),
71-
"options": {"queue": "default"},
71+
"options": {"queue": "default", "countdown": 10},
7272
},
7373
# FEEDS
7474
# every 10 minutes or according to EXTRACTION_INTERVAL
7575
"extract_general": {
7676
"task": "greedybear.tasks.extract_general",
7777
"schedule": crontab(minute=f"*/{hp_extraction_interval}"),
78-
"options": {"queue": "default"},
78+
"options": {"queue": "default", "countdown": 10},
7979
},
8080
# once a day
8181
"extract_sensors": {
@@ -106,4 +106,11 @@ def setup_loggers(*args, **kwargs):
106106
"schedule": crontab(hour=0, minute=hp_extraction_interval // 2),
107107
"options": {"queue": "default"},
108108
},
109+
# COMMANDS
110+
# once a day
111+
"command_clustering": {
112+
"task": "greedybear.tasks.cluster_commands",
113+
"schedule": crontab(hour=1, minute=3),
114+
"options": {"queue": "default"},
115+
},
109116
}

‎greedybear/cronjobs/attacks.py

+3
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,9 @@ def _get_attacker_data(self, honeypot, fields: list) -> list:
7575
hits_by_ip[hit.src_ip].append(hit.to_dict())
7676
iocs = []
7777
for ip, hits in hits_by_ip.items():
78+
# skip empty IP addresses
79+
if not ip.strip():
80+
continue
7881
dest_ports = [hit["dest_port"] for hit in hits if "dest_port" in hit]
7982
ioc = IOC(
8083
name=ip,

0 commit comments

Comments
 (0)
Please sign in to comment.