pycti 6.2.10__tar.gz → 6.2.12__tar.gz

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 pycti might be problematic. Click here for more details.

Files changed (78) hide show
  1. {pycti-6.2.10 → pycti-6.2.12}/PKG-INFO +2 -1
  2. {pycti-6.2.10 → pycti-6.2.12}/pycti/__init__.py +1 -1
  3. {pycti-6.2.10 → pycti-6.2.12}/pycti/api/opencti_api_connector.py +58 -4
  4. {pycti-6.2.10 → pycti-6.2.12}/pycti/connector/opencti_connector_helper.py +433 -14
  5. {pycti-6.2.10 → pycti-6.2.12}/pycti/entities/opencti_stix_core_object.py +6 -0
  6. {pycti-6.2.10 → pycti-6.2.12}/pycti/entities/opencti_stix_domain_object.py +6 -0
  7. {pycti-6.2.10 → pycti-6.2.12}/pycti/entities/opencti_stix_object_or_stix_relationship.py +3 -0
  8. {pycti-6.2.10 → pycti-6.2.12}/pycti/entities/opencti_vulnerability.py +26 -0
  9. {pycti-6.2.10 → pycti-6.2.12}/pycti.egg-info/PKG-INFO +2 -1
  10. {pycti-6.2.10 → pycti-6.2.12}/pycti.egg-info/requires.txt +1 -0
  11. {pycti-6.2.10 → pycti-6.2.12}/setup.cfg +1 -0
  12. {pycti-6.2.10 → pycti-6.2.12}/LICENSE +0 -0
  13. {pycti-6.2.10 → pycti-6.2.12}/README.md +0 -0
  14. {pycti-6.2.10 → pycti-6.2.12}/pycti/api/__init__.py +0 -0
  15. {pycti-6.2.10 → pycti-6.2.12}/pycti/api/opencti_api_client.py +0 -0
  16. {pycti-6.2.10 → pycti-6.2.12}/pycti/api/opencti_api_playbook.py +0 -0
  17. {pycti-6.2.10 → pycti-6.2.12}/pycti/api/opencti_api_work.py +0 -0
  18. {pycti-6.2.10 → pycti-6.2.12}/pycti/connector/__init__.py +0 -0
  19. {pycti-6.2.10 → pycti-6.2.12}/pycti/connector/opencti_connector.py +0 -0
  20. {pycti-6.2.10 → pycti-6.2.12}/pycti/connector/opencti_metric_handler.py +0 -0
  21. {pycti-6.2.10 → pycti-6.2.12}/pycti/entities/__init__.py +0 -0
  22. {pycti-6.2.10 → pycti-6.2.12}/pycti/entities/indicator/__init__.py +0 -0
  23. {pycti-6.2.10 → pycti-6.2.12}/pycti/entities/indicator/opencti_indicator_properties.py +0 -0
  24. {pycti-6.2.10 → pycti-6.2.12}/pycti/entities/opencti_attack_pattern.py +0 -0
  25. {pycti-6.2.10 → pycti-6.2.12}/pycti/entities/opencti_campaign.py +0 -0
  26. {pycti-6.2.10 → pycti-6.2.12}/pycti/entities/opencti_case_incident.py +0 -0
  27. {pycti-6.2.10 → pycti-6.2.12}/pycti/entities/opencti_case_rfi.py +0 -0
  28. {pycti-6.2.10 → pycti-6.2.12}/pycti/entities/opencti_case_rft.py +0 -0
  29. {pycti-6.2.10 → pycti-6.2.12}/pycti/entities/opencti_channel.py +0 -0
  30. {pycti-6.2.10 → pycti-6.2.12}/pycti/entities/opencti_course_of_action.py +0 -0
  31. {pycti-6.2.10 → pycti-6.2.12}/pycti/entities/opencti_data_component.py +0 -0
  32. {pycti-6.2.10 → pycti-6.2.12}/pycti/entities/opencti_data_source.py +0 -0
  33. {pycti-6.2.10 → pycti-6.2.12}/pycti/entities/opencti_event.py +0 -0
  34. {pycti-6.2.10 → pycti-6.2.12}/pycti/entities/opencti_external_reference.py +0 -0
  35. {pycti-6.2.10 → pycti-6.2.12}/pycti/entities/opencti_feedback.py +0 -0
  36. {pycti-6.2.10 → pycti-6.2.12}/pycti/entities/opencti_grouping.py +0 -0
  37. {pycti-6.2.10 → pycti-6.2.12}/pycti/entities/opencti_identity.py +0 -0
  38. {pycti-6.2.10 → pycti-6.2.12}/pycti/entities/opencti_incident.py +0 -0
  39. {pycti-6.2.10 → pycti-6.2.12}/pycti/entities/opencti_indicator.py +0 -0
  40. {pycti-6.2.10 → pycti-6.2.12}/pycti/entities/opencti_infrastructure.py +0 -0
  41. {pycti-6.2.10 → pycti-6.2.12}/pycti/entities/opencti_intrusion_set.py +0 -0
  42. {pycti-6.2.10 → pycti-6.2.12}/pycti/entities/opencti_kill_chain_phase.py +0 -0
  43. {pycti-6.2.10 → pycti-6.2.12}/pycti/entities/opencti_label.py +0 -0
  44. {pycti-6.2.10 → pycti-6.2.12}/pycti/entities/opencti_language.py +0 -0
  45. {pycti-6.2.10 → pycti-6.2.12}/pycti/entities/opencti_location.py +0 -0
  46. {pycti-6.2.10 → pycti-6.2.12}/pycti/entities/opencti_malware.py +0 -0
  47. {pycti-6.2.10 → pycti-6.2.12}/pycti/entities/opencti_malware_analysis.py +0 -0
  48. {pycti-6.2.10 → pycti-6.2.12}/pycti/entities/opencti_marking_definition.py +0 -0
  49. {pycti-6.2.10 → pycti-6.2.12}/pycti/entities/opencti_narrative.py +0 -0
  50. {pycti-6.2.10 → pycti-6.2.12}/pycti/entities/opencti_note.py +0 -0
  51. {pycti-6.2.10 → pycti-6.2.12}/pycti/entities/opencti_observed_data.py +0 -0
  52. {pycti-6.2.10 → pycti-6.2.12}/pycti/entities/opencti_opinion.py +0 -0
  53. {pycti-6.2.10 → pycti-6.2.12}/pycti/entities/opencti_report.py +0 -0
  54. {pycti-6.2.10 → pycti-6.2.12}/pycti/entities/opencti_stix.py +0 -0
  55. {pycti-6.2.10 → pycti-6.2.12}/pycti/entities/opencti_stix_core_relationship.py +0 -0
  56. {pycti-6.2.10 → pycti-6.2.12}/pycti/entities/opencti_stix_cyber_observable.py +0 -0
  57. {pycti-6.2.10 → pycti-6.2.12}/pycti/entities/opencti_stix_nested_ref_relationship.py +0 -0
  58. {pycti-6.2.10 → pycti-6.2.12}/pycti/entities/opencti_stix_sighting_relationship.py +0 -0
  59. {pycti-6.2.10 → pycti-6.2.12}/pycti/entities/opencti_task.py +0 -0
  60. {pycti-6.2.10 → pycti-6.2.12}/pycti/entities/opencti_threat_actor.py +0 -0
  61. {pycti-6.2.10 → pycti-6.2.12}/pycti/entities/opencti_threat_actor_group.py +0 -0
  62. {pycti-6.2.10 → pycti-6.2.12}/pycti/entities/opencti_threat_actor_individual.py +0 -0
  63. {pycti-6.2.10 → pycti-6.2.12}/pycti/entities/opencti_tool.py +0 -0
  64. {pycti-6.2.10 → pycti-6.2.12}/pycti/entities/opencti_vocabulary.py +0 -0
  65. {pycti-6.2.10 → pycti-6.2.12}/pycti/entities/stix_cyber_observable/__init__.py +0 -0
  66. {pycti-6.2.10 → pycti-6.2.12}/pycti/entities/stix_cyber_observable/opencti_stix_cyber_observable_deprecated.py +0 -0
  67. {pycti-6.2.10 → pycti-6.2.12}/pycti/entities/stix_cyber_observable/opencti_stix_cyber_observable_properties.py +0 -0
  68. {pycti-6.2.10 → pycti-6.2.12}/pycti/utils/__init__.py +0 -0
  69. {pycti-6.2.10 → pycti-6.2.12}/pycti/utils/constants.py +0 -0
  70. {pycti-6.2.10 → pycti-6.2.12}/pycti/utils/opencti_logger.py +0 -0
  71. {pycti-6.2.10 → pycti-6.2.12}/pycti/utils/opencti_stix2.py +0 -0
  72. {pycti-6.2.10 → pycti-6.2.12}/pycti/utils/opencti_stix2_splitter.py +0 -0
  73. {pycti-6.2.10 → pycti-6.2.12}/pycti/utils/opencti_stix2_update.py +0 -0
  74. {pycti-6.2.10 → pycti-6.2.12}/pycti/utils/opencti_stix2_utils.py +0 -0
  75. {pycti-6.2.10 → pycti-6.2.12}/pycti.egg-info/SOURCES.txt +0 -0
  76. {pycti-6.2.10 → pycti-6.2.12}/pycti.egg-info/dependency_links.txt +0 -0
  77. {pycti-6.2.10 → pycti-6.2.12}/pycti.egg-info/top_level.txt +0 -0
  78. {pycti-6.2.10 → pycti-6.2.12}/pyproject.toml +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pycti
