Source code for colander_data_converter.converters.stix2.utils
1"""
2Utility functions for STIX2 to Colander conversion and vice versa.
3"""
4
5from typing import Dict, Any, Optional
6from uuid import uuid4, UUID
7
8from pydantic import UUID4
9
10
[docs]
11def extract_uuid_from_stix2_id(stix2_id: str) -> UUID:
12 """
13 Extract a UUID from a STIX2 ID.
14
15 This function parses a STIX2 identifier string to extract the UUID portion.
16 STIX2 IDs follow the format ``{type}--{uuid}``, where the UUID is the part
17 after the double dash delimiter.
18
19 :param stix2_id: The STIX2 ID to extract the UUID from
20 :type stix2_id: str
21 :return: The extracted UUID, or a new UUID if extraction fails
22 :rtype: UUID
23
24 .. important::
25 If the input format is invalid or UUID extraction fails, a new random
26 UUID is generated and returned instead of raising an exception.
27
28 Examples:
29 >>> # Valid STIX2 ID with UUID
30 >>> stix_id = "indicator--44af6c9f-4bbc-4984-a74b-1404d1ac07ea"
31 >>> uuid_obj = extract_uuid_from_stix2_id(stix_id)
32 >>> str(uuid_obj)
33 '44af6c9f-4bbc-4984-a74b-1404d1ac07ea'
34
35 >>> # Invalid STIX2 ID format (no delimiter)
36 >>> stix_id = "indicator-invalid-format"
37 >>> uuid_obj = extract_uuid_from_stix2_id(stix_id)
38 >>> isinstance(uuid_obj, UUID) # Returns a new random UUID
39 True
40
41 >>> # Invalid UUID part
42 >>> stix_id = "indicator--not-a-valid-uuid"
43 >>> uuid_obj = extract_uuid_from_stix2_id(stix_id)
44 >>> isinstance(uuid_obj, UUID) # Returns a new random UUID
45 True
46 """
47 try:
48 if stix2_id and "--" in stix2_id:
49 # Extract the part after the "--" delimiter
50 uuid_part = stix2_id.split("--", 1)[1]
51 # Try to create a UUID from the extracted part
52 return UUID4(uuid_part, version=4)
53 except (ValueError, IndexError):
54 # If anything goes wrong, return a new UUID
55 pass
56
57 return uuid4()
58
59
[docs]
60def extract_stix2_pattern_name(stix2_pattern: str) -> Optional[str]:
61 """
62 Extract the name from a STIX 2 pattern string.
63
64 This function parses STIX2 pattern expressions to extract the field name
65 portion before the equality operator. It removes brackets and extracts
66 the left side of the comparison.
67
68 :param stix2_pattern: The STIX 2 pattern string to extract the name from
69 :type stix2_pattern: str
70 :return: The extracted name or None if no name is found
71 :rtype: Optional[str]
72
73 .. note::
74 The function handles various STIX2 pattern formats including nested
75 hash references like ``file:hashes.'SHA-256'``.
76
77 Examples:
78 >>> pattern = "[ipv4-addr:value = '192.168.1.1']"
79 >>> extract_stix2_pattern_name(pattern)
80 'ipv4-addr:value'
81
82 >>> pattern = "[file:hashes.'SHA-256' = '123abc']"
83 >>> extract_stix2_pattern_name(pattern)
84 "file:hashes.'SHA-256'"
85 """
86 _to_replace = [
87 ("[", ""),
88 ("]", ""),
89 ]
90 if "=" not in stix2_pattern:
91 return ""
92 _stix2_pattern = stix2_pattern
93 for _replace in _to_replace:
94 _stix2_pattern = _stix2_pattern.replace(_replace[0], _replace[1])
95 return _stix2_pattern.split("=")[0].strip()
96
97
[docs]
98def get_nested_value(obj: Dict[str, Any], path: str) -> Any:
99 """
100 Get a value from a nested dictionary using a dot-separated path.
101
102 This function safely navigates through nested dictionaries using a
103 dot-separated path string. It returns the value at the specified path
104 or None if any part of the path is missing or invalid.
105
106 :param obj: The dictionary to get the value from
107 :type obj: Dict[str, Any]
108 :param path: The dot-separated path to the value
109 :type path: str
110 :return: The value at the specified path, or None if not found
111 :rtype: Any
112
113 .. warning::
114 This function returns None for missing paths rather than raising
115 exceptions. Check for None return values when path existence is critical.
116
117 Examples:
118 >>> data = {
119 ... "user": {
120 ... "profile": {
121 ... "name": "John",
122 ... "age": 30
123 ... },
124 ... "settings": {
125 ... "theme": "dark"
126 ... }
127 ... }
128 ... }
129 >>> get_nested_value(data, "user.profile.name")
130 'John'
131 >>> get_nested_value(data, "user.settings.theme")
132 'dark'
133 """
134 if not path:
135 return None
136
137 parts = path.split(".")
138 current = obj
139
140 for part in parts:
141 if isinstance(current, dict) and part in current:
142 current = current[part]
143 else:
144 return None
145
146 return current
147
148
[docs]
149def set_nested_value(obj: Dict[str, Any], path: str, value: Any) -> None:
150 """
151 Set a value in a nested dictionary using a dot-separated path.
152
153 This function creates nested dictionaries as needed to set a value at
154 the specified dot-separated path. If intermediate dictionaries don't
155 exist, they are automatically created.
156
157 :param obj: The dictionary to set the value in
158 :type obj: Dict[str, Any]
159 :param path: The dot-separated path to the value
160 :type path: str
161 :param value: The value to set
162 :type value: Any
163
164 .. note::
165 The function modifies the input dictionary in-place and automatically
166 creates any missing intermediate dictionary levels.
167
168 Examples:
169 >>> data = {}
170 >>> set_nested_value(data, "user.profile.name", "John")
171 >>> data
172 {'user': {'profile': {'name': 'John'}}}
173
174 >>> # Update existing nested value
175 >>> data = {'user': {'settings': {'theme': 'light'}}}
176 >>> set_nested_value(data, "user.settings.theme", "dark")
177 >>> data
178 {'user': {'settings': {'theme': 'dark'}}}
179
180 >>> # Add new nested path to existing structure
181 >>> set_nested_value(data, "user.profile.age", 30)
182 >>> data
183 {'user': {'settings': {'theme': 'dark'}, 'profile': {'age': 30}}}
184
185 >>> # Empty path does nothing
186 >>> original = {'a': 1}
187 >>> set_nested_value(original, "", "value")
188 >>> original
189 {'a': 1}
190 """
191 if not path:
192 return
193
194 parts = path.split(".")
195 current = obj
196
197 # Navigate to the parent of the final part
198 for part in parts[:-1]:
199 if part not in current:
200 current[part] = {}
201 current = current[part]
202
203 # Set the value at the final part
204 current[parts[-1]] = value