pymisp 2.5.8.1__py3-none-any.whl → 2.5.10__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.

Potentially problematic release.


This version of pymisp might be problematic. Click here for more details.

@@ -96,6 +96,7 @@
96
96
  "description": "Layer 7 protocol of the network connection.",
97
97
  "disable_correlation": true,
98
98
  "misp-attribute": "text",
99
+ "multiple": true,
99
100
  "sane_default": [
100
101
  "HTTP",
101
102
  "HTTPS",
pymisp/mispevent.py CHANGED
@@ -1120,6 +1120,10 @@ class MISPObject(AnalystDataBehaviorMixin):
1120
1120
  Helper for object_relation when multiple is True in the template.
1121
1121
  It is the same as calling multiple times add_attribute with the same object_relation.
1122
1122
  '''
1123
+ if not attributes:
1124
+ logger.info(f"No attributes provided for object relation '{object_relation}'; skipping attribute addition.")
1125
+ return []
1126
+
1123
1127
  to_return = []
1124
1128
  for attribute in attributes:
1125
1129
  if isinstance(attribute, MISPAttribute):
@@ -373,37 +373,95 @@ class EMailObject(AbstractMISPObjectGenerator):
373
373
  # email object doesn't support display name for all email addrs
374
374
  pass
375
375
 
376
+ def extract_matches(self, pattern: re.Pattern[str], text: str) -> list[tuple[str, ...]]:
377
+ """Returns all regex matches for a given pattern in a text."""
378
+ return re.findall(pattern, text)
379
+
380
+ def add_ip_attribute(self, ip_candidate: str, received: str, seen_attributes: set[tuple[str, str]]) -> None:
381
+ """Validates and adds an IP address to MISP if it's public and not already seen during extraction."""
382
+ try:
383
+ ip = ipaddress.ip_address(ip_candidate)
384
+ if not ip.is_private and ("received-header-ip", ip_candidate) not in seen_attributes:
385
+ self.add_attribute("received-header-ip", ip_candidate, comment=received)
386
+ seen_attributes.add(("received-header-ip", ip_candidate))
387
+ except ValueError:
388
+ pass # Invalid IPs are ignored
389
+
390
+ def add_hostname_attribute(self, hostname: str, received: str, seen_attributes: set[tuple[str, str]]) -> None:
391
+ """Validates and adds a hostname to MISP if it contains a valid TLD-like format and is not already seen."""
392
+ if "." in hostname and not hostname.endswith(".") and len(hostname.split(".")[-1]) > 1:
393
+ if ("received-header-hostname", hostname) not in seen_attributes:
394
+ self.add_attribute("received-header-hostname", hostname, comment=received)
395
+ seen_attributes.add(("received-header-hostname", hostname))
396
+
397
+ def process_received_header(self, received: str, seen_attributes: set[tuple[str, str]]) -> None:
398
+ """Processes a single 'Received' header and extracts hostnames and IPs."""
399
+
400
+ # Regex patterns
401
+ received_from_regex = re.compile(
402
+ r'from\s+([\w.-]+)' # Declared sending hostname
403
+ r'(?:\s+\(([^)]+)\))?' # Reverse DNS hostname inside parentheses
404
+ )
405
+ ipv4_regex = re.compile(
406
+ r'\[(?P<ipv4_brackets>(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])\.'
407
+ r'(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])\.'
408
+ r'(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])\.'
409
+ r'(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9]))\]' # IPv4 inside []
410
+ r'|\((?P<ipv4_parentheses>(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])\.'
411
+ r'(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])\.'
412
+ r'(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])\.'
413
+ r'(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9]))\)' # IPv4 inside ()
414
+ r'|(?<=\.\s)(?P<ipv4_after_domain>(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])\.'
415
+ r'(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])\.'
416
+ r'(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])\.'
417
+ r'(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9]))\b' # IPv4 appearing after a domain.
418
+ )
419
+ ipv6_regex = re.compile(
420
+ r'\b(?:[a-fA-F0-9]{1,4}:[a-fA-F0-9]{1,4}(?::[a-fA-F0-9]{1,4}){0,6})\b'
421
+ )
422
+
423
+ # Extract hostnames
424
+ matches = self.extract_matches(received_from_regex, received)
425
+ for match in matches:
426
+ declared_sending_host = match[0].strip() if match[0] else None
427
+ reverse_dns_host = match[1].split()[0].strip("[]()").rstrip('.') if match[1] else None
428
+
429
+ if declared_sending_host:
430
+ clean_host = declared_sending_host.strip("[]()")
431
+ try:
432
+ ipaddress.ip_address(declared_sending_host)
433
+ self.add_ip_attribute(declared_sending_host, received, seen_attributes)
434
+ except ValueError:
435
+ self.add_hostname_attribute(declared_sending_host, received, seen_attributes)
436
+
437
+ if reverse_dns_host:
438
+ try:
439
+ ipaddress.ip_address(reverse_dns_host)
440
+ self.add_ip_attribute(reverse_dns_host, received, seen_attributes)
441
+ except ValueError:
442
+ self.add_hostname_attribute(reverse_dns_host, received, seen_attributes)
443
+
444
+ # Extract and add **only valid** IPv4 addresses
445
+ for ipv4_match in self.extract_matches(ipv4_regex, received):
446
+ ip_candidate = ipv4_match[0] or ipv4_match[1] or ipv4_match[2] # Select first non-empty match
447
+ if ip_candidate:
448
+ self.add_ip_attribute(ip_candidate, received, seen_attributes)
449
+
450
+ # Extract and add IPv6 addresses
451
+ for ipv6_match in self.extract_matches(ipv6_regex, received):
452
+ self.add_ip_attribute(ipv6_match, received, seen_attributes)
453
+
376
454
  def __generate_received(self) -> None:
377
455
  """
378
- Extract IP addresses from received headers that are not private. Also extract hostnames or domains.
456
+ Extracts public IP addresses and hostnames from "Received" email headers.
379
457
  """
380
- received_items = self.email.get_all("received")
381
- if received_items is None:
382
- return
383
- for received in received_items:
384
- fromstr = re.split(r"\sby\s", received)[0].strip()
385
- if fromstr.startswith('from') is not True:
386
- continue
387
- for i in ['(', ')', '[', ']']:
388
- fromstr = fromstr.replace(i, " ")
389
- tokens = fromstr.split(" ")
390
- ip = None
391
- for token in tokens:
392
- try:
393
- ip = ipaddress.ip_address(token)
394
- break
395
- except ValueError:
396
- pass # token is not IP address
397
458
 
398
- if not ip or ip.is_private:
399
- continue # skip header if IP not found or is private
459
+ received_items = self.email.get_all("Received")
460
+ if not received_items:
461
+ return
400
462
 
401
- self.add_attribute("received-header-ip", value=str(ip), comment=fromstr)
463
+ # Track added attributes to prevent duplicates (store as (type, value) tuples)
464
+ seen_attributes: set[tuple[str, str]] = set()
402
465
 
403
- # The hostnames and/or domains always come after the "Received: from"
404
- # part so we can use regex to pick up those attributes.
405
- received_from = re.findall(r'(?<=from\s)[\w\d\.\-]+\.\w{2,24}', str(received_items))
406
- try:
407
- [self.add_attribute("received-header-hostname", i) for i in received_from]
408
- except Exception:
409
- pass
466
+ for received in received_items:
467
+ self.process_received_header(received, seen_attributes)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: pymisp
3
- Version: 2.5.8.1
3
+ Version: 2.5.10
4
4
  Summary: Python API for MISP.
5
5
  License: BSD-2-Clause
6
6
  Author: Raphaël Vinot
@@ -32,7 +32,7 @@ Requires-Dist: RTFDE (>=0.1.2) ; (python_version <= "3.9") and (extra == "email"
32
32
  Requires-Dist: beautifulsoup4 (>=4.13.3) ; extra == "openioc"
33
33
  Requires-Dist: deprecated (>=1.2.18)
34
34
  Requires-Dist: docutils (>=0.21.2) ; (python_version >= "3.11") and (extra == "docs")
35
- Requires-Dist: extract_msg (>=0.53.2) ; extra == "email"
35
+ Requires-Dist: extract_msg (>=0.54.0) ; extra == "email"
36
36
  Requires-Dist: lief (>=0.16.4) ; extra == "fileobjects"
37
37
  Requires-Dist: myst-parser (>=4.0.1) ; (python_version >= "3.11") and (extra == "docs")
38
38
  Requires-Dist: oletools (>=0.60.2) ; extra == "email"
@@ -206,7 +206,7 @@ pymisp/data/misp-objects/objects/monetary-impact/definition.json,sha256=s44CoduM
206
206
  pymisp/data/misp-objects/objects/mutex/definition.json,sha256=zqun14zDa2seXkX5BGtlL_0dkT7LqTTEDagh-1lXKVs,744
207
207
  pymisp/data/misp-objects/objects/narrative/definition.json,sha256=VXEm_lcQgR7uFtMalrdbI73-ivv6HJHQVx6lPU0FYzA,2200
208
208
  pymisp/data/misp-objects/objects/netflow/definition.json,sha256=pQ_meRpiPEchaTBNTBUyUT5zPmL7QNIQgLGKdd_KTqE,4103
209
- pymisp/data/misp-objects/objects/network-connection/definition.json,sha256=seFEI1Npj5EHXt3RPP2TrZ_oq3YKDQDe0YGsZQO37LE,4224
209
+ pymisp/data/misp-objects/objects/network-connection/definition.json,sha256=6rGG8ZhW3YxgGAV_l91GFpZXk4QpyJ7iuedH5FU38HE,4248
210
210
  pymisp/data/misp-objects/objects/network-profile/definition.json,sha256=urPC6ysgZ5kaiB2L2ilL19iGmR2GNUzjO4pcUngQl5E,6175
211
211
  pymisp/data/misp-objects/objects/network-socket/definition.json,sha256=qEE1yvRnrpylHut3jFDJnPWWfsz61ZJO0-Lp40WOSjM,6571
212
212
  pymisp/data/misp-objects/objects/network-traffic/definition.json,sha256=jZSGhItwP-1Vxm7fv_IqbijXqnAvPFFKhjxolaDXudE,3144
@@ -367,7 +367,7 @@ pymisp/data/misp-objects/schema_relationships.json,sha256=MCusp9GAyuHTo3lLyBrsvl
367
367
  pymisp/data/schema-lax.json,sha256=2QICdCbtfXRJkTVjwb7xjF3ypys2wOtrUyE1ZDz_qes,8561
368
368
  pymisp/data/schema.json,sha256=79N2hObemthb_syUHksDqM4djFttsWZQDg1sTYZYxys,9178
369
369
  pymisp/exceptions.py,sha256=IgGGadv5lnLAvO7Q6AjF0vEbjoWwwDWLYwMn-8pkU_k,1965
370
- pymisp/mispevent.py,sha256=OkU-PyoKIVQ9cUyUmxD3hPnCsOW5Tn7EJcGLm81LYJQ,121354
370
+ pymisp/mispevent.py,sha256=mgIiXFj-RKJud2TBpqL8AefQOcYM2zxJsOqOmSDovPI,121525
371
371
  pymisp/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
372
372
  pymisp/tools/__init__.py,sha256=_KCihYo82e8G5cHV321ak2sgbao2GyFjf4sSTMiN_IM,2233
373
373
  pymisp/tools/_psl_faup.py,sha256=JyK8RQm8DPWvNuoF4rQpiE0rBm-Az-sr38Kl46dmWcs,7034
@@ -377,7 +377,7 @@ pymisp/tools/create_misp_object.py,sha256=PP78t4Gc7jiZtjt3MGC-0NuH976vSadSmhbaSk
377
377
  pymisp/tools/csvloader.py,sha256=d-Ox4KEehuXi9YxPE3hhf62etaj7D0pUHr5Qy4rPoqo,2588
378
378
  pymisp/tools/domainipobject.py,sha256=2w1ckOWPZvp9EW6TOAguT1Kwov72K1jJuJLqgU1whoo,847
379
379
  pymisp/tools/elfobject.py,sha256=thylyAVcAdF31II8ykVzG75Fe4Fgokc9qR90g1ybI8s,4966
380
- pymisp/tools/emailobject.py,sha256=R9y6SzXDanPSD4g5d0NB5rvrW7rnOzu6E-o8O961XsI,19143
380
+ pymisp/tools/emailobject.py,sha256=sPgVAvQFyRiONMiXYDJNibSSMWsjX1df9J3EDZ5LDEE,22680
381
381
  pymisp/tools/ext_lookups.py,sha256=acRbOVQftw7XpbjDZDrrdYzDmLDU4HmhoW48Og3UfaY,1022
382
382
  pymisp/tools/fail2banobject.py,sha256=VWxK8qWVL0AqO_YZSKmsOcaEnG_5j0jOok7OfEXWfMQ,740
383
383
  pymisp/tools/feed.py,sha256=eRG1D4fnG-2hZTFFy7SYUhGVozaAMVSiJXwxHoLP5Gg,700
@@ -398,7 +398,7 @@ pymisp/tools/update_objects.py,sha256=sp_XshzgtRjAU0Mqg8FgRTaokjVKLImyQ02xIcPSrH
398
398
  pymisp/tools/urlobject.py,sha256=PIucy1356zaljUm1NbeKmEpHpAUK9yiK2lAugcMp2t8,2489
399
399
  pymisp/tools/vehicleobject.py,sha256=bs7f4d47IBi2-VumssSM3HlqkH0viyHTLmIHQxe8Iz8,3687
400
400
  pymisp/tools/vtreportobject.py,sha256=NsdYzgqm47dywYeW8UnWmEDeIsf07xZreD2iJzFm2wg,3217
401
- pymisp-2.5.8.1.dist-info/LICENSE,sha256=1oPSVvs96qLjbJVi3mPn0yvWs-6aoIF6BNXi6pVlFmY,1615
402
- pymisp-2.5.8.1.dist-info/METADATA,sha256=yILojnXoJl-KvWP7U9zLGkw2yC3438Z4x0qkmPPOAkI,8883
403
- pymisp-2.5.8.1.dist-info/WHEEL,sha256=XbeZDeTWKc1w7CSIyre5aMDU_-PohRwTQceYnisIYYY,88
404
- pymisp-2.5.8.1.dist-info/RECORD,,
401
+ pymisp-2.5.10.dist-info/LICENSE,sha256=1oPSVvs96qLjbJVi3mPn0yvWs-6aoIF6BNXi6pVlFmY,1615
402
+ pymisp-2.5.10.dist-info/METADATA,sha256=aakG8Az0H27y7lEAPrqMrPEden9Gr1y8K5DJ_S5huwY,8882
403
+ pymisp-2.5.10.dist-info/WHEEL,sha256=fGIA9gx4Qxk2KDKeNJCbOEwSrmLtjWCwzBz351GyrPQ,88
404
+ pymisp-2.5.10.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: poetry-core 2.1.1
2
+ Generator: poetry-core 2.1.2
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any