django-restit 4.2.78__py3-none-any.whl → 4.2.83__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
account/models/member.py CHANGED
@@ -224,11 +224,11 @@ class Member(User, RestModel, MetaDataModel):
224
224
 
225
225
  @property
226
226
  def force_single_session(self):
227
- return self.hasPermission("force_single_session")
227
+ return self.hasPermission("force_single_session", ignore_su=True)
228
228
 
229
229
  @property
230
230
  def email_disabled(self):
231
- return self.hasPermission("email_disabled")
231
+ return self.hasPermission("email_disabled", ignore_su=True)
232
232
 
233
233
  @property
234
234
  def has_totp(self):
account/models/notify.py CHANGED
@@ -130,6 +130,8 @@ class NotificationRecord(models.Model, RestModel):
130
130
 
131
131
  @classmethod
132
132
  def canSend(cls):
133
+ if not settings.get("THROTTLE_EMAILS", False):
134
+ return True
133
135
  max_emails_per_minute = settings.get("MAX_EMAILS_PER_MINUTE", 30)
134
136
  last_email = NotificationRecord.objects.filter(state=1).last()
135
137
  now = datetime.now()
@@ -206,7 +208,7 @@ class NotificationRecord(models.Model, RestModel):
206
208
 
207
209
  @classmethod
208
210
  def _notifyViaEmail(cls, member, subject, message, template, context,
209
- attachments, from_email=None):
211
+ attachments, from_email=settings.DEFAULT_FROM_EMAIL):
210
212
  # lets verify the db is working
211
213
  if template:
212
214
  if context is None:
@@ -215,6 +217,8 @@ class NotificationRecord(models.Model, RestModel):
215
217
  context["body"] = message
216
218
  context["unsubscribe_token"] = member.getUUID()
217
219
  message = inbox.utils.renderTemplate(template, context)
220
+ if from_email is None:
221
+ from_email = settings.DEFAULT_FROM_EMAIL
218
222
 
219
223
  nr = NotificationMemberRecord(member=member, to_addr=member.email)
220
224
  email_record = NotificationRecord(
@@ -0,0 +1,20 @@
1
+ # Generated by Django 4.2.11 on 2024-05-13 02:34
2
+
3
+ from django.db import migrations, models
4
+ import django.db.models.deletion
5
+
6
+
7
+ class Migration(migrations.Migration):
8
+
9
+ dependencies = [
10
+ ('sessionlog', '0001_initial'),
11
+ ('auditlog', '0001_initial'),
12
+ ]
13
+
14
+ operations = [
15
+ migrations.AlterField(
16
+ model_name='persistentlog',
17
+ name='session',
18
+ field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='sessionlog.sessionlog'),
19
+ ),
20
+ ]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: django-restit
3
- Version: 4.2.78
3
+ Version: 4.2.83
4
4
  Summary: A Rest Framework for DJANGO
5
5
  License: MIT
6
6
  Author: Ian Starnes
@@ -28,9 +28,9 @@ account/models/device.py,sha256=TloXvvrx3khF3BeGFuVYn6DhXjOW0AMZb4F9Fl5nBII,5491
28
28
  account/models/feeds.py,sha256=vI7fG4ASY1M0Zjke24RdnfDcuWeATl_yR_25jPmT64g,2011
29
29
  account/models/group.py,sha256=iDD_oSgswKV_t_gXZuVK80MvICrZZqdANm2jtGtOFy8,21985
30
30
  account/models/legacy.py,sha256=zYdtv4LC0ooxPVqWM-uToPwV-lYWQLorSE6p6yn1xDw,2720
31
- account/models/member.py,sha256=v2cM7g5XoOQi_ZAlGcOcKn24zZCfAFokxUFIqNshDxc,52960
31
+ account/models/member.py,sha256=fzSVVAdbUa1knp1O4JTnYZFYRas7-zDZaOPjZAMCC1Q,52992
32
32
  account/models/membership.py,sha256=90EpAhOsGaqphDAkONP6j_qQ0OWSRaQsI8H7E7fgMkE,9249
33
- account/models/notify.py,sha256=Qzi8gLsVi8nDx8gpL4dyr0MPExYYGIDxZvHFUdCs7H4,15072
33
+ account/models/notify.py,sha256=iAq8tjyqouUelYgsMhWlchYmEAuCsKAyNIr5F8_xUeU,15258
34
34
  account/models/passkeys.py,sha256=TJxITUi4DT4_1tW2K7ZlOcRjJuMVl2NtKz7pKQU8-Tw,1516
35
35
  account/models/session.py,sha256=ELkWjB_2KXQvPtRPrvuGJpJsqrxCQX_4J53SbqGz_2U,3737
36
36
  account/models/settings.py,sha256=gOyRWBVd3BQpjfj_hJPtqX3H46ztyRAFxBrPbv11lQg,2137
@@ -65,6 +65,7 @@ auditlog/cloudwatch.py,sha256=R-B_ByVM3We26YnDoFYIQeWV31CUyS63QTojRAkfWa8,2805
65
65
  auditlog/decorators.py,sha256=ZoIv0fhZjxtMEV15NcKijW4xPF5UEScPna60zB3TxZo,6553
66
66
  auditlog/middleware.py,sha256=Q4bXg8rnm8y2fMnAsN6ha3Fz6TW8jIzLnvpu4H9SpWE,1537
67
67
  auditlog/migrations/0001_initial.py,sha256=X171gKQZIaTO9FGNG1yKTjGSZS0ZjZj5gvimF9-_kks,3309
68
+ auditlog/migrations/0002_alter_persistentlog_session.py,sha256=DkkcIobbHdbniKg5bOlRmiF-Nc4hX55Y6KuQySrCcJ8,541
68
69
  auditlog/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
69
70
  auditlog/models.py,sha256=skDAiuzR4chC-WNIaH2nm_VVcbnDD6ZtUxBwhk7UY8U,16517
70
71
  auditlog/periodic.py,sha256=AUhDeVsZtC47BJ-lklvYEegHoxAzj1RpIvRFSsM7g5E,363
@@ -79,6 +80,7 @@ inbox/migrations/0001_initial.py,sha256=P1OmbSHZGhj3wVBdFKWEzNrPdbyKzR9fFBXP8rhX
79
80
  inbox/migrations/0002_alter_message_cc.py,sha256=dsnDHCs1-dFZfSEWJmufBOs5gvNbI7u99kru6fVas0Y,380
