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.
@@ -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