3
- Version: 6.2.10
3
+ Version: 6.2.12
4
4
  Summary: Python API client for OpenCTI.
5
5
  Home-page: https://github.com/OpenCTI-Platform/client-python
6
6
  Author: Filigran
@@ -23,6 +23,7 @@ Description-Content-Type: text/markdown
23
23
  License-File: LICENSE
24
24
  Requires-Dist: datefinder~=0.7.3
25
25
  Requires-Dist: pika~=1.3.0
26
+ Requires-Dist: pydantic~=2.8.2
26
27
  Requires-Dist: python-magic~=0.4.27; sys_platform == "linux" or sys_platform == "darwin"
27
28
  Requires-Dist: python-magic-bin~=0.4.14; sys_platform == "win32"
28
29
  Requires-Dist: python_json_logger~=2.0.4
@@ -1,5 +1,5 @@
1
1
  # -*- coding: utf-8 -*-
2
- __version__ = "6.2.10"
2
+ __version__ = "6.2.12"
3
3
 
4
4
  from .api.opencti_api_client import OpenCTIApiClient
5
5
  from .api.opencti_api_connector import OpenCTIApiConnector
@@ -10,6 +10,43 @@ class OpenCTIApiConnector:
10
10
  def __init__(self, api):
11
11
  self.api = api