80
81
  inbox/migrations/0003_attachment_content_type.py,sha256=dh_km90V6R3O0-N2oNTWhWLZZ96MylRgDY7Poua9CZ8,416
81
82
  inbox/migrations/0004_mailtemplate.py,sha256=yV51UdsRWmKC5Dy34-h2bXBeYeFtjoWQ7kOw7cuYCQo,1140
83
+ inbox/migrations/0005_alter_mailbox_state.py,sha256=trr-CCLupHQ7e-tjJK08LACdxhCApGMNBTOeWFcyXnI,393
82
84
  inbox/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
83
85
  inbox/models/__init__.py,sha256=yARvP31nhJGLjqP-U_ONi2OLjiTUFspdH0AlKynt4Y8,174
84
86
  inbox/models/bounce.py,sha256=3b_pCKH3gwb3NE8I1XlVI6JeoVmobZyKidsILH-jIRg,2881
@@ -106,17 +108,18 @@ incident/migrations/0011_ticket.py,sha256=Ml5E_Qi4Z0MD89fetoOFOL3rPlVQdjaaDCcFBf
106
108
  incident/migrations/0012_rule_match_by.py,sha256=PGclGnnc_8JEsJZ8znoXm-iAC6Y0i2WM6C2cmFgdKlA,372
107
109
  incident/migrations/0013_rulecheck_is_required.py,sha256=cL7tOj5XGPpKd2f5BojIKfNJeDB1IL-jGRU6-g-Co5o,387
108
110
  incident/migrations/0014_event_group_alter_rulecheck_index.py,sha256=v3gm5k0LVoas27qUDOt7el7YtK4yjFVLeEpuFUCoXaQ,724
111
+ incident/migrations/0015_rule_title_template_alter_incident_state.py,sha256=FPUDhFwqBC39EjeknRT7BPddEf6ExCjsXVb9LMqIn3U,687
109
112
  incident/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
110
113
  incident/models/__init__.py,sha256=NMphuhb0RTMf7Ov4QkNv7iv6_I8Wtr3xQ54yjX_a31M,209
111
- incident/models/event.py,sha256=WRNzvjo0jypdnQNBksOOyi-_0kVT4qWUrDZf0Aw_MPM,7355
114
+ incident/models/event.py,sha256=Dw6fUi2tbLeA_ZRDcvGQNFkCkMGMBdtNeaLikXdAyE8,7769
112
115
  incident/models/incident.py,sha256=HPbi6J9qm7_-FMjnDUPV9NcbmP_60WU-IO9HJSpoLTY,19360
113
- incident/models/ossec.py,sha256=p1ptr-8lnaj1EP_VmPR58b2LmaYBGaYYKAMqhWK5yZM,2227
114
- incident/models/rules.py,sha256=SMlDRw_r3fGv-vmRojRLmsklqRRxDcjrSLVBIz-gadA,6884
116
+ incident/models/ossec.py,sha256=eUDRGawzuLWobKEVGKfdZisDnyjS_Hlxi0T_GCSLCCI,2252
117
+ incident/models/rules.py,sha256=aRkJ0ZnTv87nAUC1sHVkPExfb3OJ8fgHQIhnCIpIbhQ,7001
115
118
  incident/models/ticket.py,sha256=S3kqGQpYLE6Y4M9IKu_60sgW-f592xNr8uufqHnvDoU,2302
116
119
  incident/parsers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
117
- incident/parsers/ossec.py,sha256=uh9LFLUa0uL7a-3l9w3Kd7YmTZGYbS24uoDwPWM_s58,8415
120
+ incident/parsers/ossec.py,sha256=jyJmNBwnQS1tjZMwYhslnCpZviCHXnozv88BPT-ytCw,11592
118
121
  incident/periodic.py,sha256=eX1rQK6v65A9ugofTvJPSmAWei6C-3EYgzCMuGZ03jM,381
119
- incident/rpc.py,sha256=3y0rfxRR9DikmCmj3IRcMaCLtzLCMrtH64lrjY1w2Og,7992
122
+ incident/rpc.py,sha256=viJt873b8T8SiAq10EM57lF8g7ghyj3ymdkaXzh2Ass,8181
120
123
  incident/templates/email/incident_change.html,sha256=tQYphypwLukkVdwH0TB2Szz2VEJ7GnsfRS3_ZJ-MYeE,13895
121
124
  incident/templates/email/incident_msg.html,sha256=MZdKhTddUF2MpiH8Z3RTQEmW_ko1n3ajeZ11KLtiLlU,13780
122
125
  incident/templates/email/incident_new.html,sha256=W6nwFQROnyDfMlXub8s02ws4hGnJp16pfgp9xTm_aEc,15185
@@ -132,12 +135,12 @@ location/migrations/0004_remove_address_modified_by_address_group_and_more.py,sh
132
135
  location/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
133
136
  location/models/__init__.py,sha256=rZhldkoKmoJQXjBAK1IIQn7K_OOJvFtIGOGVl_szqbE,230
134
137
  location/models/address.py,sha256=wl0bToZ6VrJP923IIfzWqZY9xyKgla6A-uzj8jQFGRI,3149
135
- location/models/ip.py,sha256=ZaBFdW1tL1Q3bnS5gIY9SseiQ5xeeP_oyP1hp3czFeA,5984
138
+ location/models/ip.py,sha256=Bl-OlwEXGvKYvYSDBSsnQkeAi4ZTKs1mDt3ddc5rq80,6039
136
139
  location/models/legacy.py,sha256=8ROsUSZrjGQkUyXeJvoxPdKAWaKfUH-AL9TIeJb7krg,1994
137
140
  location/models/location.py,sha256=01dJPJecbp5orExsIGWOsBC_KkwFRIW0rGDIwyx1r0w,2316
138
141
  location/models/track.py,sha256=OdhRL1KVXlPcZkp4S6QpKc7Ctoth8VjwHs_dlZ8XHI4,1474
139
142
  location/providers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
