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

@@ -0,0 +1,142 @@
1
+ {
2
+ "attributes": {
3
+ "archived": {
4
+ "description": "Is the repository archived?",
5
+ "disable_correlation": true,
6
+ "misp-attribute": "text",
7
+ "sane_default": [
8
+ "True",
9
+ "False"
10
+ ],
11
+ "ui-priority": 1
12
+ },
13
+ "created-at": {
14
+ "description": "Date of the repository creation",
15
+ "misp-attribute": "datetime",
16
+ "ui-priority": 0
17
+ },
18
+ "description": {
19
+ "description": "Repository description",
20
+ "misp-attribute": "text",
21
+ "ui-priority": 1
22
+ },
23
+ "disabled": {
24
+ "description": "Is the repository disabled?",
25
+ "disable_correlation": true,
26
+ "misp-attribute": "text",
27
+ "sane_default": [
28
+ "True",
29
+ "False"
30
+ ],
31
+ "ui-priority": 1
32
+ },
33
+ "fork": {
34
+ "description": "Is the repository a forked repository?",
35
+ "disable_correlation": true,
36
+ "misp-attribute": "text",
37
+ "sane_default": [
38
+ "True",
39
+ "False"
40
+ ],
41
+ "ui-priority": 1
42
+ },
43
+ "forks-count": {
44
+ "description": "Number of forks",
45
+ "misp-attribute": "counter",
46
+ "ui-priority": 1
47
+ },
48
+ "full-name": {
49
+ "description": "Full name of the repository. [Username/Repository name]",
50
+ "misp-attribute": "text",
51
+ "ui-priority": 1
52
+ },
53
+ "has-downloads": {
54
+ "description": "Have the repository been downloaded?",
55
+ "disable_correlation": true,
56
+ "misp-attribute": "text",
57
+ "sane_default": [
58
+ "True",
59
+ "False"
60
+ ],
61
+ "ui-priority": 1
62
+ },
63
+ "has-wiki": {
64
+ "description": "Does the repository have a wiki?",
65
+ "disable_correlation": true,
66
+ "misp-attribute": "text",
67
+ "sane_default": [
68
+ "True",
69
+ "False"
70
+ ],
71
+ "ui-priority": 1
72
+ },
73
+ "id": {
74
+ "description": "Repository id",
75
+ "misp-attribute": "text",
76
+ "ui-priority": 1
77
+ },
78
+ "languages": {
79
+ "description": "Languages used in the repository",
80
+ "misp-attribute": "text",
81
+ "multiple": true,
82
+ "ui-priority": 1
83
+ },
84
+ "link": {
85
+ "description": "Link to the GitHub repository.",
86
+ "misp-attribute": "link",
87
+ "multiple": true,
88
+ "ui-priority": 1
89
+ },
90
+ "name": {
91
+ "description": "name of the repository. [Repository name]",
92
+ "misp-attribute": "text",
93
+ "ui-priority": 1
94
+ },
95
+ "open-issues": {
96
+ "description": "Number of open issues",
97
+ "misp-attribute": "counter",
98
+ "ui-priority": 1
99
+ },
100
+ "private": {
101
+ "description": "Is the repository private?",
102
+ "disable_correlation": true,
103
+ "misp-attribute": "text",
104
+ "sane_default": [
105
+ "True",
106
+ "False"
107
+ ],
108
+ "ui-priority": 1
109
+ },
110
+ "pushed-at": {
111
+ "description": "Date of last push",
112
+ "misp-attribute": "datetime",
113
+ "ui-priority": 0
114
+ },
115
+ "topics": {
116
+ "description": "Topics linked to the repository",
117
+ "misp-attribute": "text",
118
+ "multiple": true,
119
+ "ui-priority": 1
120
+ },
121
+ "updated-at": {
122
+ "description": "Date of the last update",
123
+ "misp-attribute": "datetime",
124
+ "ui-priority": 0
125
+ },
126
+ "username": {
127
+ "description": "Owner of the repository. [Username]",
128
+ "misp-attribute": "text",
129
+ "ui-priority": 1
130
+ }
131
+ },
132
+ "description": "GitHub repository",
133
+ "meta-category": "misc",
134
+ "name": "github-repo",
135
+ "requiredOneOf": [
136
+ "name",
137
+ "full-name",
138
+ "link"
139
+ ],
140
+ "uuid": "d2e93321-3d0c-4215-88a7-62ccb56fef89",
141
+ "version": 2
142
+ }
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.warning(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
3
+ Version: 2.5.9
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"
@@ -152,6 +152,7 @@ pymisp/data/misp-objects/objects/game-cheat/definition.json,sha256=4xqSM9PzOzuWZ
152
152
  pymisp/data/misp-objects/objects/generalizing-persuasion-framework/definition.json,sha256=6EFw1OW2Qzbp1tip2PgwYhjvqh2koo5Rl75h1TzNE-s,5590
153
153
  pymisp/data/misp-objects/objects/geolocation/definition.json,sha256=mvbU1_yi-9m69SJQWn7fh5k1MLUFIagPU2Mfp4GpjP8,3308
154
154
  pymisp/data/misp-objects/objects/git-vuln-finder/definition.json,sha256=_b_Ux9biIpYXK0gmCzGxmp0AHi1dGEaW3H_MiftHx3s,3644
155
+ pymisp/data/misp-objects/objects/github-repo/definition.json,sha256=zmGO6g5fRlvp419DKXo3HYQc3-i6_VqCGyIxnb4i4II,3483
155
156
  pymisp/data/misp-objects/objects/github-user/definition.json,sha256=CdHNDa0oLpPB25h5S-7ybEb9MSx92KbqAT7DmNckeNM,3463
156
157
  pymisp/data/misp-objects/objects/gitlab-user/definition.json,sha256=xCqY6NAG1DhtyHDCGVik6yXCGhPie4AfnXAvCk9z6qg,1188
157
158
  pymisp/data/misp-objects/objects/google-safe-browsing/definition.json,sha256=Bxo1eu_EbY8Q1mMv0y0lDv9Rn0xDwmPtesuZ8jtk4Xc,739
@@ -366,7 +367,7 @@ pymisp/data/misp-objects/schema_relationships.json,sha256=MCusp9GAyuHTo3lLyBrsvl
366
367
  pymisp/data/schema-lax.json,sha256=2QICdCbtfXRJkTVjwb7xjF3ypys2wOtrUyE1ZDz_qes,8561
367
368
  pymisp/data/schema.json,sha256=79N2hObemthb_syUHksDqM4djFttsWZQDg1sTYZYxys,9178
368
369
  pymisp/exceptions.py,sha256=IgGGadv5lnLAvO7Q6AjF0vEbjoWwwDWLYwMn-8pkU_k,1965
369
- pymisp/mispevent.py,sha256=OkU-PyoKIVQ9cUyUmxD3hPnCsOW5Tn7EJcGLm81LYJQ,121354
370
+ pymisp/mispevent.py,sha256=G6TLW-laRQRAJPb47EwZEb7ehYBn0rH4VF9oRUfDPMo,121528
370
371
  pymisp/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
371
372
  pymisp/tools/__init__.py,sha256=_KCihYo82e8G5cHV321ak2sgbao2GyFjf4sSTMiN_IM,2233
372
373
  pymisp/tools/_psl_faup.py,sha256=JyK8RQm8DPWvNuoF4rQpiE0rBm-Az-sr38Kl46dmWcs,7034
@@ -376,7 +377,7 @@ pymisp/tools/create_misp_object.py,sha256=PP78t4Gc7jiZtjt3MGC-0NuH976vSadSmhbaSk
376
377
  pymisp/tools/csvloader.py,sha256=d-Ox4KEehuXi9YxPE3hhf62etaj7D0pUHr5Qy4rPoqo,2588
377
378
  pymisp/tools/domainipobject.py,sha256=2w1ckOWPZvp9EW6TOAguT1Kwov72K1jJuJLqgU1whoo,847
378
379
  pymisp/tools/elfobject.py,sha256=thylyAVcAdF31II8ykVzG75Fe4Fgokc9qR90g1ybI8s,4966
379
- pymisp/tools/emailobject.py,sha256=R9y6SzXDanPSD4g5d0NB5rvrW7rnOzu6E-o8O961XsI,19143
380
+ pymisp/tools/emailobject.py,sha256=sPgVAvQFyRiONMiXYDJNibSSMWsjX1df9J3EDZ5LDEE,22680
380
381
  pymisp/tools/ext_lookups.py,sha256=acRbOVQftw7XpbjDZDrrdYzDmLDU4HmhoW48Og3UfaY,1022
381
382
  pymisp/tools/fail2banobject.py,sha256=VWxK8qWVL0AqO_YZSKmsOcaEnG_5j0jOok7OfEXWfMQ,740
382
383
  pymisp/tools/feed.py,sha256=eRG1D4fnG-2hZTFFy7SYUhGVozaAMVSiJXwxHoLP5Gg,700
@@ -397,7 +398,7 @@ pymisp/tools/update_objects.py,sha256=sp_XshzgtRjAU0Mqg8FgRTaokjVKLImyQ02xIcPSrH
397
398
  pymisp/tools/urlobject.py,sha256=PIucy1356zaljUm1NbeKmEpHpAUK9yiK2lAugcMp2t8,2489
398
399
  pymisp/tools/vehicleobject.py,sha256=bs7f4d47IBi2-VumssSM3HlqkH0viyHTLmIHQxe8Iz8,3687
399
400
  pymisp/tools/vtreportobject.py,sha256=NsdYzgqm47dywYeW8UnWmEDeIsf07xZreD2iJzFm2wg,3217
400
- pymisp-2.5.8.dist-info/LICENSE,sha256=1oPSVvs96qLjbJVi3mPn0yvWs-6aoIF6BNXi6pVlFmY,1615
401
- pymisp-2.5.8.dist-info/METADATA,sha256=B9sJiYfBYOAjRC0_JnQJRN788bXUNqQymcD80bWhSTQ,8881
402
- pymisp-2.5.8.dist-info/WHEEL,sha256=XbeZDeTWKc1w7CSIyre5aMDU_-PohRwTQceYnisIYYY,88
403
- pymisp-2.5.8.dist-info/RECORD,,
401
+ pymisp-2.5.9.dist-info/LICENSE,sha256=1oPSVvs96qLjbJVi3mPn0yvWs-6aoIF6BNXi6pVlFmY,1615
402
+ pymisp-2.5.9.dist-info/METADATA,sha256=YNrgux5KH0_ShGiW-FinobWdFpRqPeCEqonvdY2U2oQ,8881
403
+ pymisp-2.5.9.dist-info/WHEEL,sha256=XbeZDeTWKc1w7CSIyre5aMDU_-PohRwTQceYnisIYYY,88
404
+ pymisp-2.5.9.dist-info/RECORD,,
File without changes