stix2-utils 0.0.2__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.
- stix2_utils-0.0.2.dist-info/METADATA +33 -0
- stix2_utils-0.0.2.dist-info/RECORD +11 -0
- stix2_utils-0.0.2.dist-info/WHEEL +4 -0
- stix2utils/__init__.py +85 -0
- stix2utils/adapter.py +89 -0
- stix2utils/common.py +87 -0
- stix2utils/references.py +47 -0
- stix2utils/sco_models.py +819 -0
- stix2utils/sdo_models.py +253 -0
- stix2utils/sro_models.py +297 -0
- stix2utils/validator.py +32 -0
stix2utils/sco_models.py
ADDED
|
@@ -0,0 +1,819 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from enum import Enum
|
|
4
|
+
from typing import Annotated, Literal
|
|
5
|
+
|
|
6
|
+
from pydantic import BaseModel, ConfigDict, Field, model_validator
|
|
7
|
+
|
|
8
|
+
from stix2utils.common import Hashes, Hex, Timestamp
|
|
9
|
+
from stix2utils.references import (
|
|
10
|
+
_UUID,
|
|
11
|
+
AnyRef,
|
|
12
|
+
ArtifactRef,
|
|
13
|
+
DirectoryContainsRef,
|
|
14
|
+
DirectoryRef,
|
|
15
|
+
DomainResolvesToRef,
|
|
16
|
+
EmailAddrRef,
|
|
17
|
+
EmailFromRef,
|
|
18
|
+
FileRef,
|
|
19
|
+
IPBelongsToRef,
|
|
20
|
+
IPResolvesToRef,
|
|
21
|
+
MarkingRef,
|
|
22
|
+
MimeBodyRawRef,
|
|
23
|
+
NetworkSrcDstRef,
|
|
24
|
+
NetworkTrafficRef,
|
|
25
|
+
ProcessRef,
|
|
26
|
+
UserAccountRef,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class STIXCyberObservable(BaseModel):
|
|
31
|
+
"""Common properties shared by all STIX 2.1 SCOs.
|
|
32
|
+
|
|
33
|
+
Unlike SDOs, SCOs are not versioned: they do not carry created, modified,
|
|
34
|
+
created_by_ref, revoked, labels, confidence, lang, or external_references.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
model_config = ConfigDict(extra="forbid")
|
|
38
|
+
|
|
39
|
+
# Required common properties
|
|
40
|
+
id: str = Field(..., pattern=rf"^[a-z0-9-]+--{_UUID}$")
|
|
41
|
+
|
|
42
|
+
# Optional common properties
|
|
43
|
+
spec_version: Literal["2.1"] | None = "2.1"
|
|
44
|
+
object_marking_refs: Annotated[list[MarkingRef], Field(min_length=1)] | None = None
|
|
45
|
+
granular_markings: list[dict] | None = None
|
|
46
|
+
defanged: bool | None = None
|
|
47
|
+
extensions: dict | None = None
|
|
48
|
+
|
|
49
|
+
@model_validator(mode="after")
|
|
50
|
+
def _id_prefix_matches_type(self) -> STIXCyberObservable:
|
|
51
|
+
expected = self.type
|
|
52
|
+
prefix = self.id.split("--", 1)[0]
|
|
53
|
+
if prefix != expected:
|
|
54
|
+
msg = f"id prefix {prefix!r} does not match type {expected!r}"
|
|
55
|
+
raise ValueError(msg)
|
|
56
|
+
return self
|
|
57
|
+
|
|
58
|
+
class EncryptionAlgorithm(str, Enum):
|
|
59
|
+
AES_256_GCM = "AES-256-GCM"
|
|
60
|
+
CHACHA20_POLY1305 = "ChaCha20-Poly1305"
|
|
61
|
+
MIME_TYPE_INDICATED = "mime-type-indicated"
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class Artifact(STIXCyberObservable):
|
|
65
|
+
type: Literal["artifact"] = "artifact"
|
|
66
|
+
mime_type: str | None = None
|
|
67
|
+
payload_bin: str | None = None # base64-encoded binary
|
|
68
|
+
url: str | None = None
|
|
69
|
+
hashes: Hashes | None = None
|
|
70
|
+
encryption_algorithm: EncryptionAlgorithm | None = None
|
|
71
|
+
decryption_key: str | None = None
|
|
72
|
+
|
|
73
|
+
@model_validator(mode="after")
|
|
74
|
+
def _check_payload_or_url(self) -> Artifact:
|
|
75
|
+
if bool(self.payload_bin) == bool(self.url):
|
|
76
|
+
msg = "exactly one of payload_bin or url MUST be provided"
|
|
77
|
+
raise ValueError(msg)
|
|
78
|
+
if self.url and not self.hashes:
|
|
79
|
+
msg = "hashes MUST be present when url is present"
|
|
80
|
+
raise ValueError(msg)
|
|
81
|
+
if self.payload_bin and self.url:
|
|
82
|
+
msg = "payload_bin MUST NOT be present if url is provided"
|
|
83
|
+
raise ValueError(msg)
|
|
84
|
+
if self.decryption_key and not self.encryption_algorithm:
|
|
85
|
+
msg = "decryption_key MUST NOT be present when encryption_algorithm is absent"
|
|
86
|
+
raise ValueError(msg)
|
|
87
|
+
return self
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class AutonomousSystem(STIXCyberObservable):
|
|
91
|
+
type: Literal["autonomous-system"] = "autonomous-system"
|
|
92
|
+
number: int
|
|
93
|
+
name: str | None = None
|
|
94
|
+
rir: str | None = None
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class Directory(STIXCyberObservable):
|
|
98
|
+
type: Literal["directory"] = "directory"
|
|
99
|
+
path: str
|
|
100
|
+
path_enc: str | None = None
|
|
101
|
+
ctime: Timestamp | None = None
|
|
102
|
+
mtime: Timestamp | None = None
|
|
103
|
+
atime: Timestamp | None = None
|
|
104
|
+
contains_refs: Annotated[list[DirectoryContainsRef], Field(min_length=1)] | None = None
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class DomainName(STIXCyberObservable):
|
|
108
|
+
type: Literal["domain-name"] = "domain-name"
|
|
109
|
+
value: str
|
|
110
|
+
resolves_to_refs: Annotated[list[DomainResolvesToRef], Field(min_length=1)] | None = None
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class EmailAddress(STIXCyberObservable):
|
|
114
|
+
type: Literal["email-addr"] = "email-addr"
|
|
115
|
+
value: str
|
|
116
|
+
display_name: str | None = None
|
|
117
|
+
belongs_to_ref: UserAccountRef | None = None
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class EmailMIMEComponent(BaseModel):
|
|
121
|
+
model_config = ConfigDict(extra="forbid")
|
|
122
|
+
|
|
123
|
+
body: str | None = None
|
|
124
|
+
body_raw_ref: MimeBodyRawRef | None = None
|
|
125
|
+
content_type: str | None = None
|
|
126
|
+
content_disposition: str | None = None
|
|
127
|
+
|
|
128
|
+
@model_validator(mode="after")
|
|
129
|
+
def _check_body_or_raw(self) -> EmailMIMEComponent:
|
|
130
|
+
if not self.body and not self.body_raw_ref:
|
|
131
|
+
msg = "one of body or body_raw_ref MUST be included"
|
|
132
|
+
raise ValueError(msg)
|
|
133
|
+
return self
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
class EmailMessage(STIXCyberObservable):
|
|
137
|
+
type: Literal["email-message"] = "email-message"
|
|
138
|
+
is_multipart: bool
|
|
139
|
+
date: Timestamp | None = None
|
|
140
|
+
content_type: str | None = None
|
|
141
|
+
from_ref: EmailFromRef | None = None
|
|
142
|
+
sender_ref: EmailAddrRef | None = None
|
|
143
|
+
to_refs: Annotated[list[EmailAddrRef], Field(min_length=1)] | None = None
|
|
144
|
+
cc_refs: Annotated[list[EmailAddrRef], Field(min_length=1)] | None = None
|
|
145
|
+
bcc_refs: Annotated[list[EmailAddrRef], Field(min_length=1)] | None = None
|
|
146
|
+
message_id: str | None = None
|
|
147
|
+
subject: str | None = None
|
|
148
|
+
received_lines: Annotated[list[str], Field(min_length=1)] | None = None
|
|
149
|
+
additional_header_fields: dict | None = None
|
|
150
|
+
body: str | None = None
|
|
151
|
+
body_multipart: Annotated[list[EmailMIMEComponent], Field(min_length=1)] | None = None
|
|
152
|
+
raw_email_ref: ArtifactRef | None = None
|
|
153
|
+
|
|
154
|
+
@model_validator(mode="after")
|
|
155
|
+
def _check_multipart(self) -> EmailMessage:
|
|
156
|
+
if self.is_multipart and self.body is not None:
|
|
157
|
+
msg = "body MUST NOT be used if is_multipart is true"
|
|
158
|
+
raise ValueError(msg)
|
|
159
|
+
if not self.is_multipart and self.body_multipart is not None:
|
|
160
|
+
msg = "body_multipart MUST NOT be used if is_multipart is false"
|
|
161
|
+
raise ValueError(msg)
|
|
162
|
+
return self
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
class AlternateDataStream(BaseModel):
|
|
166
|
+
model_config = ConfigDict(extra="forbid")
|
|
167
|
+
|
|
168
|
+
name: str
|
|
169
|
+
hashes: Hashes | None = None
|
|
170
|
+
size: Annotated[int, Field(ge=0)] | None = None
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
class NTFSExt(BaseModel):
|
|
174
|
+
model_config = ConfigDict(extra="forbid")
|
|
175
|
+
|
|
176
|
+
sid: str | None = None
|
|
177
|
+
alternate_data_streams: Annotated[list[AlternateDataStream], Field(min_length=1)] | None = None
|
|
178
|
+
|
|
179
|
+
@model_validator(mode="after")
|
|
180
|
+
def _at_least_one(self) -> NTFSExt:
|
|
181
|
+
if not any(v is not None for v in (self.sid, self.alternate_data_streams)):
|
|
182
|
+
msg = "NTFS extension MUST contain at least one property"
|
|
183
|
+
raise ValueError(msg)
|
|
184
|
+
return self
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
class PDFExt(BaseModel):
|
|
188
|
+
model_config = ConfigDict(extra="forbid")
|
|
189
|
+
|
|
190
|
+
version: str | None = None
|
|
191
|
+
is_optimized: bool | None = None
|
|
192
|
+
document_info_dict: dict | None = None
|
|
193
|
+
pdfid0: str | None = None
|
|
194
|
+
pdfid1: str | None = None
|
|
195
|
+
|
|
196
|
+
@model_validator(mode="after")
|
|
197
|
+
def _at_least_one(self) -> PDFExt:
|
|
198
|
+
if not any(
|
|
199
|
+
v is not None
|
|
200
|
+
for v in (
|
|
201
|
+
self.version,
|
|
202
|
+
self.is_optimized,
|
|
203
|
+
self.document_info_dict,
|
|
204
|
+
self.pdfid0,
|
|
205
|
+
self.pdfid1,
|
|
206
|
+
)
|
|
207
|
+
):
|
|
208
|
+
msg = "PDF extension MUST contain at least one property"
|
|
209
|
+
raise ValueError(msg)
|
|
210
|
+
return self
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
class RasterImageExt(BaseModel):
|
|
214
|
+
model_config = ConfigDict(extra="forbid")
|
|
215
|
+
|
|
216
|
+
image_height: int | None = None
|
|
217
|
+
image_width: int | None = None
|
|
218
|
+
bits_per_pixel: int | None = None
|
|
219
|
+
exif_tags: dict | None = None
|
|
220
|
+
|
|
221
|
+
@model_validator(mode="after")
|
|
222
|
+
def _at_least_one(self) -> RasterImageExt:
|
|
223
|
+
if not any(
|
|
224
|
+
v is not None
|
|
225
|
+
for v in (
|
|
226
|
+
self.image_height,
|
|
227
|
+
self.image_width,
|
|
228
|
+
self.bits_per_pixel,
|
|
229
|
+
self.exif_tags,
|
|
230
|
+
)
|
|
231
|
+
):
|
|
232
|
+
msg = "Raster image extension MUST contain at least one property"
|
|
233
|
+
raise ValueError(msg)
|
|
234
|
+
return self
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
class ArchiveExt(BaseModel):
|
|
238
|
+
model_config = ConfigDict(extra="forbid")
|
|
239
|
+
|
|
240
|
+
contains_refs: Annotated[list[DirectoryContainsRef], Field(min_length=1)]
|
|
241
|
+
comment: str | None = None
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
class WindowsPEOptionalHeaderType(BaseModel):
|
|
245
|
+
model_config = ConfigDict(extra="forbid")
|
|
246
|
+
|
|
247
|
+
magic_hex: Hex | None = None
|
|
248
|
+
major_linker_version: int | None = None
|
|
249
|
+
minor_linker_version: int | None = None
|
|
250
|
+
size_of_code: Annotated[int, Field(ge=0)] | None = None
|
|
251
|
+
size_of_initialized_data: Annotated[int, Field(ge=0)] | None = None
|
|
252
|
+
size_of_uninitialized_data: Annotated[int, Field(ge=0)] | None = None
|
|
253
|
+
address_of_entry_point: int | None = None
|
|
254
|
+
base_of_code: int | None = None
|
|
255
|
+
base_of_data: int | None = None
|
|
256
|
+
image_base: int | None = None
|
|
257
|
+
section_alignment: int | None = None
|
|
258
|
+
file_alignment: int | None = None
|
|
259
|
+
major_os_version: int | None = None
|
|
260
|
+
minor_os_version: int | None = None
|
|
261
|
+
major_image_version: int | None = None
|
|
262
|
+
minor_image_version: int | None = None
|
|
263
|
+
major_subsystem_version: int | None = None
|
|
264
|
+
minor_subsystem_version: int | None = None
|
|
265
|
+
win32_version_value_hex: Hex | None = None
|
|
266
|
+
size_of_image: Annotated[int, Field(ge=0)] | None = None
|
|
267
|
+
size_of_headers: Annotated[int, Field(ge=0)] | None = None
|
|
268
|
+
checksum_hex: Hex | None = None
|
|
269
|
+
subsystem_hex: Hex | None = None
|
|
270
|
+
dll_characteristics_hex: Hex | None = None
|
|
271
|
+
size_of_stack_reserve: Annotated[int, Field(ge=0)] | None = None
|
|
272
|
+
size_of_stack_commit: Annotated[int, Field(ge=0)] | None = None
|
|
273
|
+
size_of_heap_reserve: Annotated[int, Field(ge=0)] | None = None
|
|
274
|
+
size_of_heap_commit: Annotated[int, Field(ge=0)] | None = None
|
|
275
|
+
loader_flags_hex: Hex | None = None
|
|
276
|
+
number_of_rva_and_sizes: int | None = None
|
|
277
|
+
hashes: Hashes | None = None
|
|
278
|
+
|
|
279
|
+
@model_validator(mode="after")
|
|
280
|
+
def _at_least_one(self) -> WindowsPEOptionalHeaderType:
|
|
281
|
+
if not any(v is not None for v in self.__dict__.values()):
|
|
282
|
+
msg = "PE optional header MUST contain at least one property"
|
|
283
|
+
raise ValueError(msg)
|
|
284
|
+
return self
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
class WindowsPESectionType(BaseModel):
|
|
288
|
+
model_config = ConfigDict(extra="forbid")
|
|
289
|
+
|
|
290
|
+
name: str
|
|
291
|
+
size: Annotated[int, Field(ge=0)] | None = None
|
|
292
|
+
entropy: Annotated[float, Field(ge=0.0, le=8.0)] | None = None
|
|
293
|
+
hashes: Hashes | None = None
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
class WindowsPEBinaryExt(BaseModel):
|
|
297
|
+
model_config = ConfigDict(extra="forbid")
|
|
298
|
+
|
|
299
|
+
pe_type: str # windows-pebinary-type-ov (open): dll, exe, sys
|
|
300
|
+
imphash: str | None = None
|
|
301
|
+
machine_hex: Hex | None = None
|
|
302
|
+
number_of_sections: int | None = None
|
|
303
|
+
time_date_stamp: Timestamp | None = None
|
|
304
|
+
pointer_to_symbol_table_hex: Hex | None = None
|
|
305
|
+
number_of_symbols: int | None = None
|
|
306
|
+
size_of_optional_header: Annotated[int, Field(ge=0)] | None = None
|
|
307
|
+
characteristics_hex: Hex | None = None
|
|
308
|
+
file_header_hashes: Hashes | None = None
|
|
309
|
+
optional_header: WindowsPEOptionalHeaderType | None = None
|
|
310
|
+
sections: Annotated[list[WindowsPESectionType], Field(min_length=1)] | None = None
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
class FileExtensions(BaseModel):
|
|
314
|
+
"""Predefined File object extensions."""
|
|
315
|
+
|
|
316
|
+
model_config = ConfigDict(extra="allow") # allow custom / extension-definition keys
|
|
317
|
+
|
|
318
|
+
ntfs_ext: NTFSExt | None = Field(default=None, alias="ntfs-ext")
|
|
319
|
+
raster_image_ext: RasterImageExt | None = Field(default=None, alias="raster-image-ext")
|
|
320
|
+
pdf_ext: PDFExt | None = Field(default=None, alias="pdf-ext")
|
|
321
|
+
archive_ext: ArchiveExt | None = Field(default=None, alias="archive-ext")
|
|
322
|
+
windows_pebinary_ext: WindowsPEBinaryExt | None = Field(default=None, alias="windows-pebinary-ext")
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
class File(STIXCyberObservable):
|
|
326
|
+
type: Literal["file"] = "file"
|
|
327
|
+
hashes: Hashes | None = None
|
|
328
|
+
size: Annotated[int, Field(ge=0)] | None = None
|
|
329
|
+
name: str | None = None
|
|
330
|
+
name_enc: str | None = None
|
|
331
|
+
magic_number_hex: Hex | None = None
|
|
332
|
+
mime_type: str | None = None
|
|
333
|
+
ctime: Timestamp | None = None
|
|
334
|
+
mtime: Timestamp | None = None
|
|
335
|
+
atime: Timestamp | None = None
|
|
336
|
+
parent_directory_ref: DirectoryRef | None = None
|
|
337
|
+
contains_refs: Annotated[list[AnyRef], Field(min_length=1)] | None = None
|
|
338
|
+
content_ref: ArtifactRef | None = None
|
|
339
|
+
extensions: FileExtensions | None = None
|
|
340
|
+
|
|
341
|
+
@model_validator(mode="after")
|
|
342
|
+
def _hashes_or_name(self) -> File:
|
|
343
|
+
if not self.hashes and not self.name:
|
|
344
|
+
msg = "File object MUST contain at least one of hashes or name"
|
|
345
|
+
raise ValueError(msg)
|
|
346
|
+
return self
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
# ---------------------------------------------------------------------------
|
|
350
|
+
# IPv4 Address
|
|
351
|
+
# ---------------------------------------------------------------------------
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
class IPv4Address(STIXCyberObservable):
|
|
355
|
+
type: Literal["ipv4-addr"] = "ipv4-addr"
|
|
356
|
+
value: str
|
|
357
|
+
resolves_to_refs: Annotated[list[IPResolvesToRef], Field(min_length=1)] | None = None
|
|
358
|
+
belongs_to_refs: Annotated[list[IPBelongsToRef], Field(min_length=1)] | None = None
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
# ---------------------------------------------------------------------------
|
|
362
|
+
# IPv6 Address
|
|
363
|
+
# ---------------------------------------------------------------------------
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
class IPv6Address(STIXCyberObservable):
|
|
367
|
+
type: Literal["ipv6-addr"] = "ipv6-addr"
|
|
368
|
+
value: str
|
|
369
|
+
resolves_to_refs: Annotated[list[IPResolvesToRef], Field(min_length=1)] | None = None
|
|
370
|
+
belongs_to_refs: Annotated[list[IPBelongsToRef], Field(min_length=1)] | None = None
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
# ---------------------------------------------------------------------------
|
|
374
|
+
# MAC Address
|
|
375
|
+
# ---------------------------------------------------------------------------
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
class MACAddress(STIXCyberObservable):
|
|
379
|
+
type: Literal["mac-addr"] = "mac-addr"
|
|
380
|
+
# colon-delimited, lowercase MAC-48 with leading zeros
|
|
381
|
+
value: Annotated[str, Field(pattern=r"^([0-9a-f]{2}:){5}[0-9a-f]{2}$")]
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
# ---------------------------------------------------------------------------
|
|
385
|
+
# Mutex
|
|
386
|
+
# ---------------------------------------------------------------------------
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
class Mutex(STIXCyberObservable):
|
|
390
|
+
type: Literal["mutex"] = "mutex"
|
|
391
|
+
name: str
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
# ---------------------------------------------------------------------------
|
|
395
|
+
# Network Traffic (+ extensions)
|
|
396
|
+
# ---------------------------------------------------------------------------
|
|
397
|
+
|
|
398
|
+
_Port = Annotated[int, Field(ge=0, le=65535)]
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
class HTTPRequestExt(BaseModel):
|
|
402
|
+
model_config = ConfigDict(extra="forbid")
|
|
403
|
+
|
|
404
|
+
request_method: str
|
|
405
|
+
request_value: str
|
|
406
|
+
request_version: str | None = None
|
|
407
|
+
request_header: dict | None = None
|
|
408
|
+
message_body_length: int | None = None
|
|
409
|
+
message_body_data_ref: ArtifactRef | None = None
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
class ICMPExt(BaseModel):
|
|
413
|
+
model_config = ConfigDict(extra="forbid")
|
|
414
|
+
|
|
415
|
+
icmp_type_hex: Hex
|
|
416
|
+
icmp_code_hex: Hex
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
class NetworkSocketAddressFamily(str, Enum):
|
|
420
|
+
AF_UNSPEC = "AF_UNSPEC"
|
|
421
|
+
AF_INET = "AF_INET"
|
|
422
|
+
AF_IPX = "AF_IPX"
|
|
423
|
+
AF_APPLETALK = "AF_APPLETALK"
|
|
424
|
+
AF_NETBIOS = "AF_NETBIOS"
|
|
425
|
+
AF_INET6 = "AF_INET6"
|
|
426
|
+
AF_IRDA = "AF_IRDA"
|
|
427
|
+
AF_BTH = "AF_BTH"
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
class NetworkSocketType(str, Enum):
|
|
431
|
+
SOCK_STREAM = "SOCK_STREAM"
|
|
432
|
+
SOCK_DGRAM = "SOCK_DGRAM"
|
|
433
|
+
SOCK_RAW = "SOCK_RAW"
|
|
434
|
+
SOCK_RDM = "SOCK_RDM"
|
|
435
|
+
SOCK_SEQPACKET = "SOCK_SEQPACKET"
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
class NetworkSocketExt(BaseModel):
|
|
439
|
+
model_config = ConfigDict(extra="forbid")
|
|
440
|
+
|
|
441
|
+
address_family: NetworkSocketAddressFamily
|
|
442
|
+
is_blocking: bool | None = None
|
|
443
|
+
is_listening: bool | None = None
|
|
444
|
+
options: dict | None = None
|
|
445
|
+
socket_type: NetworkSocketType | None = None
|
|
446
|
+
socket_descriptor: Annotated[int, Field(ge=0)] | None = None
|
|
447
|
+
socket_handle: int | None = None
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
class TCPExt(BaseModel):
|
|
451
|
+
model_config = ConfigDict(extra="forbid")
|
|
452
|
+
|
|
453
|
+
src_flags_hex: Hex | None = None
|
|
454
|
+
dst_flags_hex: Hex | None = None
|
|
455
|
+
|
|
456
|
+
@model_validator(mode="after")
|
|
457
|
+
def _at_least_one(self) -> TCPExt:
|
|
458
|
+
if self.src_flags_hex is None and self.dst_flags_hex is None:
|
|
459
|
+
msg = "TCP extension MUST contain at least one property"
|
|
460
|
+
raise ValueError(msg)
|
|
461
|
+
return self
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
class NetworkTrafficExtensions(BaseModel):
|
|
465
|
+
model_config = ConfigDict(extra="allow")
|
|
466
|
+
|
|
467
|
+
http_request_ext: HTTPRequestExt | None = Field(default=None, alias="http-request-ext")
|
|
468
|
+
tcp_ext: TCPExt | None = Field(default=None, alias="tcp-ext")
|
|
469
|
+
icmp_ext: ICMPExt | None = Field(default=None, alias="icmp-ext")
|
|
470
|
+
socket_ext: NetworkSocketExt | None = Field(default=None, alias="socket-ext")
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
class NetworkTraffic(STIXCyberObservable):
|
|
474
|
+
type: Literal["network-traffic"] = "network-traffic"
|
|
475
|
+
start: Timestamp | None = None
|
|
476
|
+
end: Timestamp | None = None
|
|
477
|
+
is_active: bool | None = None
|
|
478
|
+
src_ref: NetworkSrcDstRef | None = None
|
|
479
|
+
dst_ref: NetworkSrcDstRef | None = None
|
|
480
|
+
src_port: _Port | None = None
|
|
481
|
+
dst_port: _Port | None = None
|
|
482
|
+
protocols: Annotated[list[str], Field(min_length=1)]
|
|
483
|
+
src_byte_count: Annotated[int, Field(ge=0)] | None = None
|
|
484
|
+
dst_byte_count: Annotated[int, Field(ge=0)] | None = None
|
|
485
|
+
src_packets: Annotated[int, Field(ge=0)] | None = None
|
|
486
|
+
dst_packets: Annotated[int, Field(ge=0)] | None = None
|
|
487
|
+
ipfix: dict | None = None
|
|
488
|
+
src_payload_ref: ArtifactRef | None = None
|
|
489
|
+
dst_payload_ref: ArtifactRef | None = None
|
|
490
|
+
encapsulates_refs: Annotated[list[NetworkTrafficRef], Field(min_length=1)] | None = None
|
|
491
|
+
encapsulated_by_ref: NetworkTrafficRef | None = None
|
|
492
|
+
extensions: NetworkTrafficExtensions | None = None
|
|
493
|
+
|
|
494
|
+
@model_validator(mode="after")
|
|
495
|
+
def _src_or_dst(self) -> NetworkTraffic:
|
|
496
|
+
if self.src_ref is None and self.dst_ref is None:
|
|
497
|
+
msg = "Network Traffic MUST contain at least one of src_ref or dst_ref"
|
|
498
|
+
raise ValueError(msg)
|
|
499
|
+
if self.is_active is True and self.end is not None:
|
|
500
|
+
msg = "end MUST NOT be included if is_active is true (and is_active MUST be false if end is provided)"
|
|
501
|
+
raise ValueError(msg)
|
|
502
|
+
return self
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
class WindowsIntegrityLevel(str, Enum):
|
|
506
|
+
LOW = "low"
|
|
507
|
+
MEDIUM = "medium"
|
|
508
|
+
HIGH = "high"
|
|
509
|
+
SYSTEM = "system"
|
|
510
|
+
|
|
511
|
+
|
|
512
|
+
class WindowsProcessExt(BaseModel):
|
|
513
|
+
model_config = ConfigDict(extra="forbid")
|
|
514
|
+
|
|
515
|
+
aslr_enabled: bool | None = None
|
|
516
|
+
dep_enabled: bool | None = None
|
|
517
|
+
priority: Annotated[str, Field(pattern=r".*_CLASS$")] | None = None
|
|
518
|
+
owner_sid: str | None = None
|
|
519
|
+
window_title: str | None = None
|
|
520
|
+
startup_info: dict | None = None
|
|
521
|
+
integrity_level: WindowsIntegrityLevel | None = None
|
|
522
|
+
|
|
523
|
+
@model_validator(mode="after")
|
|
524
|
+
def _at_least_one(self) -> WindowsProcessExt:
|
|
525
|
+
if not any(v is not None for v in self.__dict__.values()):
|
|
526
|
+
msg = "Windows process extension MUST contain at least one property"
|
|
527
|
+
raise ValueError(msg)
|
|
528
|
+
return self
|
|
529
|
+
|
|
530
|
+
|
|
531
|
+
class WindowsServiceStartType(str, Enum):
|
|
532
|
+
SERVICE_AUTO_START = "SERVICE_AUTO_START"
|
|
533
|
+
SERVICE_BOOT_START = "SERVICE_BOOT_START"
|
|
534
|
+
SERVICE_DEMAND_START = "SERVICE_DEMAND_START"
|
|
535
|
+
SERVICE_DISABLED = "SERVICE_DISABLED"
|
|
536
|
+
SERVICE_SYSTEM_ALERT = "SERVICE_SYSTEM_ALERT"
|
|
537
|
+
|
|
538
|
+
|
|
539
|
+
class WindowsServiceType(str, Enum):
|
|
540
|
+
SERVICE_KERNEL_DRIVER = "SERVICE_KERNEL_DRIVER"
|
|
541
|
+
SERVICE_FILE_SYSTEM_DRIVER = "SERVICE_FILE_SYSTEM_DRIVER"
|
|
542
|
+
SERVICE_WIN32_OWN_PROCESS = "SERVICE_WIN32_OWN_PROCESS"
|
|
543
|
+
SERVICE_WIN32_SHARE_PROCESS = "SERVICE_WIN32_SHARE_PROCESS"
|
|
544
|
+
|
|
545
|
+
|
|
546
|
+
class WindowsServiceStatus(str, Enum):
|
|
547
|
+
SERVICE_CONTINUE_PENDING = "SERVICE_CONTINUE_PENDING"
|
|
548
|
+
SERVICE_PAUSE_PENDING = "SERVICE_PAUSE_PENDING"
|
|
549
|
+
SERVICE_PAUSED = "SERVICE_PAUSED"
|
|
550
|
+
SERVICE_RUNNING = "SERVICE_RUNNING"
|
|
551
|
+
SERVICE_START_PENDING = "SERVICE_START_PENDING"
|
|
552
|
+
SERVICE_STOP_PENDING = "SERVICE_STOP_PENDING"
|
|
553
|
+
SERVICE_STOPPED = "SERVICE_STOPPED"
|
|
554
|
+
|
|
555
|
+
|
|
556
|
+
class WindowsServiceExt(BaseModel):
|
|
557
|
+
model_config = ConfigDict(extra="forbid")
|
|
558
|
+
|
|
559
|
+
service_name: str | None = None
|
|
560
|
+
descriptions: Annotated[list[str], Field(min_length=1)] | None = None
|
|
561
|
+
display_name: str | None = None
|
|
562
|
+
group_name: str | None = None
|
|
563
|
+
start_type: WindowsServiceStartType | None = None
|
|
564
|
+
service_dll_refs: Annotated[list[FileRef], Field(min_length=1)] | None = None
|
|
565
|
+
service_type: WindowsServiceType | None = None
|
|
566
|
+
service_status: WindowsServiceStatus | None = None
|
|
567
|
+
|
|
568
|
+
@model_validator(mode="after")
|
|
569
|
+
def _at_least_one(self) -> WindowsServiceExt:
|
|
570
|
+
if not any(v is not None for v in self.__dict__.values()):
|
|
571
|
+
msg = "Windows service extension MUST contain at least one property"
|
|
572
|
+
raise ValueError(msg)
|
|
573
|
+
return self
|
|
574
|
+
|
|
575
|
+
|
|
576
|
+
class ProcessExtensions(BaseModel):
|
|
577
|
+
model_config = ConfigDict(extra="allow")
|
|
578
|
+
|
|
579
|
+
windows_process_ext: WindowsProcessExt | None = Field(default=None, alias="windows-process-ext")
|
|
580
|
+
windows_service_ext: WindowsServiceExt | None = Field(default=None, alias="windows-service-ext")
|
|
581
|
+
|
|
582
|
+
|
|
583
|
+
class Process(STIXCyberObservable):
|
|
584
|
+
type: Literal["process"] = "process"
|
|
585
|
+
is_hidden: bool | None = None
|
|
586
|
+
pid: int | None = None
|
|
587
|
+
created_time: Timestamp | None = None
|
|
588
|
+
cwd: str | None = None
|
|
589
|
+
command_line: str | None = None
|
|
590
|
+
environment_variables: dict | None = None
|
|
591
|
+
opened_connection_refs: Annotated[list[NetworkTrafficRef], Field(min_length=1)] | None = None
|
|
592
|
+
creator_user_ref: UserAccountRef | None = None
|
|
593
|
+
image_ref: FileRef | None = None
|
|
594
|
+
parent_ref: ProcessRef | None = None
|
|
595
|
+
child_refs: Annotated[list[ProcessRef], Field(min_length=1)] | None = None
|
|
596
|
+
extensions: ProcessExtensions | None = None
|
|
597
|
+
|
|
598
|
+
@model_validator(mode="after")
|
|
599
|
+
def _at_least_one_property(self) -> Process:
|
|
600
|
+
specific = (
|
|
601
|
+
self.is_hidden,
|
|
602
|
+
self.pid,
|
|
603
|
+
self.created_time,
|
|
604
|
+
self.cwd,
|
|
605
|
+
self.command_line,
|
|
606
|
+
self.environment_variables,
|
|
607
|
+
self.opened_connection_refs,
|
|
608
|
+
self.creator_user_ref,
|
|
609
|
+
self.image_ref,
|
|
610
|
+
self.parent_ref,
|
|
611
|
+
self.child_refs,
|
|
612
|
+
self.extensions,
|
|
613
|
+
)
|
|
614
|
+
if not any(v is not None for v in specific):
|
|
615
|
+
msg = "Process object MUST contain at least one property (or extension)"
|
|
616
|
+
raise ValueError(msg)
|
|
617
|
+
return self
|
|
618
|
+
|
|
619
|
+
|
|
620
|
+
class Software(STIXCyberObservable):
|
|
621
|
+
type: Literal["software"] = "software"
|
|
622
|
+
name: str
|
|
623
|
+
cpe: str | None = None
|
|
624
|
+
swid: str | None = None
|
|
625
|
+
languages: Annotated[list[str], Field(min_length=1)] | None = None
|
|
626
|
+
vendor: str | None = None
|
|
627
|
+
version: str | None = None
|
|
628
|
+
|
|
629
|
+
|
|
630
|
+
class URL(STIXCyberObservable):
|
|
631
|
+
type: Literal["url"] = "url"
|
|
632
|
+
value: str
|
|
633
|
+
|
|
634
|
+
|
|
635
|
+
class UNIXAccountExt(BaseModel):
|
|
636
|
+
model_config = ConfigDict(extra="forbid")
|
|
637
|
+
|
|
638
|
+
gid: int | None = None
|
|
639
|
+
groups: Annotated[list[str], Field(min_length=1)] | None = None
|
|
640
|
+
home_dir: str | None = None
|
|
641
|
+
shell: str | None = None
|
|
642
|
+
|
|
643
|
+
@model_validator(mode="after")
|
|
644
|
+
def _at_least_one(self) -> UNIXAccountExt:
|
|
645
|
+
if not any(v is not None for v in self.__dict__.values()):
|
|
646
|
+
msg = "UNIX account extension MUST contain at least one property"
|
|
647
|
+
raise ValueError(msg)
|
|
648
|
+
return self
|
|
649
|
+
|
|
650
|
+
|
|
651
|
+
class UserAccountExtensions(BaseModel):
|
|
652
|
+
model_config = ConfigDict(extra="allow")
|
|
653
|
+
|
|
654
|
+
unix_account_ext: UNIXAccountExt | None = Field(default=None, alias="unix-account-ext")
|
|
655
|
+
|
|
656
|
+
|
|
657
|
+
class UserAccount(STIXCyberObservable):
|
|
658
|
+
type: Literal["user-account"] = "user-account"
|
|
659
|
+
user_id: str | None = None
|
|
660
|
+
credential: str | None = None
|
|
661
|
+
account_login: str | None = None
|
|
662
|
+
account_type: str | None = None # account-type-ov (open)
|
|
663
|
+
display_name: str | None = None
|
|
664
|
+
is_service_account: bool | None = None
|
|
665
|
+
is_privileged: bool | None = None
|
|
666
|
+
can_escalate_privs: bool | None = None
|
|
667
|
+
is_disabled: bool | None = None
|
|
668
|
+
account_created: Timestamp | None = None
|
|
669
|
+
account_expires: Timestamp | None = None
|
|
670
|
+
credential_last_changed: Timestamp | None = None
|
|
671
|
+
account_first_login: Timestamp | None = None
|
|
672
|
+
account_last_login: Timestamp | None = None
|
|
673
|
+
extensions: UserAccountExtensions | None = None
|
|
674
|
+
|
|
675
|
+
@model_validator(mode="after")
|
|
676
|
+
def _at_least_one_property(self) -> UserAccount:
|
|
677
|
+
specific = (
|
|
678
|
+
self.user_id,
|
|
679
|
+
self.credential,
|
|
680
|
+
self.account_login,
|
|
681
|
+
self.account_type,
|
|
682
|
+
self.display_name,
|
|
683
|
+
self.is_service_account,
|
|
684
|
+
self.is_privileged,
|
|
685
|
+
self.can_escalate_privs,
|
|
686
|
+
self.is_disabled,
|
|
687
|
+
self.account_created,
|
|
688
|
+
self.account_expires,
|
|
689
|
+
self.credential_last_changed,
|
|
690
|
+
self.account_first_login,
|
|
691
|
+
self.account_last_login,
|
|
692
|
+
self.extensions,
|
|
693
|
+
)
|
|
694
|
+
if not any(v is not None for v in specific):
|
|
695
|
+
msg = "User Account object MUST contain at least one property"
|
|
696
|
+
raise ValueError(msg)
|
|
697
|
+
return self
|
|
698
|
+
|
|
699
|
+
|
|
700
|
+
class WindowsRegistryDatatype(str, Enum):
|
|
701
|
+
REG_NONE = "REG_NONE"
|
|
702
|
+
REG_SZ = "REG_SZ"
|
|
703
|
+
REG_EXPAND_SZ = "REG_EXPAND_SZ"
|
|
704
|
+
REG_BINARY = "REG_BINARY"
|
|
705
|
+
REG_DWORD = "REG_DWORD"
|
|
706
|
+
REG_DWORD_BIG_ENDIAN = "REG_DWORD_BIG_ENDIAN"
|
|
707
|
+
REG_DWORD_LITTLE_ENDIAN = "REG_DWORD_LITTLE_ENDIAN"
|
|
708
|
+
REG_LINK = "REG_LINK"
|
|
709
|
+
REG_MULTI_SZ = "REG_MULTI_SZ"
|
|
710
|
+
REG_RESOURCE_LIST = "REG_RESOURCE_LIST"
|
|
711
|
+
REG_FULL_RESOURCE_DESCRIPTION = "REG_FULL_RESOURCE_DESCRIPTION"
|
|
712
|
+
REG_RESOURCE_REQUIREMENTS_LIST = "REG_RESOURCE_REQUIREMENTS_LIST"
|
|
713
|
+
REG_QWORD = "REG_QWORD"
|
|
714
|
+
REG_INVALID_TYPE = "REG_INVALID_TYPE"
|
|
715
|
+
|
|
716
|
+
|
|
717
|
+
class WindowsRegistryValueType(BaseModel):
|
|
718
|
+
model_config = ConfigDict(extra="forbid")
|
|
719
|
+
|
|
720
|
+
name: str | None = None
|
|
721
|
+
data: str | None = None
|
|
722
|
+
data_type: WindowsRegistryDatatype | None = None
|
|
723
|
+
|
|
724
|
+
@model_validator(mode="after")
|
|
725
|
+
def _at_least_one(self) -> WindowsRegistryValueType:
|
|
726
|
+
if not any(v is not None for v in self.__dict__.values()):
|
|
727
|
+
msg = "Windows registry value type MUST contain at least one property"
|
|
728
|
+
raise ValueError(msg)
|
|
729
|
+
return self
|
|
730
|
+
|
|
731
|
+
|
|
732
|
+
class WindowsRegistryKey(STIXCyberObservable):
|
|
733
|
+
type: Literal["windows-registry-key"] = "windows-registry-key"
|
|
734
|
+
key: str | None = None
|
|
735
|
+
values: Annotated[list[WindowsRegistryValueType], Field(min_length=1)] | None = None
|
|
736
|
+
modified_time: Timestamp | None = None
|
|
737
|
+
creator_user_ref: UserAccountRef | None = None
|
|
738
|
+
number_of_subkeys: int | None = None
|
|
739
|
+
|
|
740
|
+
@model_validator(mode="after")
|
|
741
|
+
def _at_least_one_property(self) -> WindowsRegistryKey:
|
|
742
|
+
specific = (
|
|
743
|
+
self.key,
|
|
744
|
+
self.values,
|
|
745
|
+
self.modified_time,
|
|
746
|
+
self.creator_user_ref,
|
|
747
|
+
self.number_of_subkeys,
|
|
748
|
+
)
|
|
749
|
+
if not any(v is not None for v in specific):
|
|
750
|
+
msg = "Windows Registry Key object MUST contain at least one property"
|
|
751
|
+
raise ValueError(msg)
|
|
752
|
+
return self
|
|
753
|
+
|
|
754
|
+
|
|
755
|
+
class X509V3ExtensionsType(BaseModel):
|
|
756
|
+
model_config = ConfigDict(extra="forbid")
|
|
757
|
+
|
|
758
|
+
basic_constraints: str | None = None
|
|
759
|
+
name_constraints: str | None = None
|
|
760
|
+
policy_constraints: str | None = None
|
|
761
|
+
key_usage: str | None = None
|
|
762
|
+
extended_key_usage: str | None = None
|
|
763
|
+
subject_key_identifier: str | None = None
|
|
764
|
+
authority_key_identifier: str | None = None
|
|
765
|
+
subject_alternative_name: str | None = None
|
|
766
|
+
issuer_alternative_name: str | None = None
|
|
767
|
+
subject_directory_attributes: str | None = None
|
|
768
|
+
crl_distribution_points: str | None = None
|
|
769
|
+
inhibit_any_policy: str | None = None
|
|
770
|
+
private_key_usage_period_not_before: Timestamp | None = None
|
|
771
|
+
private_key_usage_period_not_after: Timestamp | None = None
|
|
772
|
+
certificate_policies: str | None = None
|
|
773
|
+
policy_mappings: str | None = None
|
|
774
|
+
|
|
775
|
+
@model_validator(mode="after")
|
|
776
|
+
def _at_least_one(self) -> X509V3ExtensionsType:
|
|
777
|
+
if not any(v is not None for v in self.__dict__.values()):
|
|
778
|
+
msg = "X.509 v3 extensions type MUST contain at least one property"
|
|
779
|
+
raise ValueError(msg)
|
|
780
|
+
return self
|
|
781
|
+
|
|
782
|
+
|
|
783
|
+
class X509Certificate(STIXCyberObservable):
|
|
784
|
+
type: Literal["x509-certificate"] = "x509-certificate"
|
|
785
|
+
is_self_signed: bool | None = None
|
|
786
|
+
hashes: Hashes | None = None
|
|
787
|
+
version: str | None = None
|
|
788
|
+
serial_number: str | None = None
|
|
789
|
+
signature_algorithm: str | None = None
|
|
790
|
+
issuer: str | None = None
|
|
791
|
+
validity_not_before: Timestamp | None = None
|
|
792
|
+
validity_not_after: Timestamp | None = None
|
|
793
|
+
subject: str | None = None
|
|
794
|
+
subject_public_key_algorithm: str | None = None
|
|
795
|
+
subject_public_key_modulus: str | None = None
|
|
796
|
+
subject_public_key_exponent: int | None = None
|
|
797
|
+
x509_v3_extensions: X509V3ExtensionsType | None = None
|
|
798
|
+
|
|
799
|
+
@model_validator(mode="after")
|
|
800
|
+
def _at_least_one_property(self) -> X509Certificate:
|
|
801
|
+
specific = (
|
|
802
|
+
self.is_self_signed,
|
|
803
|
+
self.hashes,
|
|
804
|
+
self.version,
|
|
805
|
+
self.serial_number,
|
|
806
|
+
self.signature_algorithm,
|
|
807
|
+
self.issuer,
|
|
808
|
+
self.validity_not_before,
|
|
809
|
+
self.validity_not_after,
|
|
810
|
+
self.subject,
|
|
811
|
+
self.subject_public_key_algorithm,
|
|
812
|
+
self.subject_public_key_modulus,
|
|
813
|
+
self.subject_public_key_exponent,
|
|
814
|
+
self.x509_v3_extensions,
|
|
815
|
+
)
|
|
816
|
+
if not any(v is not None for v in specific):
|
|
817
|
+
msg = "X.509 Certificate object MUST contain at least one specific property"
|
|
818
|
+
raise ValueError(msg)
|
|
819
|
+
return self
|