140
- location/providers/iplookup/__init__.py,sha256=t7yiXnEopAN_NZ77k2oHAhfVCmWNDkiCKA8S41jbJv0,727
143
+ location/providers/iplookup/__init__.py,sha256=I0K0HZrluCsBZ1TlGGKnyavEDZ3mT-xfE7Dtq2k-F9k,790
141
144
  location/providers/iplookup/abstractapi.py,sha256=gY8eqpjEasZtiBC6nNu960ZGL96FVwNS2JoZuP1GBO4,2419
142
145
  location/providers/iplookup/extremeip.py,sha256=QNRGhwXXsOuJL2M-xiI2pFN_6LP2HkqSUpFosu5Q04M,1345
143
146
  location/providers/iplookup/geoplugin.py,sha256=RK_6McxHYlVVMVdJ2rCafw-kqMfzMm3g_tJjBwcKXYg,2121
@@ -401,7 +404,7 @@ rest/middleware/request.py,sha256=JchRNy5L-bGd-7h-KFYekGRvREe2eCkZXKOYqIkP2hI,41
401
404
  rest/middleware/session.py,sha256=zHSoQpIzRLmpqr_JvW406wzpvU3W3gDbm5JhtzLAMlE,10240
402
405
  rest/middleware/session_store.py,sha256=1nSdeXK8PyuYgGgIufqrS6j6QpIrQ7zbMNT0ol75e6U,1901
403
406
  rest/models/__init__.py,sha256=M8pvFDq-WCF-QcM58X7pMufYYe0aaQ3U0PwGe9TKbbY,130
404
- rest/models/base.py,sha256=MIZUQStR5Y2ndSjmOSu-NSIg3SZs9IFoMlRQ2re75OE,69565
407
+ rest/models/base.py,sha256=bPcTeCX7KvhkcVMKEyLmu9eYc7ccJGkIP1n3cctJPVE,69675
405
408
  rest/models/cacher.py,sha256=eKz8TINVhWEqKhJGMsRkKZTtBUIv5rN3NHbZwOC56Uk,578
406
409
  rest/models/metadata.py,sha256=65GvfFbc26_7wJz8qEAzU7fEOZWVz0ttO5j5m_gs4hk,12860
407
410
  rest/net.py,sha256=LcB2QV6VNRtsSdmiQvYZgwQUDwOPMn_VBdRiZ6OpI-I,2974
@@ -472,6 +475,7 @@ telephony/phone_util.py,sha256=5NwSBnwBEC3EaeSeN42ggBiAQ00Ujvr6CepDjXLsCyw,5067
472
475
  telephony/rpc.py,sha256=PXPDFvgoXkCKlfMzIbt6lYZPay3fcveNj2X4Pjby7p4,3473
473
476
  wiki/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
474
477
  wiki/migrations/0001_initial.py,sha256=9jvUyjrbJrbDilRnwzQUPhPV8Xi_olEPBk_N0nycvM0,3606
478
+ wiki/migrations/0002_alter_pagemedia_entry.py,sha256=9CUnfvBmj0D4akCkux7HFuXgw9B9avE8V-iMCm5cjds,485
475
479
  wiki/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
476
480
  wiki/models/__init__.py,sha256=jE-9r_Hqpyo7ysKu9BschXOn5Zg34wUt894GwJpxA28,132
477
481
  wiki/models/faq.py,sha256=nvcEFerllQKT61kIYlasvZzRKwpXyfmQpiqkpHP1V1o,1745
@@ -502,7 +506,7 @@ ws4redis/servers/uwsgi.py,sha256=VyhoCI1DnVFqBiJYHoxqn5Idlf6uJPHvfBKgkjs34mo,172
502
506
  ws4redis/settings.py,sha256=K0yBiLUuY81iDM4Yr-k8hbvjn5VVHu5zQhmMK8Dtz0s,1536
503
507
  ws4redis/utf8validator.py,sha256=S0OlfjeGRP75aO6CzZsF4oTjRQAgR17OWE9rgZdMBZA,5122
504
508
  ws4redis/websocket.py,sha256=R0TUyPsoVRD7Y_oU7w2I6NL4fPwiz5Vl94-fUkZgLHA,14848
505
- django_restit-4.2.78.dist-info/LICENSE.md,sha256=VHN4hhEeVOoFjtG-5fVv4jesA4SWi0Z-KgOzzN6a1ps,1068
506
- django_restit-4.2.78.dist-info/METADATA,sha256=wKYW-bztLqxjiqvXiiFHkxyo4_uLm5shsfOU3xnJnbE,7645
507
- django_restit-4.2.78.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
508
- django_restit-4.2.78.dist-info/RECORD,,
509
+ django_restit-4.2.83.dist-info/LICENSE.md,sha256=VHN4hhEeVOoFjtG-5fVv4jesA4SWi0Z-KgOzzN6a1ps,1068
510
+ django_restit-4.2.83.dist-info/METADATA,sha256=rbSXb9-b2DozTJTvyuPOVSB8x-7l1fWIhCqtR013RVg,7645
511
+ django_restit-4.2.83.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
512
+ django_restit-4.2.83.dist-info/RECORD,,
@@ -0,0 +1,18 @@
1
+ # Generated by Django 4.2.11 on 2024-05-13 02:34
2
+
3
+ from django.db import migrations, models
4
+
5
+
6
+ class Migration(migrations.Migration):
7
+
8
+ dependencies = [
9
+ ('inbox', '0004_mailtemplate'),
10
+ ]
11
+
12
+ operations = [
13
+ migrations.AlterField(
14
+ model_name='mailbox',
15
+ name='state',
16
+ field=models.IntegerField(db_index=True, default=1),
17
+ ),
18
+ ]
@@ -0,0 +1,23 @@
1
+ # Generated by Django 4.2.11 on 2024-05-13 02:34
2
+
3
+ from django.db import migrations, models
4
+
5
+
6
+ class Migration(migrations.Migration):
7
+
8
+ dependencies = [
9
+ ('incident', '0014_event_group_alter_rulecheck_index'),
10
+ ]
11
+
12
+ operations = [
13
+ migrations.AddField(
14
+ model_name='rule',
15
+ name='title_template',
16
+ field=models.CharField(default=None, max_length=200, null=True),
17
+ ),
18
+ migrations.AlterField(
19
+ model_name='incident',
20
+ name='state',
21
+ field=models.IntegerField(choices=[(0, 'new'), (1, 'opened'), (2, 'paused'), (3, 'ignored'), (4, 'resolved'), (5, 'pending')], default=0),
22
+ ),
23
+ ]
incident/models/event.py CHANGED
@@ -73,6 +73,7 @@ class Event(JSONMetaData, rm.RestModel):
73
73
 
74
74
  level = models.IntegerField(default=0, db_index=True)
75
75
  category = models.CharField(max_length=124, db_index=True)
76
+ # code = models.IntegerField(default=0, db_index=True)
76
77
 
77
78
  group = models.ForeignKey(
78
79
  "account.Group", on_delete=models.SET_NULL,
@@ -154,7 +155,7 @@ class Event(JSONMetaData, rm.RestModel):
154
155
  incident = None
155
156
  action_count = 0
156
157
  if hit_rule is not None:
157
- logger.error(f"RULE HIT: {hit_rule.name}")
158
+ # logger.error(f"RULE HIT: {hit_rule.name}")
158
159
  priority = hit_rule.priority
159
160
  if hit_rule.action == "ignore":
160
161
  self.save()
@@ -183,7 +184,13 @@ class Event(JSONMetaData, rm.RestModel):
183
184
  if hit_rule is not None and hit_rule.action_after != 0:
184
185
  incident.state = INCIDENT_STATE_PENDING
185
186
  # TODO possibly make this smarter?
186
- if self.category == "ossec":
187
+ if hit_rule and hit_rule.title_template and "{" in hit_rule.title_template:
188
+ try:
189
+ incident.description = hit_rule.title_template.format(event=self)
190
+ except Exception:
191
+ logger.exception(hit_rule.title_template)
192
+ incident.description = self.description
193
+ elif self.category == "ossec":
187
194
  incident.description = f"{self.hostname}: {self.description}"
188
195
  else:
189
196
  incident.description = self.description
incident/models/ossec.py CHANGED
@@ -47,5 +47,7 @@ class ServerOssecAlert(models.Model, rm.RestModel):
47
47
  title = models.CharField(max_length=200, blank=True, null=True, default=None)
48
48
  geoip = models.ForeignKey("location.GeoIP", blank=True, null=True, default=None, on_delete=models.DO_NOTHING)
49
49
 
50
+ metadata = None
51
+
50
52
  def __str__(self):
51
53
  return f'{self.hostname}: {self.title}'
incident/models/rules.py CHANGED
@@ -43,6 +43,7 @@ class Rule(models.Model, rm.RestModel):
43
43
  "created",
44
44
  "priority",
45
45
  "name",
46
+ "title_template",
46
47
  "category",
47
48
  "priority",
48
49
  "action",
@@ -63,6 +64,7 @@ class Rule(models.Model, rm.RestModel):
63
64
  modified = models.DateTimeField(auto_now=True)
64
65
 
65
66
  name = models.CharField(max_length=200)
67
+ title_template = models.CharField(max_length=200, default=None, null=True)
66
68
  # the group the rule gets assigned to when triggered
67
69
  group = models.ForeignKey("account.Group", on_delete=models.CASCADE, null=True, default=None)
68
70
  # category allows us to limit running rules to only those with a category
incident/parsers/ossec.py CHANGED
@@ -13,19 +13,47 @@ LEVEL_REMAP_BY_RULE = {
13
13
  5710: 5
14
14
  }
15
15
 
16
+ NGINX_PARSE_PATTERN = re.compile(
17
+ r'(?P<src_ip>\d+\.\d+\.\d+\.\d+) - - \[(?P<http_time>.+?)\] '
18
+ r'(?P<http_method>\w+) (?P<http_url>.+?) (?P<http_protocol>[\w/.]+) '
19
+ r'(?P<http_status>\d+) (?P<http_bytes>\d+) (?P<http_referer>.+?) '
20
+ r'(?P<user_agent>.+?) (?P<http_elapsed>\d\.\d{3})'
21
+ )
22
+
23
+ def parse_nginx_line(line):
24
+ if "\n" in line:
25
+ for l in line.split('\n'):
26
+ match = NGINX_PARSE_PATTERN.match(l)
27
+ if match:
28
+ return match.groupdict()
29
+ return None
30
+ match = NGINX_PARSE_PATTERN.match(line)
31
+ if match:
32
+ return match.groupdict()
33
+ return None
16
34
 
17
- def removeNonAscii(input_str):
18
- """Remove all non-ASCII characters and escaped byte sequences from the input string."""
19
- # Remove escaped byte sequences
20
- cleaned_str = re.sub(r'\\x[0-9a-fA-F]{2}', '', input_str)
21
- # Remove non-ASCII characters
22
- return ''.join(char for char in cleaned_str if 32 <= ord(char) < 128)
35
+
36
+ def removeNonAscii(input_str, replacement=''):
37
+ """
38
+ Replace all non-ASCII characters and escaped byte sequences in the input string with a specified string.
39
+
40
+ Args:
41
+ input_str (str): The string to process.
42
+ replacement (str): The string to use as a replacement for non-ASCII characters and escaped byte sequences.
43
+
44
+ Returns:
45
+ str: The processed string with non-ASCII characters and byte sequences replaced.
46
+ """
47
+ # Replace escaped byte sequences with the replacement string
48
+ cleaned_str = re.sub(r'\\x[0-9a-fA-F]{2}', replacement, input_str)
49
+ # Replace non-ASCII characters with the replacement string
50
+ return ''.join(char if (32 <= ord(char) < 128 or char in '\n\r\t') else f"<r{str(ord(char))}>" for char in cleaned_str)
23
51
 
24
52
 
25
53
  def extractURL(text):
26
- match = re.search(r"GET\s+(https?://[^\s]+)\s+HTTP/\d\.\d", text)
54
+ match = re.search(r"(GET|POST|DELETE|PUT)\s+(https?://[^\s]+)\s+HTTP/\d\.\d", text)
27
55
  if match:
28
- return match.group(1)
56
+ return match.group(2)
29
57
  return None
30
58
 
31
59
 
@@ -36,6 +64,13 @@ def extractDomain(text):
36
64
  return None
37
65
 
38
66
 
67
+ def extractIP(text):
68
+ match = re.search(r"\b(?:[0-9]{1,3}\.){3}[0-9]{1,3}\b", text)
69
+ if match:
70
+ return match.group(1)
71
+ return None
72
+
73
+
39
74
  def extractUrlPath(text):
40
75
  match = re.search(r"https?://[^/]+(/[^?]*)", text)
41
76
  if match:
@@ -43,185 +78,232 @@ def extractUrlPath(text):
43
78
  return None
44
79
 
45
80
 
81
+ def extractUserAgent(text):
82
+ # this only works if the referrer is '-'
83
+ match = re.search(r"' - (.+?) \d+\.\d+ \d+'", text)
84
+ if match:
85
+ return match.group(1)
86
+ return None
87
+
88
+
46
89
  def extractMetaData(alert):
47
- irule = int(alert.rule_id)
48
- if irule in [31301, 31302, 31303]:
49
- patterns = {
50
- "src_ip": re.compile(r"Src IP: (\S+)"),
51
- "path": re.compile(r"request: (\S+ \S+)"),
52
- "http_server": re.compile(r"server: (\S+)"),
53
- "http_host": re.compile(r"host: (\S+)"),
54
- "http_referrer": re.compile(r"referrer: (\S+)")
55
- }
56
- # Search for matches in the text
57
- return {key: pattern.search(alert.text).group(1) for key, pattern in patterns.items() if pattern.search(alert.text)}
58
- return {}
90
+ return parse_alert_metadata(alert)
59
91
 
60
92
 
61
- def parseAlert(request, data):
62
- # helpers.log_print(data)
93
+ DEFAULT_META_PATTERNS = {
94
+ "src_ip": re.compile(r"Src IP: (\S+)"),
95
+ "src_port": re.compile(r"Src Port: (\S+)"),
96
+ "user": re.compile(r"User: (\S+)"),
97
+ "path": re.compile(r"request: (\S+ \S+)"),
98
+ "http_server": re.compile(r"server: (\S+),"),
99
+ "http_host": re.compile(r"host: (\S+)"),
100
+ "http_referrer": re.compile(r"referrer: (\S+),"),
101
+ "client": re.compile(r"client: (\S+),"),
102
+ "upstream": re.compile(r"upstream: (\S+),")
103
+ }
104
+
105
+
106
+ def parse_alert_metadata(alert):
107
+ patterns = DEFAULT_META_PATTERNS
108
+ if alert.rule_id == "2501":
109
+ match = re.search(r'user (\w+) (\d+\.\d+\.\d+\.\d+) port (\d+)', alert.text)
110
+ if match:
111
+ return dict(username=match.group(1), src_ip=match.group(2), src_port=match.group(3))
112
+ elif alert.rule_id.startswith("311") or alert.rule_id in ["31516", "31508", "31516"]:
113
+ data = parse_nginx_line(alert.text)
114
+ if data:
115
+ return data
116
+ elif alert.rule_id in ["31301"]:
117
+ match = re.search(r'\((?P<error_code>\d+): (?P<error_message>.*?)\)', alert.text)
118
+ data = match_patterns(patterns, alert.text)
119
+ data["action"] = "error"
120
+ if match:
121
+ data.update(match.groupdict())
122
+ return data
123
+ elif alert.rule_id in ["31302", "31303"]:
124
+ match = re.search(r'\[(warn|crit|error)\].*?: (.*?),', alert.text)
125
+ data = match_patterns(patterns, alert.text)
126
+
127
+ if match:
128
+ data["action"] = match.group(1)
129
+ emsg = match.group(2)
130
+ if emsg[0] == "*":
131
+ emsg = emsg[emsg.find(' ')+1:]
132
+ data["error_message"] = emsg
133
+ return data
134
+ elif alert.rule_id == "551":
135
+ match = re.search(r"Integrity checksum changed for: '(\S+)'", alert.text)
136
+ if match:
137
+ return dict(filename=match.group(1), action="changed")
138
+ elif alert.rule_id == "554":
139
+ match = re.search(r"New file '(\S+)' added", alert.text)
140
+ if match:
141
+ return dict(filename=match.group(1), action="added")
142
+ elif alert.rule_id == "5402":
143
+ match = re.search(r'(?P<username>[\w-]+) : PWD=(?P<pwd>\S+) ; USER=(?P<user>\w+) ; COMMAND=(?P<command>.+)', alert.text)
144
+ if match:
145
+ return match.groupdict()
146
+ match = re.search(r'(?P<username>[\w-]+) : TTY=(?P<tty>\S+) ; PWD=(?P<pwd>\S+) ; USER=(?P<user>\w+) ; COMMAND=(?P<command>.+)', alert.text)
147
+ if match:
148
+ return match.groupdict()
149
+ elif alert.rule_id in ["5501", "5502"]:
150
+ match = re.search(r"session (?P<action>\S+) for user (?P<username>\S+)*", alert.text)
151
+ if match:
152
+ return match.groupdict()
153
+ elif alert.rule_id in ["5704", "5705"]:
154
+ match = re.search(r"(?P<src_ip>\d{1,3}(?:\.\d{1,3}){3}) port (?P<src_port>\d+)", alert.text)
155
+ if match:
156
+ return match.groupdict()
157
+ elif alert.rule_id == "5715":
158
+ match = re.search(r'Accepted publickey for (?P<username>\S+) from (?P<src_ip>\d+\.\d+\.\d+\.\d+) .*: (?P<ssh_key_type>\S+) (?P<ssh_signature>\S+)', alert.text)
159
+ if match:
160
+ return match.groupdict()
161
+ elif alert.rule_id == "2932":
162
+ match = re.search(r"Installed: (\S+)", alert.text)
163
+ if match:
164
+ return dict(package=match.group(1))
165
+ return match_patterns(patterns, alert.text)
166
+
167
+ def match_patterns(patterns, text):
168
+ # Search for matches in the text
169
+ return {key: pattern.search(text).group(1) for key, pattern in patterns.items() if pattern.search(text)}
170
+
171
+
172
+ def parse_alert_json(data):
63
173
  try:
64
- data = objict.fromJSON(data.replace('\n', '\\n'))
174
+ if isinstance(data, str):
175
+ data = objict.fromJSON(data.replace('\n', '\\n'))
65
176
  except Exception:
66
177
  data = objict.fromJSON(removeNonAscii(data))
67
178
  for key in data:
68
179
  data[key] = data[key].strip()
180
+ if data.text:
181
+ data.text = removeNonAscii(data.text)
182
+ return data
69
183
 
70
- if data.rule_id in IGNORE_RULES:
71
- return None
72
- if "test" in data.hostname and data.rule_id == "533":
73
- # bug on test ossec falsely report 533 events
184
+
185
+ def ignore_alert(alert):
186
+ if alert.rule_id in IGNORE_RULES:
187
+ return True
188
+ if alert.rule_id == "510" and "/dev/.mount/utab" in alert.text:
189
+ return True
190
+ return False
191
+
192
+
193
+ def parse_alert_id(details):
194
+ match = re.search(r"Alert (\d+\.\d+):", details)
195
+ if match:
196
+ return match.group(1)
197
+ return ""
198
+
199
+
200
+ def parse_rule_details(details):
201
+ alert_id = parse_alert_id(details)
202
+ rule_pattern = r"Rule: (\d+) \(level (\d+)\) -> '([^']+)'"
203
+ match = re.search(rule_pattern, details)
204
+ if match:
205
+ return objict(
206
+ rid=int(match.group(1)), level=int(match.group(2)),
207
+ title=match.group(3), alert_id=alert_id)
208
+ return objict(alert_id=alert_id)
209
+
210
+
211
+ def parse_when(alert):
212
+ return datetime.utcfromtimestamp(int(alert.alert_id[:alert.alert_id.find(".")]))
213
+
214
+
215
+ def truncate_str(text, length):
216
+ if len(text) > length:
217
+ text = text[:length]
218
+ text = text[:text.rfind(' ')] + "..."
219
+ return text
220
+
221
+
222
+ def update_by_rule(data, geoip=None):
223
+ if data.rule_id == "2501":
224
+ data.title = f"SSH Auth Attempt {data.username}@{data.hostname} from {data.src_ip}"
225
+ elif data.rule_id == "2503" and data.src_ip:
226
+ data.title = f"SSH Auth Blocked from {data.src_ip}"
227
+ elif data.rule_id == "31101" and data.http_status:
228
+ data.title = f"Web {data.http_status} {data.http_method} {data.http_url} from {data.src_ip}"
229
+ elif data.rule_id == "31104" and data.http_status:
230
+ data.title = f"Web Attack {data.http_status} {data.http_method} {data.http_url} from {data.src_ip}"
231
+ elif data.rule_id == "31111" and data.http_status:
232
+ if geoip and geoip.isp:
233
+ data.title = f"No referrer for .js - {data.http_status} {data.http_method} {data.http_url} from {data.src_ip}({geoip.isp})"
234
+ else:
235
+ data.title = f"No referrer for .js - {data.http_status} {data.http_method} {data.http_url} from {data.src_ip}"
236
+ elif data.rule_id in ["31151", "31152", "31153"] and data.http_status:
237
+ url = truncate_str(data.http_url, 50)
238
+ data.title = f"Suspected Web Scan {url} from {data.src_ip}"
239
+ elif data.rule_id == "31120" and data.http_status:
240
+ url = truncate_str(data.http_url, 50)
241
+ data.title = f"Web Error {data.http_status} {data.http_method} {url} from {data.src_ip}"
242
+ elif data.rule_id.startswith("311") and data.http_status:
243
+ url = truncate_str(data.http_url, 50)
244
+ data.title = f"Web {data.http_status} {data.http_method} {url} from {data.src_ip}"
245
+ elif data.rule_id in ["31301", "31302", "31303"] and data.error_message:
246
+ if data.upstream and "ws/events" in data.upstream:
247
+ data.title = f"Websocket Error: {data.error_message} on {data.hostname}"
248
+ else:
249
+ emsg = truncate_str(data.error_message, 50)
250
+ data.title = f"Nginx {data.action}: {emsg} from {data.src_ip}"
251
+ elif data.rule_id == "31516" and data.http_url:
252
+ url = truncate_str(data.http_url, 50)
253
+ data.title = f"Web Suspicious {data.http_status} {data.http_method} {url} from {data.src_ip}"
254
+ elif data.rule_id == "533":
255
+ data.title = f"Network Open Port Change Detected on {data.hostname}"
256
+ elif data.rule_id == "5402":
257
+ cmd = truncate_str(data.command, 50)
258
+ data.title = f"Sudo(user: {data.user}) executed '{cmd}' on {data.hostname}"
259
+ elif data.rule_id in ["551", "554"] and data.filename:
260
+ name = truncate_str(data.filename, 50)
261
+ data.title = f"File {data.action.capitalize()} on {data.hostname}: {name}"
262
+ elif data.rule_id in ["5501", "5502"]:
263
+ if "sudo" in data.text:
264
+ data.title = f"Server Login {data.action} via sudo on {data.hostname}"
265
+ else:
266
+ data.title = f"Server Login {data.action} on {data.hostname}"
267
+ elif data.rule_id == "5715":
268
+ data.title = f"SSH Login Detected: {data.username}@{data.hostname} from {data.src_ip}"
269
+ elif data.rule_id == "2932" and data.package:
270
+ package = truncate_str(data.package, 60)
271
+ data.title = f"Package Installed on {data.hostname}: {package}"
272
+ elif data.src_ip and data.src_ip not in data.title:
273
+ data.title = f"{data.title} Source IP: {data.src_ip}"
274
+ if len(data.title) > 199:
275
+ data.title = data.title[:199]
276
+
277
+ def parse_incoming_alert(data):
278
+ alert = parse_alert_json(data)
279
+ if ignore_alert(alert):
74
280
  return None
75
- if data.rule_id == "510" and "/dev/.mount/utab" in data.text:
281
+ alert.update(parse_rule_details(alert.text))
282
+ if alert.title is None:
76
283
  return None
77
- # we care not for this field for now
78
- data.pop("logfile", None)
79
- if not data.text:
80
- raise Exception("invalid or missing json")
81
- alert = am.ServerOssecAlert(**data)
82
- alert.when = datetime.utcfromtimestamp(int(data.alert_id[:data.alert_id.find(".")]))
83
- # now lets parse the title
84
- title = alert.text[alert.text.find("Rule:") + 5:]
85
- # level to int
86
- level = title[title.find('(level') + 7:]
87
- alert.level = int(level[:level.find(')')].strip())
88
- title = title[:title.find('\n')].strip()
89
- pos = title.find("->")
90
- if pos > 0:
91
- title = title[pos + 2:]
92
- alert.title = title
93
- if alert.title.startswith("'"):
94
- alert.title = alert.title[1:-1]
95
-
96
- if data.hostname == "test":
97
- if data.rule_id == "31120" or "Web server" in title:
98
- return None
99
-
100
- # helpers.log_print(title, alert.title)
101
- # source ip (normally public ip of host)
102
- pos = alert.text.find("Src IP:")
103
- if pos > 1:
104
- src_ip = alert.text[alert.text.find("Src IP:") + 7:]
105
- alert.src_ip = src_ip[:src_ip.find('\n')].strip()
106
-
107
- irule = int(alert.rule_id)
108
- if irule == 5710:
109
- m = re.search(r"Invalid user (\S+) from (\S+)", data.text)
110
- if m and m.groups():
111
- alert.username = m.group(1)
112
- alert.src_ip = m.group(2).strip()
113
- alert.title = f"Attempt to login with invalid user: {alert.username}"
114
- else:
115
- m = re.search(r"Invalid user from (\S+)", data.text)
116
- if m and m.groups():
117
- alert.username = "(empty string)"
118
- alert.src_ip = m.group(1).strip()
119
- elif irule == 2932:
120
- m = re.search(r"Installed: (\S+)", data.text)
121
- if m and m.groups():
122
- package = m.group(1)
123
- alert.title = "Yum Package Installed: {}".format(package)
124
- elif irule == 551:
125
- # Integrity checksum changed for: '/etc/ld.so.cache'
126
- m = re.search(r"Integrity checksum changed for: '(\S+)'", data.text)
127
- if m and m.groups():
128
- action = m.group(1)
129
- alert.title = "File Changed: {}".format(action)
130
- elif irule == 5715:
131
- m = re.search(r"Accepted publickey for (\S+).*ssh2: ([^\n\r]*)", data.text)
132
- if m and m.groups():
133
- ssh_sig = m.group(2)
134
- if " " in ssh_sig:
135
- kind, ssh_sig = ssh_sig.split(' ')
136
- alert.level = 8
137
- alert.username = m.group(1)
138
- alert.ssh_sig = ssh_sig
139
- alert.ssh_kind = kind
140
- alert.title = f"SSH LOGIN:{alert.username}@{alert.hostname} from {alert.src_ip}"
141
- # member = findUserBySshSig(ssh_sig)
142
- # if member:
143
- # alert.title = "SSH LOGIN user: {}".format(member.username)
144
- elif irule == 5501 or irule == 5502:
145
- # pam_unix(sshd:session): session opened for user git by (uid=0)
146
- m = re.search(r"session (\S+) for user (\S+)*", data.text)
147
- if m and m.groups():
148
- alert.action = m.group(1)
149
- alert.username = m.group(2)
150
- alert.title = f"session {alert.action} for user {alert.username}"
151
- elif irule == 5402:
152
- # TTY=pts/0 ; PWD=/opt/mm_protector ; USER=root ; COMMAND=/sbin/iptables -F
153
- m = re.search(r"sudo(?:\[\d+\])?:\s*(\S+).*?COMMAND=([^\n\r]*)", data.text)
154
- # m = re.search(r"sudo:\s*(\S+).*COMMAND=([^\n\r]*)", data.text)
155
- if m and m.groups():
156
- alert.username = m.group(1)
157
- alert.title = "sudo {}".format(m.group(2)).replace("#040", " ")
158
- alert.level = 7
159
- elif irule == 5706:
160
- m = re.search(r"identification string from (\S+) port (\S+)", data.text)
161
- if m and m.groups():
162
- alert.src_ip = m.group(1)
163
- elif irule == 5702:
164
- m = re.search(r"getaddrinfo for (\S+)", data.text)
165
- if m and m.groups():
166
- remote_host = m.group(1)
167
- alert.title = f"Reverse lookup failed for '{remote_host}'"
168
- elif irule == 554:
169
- m = re.search(r"New file '(\S+)' added", data.text)
170
- if m and m.groups():
171
- remote_file = m.group(1)
172
- alert.title = f"New file detected: '{remote_file}'"
173
- elif irule == 31101:
174
- m = re.search(r"(GET|POST|DELETE|PUT)\s+(http://[^\s]+)\s+HTTP/\d\.\d\s+(\d+)", data.text)
175
- if m and m.groups():
176
- code = m.group(3)
177
- method = m.group(1)
178
- request_path = m.group(2)
179
- alert.title = f"HTTP {code}: {METHOD} {request_path}"
180
- elif irule == 31104 or irule == 31516:
181
- m = re.search(r"(GET|POST|DELETE|PUT)\s+(http://[^\s]+)\s+HTTP/\d\.\d\s+(\d+)", data.text)
182
- if m and m.groups():
183
- code = m.group(3)
184
- method = m.group(1)
185
- request_path = m.group(2)
186
- kind = "Common"
187
- if irule == 31516:
188
- kind = "Suspect"
189
- alert.title = f"{kind} Attack {code}: {METHOD} {request_path}"
190
- elif irule in [31301, 31302, 31303]:
191
- m = re.search(r"(\[error\]|\[crit\])[^\*]*\*\d*\s+(.*?),", data.text)
192
- if m and len(m.groups()) >=2:
193
- alert.title = error
194
- elif irule == 100020:
195
- m = re.search(r"\[(\S+)\]", data.text)
196
- if m and m.groups():
197
- alert.src_ip = m.group(1)
198
- elif "web,accesslog," in data.text and "https:" in data.text:
199
- alert.ssh_sig = extractURL(data.text)
200
- if alert.ssh_sig:
201
- alert.hostname = extractDomain(alert.ssh_sig)
202
-
203
- if alert.ext_ip is None:
284
+ alert.update(parse_alert_metadata(alert))
285
+ if alert.src_ip in ["-", None] and alert.client:
286
+ alert.src_ip = alert.client
287
+ if alert.ext_ip in ["-", None]:
204
288
  alert.ext_ip = alert.src_ip
205
- if alert.src_ip is not None and len(alert.src_ip) > 6:
206
- # lets do a lookup for the src
207
- alert.geoip = GeoIP.lookup(alert.src_ip)
208
-
209
- if irule == 31111:
210
- url = alert.ssh_sig
211
- hostname = alert.hostname
212
- if url:
213
- hostname = extractDomain(url)
214
- if alert.geoip and alert.geoip.isp:
215
- alert.title = f"Suspicious fetch of .js, {hostname} ISP: {alert.geoip.isp}"
216
- else:
217
- alert.title = f"Suspicious fetch of .js, {hostname}"
218
- # finally here we change the alert level
219
- if irule in LEVEL_REMAP_BY_RULE:
220
- alert.level = LEVEL_REMAP_BY_RULE[irule]
221
- if alert.title[0] in ["'", '"']:
222
- alert.title = alert.title[1:-1]
223
- if len(alert.title) > 80:
224
- alert.title = alert.title[:80] + "..."
225
- alert.save()
289
+ update_by_rule(alert)
226
290
  return alert
227
291
 
292
+
293
+ def parseAlert(request, data):
294
+ pdata = parse_incoming_alert(data)
295
+ if pdata is None:
296
+ return None
297
+ field_names = am.ServerOssecAlert.get_model_field_names()
298
+ soa = am.ServerOssecAlert(**{key:value for key, value in pdata.items() if key in field_names})
299
+ soa.when = parse_when(pdata)
300
+ soa.metadata = pdata
301
+ if soa.src_ip is not None and len(soa.src_ip) > 6 and soa.src_ip != "127.0.0.1":
302
+ soa.geoip = GeoIP.lookup(soa.src_ip)
303
+ update_by_rule(pdata, soa.geoip)
304
+ soa.title = pdata.title
305
+ soa.save()
306
+ return soa
307
+
308
+
309
+
incident/rpc.py CHANGED
@@ -61,6 +61,7 @@ def ossec_alert_creat_from_request(request):
61
61
  if payload:
62
62
  try:
63
63
  # TODO make this a task (background it)
64
+ rh.log_error("parsing payload", payload)
64
65
  od = ossec.parseAlert(request, payload)
65
66
  # lets now create a local event
66
67
  if od is not None:
@@ -78,14 +79,14 @@ def ossec_alert_creat_from_request(request):
78
79
  elif od.level <= 3:
79
80
  level = 8
80
81
  metadata = od.toDict(graph="default")
81
- metadata.update(ossec.extractMetaData(od))
82
+ metadata.update(od.metadata)
82
83
  # we reuse the ssh_sig because it is a text field to store urls
83
- ssh_sig = metadata.get("ssh_sig", None)
84
- if ssh_sig is not None and ssh_sig.startswith("http"):
85
- metadata["url"] = ssh_sig
86
- metadata["domain"] = ossec.extractDomain(ssh_sig)
87
- metadata["path"] = ossec.extractUrlPath(ssh_sig)
88
- metadata.pop("ssh_sig")
84
+ # ssh_sig = metadata.get("ssh_sig", None)
85
+ # if ssh_sig is not None and ssh_sig.startswith("http"):
86
+ # metadata["url"] = ssh_sig
87
+ # metadata["domain"] = ossec.extractDomain(ssh_sig)
88
+ # metadata["path"] = ossec.extractUrlPath(ssh_sig)
89
+ # metadata.pop("ssh_sig")
89
90
  if od.geoip:
90
91
  metadata["country"] = od.geoip.country
91
92
  metadata["city"] = od.geoip.city
@@ -103,7 +104,9 @@ def ossec_alert_creat_from_request(request):
103
104
  "reporter_ip": od.src_ip,
104
105
  "metadata": metadata
105
106
  })
