stidantic 0.1.0__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/__init__.py ADDED
@@ -0,0 +1,23 @@
1
+ """stidantic - A Pydantic-based STIX 2.1 library."""
2
+
3
+ __version__ = "0.1.0"
4
+ __author__ = "nicocti"
5
+ __all__ = [
6
+ "StixBundle",
7
+ "StixCore",
8
+ "StixCommon",
9
+ "StixDomain",
10
+ "StixRelationship",
11
+ "StixMeta",
12
+ "Identifier",
13
+ ]
14
+
15
+ from stidantic.bundle import StixBundle
16
+ from stidantic.types import (
17
+ Identifier,
18
+ StixCommon,
19
+ StixCore,
20
+ StixDomain,
21
+ StixMeta,
22
+ StixRelationship,
23
+ )
stidantic/__init__.pyi ADDED
@@ -0,0 +1,14 @@
1
+ """Type stubs for stidantic."""
2
+
3
+ from stidantic.bundle import StixBundle as StixBundle
4
+ from stidantic.types import (
5
+ Identifier as Identifier,
6
+ StixCommon as StixCommon,
7
+ StixCore as StixCore,
8
+ StixDomain as StixDomain,
9
+ StixMeta as StixMeta,
10
+ StixRelationship as StixRelationship,
11
+ )
12
+
13
+ __version__: str
14
+ __author__: str
stidantic/bundle.py ADDED
@@ -0,0 +1,29 @@
1
+ from typing import Annotated
2
+ from pydantic import Field
3
+ from stidantic.types import StixCore, Identifier, StixCommon
4
+ from stidantic.sdo import SDOs
5
+ from stidantic.sco import SCOs
6
+ from stidantic.sro import SROs
7
+ from stidantic.language import LanguageContent
8
+ from stidantic.marking import MarkingDefinition
9
+ from stidantic.extension import ExtensionDefinition
10
+
11
+
12
+ # 8. Stix Bundle
13
+ class StixBundle(StixCore):
14
+ id: Identifier
15
+ type: str = "bundle"
16
+ objects: list[
17
+ Annotated[
18
+ (
19
+ SROs
20
+ | SDOs
21
+ | SCOs
22
+ | MarkingDefinition
23
+ | LanguageContent
24
+ | ExtensionDefinition
25
+ ),
26
+ Field(discriminator="type"),
27
+ ]
28
+ | StixCommon
29
+ ]
stidantic/extension.py ADDED
@@ -0,0 +1,115 @@
1
+ from typing import Literal, Annotated, Self
2
+ from pydantic import Field
3
+ from pydantic.functional_validators import model_validator
4
+ from stidantic.types import StixExtension, ExtensionType, StixProp
5
+
6
+
7
+ # 7.3 Extension Definition
8
+ class ExtensionDefinition(StixExtension):
9
+ """
10
+ The STIX Extension Definition object allows producers of threat intelligence to extend existing STIX objects or
11
+ to create entirely new STIX objects in a standardized way. This object contains detailed information about the
12
+ extension and any additional properties and or objects that it defines. This extension mechanism MUST NOT be used
13
+ to redefine existing standardized objects or properties.
14
+
15
+ If a producer does not include the STIX Extension Definition object with the STIX objects that use it,
16
+ consumers should refer to section 3.3 for information in resolving references.
17
+
18
+ There are three ways to extend STIX using STIX Extensions.
19
+ - Define one or more new STIX Object types.
20
+ - Define additional properties for an existing STIX Object type as a nested property extension. This is
21
+ typically done to represent a sub-component or module of one or more STIX Object types.
22
+ - Define additional properties for an existing STIX Object type at the object's top-level. This can be done to
23
+ represent properties that form an inherent part of the definition of an object type.
24
+
25
+ When defining a new STIX Object (e.g., SDO, SCO, or SRO) all common properties associated with that type of
26
+ object (SDO, SCO, SRO) MUST be included in the schema or definition of that new STIX Object type.
27
+ Extensions that create new STIX objects MUST follow all conformance requirements for that object type
28
+ (SDO, SCO, SRO) including all of the requirements for the common properties associated with that object type.
29
+
30
+ When defining a STIX extension using the nested property extension mechanism the extensions property MUST include
31
+ the extension definition's UUID that defines the extension definition object and the extension_type property
32
+ as defined in section 3.2.
33
+
34
+ IMPORTANT NOTE: Producers using top-level property extensions should be mindful that another producer could also
35
+ define a top-level property extension using the same property names but for different purposes causing name
36
+ conflicts when both extensions are used in the same environment. This standard does not define any name conflict
37
+ resolution for new STIX Objects or for top-level properties created by this extension mechanism. However,
38
+ producers SHOULD follow industry best practices such as using unique property names that are guaranteed to avoid
39
+ duplicates across all organizations to avoid naming conflicts.
40
+ IMPORTANT NOTE: Producers using STIX extensions should be mindful that future versions of the STIX specification
41
+ MAY define objects and or properties that conflict with existing non-standardized extensions. In these cases the
42
+ meaning as defined in the STIX specification will override any and all conflicting extensions.
43
+
44
+ Specific extensions, as with specific Custom Properties, MAY NOT be supported across implementations.
45
+ A consumer that receives STIX content containing a STIX extension that it does not understand MAY refuse to
46
+ process the content or MAY ignore that extension and continue processing the content.
47
+
48
+ The 3 uses of this extension facility MAY be combined into a single Extension Definition object when appropriate.
49
+
50
+ The following example highlights where this may be useful.
51
+
52
+ Hybrid Extension Example
53
+ An intelligence producer has a monitoring network of sensors that collect a variety of cybersecurity telemetry
54
+ from each sensor where those sensors have unique data not currently defined in STIX 2.1.
55
+ The producer wishes to create an extension that other downstream consumers can receive both the high-level
56
+ summarization object but also the individual categorized telemetry from each sensor.
57
+ a) A new SDO representing the statistical summarization object.
58
+ b) A list of new properties to be added to the standard Observed Data object representing additional meta-data
59
+ information associated with the telemetry.
60
+ c) A new SCO representing a new cyber observable data type.
61
+
62
+ In this case, the producer creates a single extension that contains the following extension types:
63
+ "extension_types": [ "new-sdo", "new-sco", "property-extension" ]
64
+
65
+ Therefore, producers MAY use the hybrid extension mechanism when they wish to define a single extension that
66
+ encompasses new SDO and/or sub-component or top-level property extension properties in a related extension.
67
+
68
+ Producers SHOULD NOT use the hybrid extension mechanism if the extensions are not related to each other.
69
+ If the extensions are independent features then a producer SHOULD consider creating separate extension definitions.
70
+ """
71
+
72
+ type: Literal["extension-definition"] = "extension-definition" # pyright: ignore[reportIncompatibleVariableOverride]
73
+ # A name used for display purposes during execution, development, or debugging.
74
+ name: str
75
+ # A detailed explanation of what data the extension conveys and how it is intended to be used.
76
+ # While the description property is optional this property SHOULD be populated.
77
+ # Note that the schema property is the normative definition of the extension, and this property, if present,
78
+ # is for documentation purposes only.
79
+ description: str | None = None
80
+ # The normative definition of the extension, either as a URL or as plain text explaining the definition.
81
+ # A URL SHOULD point to a JSON schema or a location that contains information about the schema
82
+ json_schema: Annotated[str, Field(alias="schema")]
83
+ # The version of this extension. Producers of STIX extensions are encouraged to follow standard semantic
84
+ # versioning procedures where the version number follows the pattern, MAJOR.MINOR.PATCH. This will allow
85
+ # consumers to distinguish between the three different levels of compatibility typically identified by
86
+ # such versioning strings.
87
+ version: str
88
+ # This property specifies one or more extension types contained within this extension.
89
+ # When this property includes toplevel-property-extension then the extension_properties property
90
+ # SHOULD include one or more property names.
91
+ extension_types: list[ExtensionType]
92
+ # This property contains the list of new property names that are added to an object by an extension.
93
+ # This property MUST only be used when the extension_types property includes a value of toplevel-property-extension.
94
+ # In other words, when new properties are being added at the top-level of an existing object.
95
+ # The property names used in Extension STIX Object MUST be in ASCII and MUST only contain the characters a–z
96
+ # (lowercase ASCII), 0–9, and underscore (_).
97
+ # The name of a property of a Extension STIX Object MUST have a minimum length of 3 ASCII characters.
98
+ # The name of a property of a Extension STIX Object MUST be no longer than 250 ASCII characters in length.
99
+ extension_properties: list[StixProp] | None = None
100
+
101
+ @model_validator(mode="after")
102
+ def validate_extension_properties(self) -> Self:
103
+ """
104
+ extension_properties MUST only be used when the extension_types property includes a value of
105
+ toplevel-property-extension. In other words, when new properties are being added at the
106
+ top-level of an existing object.
107
+ """
108
+ if (
109
+ self.extension_properties
110
+ and ExtensionType.toplevel_property_extension not in self.extension_types
111
+ ):
112
+ raise ValueError(
113
+ "extension_types property can't be used without toplevel-property-extension in extension_types."
114
+ )
115
+ return self
stidantic/language.py ADDED
@@ -0,0 +1,35 @@
1
+ from datetime import datetime
2
+ from typing import Literal
3
+ from stidantic.types import Identifier, StixLanguage
4
+
5
+
6
+ # 7.1 Language Content
7
+ class LanguageContent(StixLanguage):
8
+ type: Literal["language-content"] = "language-content" # pyright: ignore[reportIncompatibleVariableOverride]
9
+ # The object_ref property identifies the id of the object that this Language Content applies to.
10
+ # It MUST be the identifier for a STIX Object.
11
+ object_ref: Identifier
12
+ # The object_modified property identifies the modified time of the object that this Language Content applies to.
13
+ # It MUST be an exact match for the modified time of the STIX Object being referenced.
14
+ object_modified: datetime | None = None
15
+ # The contents property contains the actual Language Content (translation).
16
+ # The keys in the dictionary MUST be RFC 5646 language codes for which language content is being provided [RFC5646].
17
+ # The values each consist of a dictionary that mirrors the properties in the target object
18
+ # (identified by object_ref and object_modified). For example, to provide a translation of the name property
19
+ # on the target object the key in the dictionary would be name.
20
+ # For each key in the nested dictionary:
21
+ # ● If the original property is a string, the corresponding property in the language content object
22
+ # MUST contain a string with the content for that property in the language of the top-level key.
23
+ # ● If the original property is a list, the corresponding property in the translation object must also be
24
+ # a list. Each item in this list recursively maps to the item at the same position in the list contained in the
25
+ # target object. The lists MUST have the same length.
26
+ # ● In the event that translations are only provided for some list items, the untranslated list items MUST
27
+ # be represented by an empty string (""). This indicates to a consumer of the Language Content object that they
28
+ # should interpolate the translated list items in the Language Content object with the corresponding (untranslated)
29
+ # list items from the original object as indicated by the object_ref property.
30
+ # ● If the original property is an object (including dictionaries), the corresponding location in the
31
+ # translation object must also be an object. Each key/value field in this object recursively maps to the object
32
+ # with the same key in the original.
33
+ # The translation object MAY contain only a subset of the translatable fields of the original. Keys that point to
34
+ # non-translatable properties in the target or to properties that do not exist in the target object MUST be ignored.
35
+ contents: dict[str, dict[str, str]]
stidantic/marking.py ADDED
@@ -0,0 +1,118 @@
1
+ from typing import Literal, Self
2
+ from enum import Enum
3
+ from datetime import datetime, timezone
4
+
5
+ from pydantic.functional_validators import model_validator
6
+ from stidantic.types import (
7
+ StixCore,
8
+ StixMarking,
9
+ Identifier,
10
+ )
11
+
12
+
13
+ # 7.2.1.3 Statement Marking
14
+ class StatementMarking(StixCore):
15
+ """
16
+ The Statement marking type defines the representation of a textual marking statement (e.g., copyright, terms of use,
17
+ etc.) in a definition. The value of the definition_type property MUST be statement when using this marking type.
18
+ Statement markings are generally not machine-readable, and this specification does not define any behavior or
19
+ actions based on their values.
20
+
21
+ Content may be marked with multiple statements of use. In other words, the same content can be marked both with a
22
+ statement saying "Copyright 2019" and a statement saying, "Terms of use are ..." and both statements apply.
23
+ """
24
+
25
+ # A Statement (e.g., copyright, terms of use) applied to the content marked by this marking definition.
26
+ statement: str
27
+
28
+
29
+ # 7.2.1.4 TLP Marking
30
+ class TLPMarking(StixCore):
31
+ """
32
+ The TLP marking type defines how you would represent a Traffic Light Protocol (TLP) marking in a definition
33
+ property. The value of the definition_type property MUST be tlp when using this marking type.
34
+ """
35
+
36
+ # The TLP level [TLP] of the content marked by this marking definition, as defined in this section.
37
+ tlp: str
38
+
39
+
40
+ # 7.2.1 Marking Definition
41
+ class MarkingDefinition(StixMarking):
42
+ """
43
+ The marking-definition object represents a specific marking. Data markings typically represent handling or
44
+ sharing requirements for data and are applied in the object_marking_refs and granular_markings properties on
45
+ STIX Objects, which reference a list of IDs for marking-definition objects.
46
+
47
+ Two marking definition types are defined in this specification: TLP, to capture TLP markings, and Statement,
48
+ to capture text marking statements. In addition, it is expected that the FIRST Information Exchange Policy (IEP)
49
+ will be included in a future version once a machine-usable specification for it has been defined.
50
+
51
+ Unlike other STIX Objects, Marking Definition objects cannot be versioned because it would allow for indirect
52
+ changes to the markings on a STIX Object. For example, if a Statement marking is changed from "Reuse Allowed" to
53
+ "Reuse Prohibited", all STIX Objects marked with that Statement marking would effectively have an updated marking
54
+ without being updated themselves. Instead, a new Statement marking with the new text should be created and the
55
+ marked objects updated to point to the new marking.
56
+ """
57
+
58
+ type: Literal["marking-definition"] = "marking-definition" # pyright: ignore[reportIncompatibleVariableOverride]
59
+ # A name used to identify the Marking Definition.
60
+ name: str | None = None
61
+ # The definition_type property identifies the type of Marking Definition. The value of the definition_type property
62
+ # SHOULD be one of the types defined in the subsections below: statement or tlp (see sections 7.2.1.3 and 7.2.1.4).
63
+ # Any new marking definitions SHOULD be specified using the extension facility described in section 7.3.
64
+ # If the extensions property is not present, this property MUST be present.
65
+ definition_type: str | None = None
66
+ # The definition property contains the marking object itself (e.g., the TLP marking as defined in section 7.2.1.4,
67
+ # the Statement marking as defined in section 7.2.1.3).
68
+ # Any new marking definitions SHOULD be specified using the extension facility described in section 7.3.
69
+ # If the extensions property is not present, this property MUST be present.
70
+ definition: TLPMarking | StatementMarking | None = None
71
+
72
+ @model_validator(mode="after")
73
+ def definition_if_no_extensions(self) -> Self:
74
+ if not self.extensions and (not self.definition and not self.definition_type):
75
+ raise ValueError(
76
+ "If the extensions property is not present, definition_type and definition properties MUST be present."
77
+ )
78
+ return self
79
+
80
+
81
+ STIX_ZERO_DATE = datetime(2017, 1, 20, 00, 00, 00, 00, tzinfo=timezone.utc)
82
+
83
+
84
+ class TLP(Enum):
85
+ """
86
+ The following standard marking definitions MUST be used to reference or represent TLP markings.
87
+ Other instances of tlp-marking MUST NOT be used or created (the only instances of TLP marking definitions
88
+ permitted are those defined here).
89
+ """
90
+
91
+ white = MarkingDefinition(
92
+ id=Identifier("marking-definition--613f2e26-407d-48c7-9eca-b8e91df99dc9"),
93
+ definition_type="tlp",
94
+ definition=TLPMarking(tlp="white"),
95
+ name="TLP:WHITE",
96
+ created=STIX_ZERO_DATE,
97
+ )
98
+ green = MarkingDefinition(
99
+ id=Identifier("marking-definition--34098fce-860f-48ae-8e50-ebd3cc5e41da"),
100
+ definition_type="tlp",
101
+ definition=TLPMarking(tlp="green"),
102
+ name="TLP:GREEN",
103
+ created=STIX_ZERO_DATE,
104
+ )
105
+ amber = MarkingDefinition(
106
+ id=Identifier("marking-definition--f88d31f6-486f-44da-b317-01333bde0b82"),
107
+ definition_type="tlp",
108
+ definition=TLPMarking(tlp="amber"),
109
+ name="TLP:AMBER",
110
+ created=STIX_ZERO_DATE,
111
+ )
112
+ red = MarkingDefinition(
113
+ id=Identifier("marking-definition--5e57c739-391a-4eb3-b6be-7d15ca92d5ed"),
114
+ definition_type="tlp",
115
+ definition=TLPMarking(tlp="red"),
116
+ name="TLP:RED",
117
+ created=STIX_ZERO_DATE,
118
+ )
stidantic/sco.py ADDED
@@ -0,0 +1,91 @@
1
+ from typing import Literal, Self
2
+ from typing_extensions import Annotated
3
+ from pydantic.functional_validators import model_validator
4
+ from pydantic import Field
5
+ from stidantic.types import StixObservable, StixBinary, StixUrl, Hashes
6
+ from stidantic.vocab import EncryptionAlgorithm
7
+
8
+
9
+ # 6.1 Artifact Object
10
+ class Artifact(StixObservable):
11
+ """
12
+ The Artifact Object permits capturing an array of bytes (8-bits),
13
+ as a base64-encoded string string, or linking to a file-like payload.
14
+ """
15
+
16
+ type: Literal["artifact"] = "artifact" # pyright: ignore[reportIncompatibleVariableOverride]
17
+ mime_type: str | None = None
18
+ payload_bin: StixBinary | None = None
19
+ url: StixUrl | None = None
20
+ hashes: Hashes | None = None
21
+ encryption_algorithm: EncryptionAlgorithm | None = None
22
+ decryption_key: str | None = None
23
+
24
+ @model_validator(mode="after")
25
+ def one_of(self) -> Self:
26
+ """
27
+ One of payload_bin or url MUST be provided.
28
+ """
29
+ if self.payload_bin or self.hashes:
30
+ return self
31
+ raise ValueError("Missing at least hashes or payload_bin property.")
32
+
33
+ @model_validator(mode="after")
34
+ def url_must_not_be_present_if_payload_bin_provided(self) -> Self:
35
+ """
36
+ URL property MUST NOT be present if payload_bin is provided.
37
+ """
38
+ if self.payload_bin and self.url:
39
+ raise ValueError(
40
+ "url property MUST NOT be present if payload_bin is provided."
41
+ )
42
+ return self
43
+
44
+ @model_validator(mode="after")
45
+ def hashes_must_be_present_if_url_provided(self) -> Self:
46
+ """
47
+ Hashes property MUST be present when the url property is present.
48
+ """
49
+ if self.url and not self.hashes:
50
+ raise ValueError("hashes MUST be present if url is provided.")
51
+ return self
52
+
53
+ @model_validator(mode="after")
54
+ def decryption_key_must_not_be_present_if_encryption_algorithm_absent(self) -> Self:
55
+ """
56
+ decryption_key property MUST NOT be present when the encryption_algorithm property is absent.
57
+ """
58
+ if not self.encryption_algorithm and self.decryption_key:
59
+ raise ValueError(
60
+ "decryption_key MUST NOT be present when the encryption_algorithm property is absent."
61
+ )
62
+ return self
63
+
64
+ class Config:
65
+ json_schema_extra: dict[str, list[str]] = {
66
+ "id_contributing_properties": ["hashes", "payload_bin"]
67
+ }
68
+
69
+
70
+ # 6.2 Autonomous System
71
+ class AutonomousSystem(StixObservable):
72
+ """
73
+ The AS object represents the properties of an Autonomous Systems (AS).
74
+ """
75
+
76
+ type: Literal["autonomous-system"] = "autonomous-system" # pyright: ignore[reportIncompatibleVariableOverride]
77
+ # Specifies the number assigned to the AS. Such assignments a
78
+ # re typically performed by a Regional Internet Registry (RIR).
79
+ number: int
80
+ # Specifies the name of the AS.
81
+ name: str | None = None
82
+ # Specifies the name of the Regional Internet Registry (RIR) that assigned the number to the AS.
83
+ rir: str | None = None
84
+
85
+ class Config:
86
+ json_schema_extra: dict[str, list[str]] = {
87
+ "id_contributing_properties": ["number"]
88
+ }
89
+
90
+
91
+ SCOs = Annotated[(Artifact | AutonomousSystem), Field(discriminator="type")]
stidantic/sdo.py ADDED
@@ -0,0 +1,87 @@
1
+ from typing import Literal, Annotated, Self
2
+ from datetime import datetime
3
+ from pydantic import Field
4
+ from pydantic.functional_validators import model_validator
5
+ from stidantic.types import StixDomain, KillChainPhase
6
+
7
+
8
+ # 4.1 Attack Pattern
9
+ class AttackPattern(StixDomain):
10
+ """
11
+ Attack Patterns are a type of TTP that describe ways that adversaries attempt to compromise targets.
12
+
13
+ Attack Patterns are used to help categorize attacks, generalize specific attacks to the patterns that they follow,
14
+ and provide detailed information about how attacks are performed. An example of an attack pattern is
15
+ "spear phishing": a common type of attack where an attacker sends a carefully crafted e-mail message
16
+ to a party with the intent of getting them to click a link or open an attachment to deliver malware.
17
+
18
+ Attack Patterns can also be more specific; spear phishing as practiced by a particular threat actor
19
+ (e.g., they might generally say that the target won a contest) can also be an Attack Pattern.
20
+ """
21
+
22
+ type: Literal["attack-pattern"] = "attack-pattern" # pyright: ignore[reportIncompatibleVariableOverride]
23
+ # The name used to identify the Attack Pattern.
24
+ name: str
25
+ # A description that provides more details and context about the Attack Pattern,
26
+ # potentially including its purpose and its key characteristics.
27
+ description: str | None = None
28
+ # Alternative names used to identify this Attack Pattern.
29
+ aliases: list[str] | None = None
30
+ # The list of kill chain phases for which this attack pattern is used.
31
+ kill_chain_phases: list[KillChainPhase] | None = None
32
+
33
+
34
+ # 4.2 Campaign
35
+ class Campaign(StixDomain):
36
+ """
37
+ A Campaign is a grouping of adversarial behaviors that describes a set of malicious activities or attacks
38
+ (sometimes called waves) that occur over a period of time against a specific set of targets.
39
+ Campaigns usually have well defined objectives and may be part of an Intrusion Set.
40
+
41
+ Campaigns are often attributed to an intrusion set and threat actors. The threat actors may reuse known
42
+ infrastructure from the intrusion set or may set up new infrastructure specific for conducting that campaign.
43
+
44
+ Campaigns can be characterized by their objectives and the incidents they cause, people or resources they target,
45
+ and the resources (infrastructure, intelligence, Malware, Tools, etc.) they use.
46
+
47
+ For example, a Campaign could be used to describe a crime syndicate's attack using a specific variant of
48
+ malware and new C2 servers against the executives of ACME Bank during the summer of 2016 in order
49
+ to gain secret information about an upcoming merger with another bank.
50
+ """
51
+
52
+ type: Literal["campaign"] = "campaign" # pyright: ignore[reportIncompatibleVariableOverride]
53
+ # A name used to identify the Campaign.
54
+ name: str
55
+ # A description that provides more details and context about the Campaign,
56
+ # potentially including its purpose and its key characteristics.
57
+ description: str | None = None
58
+ # Alternative names used to identify this Campaign.
59
+ aliases: list[str] | None = None
60
+ # The time that this Campaign was first seen.
61
+ # A summary property of data from sightings and other data that may or may not be available in STIX.
62
+ # If new sightings are received that are earlier than the first seen timestamp,
63
+ # the object may be updated to account for the new data.
64
+ first_seen: datetime | None = None
65
+ # The time that this Campaign was last seen.
66
+ # A summary property of data from sightings and other data that may or may not be available in STIX.
67
+ # If new sightings are received that are later than the last seen timestamp,
68
+ # the object may be updated to account for the new data.
69
+ last_seen: datetime | None = None
70
+ # The Campaign’s primary goal, objective, desired outcome, or intended effect
71
+ # — what the Threat Actor or Intrusion Set hopes to accomplish with this Campaign.
72
+ objective: str | None = None
73
+
74
+ @model_validator(mode="after")
75
+ def validate_first_last_interval(self) -> Self:
76
+ """
77
+ If this property and the first_seen property are both defined, then this property
78
+ MUST be greater than or equal to the timestamp in the first_seen property.
79
+ """
80
+ if self.first_seen and self.last_seen and self.first_seen > self.last_seen:
81
+ raise ValueError(
82
+ "the last_seen property MUST be greater than or equal to the timestamp in the first_seen property"
83
+ )
84
+ return self
85
+
86
+
87
+ SDOs = Annotated[(AttackPattern | Campaign), Field(discriminator="type")]