Skip to content
This repository was archived by the owner on Dec 5, 2025. It is now read-only.

Commit 4e7ff54

Browse files
[client] Add support of vocabulary (#300)
Co-authored-by: Julien Richard <[email protected]>
1 parent 0046fed commit 4e7ff54

File tree

5 files changed

+192
-5
lines changed

5 files changed

+192
-5
lines changed

pycti/api/opencti_api_client.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
from pycti.entities.opencti_stix_sighting_relationship import StixSightingRelationship
5353
from pycti.entities.opencti_threat_actor import ThreatActor
5454
from pycti.entities.opencti_tool import Tool
55+
from pycti.entities.opencti_vocabulary import Vocabulary
5556
from pycti.entities.opencti_vulnerability import Vulnerability
5657
from pycti.utils.opencti_stix2 import OpenCTIStix2
5758
from pycti.utils.opencti_stix2_utils import OpenCTIStix2Utils
@@ -150,6 +151,7 @@ def __init__(
150151
self.stix2 = OpenCTIStix2(self)
151152

152153
# Define the entities
154+
self.vocabulary = Vocabulary(self)
153155
self.label = Label(self)
154156
self.marking_definition = MarkingDefinition(self)
155157
self.external_reference = ExternalReference(self, File)
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
import json
2+
3+
4+
class Vocabulary:
5+
def __init__(self, opencti):
6+
self.opencti = opencti
7+
self.properties = """
8+
id
9+
name
10+
category {
11+
key
12+
fields {
13+
key
14+
}
15+
}
16+
"""
17+
18+
def list(self, **kwargs):
19+
filters = kwargs.get("filters", None)
20+
self.opencti.log(
21+
"info", "Listing Vocabularies with filters " + json.dumps(filters) + "."
22+
)
23+
query = (
24+
"""
25+
query Vocabularies($filters: [VocabularyFiltering!]) {
26+
vocabularies(filters: $filters) {
27+
edges {
28+
node {
29+
"""
30+
+ self.properties
31+
+ """
32+
}
33+
}
34+
}
35+
}
36+
"""
37+
)
38+
result = self.opencti.query(
39+
query,
40+
{
41+
"filters": filters,
42+
},
43+
)
44+
return self.opencti.process_multiple(result["data"]["vocabularies"])
45+
46+
def read(self, **kwargs):
47+
id = kwargs.get("id", None)
48+
filters = kwargs.get("filters", None)
49+
if id is not None:
50+
self.opencti.log("info", "Reading vocabulary {" + id + "}.")
51+
query = (
52+
"""
53+
query Vocabulary($id: String!) {
54+
vocabulary(id: $id) {
55+
"""
56+
+ self.properties
57+
+ """
58+
}
59+
}
60+
"""
61+
)
62+
result = self.opencti.query(query, {"id": id})
63+
return self.opencti.process_multiple_fields(result["data"]["vocabulary"])
64+
elif filters is not None:
65+
result = self.list(filters=filters)
66+
if len(result) > 0:
67+
return result[0]
68+
else:
69+
return None
70+
else:
71+
self.opencti.log(
72+
"error", "[opencti_vocabulary] Missing parameters: id or filters"
73+
)
74+
return None
75+
76+
def handle_vocab(self, vocab, cache, field):
77+
if "vocab_" + vocab in cache:
78+
vocab_data = cache["vocab_" + vocab]
79+
else:
80+
vocab_data = self.read_or_create_unchecked(
81+
name=vocab,
82+
required=field["required"],
83+
category=cache["category_" + field["key"]],
84+
)
85+
if vocab_data is not None:
86+
cache["vocab_" + vocab] = vocab_data
87+
return vocab_data
88+
89+
def create(self, **kwargs):
90+
name = kwargs.get("name", None)
91+
category = kwargs.get("category", None)
92+
93+
if name is not None and category is not None:
94+
self.opencti.log(
95+
"info", "Creating or Getting aliased Vocabulary {" + name + "}."
96+
)
97+
query = (
98+
"""
99+
mutation VocabularyAdd($input: VocabularyAddInput!) {
100+
vocabularyAdd(input: $input) {
101+
"""
102+
+ self.properties
103+
+ """
104+
}
105+
}
106+
"""
107+
)
108+
result = self.opencti.query(
109+
query,
110+
{
111+
"input": {
112+
"name": name,
113+
"category": category,
114+
}
115+
},
116+
)
117+
return result["data"]["vocabularyAdd"]
118+
else:
119+
self.opencti.log(
120+
"error",
121+
"[opencti_vocabulary] Missing parameters: name or category",
122+
)
123+
124+
def read_or_create_unchecked(self, **kwargs):
125+
value = kwargs.get("name", None)
126+
vocab = self.read(filters=[{"key": "name", "values": [value]}])
127+
if vocab is None:
128+
try:
129+
return self.create(**kwargs)
130+
except ValueError:
131+
return None
132+
return vocab

pycti/utils/opencti_stix2.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -295,6 +295,51 @@ def extract_embedded_relationships(
295295
if "object_marking_refs" in stix_object
296296
else []
297297
)
298+
299+
# Open vocabularies
300+
object_open_vocabularies = {}
301+
if self.mapping_cache.get("vocabularies_definition_fields") is None:
302+
self.mapping_cache["vocabularies_definition_fields"] = []
303+
query = """
304+
query getVocabCategories {
305+
vocabularyCategories {
306+
key
307+
fields{
308+
key
309+
required
310+
}
311+
}
312+
}
313+
"""
314+
result = self.opencti.query(query)
315+
for category in result["data"]["vocabularyCategories"]:
316+
for field in category["fields"]:
317+
self.mapping_cache["vocabularies_definition_fields"].append(field)
318+
self.mapping_cache["category_" + field["key"]] = category["key"]
319+
if any(
320+
field["key"] in stix_object
321+
for field in self.mapping_cache["vocabularies_definition_fields"]
322+
):
323+
for f in self.mapping_cache["vocabularies_definition_fields"]:
324+
if stix_object.get(f["key"]) is None:
325+
continue
326+
if isinstance(stix_object.get(f["key"]), list):
327+
object_open_vocabularies[f["key"]] = []
328+
for vocab in stix_object[f["key"]]:
329+
object_open_vocabularies[f["key"]].append(
330+
self.opencti.vocabulary.handle_vocab(
331+
vocab, self.mapping_cache, field=f
332+
)["name"]
333+
)
334+
else:
335+
object_open_vocabularies[
336+
f["key"]
337+
] = self.opencti.vocabulary.handle_vocab(
338+
stix_object[f["key"]], self.mapping_cache, field=f
339+
)[
340+
"name"
341+
]
342+
298343
# Object Labels
299344
object_label_ids = []
300345
if (
@@ -569,6 +614,7 @@ def extract_embedded_relationships(
569614
"created_by": created_by_id,
570615
"object_marking": object_marking_ids,
571616
"object_label": object_label_ids,
617+
"open_vocabs": object_open_vocabularies,
572618
"kill_chain_phases": kill_chain_phases_ids,
573619
"object_refs": object_refs_ids,
574620
"granted_refs": granted_refs_ids,
@@ -604,6 +650,7 @@ def import_object(
604650
created_by_id = embedded_relationships["created_by"]
605651
object_marking_ids = embedded_relationships["object_marking"]
606652
object_label_ids = embedded_relationships["object_label"]
653+
open_vocabs = embedded_relationships["open_vocabs"]
607654
kill_chain_phases_ids = embedded_relationships["kill_chain_phases"]
608655
object_refs_ids = embedded_relationships["object_refs"]
609656
external_references_ids = embedded_relationships["external_references"]
@@ -614,6 +661,7 @@ def import_object(
614661
"created_by_id": created_by_id,
615662
"object_marking_ids": object_marking_ids,
616663
"object_label_ids": object_label_ids,
664+
"open_vocabs": open_vocabs,
617665
"kill_chain_phases_ids": kill_chain_phases_ids,
618666
"object_ids": object_refs_ids,
619667
"external_references_ids": external_references_ids,
@@ -717,6 +765,7 @@ def import_observable(
717765
created_by_id = embedded_relationships["created_by"]
718766
object_marking_ids = embedded_relationships["object_marking"]
719767
object_label_ids = embedded_relationships["object_label"]
768+
open_vocabs = embedded_relationships["open_vocabs"]
720769
granted_refs_ids = embedded_relationships["granted_refs"]
721770
kill_chain_phases_ids = embedded_relationships["kill_chain_phases"]
722771
object_refs_ids = embedded_relationships["object_refs"]
@@ -728,6 +777,7 @@ def import_observable(
728777
"created_by_id": created_by_id,
729778
"object_marking_ids": object_marking_ids,
730779
"object_label_ids": object_label_ids,
780+
"open_vocabs": open_vocabs,
731781
"granted_refs_ids": granted_refs_ids,
732782
"kill_chain_phases_ids": kill_chain_phases_ids,
733783
"object_ids": object_refs_ids,
@@ -854,6 +904,7 @@ def import_relationship(
854904
created_by_id = embedded_relationships["created_by"]
855905
object_marking_ids = embedded_relationships["object_marking"]
856906
object_label_ids = embedded_relationships["object_label"]
907+
open_vocabs = embedded_relationships["open_vocabs"]
857908
granted_refs_ids = embedded_relationships["granted_refs"]
858909
kill_chain_phases_ids = embedded_relationships["kill_chain_phases"]
859910
object_refs_ids = embedded_relationships["object_refs"]
@@ -865,6 +916,7 @@ def import_relationship(
865916
"created_by_id": created_by_id,
866917
"object_marking_ids": object_marking_ids,
867918
"object_label_ids": object_label_ids,
919+
"open_vocabs": open_vocabs,
868920
"granted_refs_ids": granted_refs_ids,
869921
"kill_chain_phases_ids": kill_chain_phases_ids,
870922
"object_ids": object_refs_ids,
@@ -947,6 +999,7 @@ def import_sighting(
947999
created_by_id = embedded_relationships["created_by"]
9481000
object_marking_ids = embedded_relationships["object_marking"]
9491001
object_label_ids = embedded_relationships["object_label"]
1002+
open_vocabs = embedded_relationships["open_vocabs"]
9501003
granted_refs_ids = embedded_relationships["granted_refs"]
9511004
kill_chain_phases_ids = embedded_relationships["kill_chain_phases"]
9521005
object_refs_ids = embedded_relationships["object_refs"]
@@ -958,6 +1011,7 @@ def import_sighting(
9581011
"created_by_id": created_by_id,
9591012
"object_marking_ids": object_marking_ids,
9601013
"object_label_ids": object_label_ids,
1014+
"open_vocabs": open_vocabs,
9611015
"granted_refs_ids": granted_refs_ids,
9621016
"kill_chain_phases_ids": kill_chain_phases_ids,
9631017
"object_ids": object_refs_ids,

tests/cases/entities.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -596,7 +596,7 @@ def data(self) -> Dict:
596596
"name": "The Black Vine Cyberespionage Group",
597597
"description": "A simple report with an indicator and campaign",
598598
"published": "2016-01-20T17:00:00.000Z",
599-
"report_types": ["campaign"],
599+
"report_types": ["threat-report"],
600600
# "lang": "en",
601601
# "object_refs": [self.ipv4["id"], self.domain["id"]],
602602
}
@@ -870,7 +870,6 @@ def data(self) -> Dict:
870870
return {
871871
"type": "Tool",
872872
"description": "The Evil Org threat actor group",
873-
"tool_types": ["remote-access"],
874873
"name": "VNC",
875874
}
876875

tests/conftest.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,9 @@ def api_client(pytestconfig):
2020
)
2121
else:
2222
return OpenCTIApiClient(
23-
"https://demo.opencti.io",
24-
"7e663f91-d048-4a8b-bdfa-cdb55597942b",
25-
ssl_verify=True,
23+
"http://localhost:4000",
24+
"d434ce02-e58e-4cac-8b4c-42bf16748e84",
25+
ssl_verify=False,
2626
)
2727

2828

0 commit comments

Comments
 (0)