107
+ return rv.restStatus(request, True)
106
108
  except Exception as err:
109
+ rh.log_exception()
107
110
  stack = rh.getStackString()
108
111
  # rh.log_exception("during ossec alert", payload)
109
112
  metadata = dict(ip=request.ip, payload=payload)
@@ -115,6 +118,7 @@ def ossec_alert_creat_from_request(request):
115
118
  "category": "ossec_error",
116
119
  "metadata": metadata
117
120
  })
121
+ rh.log_error("ossec alert", request.DATA.asDict())
118
122
  return rv.restStatus(request, False, error="no alert data")
119
123
 
120
124
 
location/models/ip.py CHANGED
@@ -93,6 +93,8 @@ class GeoIP(models.Model, rm.RestModel):
93
93
  ip = ip.strip()
94
94
  if not geolocate.isIP(ip):
95
95
  ip = geolocate.dnsToIP(ip)
96
+ if ip is None:
97
+ return None
96
98
  subnet = ip[:ip.rfind(".")]
97
99
  gip = GeoIP.objects.filter(ip=ip).first()
98
100
  if gip is None:
@@ -32,4 +32,8 @@ def isIP(ip):
32
32
 
33
33
 
34
34
  def dnsToIP(name):
35
- return socket.gethostbyname(name)
35
+ try:
36
+ return socket.gethostbyname(name)
37
+ except Exception:
38
+ pass
39
+ return None
rest/models/base.py CHANGED
@@ -1718,6 +1718,10 @@ class RestModel(object):
1718
1718
  def get_related_name_fields(cls):
1719
1719
  return [f.related_name for f in cls._meta.related_objects]
1720
1720
 
1721
+ @classmethod
1722
+ def get_model_field_names(cls):
1723
+ return [f.name for f in cls._meta.get_fields()]
1724
+
1721
1725
  @classmethod
1722
1726
  def get_fk_model(cls, fieldname):
1723
1727
  '''returns None if not foreignkey, otherswise the relevant model'''
@@ -0,0 +1,19 @@
1
+ # Generated by Django 4.2.11 on 2024-05-13 02:34
2
+
3
+ from django.db import migrations, models
4
+ import django.db.models.deletion
5
+
6
+
7
+ class Migration(migrations.Migration):
8
+
9
+ dependencies = [
10
+ ('wiki', '0001_initial'),
11
+ ]
12
+
13
+ operations = [
14
+ migrations.AlterField(
15
+ model_name='pagemedia',
16
+ name='entry',
17
+ field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='media_library', to='wiki.page'),
18
+ ),
19
+ ]