stidantic 0.1.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of stidantic might be problematic. Click here for more details.

stidantic/sro.py ADDED
@@ -0,0 +1,159 @@
1
+ from typing import Literal, Self, Annotated
2
+ from datetime import datetime
3
+ from pydantic import Field
4
+ from pydantic.functional_validators import model_validator
5
+ from pydantic.types import PositiveInt
6
+ from stidantic.types import StixRelationship, Identifier, StixType
7
+
8
+
9
+ # 5.1 Relationship
10
+ class Relationship(StixRelationship):
11
+ """
12
+ The Relationship object is used to link together two SDOs or SCOs in order to describe how they are related to
13
+ each other. If SDOs and SCOs are considered "nodes" or "vertices" in the graph, the Relationship Objects (SROs)
14
+ represent "edges".
15
+
16
+ STIX defines many relationship types to link together SDOs and SCOs. These relationships are contained in the
17
+ "Relationships" table under each SDO and SCO definition. Relationship types defined in the specification SHOULD
18
+ be used to ensure consistency. An example of a specification-defined relationship is that an indicator indicates
19
+ a campaign. That relationship type is listed in the Relationships section of the Indicator SDO definition.
20
+
21
+ STIX also allows relationships from any SDO or SCO to any SDO or SCO that have not been defined in this
22
+ specification. These relationships MAY use the related-to relationship type or MAY use a user-defined
23
+ relationship type. As an example, a user might want to link malware directly to a tool.
24
+ They can do so using related-to to say that the Malware is related to the Tool but not describe how,
25
+ or they could use delivered-by (a user-defined name they determined) to indicate more detail.
26
+
27
+ Note that some relationships in STIX may seem like "shortcuts". For example, an Indicator doesn't really detect
28
+ a Campaign: it detects activity (Attack Patterns, Malware, Infrastructure, etc.) that are often used by that
29
+ campaign. While some analysts might want all of the source data and think that shortcuts are misleading,
30
+ in many cases it's helpful to provide just the key points (shortcuts) and leave out the low-level details.
31
+ In other cases, the low-level analysis may not be known or sharable, while the high-level analysis is.
32
+ For these reasons, relationships that might appear to be "shortcuts" are not excluded from STIX.
33
+ """
34
+
35
+ type: Literal["relationship"] = "relationship" # pyright: ignore[reportIncompatibleVariableOverride]
36
+ # The name used to identify the type of Relationship. This value SHOULD be an exact value listed in the
37
+ # relationships for the source and target SDO, but MAY be any string.
38
+ relationship_type: StixType
39
+ # A description that provides more details and context about the Relationship,
40
+ # potentially including its purpose and its key characteristics.
41
+ description: str | None = None
42
+ # The id of the source (from) object. The value MUST be an ID reference to an SDO or SCO
43
+ # (i.e., it cannot point to an SRO, Bundle, Language Content, or Marking Definition).
44
+ source_ref: Identifier
45
+ # The id of the target (to) object. The value MUST be an ID reference to an SDO or SCO
46
+ # (i.e., it cannot point to an SRO, Bundle, Language Content, or Marking Definition).
47
+ target_ref: Identifier
48
+ # This optional timestamp represents the earliest time at which the Relationship between the objects exists.
49
+ # If this property is a future timestamp, at the time the start_time property is defined,
50
+ # then this represents an estimate by the producer of the intelligence of the earliest
51
+ # time at which relationship will be asserted to be true.
52
+ # If it is not specified, then the earliest time at which the relationship between the objects exists
53
+ # is not defined.
54
+ start_time: datetime | None = None
55
+ # The latest time at which the Relationship between the objects exists. If this property is a future timestamp,
56
+ # at the time the stop_time property is defined, then this represents an estimate by the producer of the
57
+ # intelligence of the latest time at which relationship will be asserted to be true.
58
+ # If stop_time is not specified, then the latest time at which the relationship between
59
+ # the objects exists is either not known, not disclosed, or has no defined stop time.
60
+ stop_time: datetime | None = None
61
+
62
+ @model_validator(mode="after")
63
+ def validate_start_stop_interval(self) -> Self:
64
+ """
65
+ If start_time and stop_time are both defined, then stop_time MUST be later than the start_time value.
66
+ """
67
+ if self.start_time and self.stop_time and self.start_time > self.stop_time:
68
+ raise ValueError(
69
+ "the stop_time property MUST be greater than or equal to the timestamp in the start_time property"
70
+ )
71
+ return self
72
+
73
+
74
+ # 5.2 Sighting
75
+ class Sighting(StixRelationship):
76
+ """
77
+ A Sighting denotes the belief that something in CTI (e.g., an indicator, malware, tool, threat actor, etc.)
78
+ was seen. Sightings are used to track who and what are being targeted, how attacks are carried out, and to
79
+ track trends in attack behavior.
80
+
81
+ The Sighting relationship object is a special type of SRO; it is a relationship that contains extra properties
82
+ not present on the Generic Relationship object. These extra properties are included to represent data specific
83
+ to sighting relationships (e.g., count, representing how many times something was seen), but for other purposes
84
+ a Sighting can be thought of as a Relationship with a name of "sighting-of". Sighting is captured as a relationship
85
+ because you cannot have a sighting unless you have something that has been sighted.
86
+ Sighting does not make sense without the relationship to what was sighted.
87
+
88
+ Sighting relationships relate three aspects of the sighting:
89
+ ● What was sighted, such as the Indicator, Malware, Campaign, or other SDO (sighting_of_ref)
90
+ ● Who sighted it and/or where it was sighted, represented as an Identity (where_sighted_refs)
91
+ ● What was actually seen on systems and networks, represented as Observed Data (observed_data_refs)
92
+
93
+ What was sighted is required; a sighting does not make sense unless you say what you saw. Who sighted it,
94
+ where it was sighted, and what was actually seen are optional. In many cases it is not necessary to provide
95
+ that level of detail in order to provide value.
96
+
97
+ Sightings are used whenever any SDO has been "seen". In some cases, the object creator wishes to convey very little
98
+ information about the sighting; the details might be sensitive, but the fact that they saw a malware instance or
99
+ threat actor could still be very useful. In other cases, providing the details may be helpful or even necessary;
100
+ saying exactly which of the 1000 IP addresses in an indicator were sighted is helpful when tracking which of those
101
+ IPs is still malicious.
102
+
103
+ Sighting is distinct from Observed Data in that Sighting is an intelligence assertion ("I saw this threat actor")
104
+ while Observed Data is simply information ("I saw this file"). When you combine them by including the linked
105
+ Observed Data (observed_data_refs) from a Sighting, you can say "I saw this file, and that makes me think I saw
106
+ this threat actor".
107
+ """
108
+
109
+ type: Literal["sighting"] = "sighting" # pyright: ignore[reportIncompatibleVariableOverride]
110
+ # A description that provides more details and context about the Sighting.
111
+ description: str | None = None
112
+ # The beginning of the time window during which the SDO referenced by the sighting_of_ref property was sighted.
113
+ first_seen: datetime | None = None
114
+ # The end of the time window during which the SDO referenced by the sighting_of_ref property was sighted.
115
+ last_seen: datetime | None = None
116
+ # If present, this MUST be an integer between 0 and 999,999,999 inclusive and represents the number of times the
117
+ # SDO referenced by the sighting_of_ref property was sighted.
118
+ # Observed Data has a similar property called number_observed, which refers to the number of times the data was
119
+ # observed. These counts refer to different concepts and are distinct.
120
+ # For example, a single sighting of a DDoS bot might have many millions of observations of the network traffic
121
+ # that it generates. Thus, the Sighting count would be 1 (the bot was observed once) but the Observed Data
122
+ # number_observed would be much higher.
123
+ # As another example, a sighting with a count of 0 can be used to express that an indicator was not seen at all.
124
+ count: PositiveInt | None = None
125
+ # An ID reference to the SDO that was sighted (e.g., Indicator or Malware).
126
+ # For example, if this is a Sighting of an Indicator, that Indicator’s ID would be the value of this property.
127
+ # This property MUST reference only an SDO.
128
+ sighting_of_ref: Identifier | None = None
129
+ # A list of ID references to the Observed Data objects that contain the raw cyber data for this Sighting.
130
+ # For example, a Sighting of an Indicator with an IP address could include the Observed Data for the
131
+ # network connection that the Indicator was used to detect.
132
+ # This property MUST reference only Observed Data SDOs.
133
+ observed_data_refs: list[Identifier] | None = None
134
+ # A list of ID references to the Identity or Location objects describing the entities or types of entities
135
+ # that saw the sighting.
136
+ # Omitting the where_sighted_refs property does not imply that the sighting was seen by the object creator.
137
+ # To indicate that the sighting was seen by the object creator, an Identity representing the object creator
138
+ # should be listed in where_sighted_refs.
139
+ # This property MUST reference only Identity or Location SDOs.
140
+ where_sighted_refs: list[Identifier] | None = None
141
+ # The summary property indicates whether the Sighting should be considered summary data.
142
+ # Summary data is an aggregation of previous Sightings reports and should not be considered primary source data.
143
+ # Default value is false.
144
+ summary: bool | None = False
145
+
146
+ @model_validator(mode="after")
147
+ def validate_first_last_interval(self) -> Self:
148
+ """
149
+ If this property and the first_seen property are both defined, then this property
150
+ MUST be greater than or equal to the timestamp in the first_seen property.
151
+ """
152
+ if self.first_seen and self.last_seen and self.first_seen > self.last_seen:
153
+ raise ValueError(
154
+ "the last_seen property MUST be greater than or equal to the timestamp in the first_seen property"
155
+ )
156
+ return self
157
+
158
+
159
+ SROs = Annotated[(Relationship | Sighting), Field(discriminator="type")]
stidantic/types.py ADDED
@@ -0,0 +1,441 @@
1
+ from enum import Enum
2
+ from re import compile
3
+ from datetime import datetime
4
+ from typing import Annotated, Literal, Any, Self, Type, get_args # pyright: ignore[reportDeprecated]
5
+ from annotated_types import Ge, Le
6
+ from typing_extensions import TypedDict
7
+
8
+ from pydantic.functional_validators import AfterValidator, model_validator
9
+ from pydantic.networks import AnyUrl, UrlConstraints
10
+ from pydantic.types import Base64Bytes, StringConstraints
11
+ from pydantic_core import CoreSchema, core_schema
12
+ from pydantic import (
13
+ BaseModel,
14
+ ConfigDict,
15
+ Field,
16
+ GetCoreSchemaHandler,
17
+ SerializeAsAny,
18
+ )
19
+
20
+ from stidantic.validators import (
21
+ validate_bin_field,
22
+ validate_hex_field,
23
+ )
24
+
25
+
26
+ # Common constraints on Stix dictionnary keys.
27
+ StixKeyPattern = compile(r"^[a-zA-Z0-9\-\_]+$")
28
+ StixKeyConstraint = StringConstraints(max_length=250, pattern=StixKeyPattern)
29
+ StixKey = Annotated[str, StixKeyConstraint]
30
+
31
+ # Common constraints on Stix type names.
32
+ StixTypePattern = compile(r"^[a-zA-Z0-9\-]+$")
33
+ StixTypeConstraint = StringConstraints(pattern=StixTypePattern)
34
+ StixType = Annotated[str, StixTypeConstraint]
35
+
36
+ # Common constraints on Stix property names.
37
+ StixPropPattern = compile(r"^[a-zA-Z0-9\_]+$")
38
+ StixPropConstraint = StringConstraints(
39
+ min_length=3, max_length=250, pattern=StixPropPattern
40
+ )
41
+ StixProp = Annotated[str, StixPropConstraint]
42
+
43
+ # 2.1 Binary
44
+ # The binary data type represents a sequence of bytes. In order to allow pattern matching on custom objects,
45
+ # for all properties that use the binary type, the property name MUST end with '_bin'.
46
+ type StixBinary = Annotated[Base64Bytes, AfterValidator(validate_bin_field)]
47
+
48
+ # 2.8 Hexadecimal
49
+ # The hex data type encodes an array of octets (8-bit bytes) as hexadecimal. The string MUST consist of an even number
50
+ # of hexadecimal characters, which are the digits '0' through '9' and the lower-case letters 'a' through 'f'. In order
51
+ # to allow pattern matching on custom objects, for all properties that use the hex type,
52
+ # the property name MUST end with '_hex'.
53
+ type StixHex = Annotated[str, AfterValidator(validate_hex_field)]
54
+
55
+ # 2.3 Dictionnary
56
+ # Dictionary keys MUST be unique in each dictionary, MUST be in ASCII, and are limited to the characters
57
+ # a-z (lowercase ASCII), A-Z (uppercase ASCII), numerals 0-9, hyphen (-), and underscore (_).
58
+ # Dictionary keys MUST be no longer than 250 ASCII characters in length and SHOULD be lowercase.
59
+ type StixDict = dict[
60
+ StixKey,
61
+ Any, # pyright: ignore[reportExplicitAny]
62
+ ]
63
+
64
+
65
+ # A URL reference to an external resource [RFC3986].
66
+ type StixUrl = Annotated[AnyUrl, UrlConstraints(preserve_empty_path=True)]
67
+
68
+
69
+ # 2.9 Identifier
70
+ class Identifier(str):
71
+ @classmethod
72
+ def __get_pydantic_core_schema__(
73
+ cls,
74
+ source_type: Any, # pyright: ignore[reportExplicitAny, reportAny]
75
+ handler: GetCoreSchemaHandler,
76
+ ) -> CoreSchema:
77
+ return core_schema.no_info_after_validator_function(cls, handler(str))
78
+
79
+ def get_type(self: str) -> str:
80
+ return self.split("--", maxsplit=1)[0]
81
+
82
+
83
+ class StixCore(BaseModel):
84
+ model_config = ConfigDict( # pyright: ignore[reportUnannotatedClassAttribute]
85
+ use_enum_values=True,
86
+ validate_by_alias=True,
87
+ validate_by_name=True,
88
+ frozen=True,
89
+ extra="allow",
90
+ )
91
+
92
+
93
+ # 2.7 Hashes
94
+ class Hashes(StixCore, extra="allow"):
95
+ """
96
+ The Hashes type represents one or more cryptographic hashes, as a special set of key/value pairs.
97
+ Accordingly, the name of each hashing algorithm MUST be specified as a key in the dictionary
98
+ and MUST identify the name of the hashing algorithm used to generate the corresponding value.
99
+ This name SHOULD come from one of the values defined in the hash-algorithm-ov open vocabulary.
100
+
101
+ To enhance compatibility, the SHA-256 hash SHOULD be used whenever possible.
102
+ """
103
+
104
+ # Specifies the MD5 message digest algorithm. The corresponding hash string for this
105
+ # value MUST be a valid MD5 message digest as defined in [RFC1321].
106
+ md5: Annotated[str | None, Field(serialization_alias="MD5")] = None
107
+ # Specifies the SHA-1 (secure-hash algorithm 1) cryptographic hash function.
108
+ # The corresponding hash string for this value MUST be a valid SHA-1 message digest as defined in [RFC3174].
109
+ sha1: Annotated[str | None, Field(serialization_alias="SHA-1")] = None
110
+ # Specifies the SHA-256 cryptographic hash function (part of the SHA2 family).
111
+ # The corresponding hash string for this value MUST be a valid SHA-256 message digest as defined in [RFC6234].
112
+ sha256: Annotated[str | None, Field(serialization_alias="SHA-256")] = None
113
+ # Specifies the SHA-512 cryptographic hash function (part of the SHA2 family).
114
+ # The corresponding hash string for this value MUST be a valid SHA-512 message digest as defined in [RFC6234].
115
+ sha512: Annotated[str | None, Field(serialization_alias="SHA-512")] = None
116
+ # Specifies the SHA3-256 cryptographic hash function. The corresponding hash string
117
+ # for this value MUST be a valid SHA3-256 message digest as defined in [FIPS202].
118
+ sha3_256: Annotated[str | None, Field(serialization_alias="SHA3-256")] = None
119
+ # Specifies the SHA3-512 cryptographic hash function. The corresponding hash string
120
+ # for this value MUST be a valid SHA3-512 message digest as defined in [FIPS202].
121
+ sha3_512: Annotated[str | None, Field(serialization_alias="SHA3-512")] = None
122
+ # Specifies the ssdeep fuzzy hashing algorithm. The corresponding hash string for this
123
+ # value MUST be a valid piecewise hash as defined in the [SSDEEP] specification.
124
+ ssdeep: Annotated[str | None, Field(serialization_alias="SSDEEP")] = None
125
+ # Specifies the TLSH fuzzy hashing algorithm. The corresponding hash string for this
126
+ # value MUST be a valid 35 byte long hash as defined in the [TLSH] specification.
127
+ tlsh: Annotated[str | None, Field(serialization_alias="TLSH")] = None
128
+
129
+ @model_validator(mode="after")
130
+ def lang_or_marking_ref(self) -> "Hashes":
131
+ """
132
+ Dictionary keys MUST be unique in each hashes property, MUST be in ASCII, and are limited to the
133
+ characters a-z (lowercase ASCII), A-Z (uppercase ASCII), numerals 0-9, hyphen (-), and underscore (_).
134
+ Dictionary keys MUST have a minimum length of 3 ASCII characters
135
+ and MUST be no longer than 250 ASCII characters in length.
136
+ The value MUST be a string in the appropriate format defined by the hash type indicated in the dictionary key.
137
+ """
138
+ if self.__pydantic_extra__ and any(
139
+ not (len(key) > 250 or len(key) < 3 or StixKeyPattern.match(key))
140
+ for key in self.__pydantic_extra__.keys()
141
+ ):
142
+ raise ValueError("Invalid extra hash key.")
143
+ return self
144
+
145
+
146
+ # 2.5 External Reference
147
+ class ExternalReference(StixCore):
148
+ """
149
+ External references are used to describe pointers to information represented outside of STIX.
150
+ For example, a Malware object could use an external reference to indicate an ID for that malware
151
+ in an external database or a report could use references to represent source material.
152
+ """
153
+
154
+ # The name of the source that the external-reference is defined within (system, registry, organization, etc.).
155
+ source_name: str
156
+ # A human readable description.
157
+ description: str | None = None
158
+ # A URL reference to an external resource [RFC3986].
159
+ url: StixUrl | None = None
160
+ # Specifies a dictionary of hashes for the contents of the url. This SHOULD be provided when the url property is
161
+ # present. Dictionary keys MUST come from one of the entries listed in the hash-algorithm-ov open vocabulary.
162
+ # As stated in Section 2.7, to ensure interoperability, a SHA-256 hash SHOULD be included whenever possible.
163
+ hashes: Hashes | None = None
164
+ # An identifier for the external reference content.
165
+ external_id: str | None = None
166
+
167
+ @model_validator(mode="before")
168
+ def at_least_one(cls, values: dict[str, str]) -> dict[str, str]:
169
+ """
170
+ In addition to the source_name property, at least one of the description, url,
171
+ or external_id properties MUST be present.
172
+ """
173
+ for value in values.values():
174
+ if value:
175
+ return values
176
+ raise ValueError("Missing at least one hash value.")
177
+
178
+
179
+ # 2.11 Kill Chain Phase
180
+ class KillChainPhase(StixCore):
181
+ """
182
+ The kill-chain-phase represents a phase in a kill chain, which describes the
183
+ various phases an attacker may undertake in order to achieve their objectives.
184
+ """
185
+
186
+ # The name of the kill chain. The value of this property SHOULD be all lowercase
187
+ # and SHOULD use hyphens instead of spaces or underscores as word separators.
188
+ kill_chain_name: str
189
+ # The name of the phase in the kill chain. The value of this property SHOULD be all
190
+ # lowercase and SHOULD use hyphens instead of spaces or underscores as word separators.
191
+ phase_name: str
192
+
193
+
194
+ # 7.2.3 Granular Markings
195
+ class GranularMarking(StixCore):
196
+ """
197
+ The granular-marking type defines how the list of marking-definition objects referenced by
198
+ the marking_refs property to apply to a set of content identified by
199
+ the list of selectors in the selectors property.
200
+ """
201
+
202
+ # The lang property identifies the language of the text identified by this marking. The value of the lang property,
203
+ # if present, MUST be an [RFC5646] language code. If the marking_ref property is not present, this property MUST
204
+ # be present. If the marking_ref property is present, this property MUST NOT be present.
205
+ lang: str | None = None
206
+ # The marking_ref property specifies the ID of the marking-definition object that describes the marking.
207
+ # If the lang property is not present, this property MUST be present. If the lang property is present,
208
+ # this property MUST NOT be present.
209
+ marking_ref: Identifier | None = None
210
+ # The selectors property specifies a list of selectors for content contained within the STIX Object in which this
211
+ # property appears. Selectors MUST conform to the syntax defined below. The marking-definition referenced in
212
+ # the marking_ref property is applied to the content selected by the selectors in this list. The [RFC5646] language
213
+ # code specified by the lang property is applied to the content selected by the selectors in this list.
214
+ selectors: list[str] | None = None
215
+ # Selector Syntax:
216
+ # Selectors contained in the selectors list are strings that consist of multiple components that MUST be separated
217
+ # by the . character. Each component MUST be one of:
218
+ # ● A property name or dictionary key, e.g., description, or;
219
+ # ● A zero-based list index, specified as a non-negative integer in square brackets, e.g., [4]
220
+ # Selectors denote path traversals: the root of each selector is the STIX Object
221
+ # that the granular_markings property appears in.
222
+ # Starting from that root, for each component in the selector, properties and list items are traversed.
223
+ # When the complete list has been traversed, the value of the content is considered selected.
224
+ # Selectors MUST refer to properties or list items that are actually present on the marked object.
225
+
226
+ @model_validator(mode="after")
227
+ def lang_or_marking_ref(self) -> "GranularMarking":
228
+ if self.lang and self.marking_ref:
229
+ raise ValueError(
230
+ "Both lang and marking_ref properties MUST NOT be present."
231
+ )
232
+ if not self.lang and not self.marking_ref:
233
+ raise ValueError("Either lang or marking_ref property MUST be present.")
234
+ return self
235
+
236
+
237
+ # 10.5 Extension Types Enumeration
238
+ class ExtensionType(Enum):
239
+ new_sdo = "new-sdo"
240
+ new_sco = "new-sco"
241
+ new_sro = "new-sro"
242
+ property_extension = "property-extension"
243
+ toplevel_property_extension = "toplevel-property-extension"
244
+
245
+
246
+ class Extension(StixCore):
247
+ extension_type: ExtensionType | None = None
248
+
249
+
250
+ class ExtensionsDict(TypedDict, total=False, extra_items=SerializeAsAny[Extension]): ... # noqa: E701
251
+
252
+
253
+ # 3.2 Common Properties
254
+ class StixCommon(StixCore):
255
+ """
256
+ This section defines the common properties that MAY exist on a STIX Objects. While some STIX Objects use all
257
+ of these common properties, not all object types do.
258
+ Each type of STIX Object defines which common properties are required, which are optional,
259
+ and which are not in use.
260
+ """
261
+
262
+ # All STIX Objects and the STIX Bundle Object have an id property that uniquely identifies each instance
263
+ # of the object.
264
+ # This id MUST meet the requirements of the identifier type (see section 2.9).
265
+ # For objects that support versioning, all objects with the same id are considered different versions
266
+ # of the same object and the version of the object is identified by its modified property.
267
+ id: Identifier
268
+ # The type property identifies the type of STIX Object.
269
+ type: str
270
+ # The value of this property MUST be 2.1 for STIX Objects defined according to this specification.
271
+ spec_version: Literal["2.1"] = "2.1"
272
+ # The revoked property is only used by STIX Objects that support versioning and indicates whether the object
273
+ # has been revoked.
274
+ revoked: bool | None = None
275
+ # The labels property specifies a set of terms used to describe this object.
276
+ # Where an object has a specific property defined in the specification for characterizing
277
+ # subtypes of that object, the labels property MUST NOT be used for that purpose.
278
+ labels: list[str] | None = None
279
+ # The confidence property identifies the confidence that the creator has in the correctness of their data.
280
+ # The confidence value MUST be a number in the range of 0-100.
281
+ confidence: Annotated[int, Ge(0), Le(100)] | None = None
282
+ # The lang property identifies the language of the text content in this object.
283
+ # When present, it MUST be a language code conformant to [RFC5646].
284
+ # RFC5646 does not enforce ISO 639-1 alpha-2 or ISO 639-3 alpha-3 formats.
285
+ lang: str | None = None
286
+ # The external_references property specifies a list of external references which refers to non-STIX information.
287
+ external_references: list[ExternalReference] | None = None
288
+ # The object_marking_refs property specifies a list of id properties of marking-definition objects that apply
289
+ # to this object.
290
+ object_marking_refs: list[Identifier] | None = None
291
+ # The granular_markings property specifies a list of granular markings applied to this object.
292
+ granular_markings: list[GranularMarking] | None = None
293
+ # Specifies any extensions of the object, as a dictionary.
294
+ # Dictionary keys SHOULD be the id of a STIX Extension object or the name of a predefined object extension found
295
+ # in this specification, depending on the type of extension being used.
296
+ # The corresponding dictionary values MUST contain the contents of the extension instance.
297
+ # Each extension dictionary MAY contain the property extension_type.
298
+ # The value of this property MUST come from the extension-type-enum enumeration.
299
+ # If the extension_type property is not present, then this is a predefined extension which
300
+ # does not use the extension facility described in section 7.3.
301
+ # When this extension facility is used the extension_type property MUST be present.
302
+ extensions: ExtensionsDict | dict[str, Extension] | None = None
303
+
304
+ @classmethod
305
+ def register_new_extension(
306
+ cls,
307
+ definition: "StixCommon", # pyright: ignore[reportUnusedParameter]
308
+ extension: Type[Extension], # pyright: ignore[reportUnusedParameter, reportDeprecated]
309
+ ):
310
+ """
311
+ Dynamically update the extensions property so that new classes get deserialized based on the extension key.
312
+ """
313
+ annotations = get_args(cls.model_fields["extensions"].annotation)[0] # pyright: ignore[reportAny, reportUnusedVariable]
314
+ CustomExtensionsDict = TypedDict(
315
+ "CustomExtensionsDict",
316
+ {
317
+ definition.id: extension, # pyright: ignore[reportGeneralTypeIssues]
318
+ **annotations.__annotations__, # pyright: ignore[reportGeneralTypeIssues]
319
+ },
320
+ total=False,
321
+ extra_items=SerializeAsAny[Extension],
322
+ )
323
+ cls.model_fields["extensions"].annotation = CustomExtensionsDict | None # pyright: ignore[reportAttributeAccessIssue]
324
+ cls.model_rebuild(force=True) # pyright: ignore[reportUnusedCallResult]
325
+
326
+
327
+ class StixDomain(StixCommon):
328
+ # The created_by_ref property specifies the id property of the identity object that describes the entity that
329
+ # created this object.
330
+ created_by_ref: Identifier | None = None
331
+ # The created property represents the time at which the object was originally created.
332
+ # The created property MUST NOT be changed when creating a new version of the object.
333
+ created: datetime
334
+ # The modified property is only used by STIX Objects that support versioning and represents
335
+ # the time that this particular version of the object was last modified.
336
+ # Object creators MUST set the modified property when creating a new
337
+ # version of an object if the created property was set.
338
+ modified: datetime
339
+
340
+ @model_validator(mode="after")
341
+ def validate_modified_after_created(self) -> Self:
342
+ """
343
+ If the created property is defined, then the value of the modified property for a given object version
344
+ MUST be later than or equal to the value of the created property.
345
+ """
346
+ if self.created > self.modified:
347
+ raise ValueError(
348
+ "The modified property MUST be later than or equal to the value of the created property."
349
+ )
350
+ return self
351
+
352
+
353
+ class StixObservable(StixCommon):
354
+ # This property defines whether or not the data contained within the object has been defanged.
355
+ defanged: bool | None = None
356
+
357
+
358
+ class StixRelationship(StixCommon):
359
+ relationship_type: str
360
+ # The created property represents the time at which the object was originally created.
361
+ # The created property MUST NOT be changed when creating a new version of the object.
362
+ created: datetime
363
+ # The modified property is only used by STIX Objects that support versioning and represents
364
+ # the time that this particular version of the object was last modified.
365
+ # Object creators MUST set the modified property when creating a new
366
+ # version of an object if the created property was set.
367
+ modified: datetime
368
+ # The created_by_ref property specifies the id property of the identity object that describes the entity that
369
+ # created this object.
370
+ created_by_ref: Identifier | None = None
371
+
372
+ @model_validator(mode="after")
373
+ def validate_modified_after_created(self) -> Self:
374
+ """
375
+ If the created property is defined, then the value of the modified property for a given object version
376
+ MUST be later than or equal to the value of the created property.
377
+ """
378
+ if self.created > self.modified:
379
+ raise ValueError(
380
+ "The modified property MUST be later than or equal to the value of the created property."
381
+ )
382
+ return self
383
+
384
+
385
+ class StixMeta(StixCommon):
386
+ type: str
387
+ # The created property represents the time at which the object was originally created.
388
+ # The created property MUST NOT be changed when creating a new version of the object.
389
+ created: datetime
390
+
391
+
392
+ class StixLanguage(StixMeta):
393
+ # The created_by_ref property specifies the id property of the identity object that describes the entity that
394
+ # created this object.
395
+ created_by_ref: Identifier | None = None
396
+ # The modified property is only used by STIX Objects that support versioning and represents
397
+ # the time that this particular version of the object was last modified.
398
+ # Object creators MUST set the modified property when creating a new
399
+ # version of an object if the created property was set.
400
+ modified: datetime
401
+
402
+ @model_validator(mode="after")
403
+ def validate_modified_after_created(self) -> Self:
404
+ """
405
+ If the created property is defined, then the value of the modified property for a given object version
406
+ MUST be later than or equal to the value of the created property.
407
+ """
408
+ if self.created > self.modified:
409
+ raise ValueError(
410
+ "The modified property MUST be later than or equal to the value of the created property."
411
+ )
412
+ return self
413
+
414
+
415
+ class StixMarking(StixMeta):
416
+ # The created_by_ref property specifies the id property of the identity object that describes the entity that
417
+ # created this object.
418
+ created_by_ref: Identifier | None = None
419
+
420
+
421
+ class StixExtension(StixMeta):
422
+ # The created_by_ref property specifies the id property of the identity object that describes the entity that
423
+ # created this object.
424
+ created_by_ref: Identifier
425
+ # The modified property is only used by STIX Objects that support versioning and represents
426
+ # the time that this particular version of the object was last modified.
427
+ # Object creators MUST set the modified property when creating a new
428
+ # version of an object if the created property was set.
429
+ modified: datetime
430
+
431
+ @model_validator(mode="after")
432
+ def validate_modified_after_created(self) -> Self:
433
+ """
434
+ If the created property is defined, then the value of the modified property for a given object version
435
+ MUST be later than or equal to the value of the created property.
436
+ """
437
+ if self.created > self.modified:
438
+ raise ValueError(
439
+ "The modified property MUST be later than or equal to the value of the created property."
440
+ )
441
+ return self
@@ -0,0 +1,20 @@
1
+ from pydantic import ValidationInfo
2
+
3
+
4
+ def validate_identifier(value: str) -> str:
5
+ _type, _uuid = value.split("--")
6
+ if not _type and not _uuid:
7
+ raise ValueError("Invalid identifier format.")
8
+ return value
9
+
10
+
11
+ def validate_bin_field(value: str, info: ValidationInfo) -> str:
12
+ if info.field_name and not info.field_name.endswith("_bin"):
13
+ raise ValueError("The property name MUST end with '_bin'.")
14
+ return value
15
+
16
+
17
+ def validate_hex_field(value: str, info: ValidationInfo) -> str:
18
+ if info.field_name and not info.field_name.endswith("_hex"):
19
+ raise ValueError("The property name MUST end with '_hex'.")
20
+ return value