12
12
 
13
+ def read(self, connector_id: str) -> Dict:
14
+ """Reading the connector and its details
15
+
16
+ :return: return all the connector details
17
+ :rtype: dict
18
+ """
19
+ self.api.app_logger.info("[INFO] Getting connector details ...")
20
+ query = """
21
+ query GetConnector($id: String!) {
22
+ connector(id: $id) {
23
+ id
24
+ name
25
+ active
26
+ auto
27
+ only_contextual
28
+ connector_type
29
+ connector_scope
30
+ connector_state
31
+ connector_queue_details {
32
+ messages_number
33
+ messages_size
34
+ }
35
+ updated_at
36
+ created_at
37
+ config {
38
+ listen
39
+ listen_exchange
40
+ push
41
+ push_exchange
42
+ }
43
+ built_in
44
+ }
45
+ }
46
+ """
47
+ result = self.api.query(query, {"id": connector_id})
48
+ return result["data"]["connector"]
49
+
13
50
  def list(self) -> Dict:
14
51
  """list available connectors
15
52
 
@@ -41,27 +78,44 @@ class OpenCTIApiConnector:
41
78
  result = self.api.query(query)
42
79
  return result["data"]["connectorsForWorker"]
43
80
 
44
- def ping(self, connector_id: str, connector_state: Any) -> Dict:
81
+ def ping(
82
+ self, connector_id: str, connector_state: Any, connector_info: Dict
83
+ ) -> Dict:
45
84
  """pings a connector by id and state
46
85
 
47
86
  :param connector_id: the connectors id
48
87
  :type connector_id: str
49
88
  :param connector_state: state for the connector
50
89
  :type connector_state:
90
+ :param connector_info: all details connector
91
+ :type connector_info: Dict
51
92
  :return: the response pingConnector data dict
52
93
  :rtype: dict
53
94
  """
54
95
 
55
96
  query = """
