Many fields in STIX leverage the stixCommon:ControlledVocabularyStringType
, which acts as a base type for controlled vocabulary implementations. The STIX language defines a set of default controlled vocabularies which are found in the stix_default_vocabs.xsd
XML Schema file.
The python-stix library contains a stix.common.vocabs
module, which defines the .VocabString
class implementation of the schema ControlledVocabularyStringType
as well as .VocabString
implementations which correspond to default controlled vocabularies.
For example, the stix_default_vocabularies.xsd
schema defines a controlled vocabulary for STIX Package Intents: PackageIntentVocab-1.0
. The .stix.common.vocabs
module contains an analogous .PackageIntent
class, which acts as a derivation of .VocabString
.
Each .VocabString
implementation contains:
- A static list of class-level term attributes, each beginning with
TERM_` (e.g.,
TERM_INDICATORS``) - A tuple containing all allowed vocabulary terms:
_ALLOWED_VALUES
, which is use for input validation. This is generated via the.vocabs.register_vocab
class decorator. - Methods found on
stix.Entity
, such asto_xml()
,to_dict()
,from_dict()
, etc.
The following sections define ways of interacting with VocabString fields.
The STIX Language often suggested a default controlled vocabulary type for a given controlled vocabulary field. Each controlled vocabulary contains an enumeration of allowed terms.
Each .VocabString
implementation found in the stix.common.vocabs
module contains static class-level attributes for each vocabulary term. When setting controlled vocabulary field values, it is recommended that users take advantage of these class-level attributes.
The following demonstrates setting the Package_Intent
field with a default vocabulary term. Note that the .STIXHeader.package_intents
property returns a list. As such, we use the append()
method to add terms. Other STIX controlled vocabulary fields may only allow one value rather than a list of values.
from stix.core import STIXHeader
from stix.common.vocabs import PackageIntent
header = STIXHeader()
header.package_intents.append(PackageIntent.TERM_INDICATORS)
print(header.to_xml())
Which outputs:
<stix:STIXHeaderType>
<stix:Package_Intent xsi:type="stixVocabs:PackageIntentVocab-1.0">Indicators</stix:Package_Intent>
</stix:STIXHeaderType>
Though it is suggested, STIX content authors are not required to use the default controlled vocabulary for a given field. As such, python-stix allows users to pass in non-default values for controlled vocabulary fields.
To set a controlled vocabulary to a non-default vocabulary term, pass a .VocabString
instance into a controlled vocabulary field.
A raw .VocabString
field will contain no xsi:type
information or _ALLOWED_VALUES
members, which removes the input and schema validation requirements.
from stix.core import STIXHeader
from stix.common.vocabs import VocabString, PackageIntent
header = STIXHeader()
non_default_term = VocabString("NON-DEFAULT VOCABULARY TERM")
header.package_intents.append(non_default_term)
print(header.to_xml())
Which outputs:
<stix:STIXHeaderType>
<stix:Package_Intent>NON-DEFAULT VOCABULARY TERM</stix:Package_Intent>
</stix:STIXHeaderType>
Notice that the <stix:Package_Intent>
field does not have an xsi:type
attribute. As such, this field can contain any string value and is not bound by a controlled vocabulary enumeration of terms.
STIX allows content authors and developers to extend the ControlledVocabularyStringType
schema type for the definition of new controlled vocabularies. The python-stix library allows developers to create and register Python types which mirror the custom XML Schema vocabulary types.
The following XML Schema example shows the definition of a a new custom controlled vocabulary schema type. Instances of this schema type could be used wherever a ControlledVocabularyStringType
instance is expected (e.g., the STIX_Header/Package_Intent
field).
Filename: customVocabs.xsd
<xs:schema
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:customVocabs="http://customvocabs.com/vocabs-1"
xmlns:stixVocabs="http://stix.mitre.org/default_vocabularies-1"
xmlns:stixCommon="http://stix.mitre.org/common-1"
targetNamespace="http://customvocabs.com/vocabs-1"
elementFormDefault="qualified"
version="1.2"
xml:lang="English">
<xs:import namespace="http://stix.mitre.org/common-1" schemaLocation="http://stix.mitre.org/XMLSchema/common/1.2/stix_common.xsd"/>
<xs:complexType name="CustomVocab-1.0">
<xs:simpleContent>
<xs:restriction base="stixCommon:ControlledVocabularyStringType">
<xs:simpleType>
<xs:union memberTypes="customVocabs:CustomEnum-1.0"/>
</xs:simpleType>
<xs:attribute name="vocab_name" type="xs:string" use="optional" fixed="Test Vocab"/>
<xs:attribute name="vocab_reference" type="xs:anyURI" use="optional" fixed="http://example.com/TestVocab"/>
</xs:restriction>
</xs:simpleContent>
</xs:complexType>
<xs:simpleType name="CustomEnum-1.0">
<xs:restriction base="xs:string">
<xs:enumeration value="FOO"/>
<xs:enumeration value="BAR"/>
</xs:restriction>
</xs:simpleType>
</xs:schema>
The following STIX XML instance document shows a potential use of this field. Note the xsi:type=customVocabs:CustomVocab-1.0
on the Package_Intent
field.
Filename: customVocabs.xml
<stix:STIX_Package
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:stixExample="http://stix.mitre.org/example"
xmlns:stix="http://stix.mitre.org/stix-1"
xmlns:customVocabs="http://customvocabs.com/vocabs-1"
xsi:schemaLocation="
http://stix.mitre.org/stix-1 /path/to/stix_core.xsd
http://customvocabs.com/vocabs-1 /path/to/customVocabs.xsd"
id="stixExample:STIXPackage-33fe3b22-0201-47cf-85d0-97c02164528d"
version="1.2">
<stix:STIX_Header>
<stix:Package_Intent xsi:type="customVocabs:CustomVocab-1.0">FOO</stix:Package_Intent>
</stix:STIX_Header>
</stix:STIX_Package>
To parse content which uses custom controlled vocabularies, Python developers don't have to do anything special--you just call .STIXPackage.from_xml()
on the input and all the namespaces, xsi:types
, etc. are attached to each instance of .VocabString
. When serializing the document, the input namespaces and xsi:type
attributes are retained!
However, to create new content which utilizes a schema defined and enforced custom controlled vocabulary, developers must create a .VocabString
implementation which mirrors the schema definition.
For our CustomVocab-1.0
schema type, the Python would look like this:
from stix.common import vocabs
# Create a custom vocabulary type
@vocabs.register_vocab
class CustomVocab(vocabs.VocabString):
_namespace = 'http://customvocabs.com/vocabs-1'
_XSI_TYPE = 'customVocabs:CustomVocab-1.0'
# Valid terms
TERM_FOO = 'FOO'
TERM_BAR = 'BAR'
As you can see, we can express a lot of the same information found in the XML Schema definition, but in Python!
_namespace
: ThetargetNamespace
for our custom vocabulary_XSI_TYPE
: Thexsi:type
attribute value to write out for instances of this vocabulary.TERM_FOO|BAR
: Allowable terms for the vocabulary. These terms are collected for input validation.
Note
The @register_vocab
class decorator registers the class and its xsi:type
as a .VocabString
implementation so python-stix will know to build instances of CustomVocab
when parsed content contains CustomVocab-1.0
content.
This also inspects the class attributes for any that begin with TERM_
and collects their values for the purpose of input validation.
Warning
Before python-stix 1.2.0.0, users registered custom .VocabString
implementations via the stix.common.vocabs.add_vocab
method. This method still exists but is considered DEPRECATED in favor of the stix.common.vocabs.register_vocab
class decorator.
# builtin
from StringIO import StringIO
# python-stix modules
from stix.core import STIXPackage
from stix.common.vocabs import VocabString, register_vocab
from mixbox.namespaces import register_namespace, Namespace
XML = \
"""
<stix:STIX_Package
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:stix="http://stix.mitre.org/stix-1"
xmlns:customVocabs="http://customvocabs.com/vocabs-1"
xmlns:example="http://example.com/"
xsi:schemaLocation="
http://stix.mitre.org/stix-1 /path/to/stix_core.xsd
http://customvocabs.com/vocabs-1 /path/to/customVocabs.xsd"
id="example:STIXPackage-33fe3b22-0201-47cf-85d0-97c02164528d"
version="1.2">
<stix:STIX_Header>
<stix:Package_Intent xsi:type="customVocabs:CustomVocab-1.0">FOO</stix:Package_Intent>
</stix:STIX_Header>
</stix:STIX_Package>
"""
# Create a VocabString class for our CustomVocab-1.0 vocabulary which
@register_vocab
class CustomVocab(VocabString):
_namespace = 'http://customvocabs.com/vocabs-1'
_XSI_TYPE = 'customVocabs:CustomVocab-1.0'
TERM_FOO = 'FOO'
TERM_BAR = 'BAR'
register_namespace(Namespace(CustomVocab._namespace, "customVocabNS"))
# Parse the input document
sio = StringIO(XML)
package = STIXPackage.from_xml(sio)
# Retrieve the first (and only) Package_Intent entry
package_intent = package.stix_header.package_intents[0]
# Print information about the input Package_Intent
print('%s %s %s' % (type(package_intent), package_intent.xsi_type, package_intent))
# Add another Package Intent
bar = CustomVocab('BAR')
package.stix_header.add_package_intent(bar)
# This will include the 'BAR' CustomVocab entry
print(package.to_xml())