56
- mutation PingConnector($id: ID!, $state: String) {
57
- pingConnector(id: $id, state: $state) {
97
+ mutation PingConnector($id: ID!, $state: String, $connectorInfo: ConnectorInfoInput ) {
98
+ pingConnector(id: $id, state: $state, connectorInfo: $connectorInfo) {
58
99
  id
59
100
  connector_state
101
+ connector_info {
102
+ run_and_terminate
103
+ buffering
104
+ queue_threshold
105
+ queue_messages_size
106
+ next_run_datetime
107
+ last_run_datetime
108
+ }
60
109
  }
61
110
  }
62
111
  """
63
112
  result = self.api.query(
64
- query, {"id": connector_id, "state": json.dumps(connector_state)}
113
+ query,
114
+ {
115
+ "id": connector_id,
116
+ "state": json.dumps(connector_state),
117
+ "connectorInfo": connector_info,
118
+ },
65
119
  )
66
120
  return result["data"]["pingConnector"]
67
121
 
@@ -5,6 +5,7 @@ import datetime
5
5
  import json
6
6
  import os
7
7
  import queue
8
+ import sched
8
9
  import signal
9
10
  import ssl
10
11
  import sys
@@ -12,12 +13,14 @@ import tempfile
12
13
  import threading
13
14
  import time
14
15
  import uuid
16
+ from enum import Enum
15
17
  from queue import Queue
16
18
  from typing import Callable, Dict, List, Optional, Union
17
19
 
18
20
  import pika
19
21
  from filigran_sseclient import SSEClient
20
22
  from pika.exceptions import NackError, UnroutableError
23
+ from pydantic import TypeAdapter
21
24
 
22
25
  from pycti.api.opencti_api_client import OpenCTIApiClient
23
26
  from pycti.connector.opencti_connector import OpenCTIConnector
@@ -419,7 +422,14 @@ class ListenQueue(threading.Thread):
419
422
 
420
423
  class PingAlive(threading.Thread):
421
424
  def __init__(
422
- self, connector_logger, connector_id, api, get_state, set_state, metric
425
+ self,
426
+ connector_logger,
427
+ connector_id,
428
+ api,
429
+ get_state,
430
+ set_state,
431
+ metric,
432
+ connector_info,
423
433
  ) -> None:
424
434
  threading.Thread.__init__(self, daemon=True)
425
435
  self.connector_logger = connector_logger
@@ -430,13 +440,20 @@ class PingAlive(threading.Thread):
430
440
  self.set_state = set_state
431
441
  self.exit_event = threading.Event()
432
442
  self.metric = metric
443
+ self.connector_info = connector_info
433
444
 
434
445
  def ping(self) -> None:
435
446
  while not self.exit_event.is_set():
436
447
  try:
437
448
  self.connector_logger.debug("PingAlive running.")
438
449
  initial_state = self.get_state()
439
- result = self.api.connector.ping(self.connector_id, initial_state)
450
+ connector_info = self.connector_info.all_details
451
+ self.connector_logger.debug(
452
+ "PingAlive ConnectorInfo", {"connector_info": connector_info}
453
+ )
454
+ result = self.api.connector.ping(
455
+ self.connector_id, initial_state, connector_info
456
+ )
440
457
  remote_state = (
441
458
  json.loads(result["connector_state"])
442
459
  if result["connector_state"] is not None
@@ -646,6 +663,83 @@ class ListenStream(threading.Thread):
646
663
  self.exit_event.set()
647
664
 
648
665
 
666
+ class ConnectorInfo:
667
+ def __init__(
668
+ self,
669
+ run_and_terminate: bool = False,
670
+ buffering: bool = False,
671
+ queue_threshold: float = 500,
672
+ queue_messages_size: float = 0,
673
+ next_run_datetime: datetime = None,
674
+ last_run_datetime: datetime = None,
675
+ ):
676
+ self._run_and_terminate = run_and_terminate
677
+ self._buffering = buffering
678
+ self._queue_threshold = queue_threshold
679
+ self._queue_messages_size = queue_messages_size
680
+ self._next_run_datetime = next_run_datetime
681
+ self._last_run_datetime = last_run_datetime
682
+
683
+ @property
684
+ def all_details(self):
685
+ return {
686
+ "run_and_terminate": self._run_and_terminate,
687
+ "buffering": self._buffering,
688
+ "queue_threshold": self._queue_threshold,
689
+ "queue_messages_size": self._queue_messages_size,
690
+ "next_run_datetime": self._next_run_datetime,
691
+ "last_run_datetime": self._last_run_datetime,
692
+ }
693
+
694
+ @property
695
+ def run_and_terminate(self) -> bool:
696
+ return self._run_and_terminate
697
+
698
+ @run_and_terminate.setter
699
+ def run_and_terminate(self, value):
700
+ self._run_and_terminate = value
701
+
702
+ @property
703
+ def buffering(self) -> bool:
704
+ return self._buffering
705
+
706
+ @buffering.setter
707
+ def buffering(self, value):
708
+ self._buffering = value
709
+
710
+ @property
711
+ def queue_threshold(self) -> float:
712
+ return self._queue_threshold
713
+
714
+ @queue_threshold.setter
715
+ def queue_threshold(self, value):
716
+ self._queue_threshold = value
717
+
718
+ @property
719
+ def queue_messages_size(self) -> float:
720
+ return self._queue_messages_size
721
+
722
+ @queue_messages_size.setter
723
+ def queue_messages_size(self, value):
724
+ self._queue_messages_size = value
725
+
726
+ @property
727
+ def next_run_datetime(self) -> datetime:
728
+ return self._next_run_datetime
729
+
730
+ @next_run_datetime.setter
731
+ def next_run_datetime(self, value):
732
+ self._next_run_datetime = value
733
+
734
+ @property
735
+ def last_run_datetime(self) -> datetime:
736
+ return self._last_run_datetime
737
+
738
+ @last_run_datetime.setter
739
+ def last_run_datetime(self, value):
740
+ self._last_run_datetime = value
741
+
742
+
649
743
  class OpenCTIConnectorHelper: # pylint: disable=too-many-public-methods
650
744
  """Python API for OpenCTI connector
651
745
 
@@ -653,6 +747,14 @@ class OpenCTIConnectorHelper: # pylint: disable=too-many-public-methods
653
747
  :type config: Dict
654
748
  """
655
749
 
750
+ class TimeUnit(Enum):
751
+ SECONDS = 1
752
+ MINUTES = 60
753
+ HOURS = 3600
754
+ DAYS = 86400
755
+ WEEKS = 604800
756
+ YEARS = 31536000
757
+
656
758
  def __init__(self, config: Dict, playbook_compatible=False) -> None:
657
759
  sys.excepthook = killProgramHook
658
760
 
@@ -677,6 +779,15 @@ class OpenCTIConnectorHelper: # pylint: disable=too-many-public-methods
677
779
  self.connect_type = get_config_variable(
678
780
  "CONNECTOR_TYPE", ["connector", "type"], config
679
781
  )
782
+ self.connect_queue_threshold = get_config_variable(
783
+ "CONNECTOR_QUEUE_THRESHOLD",
784
+ ["connector", "queue_threshold"],
785
+ config,
786
+ default=500, # Mo
787
+ )
788
+ self.connect_duration_period = get_config_variable(
789
+ "CONNECTOR_DURATION_PERIOD", ["connector", "duration_period"], config
790
+ )
680
791
  self.connect_live_stream_id = get_config_variable(
681
792
  "CONNECTOR_LIVE_STREAM_ID",
682
793
  ["connector", "live_stream_id"],
@@ -775,6 +886,7 @@ class OpenCTIConnectorHelper: # pylint: disable=too-many-public-methods
775
886
  False,
776
887
  False,
777
888
  )
889
+ self.scheduler = sched.scheduler(time.time, time.sleep)
778
890
  # Start up the server to expose the metrics.
779
891
  expose_metrics = get_config_variable(
780
892
  "CONNECTOR_EXPOSE_METRICS",
@@ -786,7 +898,8 @@ class OpenCTIConnectorHelper: # pylint: disable=too-many-public-methods
786
898
  metrics_port = get_config_variable(
787
899
  "CONNECTOR_METRICS_PORT", ["connector", "metrics_port"], config, True, 9095
788
900
  )
789
-
901
+ # Initialize ConnectorInfo instance
902
+ self.connector_info = ConnectorInfo()
790
903
  # Initialize configuration
791
904
  # - Classic API that will be directly attached to the connector rights
792
905
  self.api = OpenCTIApiClient(
@@ -881,15 +994,34 @@ class OpenCTIConnectorHelper: # pylint: disable=too-many-public-methods
881
994
 
882
995
  # Start ping thread
883
996
  if not self.connect_run_and_terminate:
884
- self.ping = PingAlive(
885
- self.connector_logger,
886
- self.connector.id,
887
- self.api,
888
- self.get_state,
889
- self.set_state,
890
- self.metric,
891
- )
892
- self.ping.start()
997
+
998
+ is_run_and_terminate = False
999
+ if self.connect_duration_period == 0:
1000
+ is_run_and_terminate = True
1001
+
1002
+ if isinstance(self.connect_duration_period, str):
1003
+ if self.connect_duration_period == "0":
1004
+ is_run_and_terminate = True
1005
+ else:
1006
+ # Calculates and validate the duration period in seconds
1007
+ timedelta_adapter = TypeAdapter(datetime.timedelta)
1008
+ td = timedelta_adapter.validate_python(self.connect_duration_period)
1009
+ duration_period_in_seconds = int(td.total_seconds())
1010
+
1011
+ if duration_period_in_seconds == 0:
1012
+ is_run_and_terminate = True
1013
+
1014
+ if self.connect_duration_period is None or not is_run_and_terminate:
1015
+ self.ping = PingAlive(
1016
+ self.connector_logger,
1017
+ self.connector.id,
1018
+ self.api,
1019
+ self.get_state,
1020
+ self.set_state,
1021
+ self.metric,
1022
+ self.connector_info,
1023
+ )
1024
+ self.ping.start()
893
1025
 
894
1026
  # self.listen_stream = None
895
1027
  self.listen_queue = None
@@ -972,7 +1104,13 @@ class OpenCTIConnectorHelper: # pylint: disable=too-many-public-methods
972
1104
  def force_ping(self):
973
1105
  try:
974
1106
  initial_state = self.get_state()
975
- result = self.api.connector.ping(self.connector_id, initial_state)
1107
+ connector_info = self.connector_info.all_details
1108
+ self.connector_logger.debug(
1109
+ "ForcePing ConnectorInfo", {"connector_info": connector_info}
1110
+ )
1111
+ result = self.api.connector.ping(
1112
+ self.connector_id, initial_state, connector_info
1113
+ )
976
1114
  remote_state = (
977
1115
  json.loads(result["connector_state"])
978
1116
  if result["connector_state"] is not None
@@ -980,11 +1118,292 @@ class OpenCTIConnectorHelper: # pylint: disable=too-many-public-methods
980
1118
  else None
981
1119
  )
982
1120
  if initial_state != remote_state:
983
- self.api.connector.ping(self.connector_id, initial_state)
1121
+ self.api.connector.ping(
1122
+ self.connector_id, initial_state, connector_info
1123
+ )
984
1124
  except Exception as e: # pylint: disable=broad-except
985
1125
  self.metric.inc("error_count")
986
1126
  self.connector_logger.error("Error pinging the API", {"reason": str(e)})
987
1127
 
1128
+ def next_run_datetime(self, duration_period_in_seconds: Union[int, float]) -> None:
1129
+ """
1130
+ Lets you know what the next run of the scheduler will be in iso datetime format
1131
+
1132
+ :param duration_period_in_seconds: Duration in seconds
1133
+ :return: None
1134
+ """
1135
+ try:
1136
+ duration_timedelta = datetime.timedelta(seconds=duration_period_in_seconds)
1137
+ next_datetime = datetime.datetime.utcnow() + duration_timedelta
1138
+ # Set next_run_datetime
1139
+ self.connector_info.next_run_datetime = next_datetime.strftime(
1140
+ "%Y-%m-%dT%H:%M:%SZ"
1141
+ )
1142
+ self.connector_logger.info(
1143
+ "[INFO] Schedule next run of connector: ",
1144
+ {"next_run_datetime": self.connector_info.next_run_datetime},
1145
+ )
1146
+ return
1147
+ except Exception as err:
1148
+ self.metric.inc("error_count")
1149
+ self.connector_logger.error(
1150
+ "[ERROR] An error occurred while calculating the next run in datetime",
1151
+ {"reason": str(err)},
1152
+ )
1153
+ sys.excepthook(*sys.exc_info())
1154
+
1155
+ def last_run_datetime(self) -> None:
1156
+ """
1157
+ Lets you know what the last run of the connector the scheduler processed, will be in iso datetime format
1158
+
1159
+ :return: None
1160
+ """
1161
+ try:
1162
+ current_datetime = datetime.datetime.utcnow()
1163
+ # Set last_run_datetime
1164
+ self.connector_info.last_run_datetime = current_datetime.strftime(
1165
+ "%Y-%m-%dT%H:%M:%SZ"
1166
+ )
1167
+ return
1168
+ except Exception as err:
1169
+ self.metric.inc("error_count")
1170
+ self.connector_logger.error(
1171
+ "[ERROR] An error occurred while converting the last run in datetime",
1172
+ {"reason": str(err)},
1173
+ )
1174
+ sys.excepthook(*sys.exc_info())
1175
+
1176
+ def check_connector_buffering(self) -> bool:
1177
+ """
1178
+ Lets you know if the RabbitMQ queue has exceeded the allowed threshold defined by the connector or not
1179
+ :return: boolean
1180
+ """
1181
+ try:
1182
+ connector_details = self.api.connector.read(connector_id=self.connector_id)
1183
+
1184
+ if connector_details and connector_details.get("id") is not None:
1185
+ connector_queue_id = connector_details["id"]
1186
+ connector_queue_details = connector_details["connector_queue_details"]
1187
+
1188
+ queue_messages_size_byte = connector_queue_details["messages_size"]
1189
+ queue_threshold = self.connect_queue_threshold
1190
+
1191
+ # Convert queue_messages_size to Mo (decimal)
1192
+ queue_messages_size_mo = queue_messages_size_byte / 1000000
1193
+
1194
+ self.connector_logger.debug(
1195
+ "[DEBUG] Connector queue details ...",
1196
+ {
1197
+ "connector_queue_id": connector_queue_id,
1198
+ "queue_threshold": queue_threshold,
1199
+ "messages_number": connector_queue_details["messages_number"],
1200
+ "queue_messages_size": queue_messages_size_mo,
1201
+ },
1202
+ )
1203
+
1204
+ # Set the connector info
1205
+ self.connector_info.queue_messages_size = queue_messages_size_mo
1206
+ self.connector_info.queue_threshold = queue_threshold
1207
+
1208
+ if float(queue_messages_size_mo) < float(queue_threshold):
1209
+ # Set buffering
1210
+ self.connector_info.buffering = False
1211
+ return False
1212
+ else:
1213
+ self.connector_logger.info(
1214
+ "[INFO] Connector will not run until the queue messages size is reduced under queue threshold"
1215
+ )
1216
+ # Set buffering
1217
+ self.connector_info.buffering = True
1218
+ return True
1219
+
1220
+ else:
1221
+ self.metric.inc("error_count")
1222
+ self.connector_logger.error(
1223
+ "[ERROR] An error occurred while retrieving connector details"
1224
+ )
1225
+ sys.excepthook(*sys.exc_info())
1226
+ except Exception as err:
1227
+ self.metric.inc("error_count")
1228
+ self.connector_logger.error(
1229
+ "[ERROR] An error occurred while checking the queue size",
1230
+ {"reason": str(err)},
1231
+ )
1232
+ sys.excepthook(*sys.exc_info())
1233
+
1234
+ def schedule_unit(
1235
+ self,
1236
+ message_callback: Callable[[], None],
1237
+ duration_period: Union[int, float, str],
1238
+ time_unit: TimeUnit,
1239
+ ) -> None:
1240
+ """
1241
+ This (deprecated) method is there to manage backward compatibility of intervals on connectors,
1242
+ allows you to calculate the duration period of connectors in seconds with time_unit and will be
1243
+ replaced by the "schedule_iso" method. It uses a TimeUnit enum.
1244
+
1245
+ :param message_callback: Corresponds to the connector process
1246
+ :param duration_period: Corresponds to the connector interval, it can vary depending on the connector
1247
+ configuration.
1248
+ :param time_unit: The unit of time for the duration_period.
1249
+ Enum TimeUnit Valid (YEARS, WEEKS, DAYS, HOURS, MINUTES, SECONDS)
1250
+ :return: None
1251
+ """
1252
+ try:
1253
+ # Calculates the duration period in seconds
1254
+ time_unit_in_seconds = time_unit.value
1255
+ duration_period_in_seconds = float(duration_period) * time_unit_in_seconds
1256
+
1257
+ # Start schedule_process
1258
+ self.schedule_process(message_callback, duration_period_in_seconds)
1259
+
1260
+ except Exception as err:
1261
+ self.metric.inc("error_count")
1262
+ self.connector_logger.error(
1263
+ "[ERROR] An unexpected error occurred during schedule_unit",
1264
+ {"reason": str(err)},
1265
+ )
1266
+ sys.excepthook(*sys.exc_info())
1267
+
1268
+ def schedule_iso(
1269
+ self, message_callback: Callable[[], None], duration_period: str
1270
+ ) -> None:
1271
+ """
1272
+ This method allows you to calculate the duration period of connectors in seconds from ISO 8601 format
1273
+ and start the scheduler process.
1274
+
1275
+ :param message_callback: Corresponds to the connector process
1276
+ :param duration_period: Corresponds to a string in ISO 8601 format "P18Y9W4DT11H9M8S"
1277
+ :return: None
1278
+ """
1279
+ try:
1280
+ if duration_period == "0":
1281
+ duration_period_in_seconds = 0
1282
+ else:
1283
+ # Calculates and validate the duration period in seconds
1284
+ timedelta_adapter = TypeAdapter(datetime.timedelta)
1285
+ td = timedelta_adapter.validate_python(duration_period)
1286
+ duration_period_in_seconds = int(td.total_seconds())
1287
+
1288
+ # Start schedule_process
1289
+ self.schedule_process(message_callback, duration_period_in_seconds)
1290
+
1291
+ except Exception as err:
1292
+ self.metric.inc("error_count")
1293
+ self.connector_logger.error(
1294
+ "[ERROR] An unexpected error occurred during schedule_iso",
1295
+ {"reason": str(err)},
1296
+ )
1297
+ sys.excepthook(*sys.exc_info())
1298
+
1299
+ def _schedule_process(
1300
+ self,
1301
+ scheduler: sched.scheduler,
1302
+ message_callback: Callable[[], None],
1303
+ duration_period: Union[int, float],
1304
+ ) -> None:
1305
+ """
1306
+ When scheduling, the function retrieves the details of the connector queue,
1307
+ and the connector process starts only if the size of the queue messages is less than or
1308
+ equal to the queue_threshold variable.
1309
+
1310
+ :param scheduler: Scheduler contains a list of all tasks to be started
1311
+ :param message_callback: Corresponds to the connector process
1312
+ :param duration_period: Corresponds to the connector's interval
1313
+ :return: None
1314
+ """
1315
+ try:
1316
+ self.connector_logger.info("[INFO] Starting schedule")
1317
+ check_connector_buffering = self.check_connector_buffering()
1318
+
1319
+ if not check_connector_buffering:
1320
+ # Start running the connector
1321
+ message_callback()
1322
+ # Lets you know what is the last run of the connector datetime
1323
+ self.last_run_datetime()
1324
+
1325
+ except Exception as err:
1326
+ self.metric.inc("error_count")
1327
+ self.connector_logger.error(
1328
+ "[ERROR] An error occurred while checking the queue size",
1329
+ {"reason": str(err)},
1330
+ )
1331
+ sys.excepthook(*sys.exc_info())
1332
+
1333
+ finally:
1334
+ # Lets you know what the next run of the scheduler will be
1335
+ self.next_run_datetime(duration_period)
1336
+ # Then schedule the next execution
1337
+ scheduler.enter(
1338
+ duration_period,
1339
+ 1,
1340
+ self._schedule_process,
1341
+ (scheduler, message_callback, duration_period),
1342
+ )
1343
+
1344
+ def schedule_process(
1345
+ self, message_callback: Callable[[], None], duration_period: Union[int, float]
1346
+ ) -> None:
1347
+ """
1348
+ This method schedules the execution of a connector process.
1349
+ If `duration_period' is zero or `self.connect_run_and_terminate' is True, the process will run and terminate.
1350
+ Otherwise, it schedules the next run based on the interval.
1351
+
1352
+ :param message_callback: Corresponds to the connector process
1353
+ :param duration_period: Corresponds to the connector's interval in seconds
1354
+ :return: None
1355
+ """
1356
+ try:
1357
+ # In the case where the duration_period_converted is zero, we consider it to be a run and terminate
1358
+ if self.connect_run_and_terminate or duration_period == 0:
1359
+ self.connector_logger.info("[INFO] Starting run and terminate")
1360
+ # Set run_and_terminate
1361
+ self.connector_info.run_and_terminate = True
1362
+ check_connector_buffering = self.check_connector_buffering()
1363
+
1364
+ if not check_connector_buffering:
1365
+ # Start running the connector
1366
+ message_callback()
1367
+
1368
+ # Lets you know what is the last run of the connector datetime
1369
+ self.last_run_datetime()
1370
+ self.connector_logger.info("[INFO] Closing run and terminate")
1371
+ self.force_ping()
1372
+ sys.exit(0)
1373
+ else:
1374
+ # Start running the connector
1375
+ message_callback()
1376
+ # Set queue_threshold and queue_messages_size for the first run
1377
+ self.check_connector_buffering()
1378
+ # Lets you know what is the last run of the connector datetime
1379
+ self.last_run_datetime()
1380
+ # Lets you know what the next run of the scheduler will be
1381
+ self.next_run_datetime(duration_period)
1382
+
1383
+ # Then schedule the next execution
1384
+ self.scheduler.enter(
1385
+ duration_period,
1386
+ 1,
1387
+ self._schedule_process,
1388
+ (self.scheduler, message_callback, duration_period),
1389
+ )
1390
+ self.scheduler.run()
1391
+
1392
+ except SystemExit:
1393
+ self.connector_logger.info("SystemExit caught, stopping the scheduler")
1394
+ if self.connect_run_and_terminate:
1395
+ self.connector_logger.info("[INFO] Closing run and terminate")
1396
+ self.force_ping()
1397
+ sys.exit(0)
1398
+
1399
+ except Exception as err:
1400
+ self.metric.inc("error_count")
1401
+ self.connector_logger.error(
1402
+ "[ERROR] An unexpected error occurred during schedule",
1403
+ {"reason": str(err)},
1404
+ )
1405
+ sys.excepthook(*sys.exc_info())
1406
+
988
1407
  def listen(
989
1408
  self,
990
1409
  message_callback: Callable[[Dict], str],
@@ -359,6 +359,9 @@ class StixCoreObject:
359
359
  x_opencti_cvss_attack_vector
360
360
  x_opencti_cvss_integrity_impact
361
361
  x_opencti_cvss_availability_impact
362
+ x_opencti_cisa_kev
363
+ x_opencti_epss_score
364
+ x_opencti_epss_percentile
362
365
  }
363
366
  ... on Incident {
364
367
  name
@@ -1023,6 +1026,9 @@ class StixCoreObject:
1023
1026
  x_opencti_cvss_attack_vector
1024
1027
  x_opencti_cvss_integrity_impact
1025
1028
  x_opencti_cvss_availability_impact
1029
+ x_opencti_cisa_kev
1030
+ x_opencti_epss_score
1031
+ x_opencti_epss_percentile
1026
1032
  }
1027
1033
  ... on Incident {
1028
1034
  name
@@ -482,6 +482,9 @@ class StixDomainObject:
482
482
  x_opencti_cvss_attack_vector
483
483
  x_opencti_cvss_integrity_impact
484
484
  x_opencti_cvss_availability_impact
485
+ x_opencti_cisa_kev
486
+ x_opencti_epss_score
487
+ x_opencti_epss_percentile
485
488
  }
486
489
  ... on Incident {
487
490
  name
@@ -977,6 +980,9 @@ class StixDomainObject:
977
980
  x_opencti_cvss_attack_vector
978
981
  x_opencti_cvss_integrity_impact
979
982
  x_opencti_cvss_availability_impact
983
+ x_opencti_cisa_kev
984
+ x_opencti_epss_score
985
+ x_opencti_epss_percentile
980
986
  }
981
987
  ... on Incident {
982
988
  name
@@ -298,6 +298,9 @@ class StixObjectOrStixRelationship:
298
298
  x_opencti_cvss_attack_vector
299
299
  x_opencti_cvss_integrity_impact
300
300
  x_opencti_cvss_availability_impact
301
+ x_opencti_cisa_kev
302
+ x_opencti_epss_score
303
+ x_opencti_epss_percentile
301
304
  }
302
305
  ... on Incident {
303
306
  name
@@ -106,6 +106,8 @@ class Vulnerability:
106
106
  x_opencti_cvss_availability_impact
107
107
  x_opencti_cvss_confidentiality_impact
108
108
  x_opencti_cisa_kev
109
+ x_opencti_epss_score
110
+ x_opencti_epss_percentile
109
111
  importFiles {
110
112
  edges {
111
113
  node {
@@ -285,6 +287,8 @@ class Vulnerability:
285
287
  x_opencti_cvss_base_severity = kwargs.get("x_opencti_cvss_base_severity", None)
286
288
  x_opencti_cvss_attack_vector = kwargs.get("x_opencti_cvss_attack_vector", None)
287
289
  x_opencti_cisa_kev = kwargs.get("x_opencti_cisa_kev", None)
290
+ x_opencti_epss_score = kwargs.get("x_opencti_epss_score", None)
291
+ x_opencti_epss_percentile = kwargs.get("x_opencti_epss_percentile", None)
288
292
  x_opencti_cvss_integrity_impact = kwargs.get(
289
293
  "x_opencti_cvss_integrity_impact", None
290
294
  )
@@ -336,6 +340,8 @@ class Vulnerability:
336
340
  "x_opencti_cvss_availability_impact": x_opencti_cvss_availability_impact,
337
341
  "x_opencti_cvss_confidentiality_impact": x_opencti_cvss_confidentiality_impact,
338
342
  "x_opencti_cisa_kev": x_opencti_cisa_kev,
343
+ "x_opencti_epss_score": x_opencti_epss_score,
344
+ "x_opencti_epss_percentile": x_opencti_epss_percentile,
339
345
  "x_opencti_stix_ids": x_opencti_stix_ids,
340
346
  "x_opencti_workflow_id": x_opencti_workflow_id,
341
347
  "update": update,
@@ -444,6 +450,16 @@ class Vulnerability:
444
450
  stix_object["x_opencti_cisa_kev"] = (
445
451
  self.opencti.get_attribute_in_extension("cisa_kev", stix_object)
446
452
  )
453
+ if "x_opencti_epss_score" not in stix_object:
454
+ stix_object["x_opencti_epss_score"] = (
455
+ self.opencti.get_attribute_in_extension("epss_score", stix_object)
456
+ )
457
+ if "x_opencti_epss_percentile" not in stix_object:
458
+ stix_object["x_opencti_epss_percentile"] = (
459
+ self.opencti.get_attribute_in_extension(
460
+ "epss_percentile", stix_object
461
+ )
462
+ )
447
463
 
448
464
  return self.create(
449
465
  stix_id=stix_object["id"],
@@ -531,6 +547,16 @@ class Vulnerability:
531
547
  if "x_opencti_cisa_kev" in stix_object
532
548
  else None
533
549
  ),
550
+ x_opencti_epss_score=(
551
+ stix_object["x_opencti_epss_score"]
552
+ if "x_opencti_epss_score" in stix_object
553
+ else None
554
+ ),
555
+ x_opencti_epss_percentile=(
556
+ stix_object["x_opencti_epss_percentile"]
557
+ if "x_opencti_epss_percentile" in stix_object
558
+ else None
559
+ ),
534
560
  update=update,
535
561
  )
536
562
  else:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pycti
3
- Version: 6.2.10
3
+ Version: 6.2.12
4
4
  Summary: Python API client for OpenCTI.
5
5
  Home-page: https://github.com/OpenCTI-Platform/client-python
6
6
  Author: Filigran
@@ -23,6 +23,7 @@ Description-Content-Type: text/markdown
23
23
  License-File: LICENSE
24
24
  Requires-Dist: datefinder~=0.7.3
25
25
  Requires-Dist: pika~=1.3.0
26
+ Requires-Dist: pydantic~=2.8.2
26
27
  Requires-Dist: python-magic~=0.4.27; sys_platform == "linux" or sys_platform == "darwin"
27
28
  Requires-Dist: python-magic-bin~=0.4.14; sys_platform == "win32"
28
29
  Requires-Dist: python_json_logger~=2.0.4
@@ -1,5 +1,6 @@
1
1
  datefinder~=0.7.3
2
2
  pika~=1.3.0
3
+ pydantic~=2.8.2
3
4
  python_json_logger~=2.0.4
4
5
  PyYAML~=6.0
5
6
  requests~=2.32.2
@@ -36,6 +36,7 @@ include_package_data = True
36
36
  install_requires =
37
37
  datefinder~=0.7.3
38
38
  pika~=1.3.0
39
+ pydantic~=2.8.2
39
40
  python-magic~=0.4.27; sys_platform == 'linux' or sys_platform == 'darwin'
40
41
  python-magic-bin~=0.4.14; sys_platform == 'win32'
41
42
  python_json_logger~=2.0.4
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes