├── .coveragerc
├── .github
└── pull_request_template.md
├── .gitignore
├── .pre-commit-config.yaml
├── .travis.yml
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── examples
└── custom.py
├── openc2
├── __init__.py
├── base.py
├── core.py
├── custom.py
├── exceptions.py
├── properties.py
├── utils.py
├── v10
│ ├── __init__.py
│ ├── actuators.py
│ ├── args.py
│ ├── common.py
│ ├── message.py
│ ├── slpf.py
│ └── targets.py
└── version.py
├── setup.py
├── tests
├── __init__.py
├── generate.py
├── test_actuator.py
├── test_args.py
├── test_cmd.py
├── test_hashes_property.py
├── test_properties.py
├── test_readme.py
├── test_response.py
├── test_slpf.py
├── test_targets.py
├── v10
│ ├── test_data_types.py
│ ├── test_features.py
│ └── test_stix2.py
└── validate.py
└── tox.ini
/.coveragerc:
--------------------------------------------------------------------------------
1 | [run]
2 | source =
3 | openc2
4 |
5 | [report]
6 | include =
7 | openc2/*
8 |
--------------------------------------------------------------------------------
/.github/pull_request_template.md:
--------------------------------------------------------------------------------
1 | - [ ] I have signed the [Contributor License Agreements (CLAs)](https://www.oasis-open.org/resources/open-repositories/cla).
2 | - [ ] I have reviewed the [CONTRIBUTING.md](https://github.com/oasis-open/openc2-lycan-python/blob/master/CONTRIBUTING.md) file.
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.pyc
2 | build
3 | .coverage
4 | lycan.egg-info
5 |
6 | # Byte-compiled / optimized / DLL files
7 | __pycache__/
8 | *.py[cod]
9 | *$py.class
10 |
11 | # C extensions
12 | *.so
13 |
14 | # Distribution / packaging
15 | .Python
16 | build/
17 | develop-eggs/
18 | dist/
19 | downloads/
20 | eggs/
21 | .eggs/
22 | lib/
23 | lib64/
24 | parts/
25 | sdist/
26 | var/
27 | wheels/
28 | pip-wheel-metadata/
29 | share/python-wheels/
30 | *.egg-info/
31 | .installed.cfg
32 | *.egg
33 | MANIFEST
34 |
35 | # PyInstaller
36 | # Usually these files are written by a python script from a template
37 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
38 | *.manifest
39 | *.spec
40 |
41 | # Installer logs
42 | pip-log.txt
43 | pip-delete-this-directory.txt
44 |
45 | # Unit test / coverage reports
46 | htmlcov/
47 | .tox/
48 | .nox/
49 | .coverage
50 | .coverage.*
51 | .cache
52 | nosetests.xml
53 | coverage.xml
54 | *.cover
55 | *.py,cover
56 | .hypothesis/
57 | .pytest_cache/
58 | cover/
59 |
60 | # Translations
61 | *.mo
62 | *.pot
63 |
64 | # Django stuff:
65 | *.log
66 | local_settings.py
67 | db.sqlite3
68 | db.sqlite3-journal
69 |
70 | # Flask stuff:
71 | instance/
72 | .webassets-cache
73 |
74 | # Scrapy stuff:
75 | .scrapy
76 |
77 | # Sphinx documentation
78 | docs/_build/
79 |
80 | # PyBuilder
81 | target/
82 |
83 | # Jupyter Notebook
84 | .ipynb_checkpoints
85 |
86 | # IPython
87 | profile_default/
88 | ipython_config.py
89 |
90 | # pyenv
91 | # For a library or package, you might want to ignore these files since the code is
92 | # intended to run in multiple environments; otherwise, check them in:
93 | # .python-version
94 |
95 | # pipenv
96 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
97 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
98 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
99 | # install all needed dependencies.
100 | #Pipfile.lock
101 |
102 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow
103 | __pypackages__/
104 |
105 | # Celery stuff
106 | celerybeat-schedule
107 | celerybeat.pid
108 |
109 | # SageMath parsed files
110 | *.sage.py
111 |
112 | # Environments
113 | .env
114 | .venv
115 | env/
116 | venv/
117 | ENV/
118 | env.bak/
119 | venv.bak/
120 |
121 | # Spyder project settings
122 | .spyderproject
123 | .spyproject
124 |
125 | # Rope project settings
126 | .ropeproject
127 |
128 | # mkdocs documentation
129 | /site
130 |
131 | # mypy
132 | .mypy_cache/
133 | .dmypy.json
134 | dmypy.json
135 |
136 | # Pyre type checker
137 | .pyre/
138 |
139 | # pytype static type analyzer
140 | .pytype/
141 |
--------------------------------------------------------------------------------
/.pre-commit-config.yaml:
--------------------------------------------------------------------------------
1 | exclude: '^(site_scons\/builders)|^(external)'
2 | fail_fast: true
3 | repos:
4 | - repo: https://github.com/ambv/black
5 | rev: stable
6 | hooks:
7 | - id: black
8 | language_version: python3
9 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | sudo: false
2 | language: python
3 | python:
4 | - "3.6"
5 | - "3.8"
6 | install: pip install tox-travis coveralls six
7 | script: tox
8 | after_success: coveralls
9 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 |
2 |
Contributing
3 |
4 |
9 |
10 |
11 |
15 |
16 |
17 |
18 |
Because different licenses apply to the OASIS TC's specification work, and this TC Open Repository, there is no guarantee that the licensure of specific repository material will be compatible with licensing requirements of an implementation of a TC's specification. Please refer to the LICENSE file for the terms of this material, and to the OASIS IPR Policy for the terms applicable to the TC's specifications, including any applicable declarations.
19 |
20 |
21 |
22 |
23 |
24 |
Formally, "contribution" to this TC Open Repository refers to content merged into the "Code" repository (repository changes represented by code commits), following the GitHub definition of contributor: "someone who has contributed to a project by having a pull request merged but does not have collaborator [i.e., direct write] access." Anyone who signs the TC Open Repository Individual Contributor License Agreement (CLA), signifying agreement with the licensing requirement, may contribute substantive content — subject to evaluation of a GitHub pull request. The main web page for this repository, as with any GitHub public repository, displays a link to a document listing contributions to the repository's default branch (filtered by Commits, Additions, and Deletions).
25 |
26 |
This TC Open Repository, as with GitHub public repositories generally, also accepts public feedback from any GitHub user. Public feedback includes opening issues, authoring and editing comments, participating in conversations, making wiki edits, creating repository stars, and making suggestions via pull requests. Such feedback does not constitute an OASIS TC Open Repository contribution. Some details are presented under "Read permissions" in the table of permission levels for a GitHub organization. Technical content intended as a substantive contribution (repository "Code") to an TC Open Repository is subject to evaluation, and requires a signed Individual CLA.
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
OASIS TC Open Repositories use the familiar fork-and-pull collaboration model supported by GitHub and other distributed version-control systems. Any GitHub user wishing to contribute should fork the repository, make additions or other modifications, and then submit a pull request. GitHub pull requests should be accompanied by supporting comments and/or issues. Community conversations about pull requests, supported by GitHub notifications, will provide the basis for a consensus determination to merge, modify, close, or take other action, as communicated by the repository Maintainers.
35 |
36 |
37 |
38 |
39 |
40 |
Questions or comments about this TC Open Repository's activities should be composed as GitHub issues or comments. If use of an issue/comment is not possible or appropriate, questions may be directed by email to the repository Maintainer(s). Please send general questions about TC Open Repository participation to OASIS Staff at repository-admin@oasis-open.org and any specific CLA-related questions to repository-cla@oasis-open.org.
41 |
42 |
43 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright 2018 OASIS Open
4 |
5 | Permission is hereby granted, free of charge, to any
6 | person obtaining a copy of this software and associated
7 | documentation files (the "Software"), to deal in the
8 | Software without restriction, including without limitation
9 | the rights to use, copy, modify, merge, publish, distribute,
10 | sublicense, and/or sell copies of the Software, and to
11 | permit persons to whom the Software is furnished to do
12 | so, subject to the following conditions:
13 |
14 | The above copyright notice and this permission notice
15 | shall be included in all copies or substantial portions
16 | of the Software.
17 |
18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
19 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
20 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
21 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
22 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR
23 | ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
24 | ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
26 | OR OTHER DEALINGS IN THE SOFTWARE.
27 |
28 | [Source: https://opensource.org/licenses/MIT ]
29 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Lycan
2 | [](https://www.python.org/)
3 | [](https://travis-ci.org/open-oasis/openc2-lycan-python)
4 | [](https://coveralls.io/github/oasis-open/openc2-lycan-python)
5 |
6 | Lycan is an implementation of the OpenC2 OASIS standard for command and control messaging. The current implementation is based on the Language Specification v1.0.
7 |
8 | Given the influence of STIX/CyBoX on OpenC2, this library is heavily based on the [STIX 2 Python API](https://github.com/oasis-open/cti-python-stix2) internals. Property validation and object extension support aligns with STIX2 conventions and OpenC2 custom properties also support stix2 properties.
9 |
10 | ## Installation
11 |
12 | Install with [pip](https://pip.pypa.io/en/stable):
13 |
14 | ```bash
15 | $ pip install openc2
16 | ```
17 |
18 | ## Usage
19 |
20 | ```python
21 | import openc2
22 | import stix2
23 |
24 | # encode
25 | cmd = openc2.v10.Command(
26 | action="deny",
27 | target=openc2.v10.IPv4Address(ipv4_net="1.2.3.4"),
28 | args=openc2.v10.Args(response_requested="complete"),
29 | )
30 | msg = cmd.serialize()
31 |
32 | # decode
33 | cmd = openc2.parse(msg)
34 | if cmd.action == "deny" and cmd.target.type == "ipv4_net":
35 |
36 | if cmd.args.response_requested == "complete":
37 | resp = openc2.v10.Response(status=200)
38 | msg = resp.serialize()
39 |
40 | # custom actuator
41 | @openc2.v10.CustomActuator(
42 | "x-acme-widget",
43 | [
44 | ("name", openc2.properties.StringProperty(required=True)),
45 | ("version", stix2.properties.FloatProperty()),
46 | ],
47 | )
48 | class AcmeWidgetActuator(object):
49 | def __init__(self, version=None, **kwargs):
50 | if version and version < 1.0:
51 | raise ValueError("'%f' is not a supported version." % version)
52 |
53 | widget = AcmeWidgetActuator(name="foo", version=1.1)
54 | ```
55 |
56 |
57 |
58 |
59 |
This GitHub public repository ( https://github.com/oasis-open/openc2-lycan-python ) was created at the request of the OASIS Open Command and Control (OpenC2) TC as an OASIS TC Open Repository to support development of open source resources related to Technical Committee work.
60 |
61 |
While this TC Open Repository remains associated with the sponsor TC, its development priorities, leadership, intellectual property terms, participation rules, and other matters of governance are separate and distinct from the OASIS TC Process and related policies.
62 |
63 |
All contributions made to this TC Open Repository are subject to open source license terms expressed in the MIT License. That license was selected as the declared "Applicable License" when the TC Open Repository was created.
64 |
65 |
As documented in "Public Participation Invited", contributions to this OASIS TC Open Repository are invited from all parties, whether affiliated with OASIS or not. Participants must have a GitHub account, but no fees or OASIS membership obligations are required. Participation is expected to be consistent with the OASIS TC Open Repository Guidelines and Procedures, the open source LICENSE designated for this particular repository, and the requirement for an Individual Contributor License Agreement that governs intellectual property.
66 |
67 |
68 |
69 |
70 |
71 |
72 |
Statement of Purpose for this OASIS TC Open Repository (openc2-lycan-python) as proposed and approved [bis] by the TC:
73 |
74 |
The purpose of this OASIS TC Open repository is to develop and maintain a python implementation of OpenC2, and to provide a python codebase to facilitate other prototype efforts. The python library is designed to support transformations between data-interchange formats (such as JSON) and python language objects.
75 |
76 |
The OASIS OpenC2 Technical Committee was chartered to address matters as they pertain to command and control of cyber defense technologies, and to maintain a library of prototype implementations.
77 |
78 |
79 |
80 |
81 |
82 |
Repository Maintainers may include here any clarifications — any additional sections, subsections, and paragraphs that the Maintainer(s) wish to add as descriptive text, reflecting (sub-) project status, milestones, releases, modifications to statement of purpose, etc. The project Maintainers will create and maintain this content on behalf of the participants.
83 |
84 |
85 |
86 |
87 |
88 |
TC Open Repository Maintainers are responsible for oversight of this project's community development activities, including evaluation of GitHub pull requests and preserving open source principles of openness and fairness. Maintainers are recognized and trusted experts who serve to implement community goals and consensus design preferences.
89 |
90 |
Initially, the associated TC members have designated one or more persons to serve as Maintainer(s); subsequently, participating community members may select additional or substitute Maintainers, per consensus agreements.
91 |
92 |
Current Maintainers of this TC Open Repository
93 |
94 |
98 |
99 |
100 |
101 |
112 |
113 |
114 |
115 |
Questions or comments about this TC Open Repository's activities should be composed as GitHub issues or comments. If use of an issue/comment is not possible or appropriate, questions may be directed by email to the Maintainer(s) listed above. Please send general questions about TC Open Repository participation to OASIS Staff at repository-admin@oasis-open.org and any specific CLA-related questions to repository-cla@oasis-open.org.
116 |
117 |
118 |
--------------------------------------------------------------------------------
/examples/custom.py:
--------------------------------------------------------------------------------
1 | import openc2
2 | import stix2
3 | import json
4 | import collections
5 |
6 |
7 | @openc2.properties.CustomProperty(
8 | "x-thing",
9 | [
10 | ("uid", stix2.properties.StringProperty()),
11 | ("name", stix2.properties.StringProperty()),
12 | ("version", stix2.properties.StringProperty()),
13 | ],
14 | )
15 | class CustomTargetProperty(object):
16 | pass
17 |
18 |
19 | @openc2.CustomTarget("x-thing:id", [("id", CustomTargetProperty())])
20 | class CustomTarget(object):
21 | pass
22 |
23 |
24 | @openc2.CustomArgs("whatever-who-cares", [("custom_args", CustomTargetProperty())])
25 | class CustomArgs(object):
26 | pass
27 |
28 |
29 | @openc2.CustomActuator(
30 | "x-acme-widget",
31 | [
32 | ("name", stix2.properties.StringProperty(required=True)),
33 | ("version", CustomTargetProperty()),
34 | ],
35 | )
36 | class AcmeWidgetActuator(object):
37 | pass
38 |
39 |
40 | def main():
41 | print("=== Creating Command")
42 | tp = CustomTargetProperty(name="target")
43 | print("target property", tp)
44 | t = CustomTarget(id=tp)
45 | print("target", t)
46 | args = CustomArgs(custom_args=CustomTargetProperty(name="args"))
47 | print("args", args)
48 | act = AcmeWidgetActuator(
49 | name="hello", version=CustomTargetProperty(name="actuator")
50 | )
51 | print("actuator", act)
52 | cmd = openc2.Command(action="query", target=t, args=args, actuator=act)
53 |
54 | d = json.loads(cmd.serialize())
55 | print("=== COMMAND START ===")
56 | print(d)
57 | print("=== COMMAND END ===")
58 | print()
59 |
60 | print("=== Parsing command back to command ===")
61 | cmd2 = openc2.Command(**d)
62 | print("=== COMMAND START ===")
63 | print(cmd2)
64 | print("=== COMMAND END ===")
65 |
66 | assert cmd == cmd2
67 |
68 |
69 | if __name__ == "__main__":
70 | main()
71 |
--------------------------------------------------------------------------------
/openc2/__init__.py:
--------------------------------------------------------------------------------
1 | #
2 | # The MIT License (MIT)
3 | #
4 | # Copyright 2019 AT&T Intellectual Property. All other rights reserved.
5 | #
6 | # Permission is hereby granted, free of charge, to any person obtaining a copy of this software
7 | # and associated documentation files (the "Software"), to deal in the Software without
8 | # restriction, including without limitation the rights to use, copy, modify, merge, publish,
9 | # distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
10 | # Software is furnished to do so, subject to the following conditions:
11 | #
12 | # The above copyright notice and this permission notice shall be included in all copies or
13 | # substantial portions of the Software.
14 | #
15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
17 | # FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18 | # COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
19 | # AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20 | # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21 | #
22 |
23 | """
24 | .. module: lycan
25 | :platform: Unix
26 |
27 | .. version:: $$VERSION$$
28 | .. moduleauthor:: Michael Stair
29 |
30 | """
31 |
32 |
33 | from .core import _collect_openc2_mappings
34 | from .version import __version__
35 |
36 | from . import exceptions
37 | from . import core
38 | from . import base
39 | from . import utils
40 | from .utils import parse
41 | from . import properties
42 | from . import custom
43 |
44 | from . import v10
45 |
46 |
47 | _collect_openc2_mappings()
48 |
--------------------------------------------------------------------------------
/openc2/base.py:
--------------------------------------------------------------------------------
1 | #
2 | # The MIT License (MIT)
3 | #
4 | # Copyright 2019 AT&T Intellectual Property. All other rights reserved.
5 | #
6 | # Permission is hereby granted, free of charge, to any person obtaining a copy of this software
7 | # and associated documentation files (the "Software"), to deal in the Software without
8 | # restriction, including without limitation the rights to use, copy, modify, merge, publish,
9 | # distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
10 | # Software is furnished to do so, subject to the following conditions:
11 | #
12 | # The above copyright notice and this permission notice shall be included in all copies or
13 | # substantial portions of the Software.
14 | #
15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
17 | # FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18 | # COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
19 | # AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20 | # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21 | #
22 |
23 | """
24 | .. module: openc2.base
25 | :platform: Unix
26 |
27 | .. version:: $$VERSION$$
28 | .. moduleauthor:: Michael Stair
29 |
30 | """
31 |
32 | import copy
33 | import json
34 | from . import exceptions
35 | import datetime
36 |
37 | from collections.abc import Mapping
38 |
39 |
40 | class OpenC2JSONEncoder(json.JSONEncoder):
41 | def default(self, obj):
42 | if isinstance(obj, _OpenC2Base):
43 | tmp_obj = dict(copy.deepcopy(obj))
44 | if isinstance(obj, (_Target, _Actuator)):
45 | # collapse targets with a single specifier (ie, DomainName)
46 | if len(obj._properties) == 1 and obj._type in obj._properties.keys():
47 | tmp_obj = tmp_obj.get(obj._type)
48 | # handle custom target specifiers
49 | if ":" in obj._type:
50 | nsid, _type = obj._type.split(":")
51 | tmp_obj = tmp_obj.get(_type)
52 | # for target/actuators, return type and specifiers dict
53 | return {obj._type: tmp_obj}
54 | else:
55 | return tmp_obj
56 | else:
57 | try:
58 | # support stix2 objects even tho stix2 isn't a dependency
59 | from stix2.base import STIXJSONEncoder
60 |
61 | return STIXJSONEncoder.default(self, obj)
62 | except:
63 | pass
64 |
65 | return super(OpenC2JSONEncoder, self).default(obj)
66 |
67 |
68 | def get_required_properties(properties):
69 | return (k for k, v in properties.items() if v.required)
70 |
71 |
72 | class _OpenC2Base(Mapping):
73 | def object_properties(self):
74 | props = set(self._properties.keys())
75 | custom_props = list(set(self._inner.keys()) - props)
76 | custom_props.sort()
77 |
78 | all_properties = list(self._properties.keys())
79 | all_properties.extend(custom_props) # Any custom properties to the bottom
80 |
81 | return all_properties
82 |
83 | def _check_property(self, prop_name, prop, kwargs):
84 | if prop_name not in kwargs:
85 | if hasattr(prop, "default"):
86 | value = prop.default()
87 | kwargs[prop_name] = value
88 |
89 | if prop_name in kwargs:
90 | try:
91 | kwargs[prop_name] = prop.clean(kwargs[prop_name])
92 | except exceptions.InvalidValueError:
93 | # No point in wrapping InvalidValueError in another
94 | # InvalidValueError... so let those propagate.
95 | raise
96 | except Exception as exc:
97 | raise exceptions.InvalidValueError(
98 | self.__class__, prop_name, str(exc)
99 | ) from exc
100 |
101 | def check_mutually_exclusive_properties(
102 | self, list_of_properties, at_least_one=True
103 | ):
104 | current_properties = self.properties_populated()
105 | count = len(set(list_of_properties).intersection(current_properties))
106 | # at_least_one allows for xor to be checked
107 | if count > 1 or (at_least_one and count == 0):
108 | raise exceptions.MutuallyExclusivePropertiesError(
109 | self.__class__, list_of_properties
110 | )
111 |
112 | def check_at_least_one_property(self, list_of_properties=None):
113 | if not list_of_properties:
114 | list_of_properties = sorted(list(self.__class__._properties.keys()))
115 |
116 | current_properties = self.properties_populated()
117 | list_of_properties_populated = set(list_of_properties).intersection(
118 | current_properties
119 | )
120 |
121 | if list_of_properties and (
122 | not list_of_properties_populated
123 | or list_of_properties_populated == set(["extensions"])
124 | ):
125 | raise exceptions.AtLeastOnePropertyError(self.__class__, list_of_properties)
126 |
127 | # this isn't used, but may be used in the future
128 | # def check_properties_dependency(
129 | # self, list_of_properties, list_of_dependent_properties
130 | # ):
131 | # failed_dependency_pairs = []
132 | # for p in list_of_properties:
133 | # for dp in list_of_dependent_properties:
134 | # if not self.get(p) and self.get(dp):
135 | # failed_dependency_pairs.append((p, dp))
136 | # if failed_dependency_pairs:
137 | # raise exceptions.DependentPropertiesError(
138 | # self.__class__, failed_dependency_pairs
139 | # )
140 |
141 | def check_object_constraints(self):
142 | """
143 | Meant to be overriden by subclasses. This is called after instance creation
144 | """
145 | pass
146 |
147 | def __init__(self, allow_custom=False, **kwargs):
148 | cls = self.__class__
149 | self._allow_custom = allow_custom
150 |
151 | # Detect any keyword arguments not allowed for a specific type
152 | if not self._allow_custom:
153 | extra_kwargs = list(set(kwargs) - set(self._properties))
154 | if extra_kwargs:
155 | raise exceptions.ExtraPropertiesError(cls, extra_kwargs)
156 |
157 | # Remove any keyword arguments whose value is None or [] (i.e. empty list)
158 | setting_kwargs = {}
159 | props = kwargs.copy()
160 | for prop_name, prop_value in props.items():
161 | if prop_value is not None and prop_value != []:
162 | setting_kwargs[prop_name] = prop_value
163 |
164 | # Detect any missing required properties
165 | required_properties = set(get_required_properties(self._properties))
166 | missing_kwargs = required_properties - set(setting_kwargs)
167 | if missing_kwargs:
168 | raise exceptions.MissingPropertiesError(cls, missing_kwargs)
169 |
170 | for prop_name, prop_metadata in self._properties.items():
171 | self._check_property(prop_name, prop_metadata, setting_kwargs)
172 |
173 | # Cache defaulted optional properties for serialization
174 | defaulted = []
175 | for name, prop in self._properties.items():
176 | try:
177 | if (
178 | not prop.required
179 | and not hasattr(prop, "_fixed_value")
180 | and prop.default() == setting_kwargs[name]
181 | ):
182 | defaulted.append(name)
183 | except (AttributeError, KeyError):
184 | continue
185 | self._defaulted_optional_properties = defaulted
186 |
187 | self._inner = setting_kwargs
188 |
189 | self.check_object_constraints()
190 |
191 | def serialize(self, pretty=False, **kwargs):
192 | if pretty:
193 | kwargs.update({"indent": 4, "separators": (",", ": ")})
194 | return json.dumps(self, cls=OpenC2JSONEncoder, **kwargs)
195 |
196 | def __iter__(self):
197 | return iter(self._inner)
198 |
199 | def __len__(self):
200 | return len(self._inner)
201 |
202 | # OpenC2 doesnt have 'type' properties in commands - or it can't now
203 | def __getattr__(self, name):
204 | # Pickle-proofing: pickle invokes this on uninitialized instances (i.e.
205 | # __init__ has not run). So no "self" attributes are set yet. The
206 | # usual behavior of this method reads an __init__-assigned attribute,
207 | # which would cause infinite recursion. So this check disables all
208 | # attribute reads until the instance has been properly initialized.
209 | # See https://github.com/oasis-open/cti-python-stix2/blob/master/stix2/base.py#L209
210 | if name == "type":
211 | return self._type
212 |
213 | unpickling = "_inner" not in self.__dict__
214 | if not unpickling and name in self:
215 | return self.__getitem__(name)
216 | raise AttributeError(
217 | "'%s' object has no attribute '%s'" % (self.__class__.__name__, name)
218 | )
219 |
220 | def __setattr__(self, name, value):
221 | if name == "type":
222 | raise exceptions.ImmutableError(self.__class__, name)
223 | super(_OpenC2Base, self).__setattr__(name, value)
224 |
225 | def __getitem__(self, key):
226 | if key == "type":
227 | return self._type
228 |
229 | return self._inner[key]
230 |
231 | def __str__(self):
232 | return self.serialize(pretty=True)
233 |
234 | def __repr__(self):
235 | props = [(k, self[k]) for k in self.object_properties() if self.get(k)]
236 | return "{0}({1})".format(
237 | self.__class__.__name__,
238 | ", ".join(["{0!s}={1!r}".format(k, v) for k, v in props]),
239 | )
240 |
241 | def __deepcopy__(self, memo):
242 | # Assume: we can ignore the memo argument, because no object will ever contain the same sub-object multiple times.
243 | new_inner = copy.deepcopy(self._inner, memo)
244 | cls = type(self)
245 | new_inner["allow_custom"] = self._allow_custom
246 | return cls(**new_inner)
247 |
248 | def properties_populated(self):
249 | return list(self._inner.keys())
250 |
251 | def clone(self, **kwargs):
252 | """
253 | Clone and object and assign new values
254 | """
255 | unchangable_properties = []
256 |
257 | try:
258 | new_obj_inner = copy.deepcopy(self._inner)
259 | except AttributeError:
260 | new_obj_inner = copy.deepcopy(self)
261 | properties_to_change = kwargs.keys()
262 |
263 | # XXX: Make sure certain properties aren't trying to change
264 | for prop in ["type", "_type"]:
265 | if prop in properties_to_change:
266 | unchangable_properties.append(prop)
267 | if unchangable_properties:
268 | raise exceptions.UnmodifiablePropertyError(unchangable_properties)
269 |
270 | if "allow_custom" not in kwargs:
271 | kwargs["allow_custom"] = self._allow_custom
272 |
273 | cls = type(self)
274 |
275 | new_obj_inner.update(kwargs)
276 | # Exclude properties with a value of 'None' in case data is not an instance of a _OpenC2Base subclass
277 | return cls(**{k: v for k, v in new_obj_inner.items() if v is not None})
278 |
279 |
280 | class _OpenC2DataType(_OpenC2Base):
281 | pass
282 |
283 |
284 | class _Target(_OpenC2Base):
285 | pass
286 |
287 |
288 | class _Actuator(_OpenC2Base):
289 | pass
290 |
--------------------------------------------------------------------------------
/openc2/core.py:
--------------------------------------------------------------------------------
1 | #
2 | # The MIT License (MIT)
3 | #
4 | # Copyright 2019 AT&T Intellectual Property. All other rights reserved.
5 | #
6 | # Permission is hereby granted, free of charge, to any person obtaining a copy of this software
7 | # and associated documentation files (the "Software"), to deal in the Software without
8 | # restriction, including without limitation the rights to use, copy, modify, merge, publish,
9 | # distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
10 | # Software is furnished to do so, subject to the following conditions:
11 | #
12 | # The above copyright notice and this permission notice shall be included in all copies or
13 | # substantial portions of the Software.
14 | #
15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
17 | # FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18 | # COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
19 | # AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20 | # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21 | #
22 |
23 | """
24 | .. module: openc2.core
25 | :platform: Unix
26 |
27 | .. version:: $$VERSION$$
28 | .. moduleauthor:: Michael Stair
29 |
30 | """
31 |
32 | import copy
33 | import importlib
34 | import pkgutil
35 | import re
36 | from . import exceptions
37 |
38 | OPENC2_OBJ_MAPS = {}
39 |
40 | from . import utils
41 |
42 |
43 | def _register_extension(new_type, object_type, version=None):
44 | EXT_MAP = OPENC2_OBJ_MAPS["extensions"]
45 | EXT_MAP[object_type][new_type._type] = new_type
46 |
47 |
48 | def _collect_openc2_mappings():
49 | if not OPENC2_OBJ_MAPS:
50 | top_level_module = importlib.import_module("openc2")
51 | path = top_level_module.__path__
52 | prefix = str(top_level_module.__name__) + "."
53 |
54 | for module_loader, name, is_pkg in pkgutil.walk_packages(
55 | path=path, prefix=prefix
56 | ):
57 | ver = name.split(".")[1]
58 | if re.match(r"openc2\.v1[0-9]$", name) and is_pkg:
59 | mod = importlib.import_module(name, str(top_level_module.__name__))
60 | OPENC2_OBJ_MAPS["objects"] = mod.OBJ_MAP
61 | OPENC2_OBJ_MAPS["targets"] = mod.OBJ_MAP_TARGET
62 | OPENC2_OBJ_MAPS["args"] = mod.OBJ_MAP_ARGS
63 | OPENC2_OBJ_MAPS["actuators"] = mod.OBJ_MAP_ACTUATOR
64 | OPENC2_OBJ_MAPS["extensions"] = mod.EXT_MAP
65 |
--------------------------------------------------------------------------------
/openc2/custom.py:
--------------------------------------------------------------------------------
1 | #
2 | # The MIT License (MIT)
3 | #
4 | # Copyright 2019 AT&T Intellectual Property. All other rights reserved.
5 | #
6 | # Permission is hereby granted, free of charge, to any person obtaining a copy of this software
7 | # and associated documentation files (the "Software"), to deal in the Software without
8 | # restriction, including without limitation the rights to use, copy, modify, merge, publish,
9 | # distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
10 | # Software is furnished to do so, subject to the following conditions:
11 | #
12 | # The above copyright notice and this permission notice shall be included in all copies or
13 | # substantial portions of the Software.
14 | #
15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
17 | # FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18 | # COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
19 | # AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20 | # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21 | #
22 |
23 | """
24 | .. module: openc2.custom
25 | :platform: Unix
26 |
27 | .. version:: $$VERSION$$
28 | .. moduleauthor:: Michael Stair
29 |
30 | """
31 |
32 | from collections import OrderedDict
33 | from .base import _OpenC2Base, _Target, _Actuator
34 | from .core import OPENC2_OBJ_MAPS, _register_extension
35 | from . import exceptions
36 | from . import properties as openc2_properties
37 |
38 |
39 | def _cls_init(cls, obj, kwargs):
40 | if getattr(cls, "__init__", object.__init__) is not object.__init__:
41 | cls.__init__(obj, **kwargs)
42 |
43 |
44 | def _check_custom_properties(cls, properties):
45 | if "type" in properties.keys():
46 | raise exceptions.PropertyPresenceError("'type' is reserved", cls)
47 |
48 |
49 | def _custom_target_builder(cls, type, properties, version):
50 | class _CustomTarget(cls, _Target):
51 |
52 | try:
53 | nsid, target = type.split(":")
54 | except ValueError:
55 | raise ValueError(
56 | "Invalid Extended Target name '%s': must be namespace:target format"
57 | % type
58 | )
59 |
60 | if len(nsid) > 16:
61 | raise ValueError(
62 | "Invalid namespace '%s': must be less than 16 characters" % type
63 | )
64 |
65 | if not properties or not isinstance(properties, list):
66 | raise ValueError(
67 | "Must supply a list, containing tuples. For example, [('property1', IntegerProperty())]"
68 | )
69 |
70 | _type = type
71 | _properties = OrderedDict(properties)
72 |
73 | _check_custom_properties(cls, _properties)
74 |
75 | def __init__(self, **kwargs):
76 | _Target.__init__(self, **kwargs)
77 | _cls_init(cls, self, kwargs)
78 |
79 | _register_extension(_CustomTarget, object_type="targets", version=version)
80 | return _CustomTarget
81 |
82 |
83 | def _custom_actuator_builder(cls, type, properties, version):
84 | class _CustomActuator(cls, _Actuator):
85 |
86 | if not type.startswith("x-"):
87 | raise ValueError(
88 | "Invalid Extended Actuator name '%s': must start with x-" % type
89 | )
90 |
91 | if not properties or not isinstance(properties, list):
92 | raise ValueError(
93 | "Must supply a list, containing tuples. For example, [('property1', IntegerProperty())]"
94 | )
95 |
96 | _type = type
97 | _properties = OrderedDict(properties)
98 |
99 | _check_custom_properties(cls, _properties)
100 |
101 | def __init__(self, **kwargs):
102 | _Actuator.__init__(self, **kwargs)
103 | _cls_init(cls, self, kwargs)
104 |
105 | _register_extension(_CustomActuator, object_type="actuators", version=version)
106 | return _CustomActuator
107 |
108 |
109 | def _custom_args_builder(cls, type, properties, version):
110 | class _CustomArgs(cls, _OpenC2Base):
111 |
112 | if not properties or not isinstance(properties, list):
113 | raise ValueError(
114 | "Must supply a list, containing tuples. For example, [('property1', IntegerProperty())]"
115 | )
116 |
117 | _type = type
118 | _properties = OrderedDict(properties)
119 |
120 | _check_custom_properties(cls, _properties)
121 |
122 | def __init__(self, **kwargs):
123 | _OpenC2Base.__init__(self, **kwargs)
124 | _cls_init(cls, self, kwargs)
125 |
126 | _register_extension(_CustomArgs, object_type="args", version=version)
127 | return _CustomArgs
128 |
129 |
130 | def _custom_property_builder(cls, type, properties, version):
131 | class _CustomProperty(cls, _OpenC2Base, openc2_properties.Property):
132 |
133 | if not properties or not isinstance(properties, list):
134 | raise ValueError(
135 | "Must supply a list, containing tuples. For example, [('property1', IntegerProperty())]"
136 | )
137 |
138 | _type = type
139 | _properties = OrderedDict(properties)
140 |
141 | _check_custom_properties(cls, _properties)
142 |
143 | def __init__(
144 | self, required=False, fixed=None, default=None, allow_custom=False, **kwargs
145 | ):
146 |
147 | _OpenC2Base.__init__(self, allow_custom=allow_custom, **kwargs)
148 | _cls_init(cls, self, kwargs)
149 | openc2_properties.Property.__init__(
150 | self, required=required, fixed=fixed, default=default
151 | )
152 |
153 | if kwargs:
154 | self.clean(self)
155 |
156 | def __call__(self, _value=None, **kwargs):
157 | """__init__ for when using an instance
158 | Example: Used by ListProperty to handle lists that have been defined with
159 | either a class or an instance.
160 | """
161 | if _value:
162 | return _value
163 |
164 | value = self.__class__(**kwargs)
165 | return self.clean(value)
166 |
167 | _register_extension(_CustomProperty, object_type="properties", version=version)
168 | return _CustomProperty
169 |
--------------------------------------------------------------------------------
/openc2/exceptions.py:
--------------------------------------------------------------------------------
1 | """OpenC2 Error Classes."""
2 |
3 |
4 | class OpenC2Error(Exception):
5 | """Base class for errors generated in the openc2 library."""
6 |
7 |
8 | class ObjectConfigurationError(OpenC2Error):
9 | """
10 | Represents specification violations regarding the composition of OpenC2
11 | objects.
12 | """
13 |
14 | pass
15 |
16 |
17 | class InvalidValueError(ObjectConfigurationError):
18 | """An invalid value was provided to a OpenC2 object's ``__init__``."""
19 |
20 | def __init__(self, cls, prop_name, reason):
21 | super(InvalidValueError, self).__init__()
22 | self.cls = cls
23 | self.prop_name = prop_name
24 | self.reason = reason
25 |
26 | def __str__(self):
27 | msg = "Invalid value for {0.cls.__name__} '{0.prop_name}': {0.reason}"
28 | return msg.format(self)
29 |
30 |
31 | class PropertyPresenceError(ObjectConfigurationError):
32 | """
33 | Represents an invalid combination of properties on a OpenC2 object. This
34 | class can be used directly when the object requirements are more
35 | complicated and none of the more specific exception subclasses apply.
36 | """
37 |
38 | def __init__(self, message, cls):
39 | super(PropertyPresenceError, self).__init__(message)
40 | self.cls = cls
41 |
42 |
43 | class MissingPropertiesError(PropertyPresenceError):
44 | """Missing one or more required properties when constructing OpenC2 object."""
45 |
46 | def __init__(self, cls, properties):
47 | self.properties = sorted(properties)
48 |
49 | msg = "No values for required properties for {0}: ({1}).".format(
50 | cls.__name__, ", ".join(x for x in self.properties),
51 | )
52 |
53 | super(MissingPropertiesError, self).__init__(msg, cls)
54 |
55 |
56 | class ExtraPropertiesError(PropertyPresenceError):
57 | """One or more extra properties were provided when constructing OpenC2 object."""
58 |
59 | def __init__(self, cls, properties):
60 | self.properties = sorted(properties)
61 |
62 | msg = "Unexpected properties for {0}: ({1}).".format(
63 | cls.__name__, ", ".join(x for x in self.properties),
64 | )
65 |
66 | super(ExtraPropertiesError, self).__init__(msg, cls)
67 |
68 |
69 | class ParseError(OpenC2Error):
70 | """Could not parse object."""
71 |
72 | def __init__(self, msg):
73 | super(ParseError, self).__init__(msg)
74 |
75 |
76 | class CustomContentError(OpenC2Error):
77 | """Custom OpenC2 Content (SDO, Observable, Extension, etc.) detected."""
78 |
79 | def __init__(self, msg):
80 | super(CustomContentError, self).__init__(msg)
81 |
82 |
83 | class ImmutableError(OpenC2Error):
84 | """Attempted to modify an object after creation."""
85 |
86 | def __init__(self, cls, key):
87 | super(ImmutableError, self).__init__()
88 | self.cls = cls
89 | self.key = key
90 |
91 | def __str__(self):
92 | msg = "Cannot modify '{0.key}' property in '{0.cls.__name__}' after creation."
93 | return msg.format(self)
94 |
95 |
96 | class DictionaryKeyError(ObjectConfigurationError):
97 | """Dictionary key does not conform to the correct format."""
98 |
99 | def __init__(self, key, reason):
100 | super(DictionaryKeyError, self).__init__()
101 | self.key = key
102 | self.reason = reason
103 |
104 | def __str__(self):
105 | msg = "Invalid dictionary key {0.key}: ({0.reason})."
106 | return msg.format(self)
107 |
108 |
109 | class OpenC2DeprecationWarning(DeprecationWarning):
110 | """
111 | Represents usage of a deprecated component of a OpenC2 specification.
112 | """
113 |
114 | pass
115 |
116 |
117 | class MutuallyExclusivePropertiesError(PropertyPresenceError):
118 | """Violating interproperty mutually exclusive constraint of a OpenC2 object type."""
119 |
120 | def __init__(self, cls, properties):
121 | self.properties = sorted(properties)
122 |
123 | msg = "The ({1}) properties for {0} are mutually exclusive.".format(
124 | cls.__name__, ", ".join(x for x in self.properties),
125 | )
126 |
127 | super(MutuallyExclusivePropertiesError, self).__init__(msg, cls)
128 |
129 |
130 | class AtLeastOnePropertyError(PropertyPresenceError):
131 | """Violating a constraint of a OpenC2 object type that at least one of the given properties must be populated."""
132 |
133 | def __init__(self, cls, properties):
134 | self.properties = sorted(properties)
135 |
136 | msg = (
137 | "At least one of the ({1}) properties for {0} must be "
138 | "populated.".format(cls.__name__, ", ".join(x for x in self.properties),)
139 | )
140 |
141 | super(AtLeastOnePropertyError, self).__init__(msg, cls)
142 |
143 |
144 | class UnmodifiablePropertyError(OpenC2Error):
145 | """Attempted to modify an unmodifiable property of object when creating a new version."""
146 |
147 | def __init__(self, unchangable_properties):
148 | super(UnmodifiablePropertyError, self).__init__()
149 | self.unchangable_properties = unchangable_properties
150 |
151 | def __str__(self):
152 | msg = "These properties cannot be changed when making a new version: {0}."
153 | return msg.format(", ".join(self.unchangable_properties))
154 |
155 |
156 | # These aren't needed now, but might be needed in future language specifications
157 |
158 | # class DependentPropertiesError(PropertyPresenceError):
159 | # """Violating interproperty dependency constraint of a OpenC2 object type."""
160 |
161 | # def __init__(self, cls, dependencies):
162 | # self.dependencies = dependencies
163 |
164 | # msg = "The property dependencies for {0}: ({1}) are not met.".format(
165 | # cls.__name__, ", ".join(name for x in self.dependencies for name in x),
166 | # )
167 |
168 | # super(DependentPropertiesError, self).__init__(msg, cls)
169 |
--------------------------------------------------------------------------------
/openc2/properties.py:
--------------------------------------------------------------------------------
1 | #
2 | # The MIT License (MIT)
3 | #
4 | # Copyright 2019 AT&T Intellectual Property. All other rights reserved.
5 | #
6 | # Permission is hereby granted, free of charge, to any person obtaining a copy of this software
7 | # and associated documentation files (the "Software"), to deal in the Software without
8 | # restriction, including without limitation the rights to use, copy, modify, merge, publish,
9 | # distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
10 | # Software is furnished to do so, subject to the following conditions:
11 | #
12 | # The above copyright notice and this permission notice shall be included in all copies or
13 | # substantial portions of the Software.
14 | #
15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
17 | # FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18 | # COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
19 | # AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20 | # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21 | #
22 |
23 | """
24 | .. module: openc2.properties
25 | :platform: Unix
26 |
27 | .. version:: $$VERSION$$
28 | .. moduleauthor:: Michael Stair
29 |
30 | """
31 | import base64
32 | import binascii
33 | from .base import _OpenC2Base
34 | from collections import OrderedDict
35 | from .custom import _custom_property_builder
36 | from . import utils
37 | from . import exceptions
38 | import re, inspect
39 | import itertools
40 | import datetime
41 | import copy
42 | from collections import OrderedDict
43 | from collections.abc import Mapping
44 |
45 |
46 | class Property(object):
47 | """Represent a property of STIX data type.
48 | Subclasses can define the following attributes as keyword arguments to
49 | ``__init__()``.
50 | Args:
51 | required (bool): If ``True``, the property must be provided when
52 | creating an object with that property. No default value exists for
53 | these properties. (Default: ``False``)
54 | fixed: This provides a constant default value. Users are free to
55 | provide this value explicity when constructing an object (which
56 | allows you to copy **all** values from an existing object to a new
57 | object), but if the user provides a value other than the ``fixed``
58 | value, it will raise an error. This is semantically equivalent to
59 | defining both:
60 | - a ``clean()`` function that checks if the value matches the fixed
61 | value, and
62 | - a ``default()`` function that returns the fixed value.
63 | Subclasses can also define the following functions:
64 | - ``def clean(self, value) -> any:``
65 | - Return a value that is valid for this property. If ``value`` is not
66 | valid for this property, this will attempt to transform it first. If
67 | ``value`` is not valid and no such transformation is possible, it
68 | should raise an exception.
69 | - ``def default(self):``
70 | - provide a default value for this property.
71 | - ``default()`` can return the special value ``NOW`` to use the current
72 | time. This is useful when several timestamps in the same object
73 | need to use the same default value, so calling now() for each
74 | property-- likely several microseconds apart-- does not work.
75 | Subclasses can instead provide a lambda function for ``default`` as a
76 | keyword argument. ``clean`` should not be provided as a lambda since
77 | lambdas cannot raise their own exceptions.
78 | When instantiating Properties, ``required`` and ``default`` should not be
79 | used together. ``default`` implies that the property is required in the
80 | specification so this function will be used to supply a value if none is
81 | provided. ``required`` means that the user must provide this; it is
82 | required in the specification and we can't or don't want to create a
83 | default value.
84 | """
85 |
86 | def _default_clean(self, value):
87 | if value != self._fixed_value:
88 | raise ValueError("must equal '{}'.".format(self._fixed_value))
89 | return value
90 |
91 | def __init__(self, required=False, fixed=None, default=None):
92 | self.required = required
93 | if fixed:
94 | self._fixed_value = fixed
95 | self.clean = self._default_clean
96 | self.default = lambda: fixed
97 | if default:
98 | self.default = default
99 |
100 | def clean(self, value):
101 | return value
102 |
103 | def __call__(self, value=None):
104 | """Used by ListProperty to handle lists that have been defined with
105 | either a class or an instance.
106 | """
107 | return value
108 |
109 |
110 | class EmbeddedObjectProperty(Property):
111 | def __init__(self, type, **kwargs):
112 | self.type = type
113 | super(EmbeddedObjectProperty, self).__init__(**kwargs)
114 |
115 | def clean(self, value):
116 | if isinstance(self.type, _OpenC2Base) and isinstance(
117 | self.type, Property
118 | ): # is a Custom Property
119 | return self.type.clean(value)
120 | elif type(value) is dict:
121 | value = self.type(**value)
122 | elif not isinstance(value, self.type):
123 | raise ValueError("must be of type {}.".format(self.type.__name__))
124 | return value
125 |
126 |
127 | class ListProperty(Property):
128 | def __init__(self, contained, **kwargs):
129 | """
130 | ``contained`` should be a function which returns an object from the value.
131 | """
132 | if inspect.isclass(contained) and issubclass(contained, Property):
133 | # If it's a class and not an instance, instantiate it so that
134 | # clean() can be called on it, and ListProperty.clean() will
135 | # use __call__ when it appends the item.
136 | self.contained = contained()
137 | else:
138 | self.contained = contained
139 | super(ListProperty, self).__init__(**kwargs)
140 |
141 | def clean(self, value):
142 | try:
143 | iter(value)
144 | except TypeError:
145 | raise ValueError("must be an iterable.")
146 |
147 | if isinstance(value, (_OpenC2Base, (str,))):
148 | value = [value]
149 |
150 | result = []
151 | for item in value:
152 | try:
153 | valid = self.contained.clean(item)
154 | except ValueError:
155 | raise
156 | except AttributeError:
157 | # type of list has no clean() function (eg. built in Python types)
158 | # TODO Should we raise an error here?
159 | valid = item
160 |
161 | if type(self.contained) is EmbeddedObjectProperty:
162 | obj_type = self.contained.type
163 | elif type(self.contained) is DictionaryProperty:
164 | obj_type = dict
165 | else:
166 | obj_type = self.contained
167 |
168 | if isinstance(valid, Mapping):
169 | try:
170 | valid._allow_custom
171 | except AttributeError:
172 | result.append(obj_type(**valid))
173 | else:
174 | result.append(obj_type(allow_custom=True, **valid))
175 | else:
176 | result.append(obj_type(valid))
177 |
178 | return result
179 |
180 |
181 | class StringProperty(Property):
182 | def __init__(self, **kwargs):
183 | super(StringProperty, self).__init__(**kwargs)
184 |
185 | def clean(self, value):
186 | if not isinstance(value, str):
187 | return str(value)
188 | return value
189 |
190 |
191 | class EnumProperty(StringProperty):
192 | def __init__(self, allowed, **kwargs):
193 | if type(allowed) is not list:
194 | raise ValueError("allowed must be a list")
195 | self.allowed = allowed
196 | super(EnumProperty, self).__init__(**kwargs)
197 |
198 | def clean(self, value):
199 | cleaned_value = super(EnumProperty, self).clean(value)
200 | if cleaned_value not in self.allowed:
201 | raise ValueError(
202 | "value '{}' is not valid for this enumeration.".format(cleaned_value)
203 | )
204 |
205 | return cleaned_value
206 |
207 |
208 | class BinaryProperty(Property):
209 | def clean(self, value):
210 | try:
211 | base64.b64decode(value)
212 | except (binascii.Error, TypeError):
213 | raise ValueError("must contain a base64 encoded string")
214 | return value
215 |
216 |
217 | class IntegerProperty(Property):
218 | def __init__(self, min=None, max=None, **kwargs):
219 | self.min = min
220 | self.max = max
221 | super(IntegerProperty, self).__init__(**kwargs)
222 |
223 | def clean(self, value):
224 | try:
225 | value = int(value)
226 | except Exception:
227 | raise ValueError("must be an integer.")
228 |
229 | if self.min is not None and value < self.min:
230 | msg = "minimum value is {}. received {}".format(self.min, value)
231 | raise ValueError(msg)
232 |
233 | if self.max is not None and value > self.max:
234 | msg = "maximum value is {}. received {}".format(self.max, value)
235 | raise ValueError(msg)
236 |
237 | return value
238 |
239 |
240 | class DateTimeProperty(IntegerProperty):
241 | """
242 | Value is the number of milliseconds since 00:00:00 UTC, 1 January 1970
243 | """
244 |
245 | def __init__(self, **kwargs):
246 | super(DateTimeProperty, self).__init__(**kwargs)
247 |
248 | def clean(self, value):
249 | if isinstance(value, datetime.datetime):
250 | value = value.astimezone(datetime.timezone.utc)
251 | return super(DateTimeProperty, self).clean(value.timestamp() * 1000)
252 |
253 | value = super(DateTimeProperty, self).clean(value)
254 | self.datetime(value)
255 | return value
256 |
257 | def datetime(self, value):
258 | return datetime.datetime.utcfromtimestamp(value / 1000.0)
259 |
260 |
261 | class FloatProperty(Property):
262 | def __init__(self, min=None, max=None, **kwargs):
263 | self.min = min
264 | self.max = max
265 | super(FloatProperty, self).__init__(**kwargs)
266 |
267 | def clean(self, value):
268 | try:
269 | value = float(value)
270 | except Exception:
271 | raise ValueError("must be a float.")
272 |
273 | if self.min is not None and value < self.min:
274 | msg = "minimum value is {}. received {}".format(self.min, value)
275 | raise ValueError(msg)
276 |
277 | if self.max is not None and value > self.max:
278 | msg = "maximum value is {}. received {}".format(self.max, value)
279 | raise ValueError(msg)
280 |
281 | return value
282 |
283 |
284 | class DictionaryProperty(Property):
285 | def __init__(self, allowed_keys=None, allowed_key_regex=None, **kwargs):
286 | self.allowed_keys = allowed_keys
287 | self.allowed_key_regex = allowed_key_regex
288 | super(DictionaryProperty, self).__init__(**kwargs)
289 |
290 | def clean(self, value):
291 | try:
292 | dictified = utils._get_dict(value)
293 | except ValueError:
294 | raise ValueError("The dictionary property must contain a dictionary")
295 | for k in dictified.keys():
296 | if self.allowed_keys and k not in self.allowed_keys:
297 | raise exceptions.DictionaryKeyError(k, "Key not allowed")
298 | if self.allowed_key_regex and not re.match(self.allowed_key_regex, k):
299 | msg = (
300 | "contains characters other than lowercase a-z, "
301 | "uppercase A-Z, numerals 0-9, hyphen (-), or "
302 | "underscore (_)"
303 | )
304 | raise exceptions.DictionaryKeyError(k, msg)
305 |
306 | return dictified
307 |
308 |
309 | class PayloadProperty(Property):
310 | def clean(self, value):
311 | from .v10.common import Payload
312 |
313 | obj = Payload(**value)
314 |
315 | return obj
316 |
317 |
318 | class ProcessProperty(Property):
319 | def clean(self, value):
320 | from .v10.targets import Process
321 |
322 | if value.get("process"):
323 | process = Process(**value.get("process"))
324 | else:
325 | process = Process(**value)
326 | return process
327 |
328 |
329 | class FileProperty(Property):
330 | def __init__(self, required=False, fixed=None, default=None, version="1.0"):
331 | super(FileProperty, self).__init__(
332 | required=required, fixed=fixed, default=default
333 | )
334 | self._version = version
335 |
336 | def clean(self, value):
337 | if self._version == "1.0":
338 | from .v10.targets import File
339 |
340 | if value.get("file"):
341 | file = File(**value.get("file"))
342 | else:
343 | file = File(**value)
344 | return file
345 | return value
346 |
347 |
348 | # openc2 1.0 spec only supports md5, sha1, sha256
349 | HASHES_REGEX = {
350 | "md5": (r"^[A-F0-9]{32}$", "md5"),
351 | "sha1": (r"^[A-F0-9]{40}$", "sha1"),
352 | "sha256": (r"^[A-F0-9]{64}$", "sha256"),
353 | }
354 |
355 |
356 | class HashesProperty(DictionaryProperty):
357 | def clean(self, value):
358 |
359 | clean_dict = super(HashesProperty, self).clean(value)
360 | for k, v in clean_dict.items():
361 | if k in HASHES_REGEX:
362 | vocab_key = HASHES_REGEX[k][1]
363 | if not re.match(HASHES_REGEX[k][0], v):
364 | raise ValueError(
365 | "'{0}' is not a valid {1} hash".format(v, vocab_key)
366 | )
367 | if k != vocab_key:
368 | clean_dict[vocab_key] = clean_dict[k]
369 | del clean_dict[k]
370 | else:
371 | raise ValueError("'{1}' is not a valid hash".format(v, k))
372 | if len(clean_dict) < 1:
373 | raise ValueError("must not be empty.")
374 | return clean_dict
375 |
376 |
377 | class ComponentProperty(Property):
378 | def __init__(self, component_type=None, allow_custom=False, *args, **kwargs):
379 | super(ComponentProperty, self).__init__(*args, **kwargs)
380 | self.allow_custom = allow_custom
381 | self._component_type = component_type
382 |
383 | def clean(self, value):
384 | if not self._component_type:
385 | raise ValueError("This property requires a component type")
386 | dictified = {}
387 | try:
388 | if isinstance(value, _OpenC2Base):
389 | dictified[value._type] = utils._get_dict(value)
390 | else:
391 | dictified = utils._get_dict(value)
392 | except ValueError:
393 | raise ValueError("This property may only contain a dictionary or object")
394 | parsed_obj = utils.parse_component(
395 | dictified,
396 | allow_custom=self.allow_custom,
397 | component_type=self._component_type,
398 | )
399 | return parsed_obj
400 |
401 |
402 | class TargetProperty(ComponentProperty):
403 | def __init__(self, allow_custom=False, *args, **kwargs):
404 | super(TargetProperty, self).__init__(
405 | allow_custom=allow_custom, component_type="targets", *args, **kwargs
406 | )
407 |
408 |
409 | class ActuatorProperty(ComponentProperty):
410 | def __init__(self, allow_custom=False, *args, **kwargs):
411 | super(ActuatorProperty, self).__init__(
412 | allow_custom=allow_custom, component_type="actuators", *args, **kwargs
413 | )
414 |
415 |
416 | class ArgsProperty(DictionaryProperty):
417 | def __init__(self, allow_custom=True, *args, **kwargs):
418 | super(ArgsProperty, self).__init__(allow_custom, *args, **kwargs)
419 | self.allow_custom = allow_custom
420 |
421 | def clean(self, value):
422 | dictified = {}
423 | try:
424 | dictified = utils._get_dict(value)
425 | except ValueError:
426 | raise ValueError("This property may only contain a dictionary or object")
427 | parsed_obj = utils.parse_args(dictified, allow_custom=self.allow_custom)
428 | return parsed_obj
429 |
430 |
431 | def CustomProperty(type="x-acme", properties=None, version="1.0"):
432 | def wrapper(cls):
433 | _properties = list(
434 | itertools.chain.from_iterable(
435 | [
436 | [x for x in properties if not x[0].startswith("x_")],
437 | sorted(
438 | [x for x in properties if x[0].startswith("x_")],
439 | key=lambda x: x[0],
440 | ),
441 | ]
442 | )
443 | )
444 | return _custom_property_builder(cls, type, _properties, version)
445 |
446 | return wrapper
447 |
--------------------------------------------------------------------------------
/openc2/utils.py:
--------------------------------------------------------------------------------
1 | import json
2 | import copy
3 | from . import exceptions
4 | from .core import OPENC2_OBJ_MAPS
5 |
6 |
7 | def _get_data_info(data, component_type, allow_custom=False):
8 | obj = _get_dict(data)
9 | obj = copy.deepcopy(obj)
10 | try:
11 | _type = list(obj.keys())[0]
12 | nsid = _type
13 | _specifiers = list(obj.values())[0]
14 | except IndexError:
15 | raise exceptions.ParseError(
16 | "Can't parse object that contains an invalid field: %s" % str(data)
17 | )
18 |
19 | try:
20 | OBJ_MAP = OPENC2_OBJ_MAPS[component_type]
21 | obj_class = OBJ_MAP[_type]
22 | except KeyError:
23 | # check for extension
24 | try:
25 | EXT_MAP = OPENC2_OBJ_MAPS["extensions"]
26 | if component_type == "properties" and ":" in _type:
27 | obj_class = EXT_MAP[component_type][_type.split(":")[0]]
28 | else:
29 | obj_class = EXT_MAP[component_type][_type]
30 | except KeyError:
31 | if allow_custom:
32 | obj_class = dict
33 | else:
34 | raise exceptions.CustomContentError(
35 | "Can't parse unknown target/actuator type '%s'!" % _type
36 | )
37 |
38 | # extended targets
39 | if component_type == "targets" and ":" in _type:
40 | nsid, target = _type.split(":")
41 | obj = {target: obj[_type]}
42 | _type = target
43 |
44 | if isinstance(_specifiers, dict):
45 | obj = obj[_type]
46 |
47 | return (obj, obj_class, _type, nsid)
48 |
49 |
50 | def _get_dict(data):
51 | """Return data as a dictionary.
52 | Input can be a dictionary, string, or file-like object.
53 | """
54 |
55 | if type(data) is dict:
56 | return data
57 | else:
58 | try:
59 | return json.loads(data)
60 | except TypeError:
61 | pass
62 | try:
63 | return json.load(data)
64 | except AttributeError:
65 | pass
66 | try:
67 | return dict(data)
68 | except (ValueError, TypeError):
69 | raise ValueError("Cannot convert '%s' to dictionary." % str(data))
70 |
71 |
72 | def parse(data, allow_custom=False, version=None):
73 | # convert OpenC2 object to dict, if not already
74 | obj = _get_dict(data)
75 | # convert dict to full python-openc2 obj
76 | obj = dict_to_openc2(obj, allow_custom, version)
77 |
78 | return obj
79 |
80 |
81 | def dict_to_openc2(openc2_dict, allow_custom=False, version=None):
82 | message_type = None
83 | if "action" in openc2_dict:
84 | message_type = "command"
85 | elif "status" in openc2_dict:
86 | message_type = "response"
87 | else:
88 | raise exceptions.ParseError(
89 | "Can't parse object that is not valid command or response: %s"
90 | % str(openc2_dict)
91 | )
92 |
93 | OBJ_MAP = OPENC2_OBJ_MAPS["objects"]
94 | try:
95 | obj_class = OBJ_MAP[message_type]
96 | except KeyError:
97 | if allow_custom:
98 | return openc2_dict
99 | raise exceptions.ParseError(
100 | "Can't parse unknown object type '%s'! For custom types, use the CustomObject decorator."
101 | % openc2_dict["type"]
102 | )
103 |
104 | return obj_class(allow_custom=allow_custom, **openc2_dict)
105 |
106 |
107 | def parse_component(data, allow_custom=False, version=None, component_type=None):
108 | (obj, obj_class, _type, nsid) = _get_data_info(
109 | data, allow_custom=allow_custom, component_type=component_type
110 | )
111 |
112 | try:
113 | return obj_class(allow_custom=allow_custom, **obj)
114 | except:
115 | if component_type != "properties":
116 | parsed_obj = parse_component(
117 | data, allow_custom=allow_custom, component_type="properties",
118 | )
119 |
120 | sub_type = _type
121 | if nsid != _type:
122 | _type = "%s:%s" % (nsid, _type)
123 |
124 | return obj_class(**{sub_type: parsed_obj})
125 | raise
126 |
127 |
128 | def parse_target(data, allow_custom=False, version=None):
129 | return parse_component(data, allow_custom, version, component_type="targets")
130 |
131 |
132 | def parse_actuator(data, allow_custom=False, version=None):
133 | return parse_component(data, allow_custom, version, component_type="actuators")
134 |
135 |
136 | def parse_args(data, allow_custom=False, version=None):
137 | dictified = copy.deepcopy(data)
138 | default_args = list(OPENC2_OBJ_MAPS["args"]["args"]._properties.keys())
139 | specific_type_map = OPENC2_OBJ_MAPS["extensions"]["args"]
140 | # iterate over each key and if its not in the default args, check extensions
141 | for key, subvalue in dictified.items():
142 | if key in default_args:
143 | continue
144 | # handle embedded custom args
145 | if key in specific_type_map:
146 | cls = specific_type_map[key]
147 | if type(subvalue) is dict:
148 | if allow_custom:
149 | subvalue["allow_custom"] = True
150 | dictified[key] = cls(**subvalue)
151 | else:
152 | dictified[key] = cls(**subvalue)
153 | elif type(subvalue) is cls:
154 | # If already an instance of an _Extension class, assume it's valid
155 | dictified[key] = subvalue
156 | else:
157 | raise ValueError("Cannot determine extension type.")
158 | else:
159 | if allow_custom:
160 | dictified[key] = subvalue
161 | else:
162 | raise exceptions.CustomContentError(
163 | "Can't parse unknown extension type: {}".format(key)
164 | )
165 | try:
166 | OBJ_MAP = OPENC2_OBJ_MAPS["args"]
167 | obj_class = OBJ_MAP["args"]
168 | except KeyError:
169 | raise exceptions.CustomContentError("Can't parse args '%s!" % data)
170 |
171 | return obj_class(allow_custom=allow_custom, **dictified)
172 |
--------------------------------------------------------------------------------
/openc2/v10/__init__.py:
--------------------------------------------------------------------------------
1 | #
2 | # The MIT License (MIT)
3 | #
4 | # Copyright 2019 AT&T Intellectual Property. All other rights reserved.
5 | #
6 | # Permission is hereby granted, free of charge, to any person obtaining a copy of this software
7 | # and associated documentation files (the "Software"), to deal in the Software without
8 | # restriction, including without limitation the rights to use, copy, modify, merge, publish,
9 | # distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
10 | # Software is furnished to do so, subject to the following conditions:
11 | #
12 | # The above copyright notice and this permission notice shall be included in all copies or
13 | # substantial portions of the Software.
14 | #
15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
17 | # FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18 | # COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
19 | # AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20 | # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21 | #
22 |
23 | """
24 | .. module: openc2.v10
25 | :platform: Unix
26 |
27 | .. version:: $$VERSION$$
28 | .. moduleauthor:: Michael Stair
29 |
30 | """
31 |
32 | from .message import Command, Response
33 |
34 | from .targets import (
35 | Artifact,
36 | Device,
37 | DomainName,
38 | EmailAddress,
39 | Features,
40 | File,
41 | InternationalizedDomainName,
42 | InternationalizedEmailAddress,
43 | IPv4Address,
44 | IPv6Address,
45 | IPv4Connection,
46 | IPv6Connection,
47 | IRI,
48 | MACAddress,
49 | Process,
50 | Properties,
51 | URI,
52 | CustomTarget,
53 | )
54 |
55 | from .common import Payload
56 | from .args import Args, CustomArgs
57 | from .actuators import CustomActuator
58 | from .slpf import SLPFTarget, SLPFActuator, SLPFArgs
59 |
60 | OBJ_MAP = {"command": Command, "response": Response}
61 |
62 | OBJ_MAP_TARGET = {
63 | "artifact": Artifact,
64 | "device": Device,
65 | "domain_name": DomainName,
66 | "email_addr": EmailAddress,
67 | "features": Features,
68 | "file": File,
69 | "idn_domain_name": InternationalizedDomainName,
70 | "idn_email_addr": InternationalizedEmailAddress,
71 | "ipv4_net": IPv4Address,
72 | "ipv6_net": IPv6Address,
73 | "ipv4_connection": IPv4Connection,
74 | "ipv6_connection": IPv6Connection,
75 | "iri": IRI,
76 | "mac_addr": MACAddress,
77 | "process": Process,
78 | "properties": Properties,
79 | "uri": URI,
80 | "slpf:rule_number": SLPFTarget,
81 | }
82 |
83 | OBJ_MAP_ACTUATOR = {"slpf": SLPFActuator}
84 |
85 | OBJ_MAP_ARGS = {
86 | "args": Args,
87 | }
88 |
89 | EXT_MAP = {
90 | "targets": {"slpf:rule_number": SLPFTarget},
91 | "actuators": {"slpf": SLPFActuator},
92 | "args": {"slpf": SLPFArgs},
93 | "properties": {},
94 | }
95 |
--------------------------------------------------------------------------------
/openc2/v10/actuators.py:
--------------------------------------------------------------------------------
1 | #
2 | # The MIT License (MIT)
3 | #
4 | # Copyright 2019 AT&T Intellectual Property. All other rights reserved.
5 | #
6 | # Permission is hereby granted, free of charge, to any person obtaining a copy of this software
7 | # and associated documentation files (the "Software"), to deal in the Software without
8 | # restriction, including without limitation the rights to use, copy, modify, merge, publish,
9 | # distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
10 | # Software is furnished to do so, subject to the following conditions:
11 | #
12 | # The above copyright notice and this permission notice shall be included in all copies or
13 | # substantial portions of the Software.
14 | #
15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
17 | # FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18 | # COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
19 | # AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20 | # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21 | #
22 |
23 | """
24 | .. module: openc2.v10.actuators
25 | :platform: Unix
26 |
27 | .. version:: $$VERSION$$
28 | .. moduleauthor:: Michael Stair
29 |
30 | """
31 |
32 | from ..custom import _custom_actuator_builder
33 |
34 | import itertools
35 |
36 | # from collections import OrderedDict
37 |
38 |
39 | def CustomActuator(type="x-acme", properties=None):
40 | def wrapper(cls):
41 | _properties = list(
42 | itertools.chain.from_iterable(
43 | [
44 | [x for x in properties if not x[0].startswith("x_")],
45 | sorted(
46 | [x for x in properties if x[0].startswith("x_")],
47 | key=lambda x: x[0],
48 | ),
49 | ]
50 | )
51 | )
52 | return _custom_actuator_builder(cls, type, _properties, "2.1")
53 |
54 | return wrapper
55 |
--------------------------------------------------------------------------------
/openc2/v10/args.py:
--------------------------------------------------------------------------------
1 | #
2 | # The MIT License (MIT)
3 | #
4 | # Copyright 2019 AT&T Intellectual Property. All other rights reserved.
5 | #
6 | # Permission is hereby granted, free of charge, to any person obtaining a copy of this software
7 | # and associated documentation files (the "Software"), to deal in the Software without
8 | # restriction, including without limitation the rights to use, copy, modify, merge, publish,
9 | # distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
10 | # Software is furnished to do so, subject to the following conditions:
11 | #
12 | # The above copyright notice and this permission notice shall be included in all copies or
13 | # substantial portions of the Software.
14 | #
15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
17 | # FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18 | # COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
19 | # AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20 | # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21 | #
22 |
23 | """
24 | .. module: openc2.v10.args
25 | :platform: Unix
26 |
27 | .. version:: $$VERSION$$
28 | .. moduleauthor:: Michael Stair
29 |
30 | """
31 | import openc2
32 | from ..custom import _custom_args_builder
33 |
34 | import itertools
35 | from collections import OrderedDict
36 |
37 |
38 | class Args(openc2.base._OpenC2Base):
39 | _type = "args"
40 | _properties = OrderedDict(
41 | [
42 | ("start_time", openc2.properties.DateTimeProperty()),
43 | ("stop_time", openc2.properties.DateTimeProperty()),
44 | ("duration", openc2.properties.IntegerProperty(min=0)),
45 | (
46 | "response_requested",
47 | openc2.properties.EnumProperty(
48 | allowed=["none", "ack", "status", "complete"]
49 | ),
50 | ),
51 | ]
52 | )
53 |
54 | def check_object_constraints(self):
55 | super(Args, self).check_object_constraints()
56 | if "stop_time" in self and "start_time" in self and "duration" in self:
57 | raise openc2.exceptions.PropertyPresenceError(
58 | "start_time, stop_time, duration: Only two of the three are allowed on any given Command and the third is derived from the equation stop_time = start_time + duration.",
59 | self.__class__,
60 | )
61 |
62 | if "stop_time" in self and "start_time" in self:
63 | if self.stop_time < self.start_time:
64 | raise openc2.exceptions.InvalidValueError(
65 | self.__class__,
66 | "stop_time",
67 | reason="stop_time must be greater than start_time",
68 | )
69 |
70 |
71 | def CustomArgs(type="x-acme", properties=None):
72 | def wrapper(cls):
73 | _properties = list(
74 | itertools.chain.from_iterable(
75 | [
76 | [x for x in properties if not x[0].startswith("x_")],
77 | sorted(
78 | [x for x in properties if x[0].startswith("x_")],
79 | key=lambda x: x[0],
80 | ),
81 | ]
82 | )
83 | )
84 | return _custom_args_builder(cls, type, _properties, "2.1")
85 |
86 | return wrapper
87 |
--------------------------------------------------------------------------------
/openc2/v10/common.py:
--------------------------------------------------------------------------------
1 | #
2 | # The MIT License (MIT)
3 | #
4 | # Copyright 2019 AT&T Intellectual Property. All other rights reserved.
5 | #
6 | # Permission is hereby granted, free of charge, to any person obtaining a copy of this software
7 | # and associated documentation files (the "Software"), to deal in the Software without
8 | # restriction, including without limitation the rights to use, copy, modify, merge, publish,
9 | # distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
10 | # Software is furnished to do so, subject to the following conditions:
11 | #
12 | # The above copyright notice and this permission notice shall be included in all copies or
13 | # substantial portions of the Software.
14 | #
15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
17 | # FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18 | # COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
19 | # AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20 | # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21 | #
22 |
23 | """
24 | .. module: openc2.v10.common
25 | :platform: Unix
26 |
27 | .. version:: $$VERSION$$
28 | .. moduleauthor:: Michael Stair
29 |
30 | """
31 |
32 | import openc2
33 | from collections import OrderedDict
34 |
35 |
36 | class Payload(openc2.base._OpenC2DataType):
37 | _type = "payload"
38 | _properties = OrderedDict(
39 | [
40 | ("bin", openc2.properties.BinaryProperty()),
41 | ("url", openc2.properties.StringProperty()),
42 | ]
43 | )
44 |
45 | def check_object_constraints(self):
46 | super(Payload, self).check_object_constraints()
47 | self.check_mutually_exclusive_properties(["bin", "url"])
48 |
--------------------------------------------------------------------------------
/openc2/v10/message.py:
--------------------------------------------------------------------------------
1 | #
2 | # The MIT License (MIT)
3 | #
4 | # Copyright 2019 AT&T Intellectual Property. All other rights reserved.
5 | #
6 | # Permission is hereby granted, free of charge, to any person obtaining a copy of this software
7 | # and associated documentation files (the "Software"), to deal in the Software without
8 | # restriction, including without limitation the rights to use, copy, modify, merge, publish,
9 | # distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
10 | # Software is furnished to do so, subject to the following conditions:
11 | #
12 | # The above copyright notice and this permission notice shall be included in all copies or
13 | # substantial portions of the Software.
14 | #
15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
17 | # FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18 | # COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
19 | # AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20 | # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21 | #
22 |
23 | """
24 | .. module: openc2.v10.message
25 | :platform: Unix
26 |
27 | .. version:: $$VERSION$$
28 | .. moduleauthor:: Michael Stair
29 |
30 | """
31 |
32 | import openc2
33 |
34 | from collections import OrderedDict
35 |
36 |
37 | class Command(openc2.base._OpenC2Base):
38 | _type = "command"
39 | _properties = OrderedDict(
40 | [
41 | (
42 | "action",
43 | openc2.properties.EnumProperty(
44 | allowed=[
45 | "scan",
46 | "locate",
47 | "query",
48 | "deny",
49 | "contain",
50 | "allow",
51 | "start",
52 | "stop",
53 | "restart",
54 | "cancel",
55 | "set",
56 | "update",
57 | "redirect",
58 | "create",
59 | "delete",
60 | "detonate",
61 | "restore",
62 | "copy",
63 | "investigate",
64 | "remediate",
65 | ],
66 | required=True,
67 | ),
68 | ),
69 | ("target", openc2.properties.TargetProperty(required=True)),
70 | ("args", openc2.properties.ArgsProperty()),
71 | ("actuator", openc2.properties.ActuatorProperty()),
72 | ("command_id", openc2.properties.StringProperty()),
73 | ]
74 | )
75 |
76 |
77 | class Response(openc2.base._OpenC2Base):
78 | _type = "response"
79 | _properties = OrderedDict(
80 | [
81 | ("status", openc2.properties.IntegerProperty(required=True)),
82 | ("status_text", openc2.properties.StringProperty()),
83 | ("results", openc2.properties.DictionaryProperty()),
84 | ]
85 | )
86 |
--------------------------------------------------------------------------------
/openc2/v10/slpf.py:
--------------------------------------------------------------------------------
1 | #
2 | # The MIT License (MIT)
3 | #
4 | # Copyright 2019 AT&T Intellectual Property. All other rights reserved.
5 | #
6 | # Permission is hereby granted, free of charge, to any person obtaining a copy of this software
7 | # and associated documentation files (the "Software"), to deal in the Software without
8 | # restriction, including without limitation the rights to use, copy, modify, merge, publish,
9 | # distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
10 | # Software is furnished to do so, subject to the following conditions:
11 | #
12 | # The above copyright notice and this permission notice shall be included in all copies or
13 | # substantial portions of the Software.
14 | #
15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
17 | # FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18 | # COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
19 | # AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20 | # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21 | #
22 |
23 | """
24 | .. module: openc2.v10.slpf
25 | :platform: Unix
26 |
27 | .. version:: $$VERSION$$
28 | .. moduleauthor:: Michael Stair
29 |
30 | """
31 | import openc2
32 |
33 | import itertools
34 | from collections import OrderedDict
35 |
36 |
37 | class SLPFActuator(openc2.base._Actuator):
38 | _type = "slpf"
39 | _properties = OrderedDict(
40 | [
41 | ("hostname", openc2.properties.StringProperty()),
42 | ("named_group", openc2.properties.StringProperty()),
43 | ("asset_id", openc2.properties.StringProperty()),
44 | (
45 | "asset_tuple",
46 | openc2.properties.ListProperty(openc2.properties.StringProperty),
47 | ),
48 | ]
49 | )
50 |
51 | def check_object_constraints(self):
52 | super(SLPFActuator, self).check_object_constraints()
53 |
54 | if "asset_tuple" in self:
55 | if len(self.asset_tuple) > 10:
56 | raise openc2.exceptions.InvalidValueError(
57 | self.__class__, "asset_tuple", "Maximum of 10 features allowed"
58 | )
59 |
60 |
61 | class SLPFTarget(openc2.base._Target):
62 | _type = "slpf:rule_number"
63 | _properties = OrderedDict(
64 | [("rule_number", openc2.properties.StringProperty(required=True)),]
65 | )
66 |
67 |
68 | class SLPFArgs(openc2.base._OpenC2Base):
69 | _type = "slpf"
70 | _properties = OrderedDict(
71 | [
72 | (
73 | "drop_process",
74 | openc2.properties.EnumProperty(
75 | allowed=["none", "reject", "false_ack",]
76 | ),
77 | ),
78 | (
79 | "persistent",
80 | openc2.properties.EnumProperty(
81 | allowed=["none", "reject", "false_ack",]
82 | ),
83 | ),
84 | (
85 | "direction",
86 | openc2.properties.EnumProperty(allowed=["both", "ingress", "egress",]),
87 | ),
88 | ("insert_rule", openc2.properties.IntegerProperty()),
89 | ]
90 | )
91 |
92 |
93 | class SLPF(openc2.base._OpenC2Base):
94 | _type = "slpf"
95 | _properties = OrderedDict(
96 | [
97 | (
98 | "action",
99 | openc2.properties.EnumProperty(
100 | allowed=["query", "deny", "allow", "update", "delete",],
101 | required=True,
102 | ),
103 | ),
104 | ("target", openc2.properties.TargetProperty(required=True)),
105 | ("args", openc2.properties.ArgsProperty()),
106 | ("actuator", openc2.properties.ActuatorProperty()),
107 | ("command_id", openc2.properties.StringProperty()),
108 | ]
109 | )
110 |
111 | def check_object_constraints(self):
112 | super(SLPF, self).check_object_constraints()
113 | if not isinstance(self.target, openc2.base._Target) or not self.target.type in [
114 | "features",
115 | "file",
116 | "ipv4_net",
117 | "ipv6_net",
118 | "ipv4_connection",
119 | "ipv6_connection",
120 | "slpf:rule_number",
121 | ]:
122 | raise ValueError("Unsupported target (%s)" % self.target)
123 | if "actuator" in self and not isinstance(self.actuator, SLPFActuator):
124 | raise ValueError("Unsupported actuator (%s)" % self.actuator._type)
125 |
--------------------------------------------------------------------------------
/openc2/v10/targets.py:
--------------------------------------------------------------------------------
1 | #
2 | # The MIT License (MIT)
3 | #
4 | # Copyright 2019 AT&T Intellectual Property. All other rights reserved.
5 | #
6 | # Permission is hereby granted, free of charge, to any person obtaining a copy of this software
7 | # and associated documentation files (the "Software"), to deal in the Software without
8 | # restriction, including without limitation the rights to use, copy, modify, merge, publish,
9 | # distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
10 | # Software is furnished to do so, subject to the following conditions:
11 | #
12 | # The above copyright notice and this permission notice shall be included in all copies or
13 | # substantial portions of the Software.
14 | #
15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
17 | # FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18 | # COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
19 | # AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20 | # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21 | #
22 |
23 | """
24 | .. module: openc2.v10.targets
25 | :platform: Unix
26 |
27 | .. version:: $$VERSION$$
28 | .. moduleauthor:: Michael Stair
29 |
30 | """
31 |
32 |
33 | import openc2
34 |
35 | import itertools
36 | import copy
37 | from collections import OrderedDict
38 |
39 |
40 | class Artifact(openc2.base._Target):
41 | _type = "artifact"
42 | _properties = OrderedDict(
43 | [
44 | ("mime_type", openc2.properties.StringProperty()),
45 | ("payload", openc2.properties.PayloadProperty()),
46 | ("hashes", openc2.properties.HashesProperty()),
47 | ]
48 | )
49 |
50 | def check_object_constraints(self):
51 | super(Artifact, self).check_object_constraints()
52 | self.check_at_least_one_property()
53 |
54 |
55 | class Device(openc2.base._Target):
56 | _type = "device"
57 | _properties = OrderedDict(
58 | [
59 | ("hostname", openc2.properties.StringProperty()),
60 | ("idn_hostname", openc2.properties.StringProperty()),
61 | ("device_id", openc2.properties.StringProperty()),
62 | ]
63 | )
64 |
65 |
66 | class DomainName(openc2.base._Target):
67 | _type = "domain_name"
68 | _properties = OrderedDict(
69 | [("domain_name", openc2.properties.StringProperty(required=True)),]
70 | )
71 |
72 |
73 | class EmailAddress(openc2.base._Target):
74 | _type = "email_addr"
75 | _properties = OrderedDict(
76 | [("email_addr", openc2.properties.StringProperty(required=True)),]
77 | )
78 |
79 |
80 | class Features(openc2.base._Target):
81 | _type = "features"
82 | _properties = OrderedDict(
83 | [
84 | (
85 | "features",
86 | openc2.properties.ListProperty(
87 | openc2.properties.EnumProperty(
88 | allowed=["versions", "pairs", "profiles", "rate_limit"]
89 | ),
90 | default=lambda: [],
91 | ),
92 | )
93 | ]
94 | )
95 |
96 | def __init__(self, features=None, *args, **kwargs):
97 | # check_object_constraints wasn't called unless features was declared here
98 | super(Features, self).__init__(features=features, *args, **kwargs)
99 |
100 | def check_object_constraints(self):
101 | super(Features, self).check_object_constraints()
102 |
103 | seen = []
104 | for feature in self.features:
105 | if feature in seen:
106 | raise openc2.exceptions.InvalidValueError(
107 | self.__class__,
108 | "features",
109 | "A Producer MUST NOT send a list containing more than one instance of any Feature.",
110 | )
111 | seen.append(feature)
112 |
113 |
114 | class File(openc2.base._Target):
115 | _type = "file"
116 | _properties = OrderedDict(
117 | [
118 | ("name", openc2.properties.StringProperty()),
119 | ("path", openc2.properties.StringProperty()),
120 | ("hashes", openc2.properties.HashesProperty()),
121 | ]
122 | )
123 |
124 | def check_object_constraints(self):
125 | super(File, self).check_object_constraints()
126 | self.check_at_least_one_property()
127 |
128 |
129 | class InternationalizedDomainName(openc2.base._Target):
130 | _type = "idn_domain_name"
131 | _properties = OrderedDict(
132 | [("idn_domain_name", openc2.properties.StringProperty(required=True)),]
133 | )
134 |
135 |
136 | class InternationalizedEmailAddress(openc2.base._Target):
137 | _type = "idn_email_addr"
138 | _properties = OrderedDict(
139 | [("idn_email_addr", openc2.properties.StringProperty(required=True)),]
140 | )
141 |
142 |
143 | class IPv4Address(openc2.base._Target):
144 | _type = "ipv4_net"
145 | _properties = OrderedDict(
146 | [("ipv4_net", openc2.properties.StringProperty(required=True)),]
147 | )
148 |
149 |
150 | class IPv6Address(openc2.base._Target):
151 | _type = "ipv6_net"
152 | _properties = OrderedDict(
153 | [("ipv6_net", openc2.properties.StringProperty(required=True)),]
154 | )
155 |
156 |
157 | class IPv4Connection(openc2.base._Target):
158 | _type = "ipv4_connection"
159 | _properties = OrderedDict(
160 | [
161 | ("src_addr", openc2.properties.StringProperty()),
162 | ("src_port", openc2.properties.IntegerProperty(min=0, max=65535)),
163 | ("dst_addr", openc2.properties.StringProperty()),
164 | ("dst_port", openc2.properties.IntegerProperty(min=0, max=65535)),
165 | (
166 | "protocol",
167 | openc2.properties.EnumProperty(allowed=["icmp", "tcp", "udp", "sctp"]),
168 | ),
169 | ]
170 | )
171 |
172 | def check_object_constraints(self):
173 | super(IPv4Connection, self).check_object_constraints()
174 | self.check_at_least_one_property()
175 |
176 |
177 | class IPv6Connection(openc2.base._Target):
178 | _type = "ipv6_connection"
179 | _properties = OrderedDict(
180 | [
181 | ("src_addr", openc2.properties.StringProperty()),
182 | ("src_port", openc2.properties.IntegerProperty(min=0, max=65535)),
183 | ("dst_addr", openc2.properties.StringProperty()),
184 | ("dst_port", openc2.properties.IntegerProperty(min=0, max=65535)),
185 | (
186 | "protocol",
187 | openc2.properties.EnumProperty(allowed=["icmp", "tcp", "udp", "sctp"]),
188 | ),
189 | ]
190 | )
191 |
192 | def check_object_constraints(self):
193 | super(IPv6Connection, self).check_object_constraints()
194 | self.check_at_least_one_property()
195 |
196 |
197 | class IRI(openc2.base._Target):
198 | _type = "iri"
199 | _properties = OrderedDict(
200 | [("iri", openc2.properties.StringProperty(required=True)),]
201 | )
202 |
203 |
204 | class MACAddress(openc2.base._Target):
205 | _type = "mac_addr"
206 | _properties = OrderedDict(
207 | [("mac_addr", openc2.properties.StringProperty(required=True)),]
208 | )
209 |
210 |
211 | class Process(openc2.base._Target):
212 | _type = "process"
213 | _properties = OrderedDict(
214 | [
215 | ("pid", openc2.properties.IntegerProperty()),
216 | ("name", openc2.properties.StringProperty()),
217 | ("cwd", openc2.properties.StringProperty()),
218 | ("executable", openc2.properties.FileProperty()),
219 | ("parent", openc2.properties.ProcessProperty()),
220 | ("command_line", openc2.properties.StringProperty()),
221 | ]
222 | )
223 |
224 | def __init__(self, allow_custom=False, **kwargs):
225 | super(Process, self).__init__(allow_custom, **kwargs)
226 |
227 | def check_object_constraints(self):
228 | super(Process, self).check_object_constraints()
229 | self.check_at_least_one_property()
230 |
231 |
232 | class Properties(openc2.base._Target):
233 | _type = "properties"
234 | _properties = OrderedDict(
235 | [
236 | (
237 | "properties",
238 | openc2.properties.ListProperty(openc2.properties.StringProperty),
239 | ),
240 | ]
241 | )
242 |
243 |
244 | class URI(openc2.base._Target):
245 | _type = "uri"
246 | _properties = OrderedDict(
247 | [("uri", openc2.properties.StringProperty(required=True)),]
248 | )
249 |
250 |
251 | def CustomTarget(type="x-acme", properties=None):
252 | def wrapper(cls):
253 | _properties = list(
254 | itertools.chain.from_iterable(
255 | [
256 | [x for x in properties if not x[0].startswith("x_")],
257 | sorted(
258 | [x for x in properties if x[0].startswith("x_")],
259 | key=lambda x: x[0],
260 | ),
261 | ]
262 | )
263 | )
264 | return openc2.custom._custom_target_builder(cls, type, _properties, "2.1")
265 |
266 | return wrapper
267 |
--------------------------------------------------------------------------------
/openc2/version.py:
--------------------------------------------------------------------------------
1 | #
2 | # The MIT License (MIT)
3 | #
4 | # Copyright 2019 AT&T Intellectual Property. All other rights reserved.
5 | #
6 | # Permission is hereby granted, free of charge, to any person obtaining a copy of this software
7 | # and associated documentation files (the "Software"), to deal in the Software without
8 | # restriction, including without limitation the rights to use, copy, modify, merge, publish,
9 | # distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
10 | # Software is furnished to do so, subject to the following conditions:
11 | #
12 | # The above copyright notice and this permission notice shall be included in all copies or
13 | # substantial portions of the Software.
14 | #
15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
17 | # FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18 | # COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
19 | # AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20 | # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21 | #
22 |
23 | """
24 | .. module: openc2.version
25 | :platform: Unix
26 |
27 | .. version:: $$VERSION$$
28 | .. moduleauthor:: Michael Stair
29 |
30 | """
31 |
32 | __version__ = "2.0.0"
33 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | #
2 | # The MIT License (MIT)
3 | #
4 | # Copyright 2019 AT&T Intellectual Property. All other rights reserved.
5 | #
6 | # Permission is hereby granted, free of charge, to any person obtaining a copy of this software
7 | # and associated documentation files (the "Software"), to deal in the Software without
8 | # restriction, including without limitation the rights to use, copy, modify, merge, publish,
9 | # distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
10 | # Software is furnished to do so, subject to the following conditions:
11 | #
12 | # The above copyright notice and this permission notice shall be included in all copies or
13 | # substantial portions of the Software.
14 | #
15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
17 | # FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18 | # COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
19 | # AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20 | # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21 | #
22 |
23 | import os
24 | from distutils.core import setup
25 | from setuptools import find_packages
26 |
27 | BASE_DIR = os.path.dirname(os.path.abspath(__file__))
28 | VERSION_FILE = os.path.join(BASE_DIR, 'openc2', 'version.py')
29 |
30 | def get_version():
31 | with open(VERSION_FILE) as f:
32 | for line in f.readlines():
33 | if line.startswith('__version__'):
34 | version = line.split()[-1].strip('"')
35 | return version
36 | raise AttributeError("Package does not have a __version__")
37 |
38 | setup(
39 | name='openc2',
40 | version=get_version(),
41 | description='Produce and consume OpenC2 JSON messages',
42 | author='OASIS Open Command and Control (OpenC2) Technical Committee (TC)',
43 | url='https://github.com/oasis-open/openc2-lycan-python',
44 | long_description_content_type='text/markdown',
45 | long_description=open('README.md').read(),
46 | keywords='openc2 cyber',
47 | packages=find_packages(exclude=["tests"]),
48 | license='MIT',
49 | include_package_data=True,
50 | install_requires=[],
51 | extra_require={
52 | 'stix': ['stix2']
53 | }
54 | )
55 |
--------------------------------------------------------------------------------
/tests/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oasis-open/openc2-lycan-python/8eca0db245aae8f3b1985b1535f4e551a51b590f/tests/__init__.py
--------------------------------------------------------------------------------
/tests/generate.py:
--------------------------------------------------------------------------------
1 | from openc2 import parse
2 | from openc2 import (
3 | parse,
4 | Command,
5 | Args,
6 | Payload,
7 | Device,
8 | DomainName,
9 | EmailAddress,
10 | IPv4Connection,
11 | Response,
12 | File,
13 | Artifact,
14 | Process,
15 | Features,
16 | InternationalizedDomainName,
17 | InternationalizedEmailAddress,
18 | IPv4Address,
19 | IPv6Address,
20 | IPv6Connection,
21 | MACAddress,
22 | IRI,
23 | Properties,
24 | URI,
25 | )
26 |
27 | # Artifact
28 | hashes = {
29 | "sha1": "1234567890ABCDEF1234567890ABCDEF12345678",
30 | "sha256": "1234567890ABCDEF1234567890ABCDEF1234567890ABCDEF1234567890ABDEF1",
31 | "md5": "1234567890ABCDEF1234567890ABCDEF",
32 | }
33 | p = Payload(url="www.testurl.com")
34 | a = Artifact(payload=p, hashes=hashes, mime_type="My MIME Type")
35 | cmd = Command(action="contain", target=a)
36 | with open("Artifact.json", "w") as OUT:
37 | OUT.write(cmd.serialize(pretty=True) + "\n")
38 |
39 | # Artifact with args
40 | cmd = Command(
41 | action="start",
42 | target=a,
43 | args=Args(
44 | start_time=1568209029693,
45 | stop_time=1568209059693,
46 | response_requested="complete",
47 | ),
48 | )
49 | with open("Artifact2.json", "w") as OUT:
50 | OUT.write(cmd.serialize(pretty=True) + "\n")
51 |
52 | # Artifact with args
53 | cmd = Command(
54 | action="start",
55 | target=a,
56 | args=Args(duration=30000, start_time=1568209029693, response_requested="complete",),
57 | )
58 | with open("Artifact3.json", "w") as OUT:
59 | OUT.write(cmd.serialize(pretty=True) + "\n")
60 |
61 | # Artifact with bin
62 | cmd = Command(
63 | action="stop",
64 | target=Artifact(
65 | payload=Payload(bin="YmluIGRhdGE="), hashes=hashes, mime_type="My MIME Type"
66 | ),
67 | )
68 | with open("Artifact4.json", "w") as OUT:
69 | OUT.write(cmd.serialize(pretty=True) + "\n")
70 |
71 | # Artifact (update)
72 | cmd = Command(action="update", target=a)
73 | with open("Artifact5.json", "w") as OUT:
74 | OUT.write(cmd.serialize(pretty=True) + "\n")
75 |
76 | # Device
77 | cmd = Command(
78 | action="allow",
79 | target=Device(
80 | hostname="device hostname",
81 | idn_hostname="device idn hostname",
82 | device_id="Device id",
83 | ),
84 | )
85 | with open("Device.json", "w") as OUT:
86 | OUT.write(cmd.serialize(pretty=True) + "\n")
87 |
88 | # Domain Name
89 | cmd = Command(action="cancel", target=DomainName(domain_name="Domain name"))
90 | with open("DomainName.json", "w") as OUT:
91 | OUT.write(cmd.serialize(pretty=True) + "\n")
92 |
93 | # Email Address
94 | cmd = Command(action="copy", target=EmailAddress(email_addr="Email address"))
95 | with open("EmailAddress.json", "w") as OUT:
96 | OUT.write(cmd.serialize(pretty=True) + "\n")
97 |
98 | # Features
99 | cmd = Command(
100 | action="create",
101 | target=Features(features=["versions", "profiles", "pairs", "rate_limit"]),
102 | )
103 | with open("Features.json", "w") as OUT:
104 | OUT.write(cmd.serialize(pretty=True) + "\n")
105 |
106 | # File
107 | f = File(name="File name", path="File path", hashes=hashes)
108 | cmd = Command(action="delete", target=f)
109 | with open("File.json", "w") as OUT:
110 | OUT.write(cmd.serialize(pretty=True) + "\n")
111 |
112 | # IdnDomainName
113 | cmd = Command(
114 | action="deny", target=InternationalizedDomainName(idn_domain_name="IDN Domain name")
115 | )
116 | with open("IdnDomainName.json", "w") as OUT:
117 | OUT.write(cmd.serialize(pretty=True) + "\n")
118 |
119 | # IdnEmailAddress
120 | cmd = Command(
121 | action="detonate",
122 | target=InternationalizedEmailAddress(idn_email_addr="IDN Email address"),
123 | )
124 | with open("IdnEmailAddress.json", "w") as OUT:
125 | OUT.write(cmd.serialize(pretty=True) + "\n")
126 |
127 | # Ipv4Connection
128 | cmd = Command(
129 | action="investigate",
130 | target=IPv4Connection(
131 | src_addr="10.0.0.0/24",
132 | src_port=8443,
133 | dst_addr="10.0.0.0/24",
134 | dst_port=9443,
135 | protocol="tcp",
136 | ),
137 | )
138 | with open("Ipv4Connection.json", "w") as OUT:
139 | OUT.write(cmd.serialize(pretty=True) + "\n")
140 |
141 | # Ipv4Net
142 | cmd = Command(action="locate", target=IPv4Address(ipv4_net="10.0.0.0/24"))
143 | with open("Ipv4Net.json", "w") as OUT:
144 | OUT.write(cmd.serialize(pretty=True) + "\n")
145 |
146 | # Ipv6Connection
147 | cmd = Command(
148 | action="query",
149 | target=IPv6Connection(
150 | src_addr="AE:00:E4:F1:04:65/24",
151 | src_port=8443,
152 | dst_addr="AE:00:E4:F1:04:65/24",
153 | dst_port=9443,
154 | protocol="tcp",
155 | ),
156 | )
157 | with open("Ipv6Connection.json", "w") as OUT:
158 | OUT.write(cmd.serialize(pretty=True) + "\n")
159 |
160 | # Ipv6Net
161 | cmd = Command(action="locate", target=IPv6Address(ipv6_net="AE:00:E4:F1:04:65/24"))
162 | with open("Ipv6Net.json", "w") as OUT:
163 | OUT.write(cmd.serialize(pretty=True) + "\n")
164 |
165 | # Iri
166 | cmd = Command(action="remediate", target=IRI(iri="My IRI identifier"))
167 | with open("Iri.json", "w") as OUT:
168 | OUT.write(cmd.serialize(pretty=True) + "\n")
169 |
170 | # MacAddress
171 | cmd = Command(
172 | action="restart", target=MACAddress(mac_addr="VGhpcyBpcyBteSBtYWMgYWRkcmVzcw==")
173 | )
174 | with open("MacAddress.json", "w") as OUT:
175 | OUT.write(cmd.serialize(pretty=True) + "\n")
176 |
177 | # Parent
178 | parent = Process(pid=43521, name="Process parent name", cwd="Process parent CWD")
179 | cmd = Command(
180 | action="restore",
181 | target=Process(
182 | pid=12354,
183 | name="Process name",
184 | cwd="Process CWD",
185 | executable=f,
186 | parent=parent,
187 | command_line="Process command line statement",
188 | ),
189 | )
190 | with open("Process.json", "w") as OUT:
191 | OUT.write(cmd.serialize(pretty=True) + "\n")
192 |
193 | # Properties
194 | cmd = Command(action="set", target=URI(uri="www.myuri.com"))
195 | with open("URI.json", "w") as OUT:
196 | OUT.write(cmd.serialize(pretty=True) + "\n")
197 |
198 | # Properties
199 | cmd = Command(
200 | action="scan", target=Properties(properties=["Tag1", "Tag2", "Tag3", "Tag4"])
201 | )
202 | with open("Properties.json", "w") as OUT:
203 | OUT.write(cmd.serialize(pretty=True) + "\n")
204 |
--------------------------------------------------------------------------------
/tests/test_actuator.py:
--------------------------------------------------------------------------------
1 | import openc2
2 | import pytest
3 | import json
4 | import sys
5 |
6 |
7 | def test_actuator_requested():
8 | @openc2.v10.CustomActuator("x-thing", [("id", openc2.properties.StringProperty())])
9 | class MyCustomActuator(object):
10 | pass
11 |
12 | foo = MyCustomActuator(id="id")
13 | assert foo
14 | assert foo.id == "id"
15 |
16 | bar = openc2.utils.parse_actuator(json.loads(foo.serialize()))
17 | assert bar == foo
18 |
19 | with pytest.raises(ValueError):
20 |
21 | @openc2.v10.CustomActuator(
22 | "invalid_target", [("id", openc2.properties.StringProperty())]
23 | )
24 | class CustomInvalid(object):
25 | pass
26 |
27 | with pytest.raises(ValueError):
28 |
29 | @openc2.v10.CustomActuator(
30 | "over_16_chars_long_aaaaaaaaaaaaaaaaaaaa",
31 | [("id", openc2.properties.StringProperty())],
32 | )
33 | class CustomInvalid(object):
34 | pass
35 |
36 | with pytest.raises(ValueError):
37 |
38 | @openc2.v10.CustomActuator("x-thing:noprops", [])
39 | class CustomInvalid(object):
40 | pass
41 |
--------------------------------------------------------------------------------
/tests/test_args.py:
--------------------------------------------------------------------------------
1 | import openc2
2 | import pytest
3 | import json
4 | import sys
5 | import datetime
6 |
7 |
8 | def test_args_response_requested():
9 | valid = ["none", "ack", "status", "complete"]
10 | for v in valid:
11 | a = openc2.v10.Args(response_requested=v)
12 | print(a)
13 | ser_a = json.loads(a.serialize())
14 | b = openc2.v10.Args(**ser_a)
15 | assert a == b
16 |
17 | with pytest.raises(openc2.exceptions.InvalidValueError):
18 | openc2.v10.Args(response_requested="bad")
19 |
20 |
21 | def test_args_eq():
22 | foo = openc2.v10.Args(response_requested="none")
23 | bar = openc2.v10.Args(response_requested="none")
24 | assert foo == bar
25 |
26 | bar = openc2.v10.Args(response_requested="status")
27 | assert foo != bar
28 |
29 |
30 | def test_args_property():
31 | foo = openc2.properties.ArgsProperty()
32 | assert foo.clean({"duration": 500})
33 | with pytest.raises(ValueError):
34 | foo.clean("bad")
35 |
36 |
37 | def test_args_start_time():
38 | # Value is the number of milliseconds since 00:00:00 UTC, 1 January 1970
39 |
40 | for time in range(0, 15):
41 | a = openc2.v10.Args(start_time=time)
42 | assert a.start_time == time
43 |
44 | invalid = ["a", "invalid", sys.maxsize]
45 |
46 | for item in invalid:
47 | with pytest.raises(openc2.exceptions.InvalidValueError):
48 | openc2.v10.Args(start_time=item)
49 |
50 | # xxx: floats are currently valid and passing
51 |
52 |
53 | def test_args_stop_time():
54 | # Value is the number of milliseconds since 00:00:00 UTC, 1 January 1970
55 |
56 | for time in range(0, 15):
57 | a = openc2.v10.Args(stop_time=time)
58 | assert a.stop_time == time
59 |
60 | invalid = ["a", "invalid", sys.maxsize]
61 |
62 | for item in invalid:
63 | with pytest.raises(openc2.exceptions.InvalidValueError):
64 | openc2.v10.Args(stop_time=item)
65 |
66 | # xxx: floats etc is currently valid and passing
67 |
68 |
69 | def test_args_duration():
70 | # Value is the number of milliseconds since 00:00:00 UTC, 1 January 1970
71 |
72 | for time in range(0, 15):
73 | a = openc2.v10.Args(duration=time)
74 | assert a.duration == time
75 |
76 | a = openc2.v10.Args(duration=sys.maxsize)
77 | assert a.duration == sys.maxsize
78 |
79 | invalid = ["a", "invalid", -1]
80 |
81 | for item in invalid:
82 | with pytest.raises(openc2.exceptions.InvalidValueError):
83 | openc2.v10.Args(duration=item)
84 |
85 | try:
86 | openc2.v10.Args(duration=invalid[0])
87 | except Exception as e:
88 | assert "Invalid value" in str(e)
89 |
90 | # xxx: floats etc is currently valid and passing
91 |
92 |
93 | def test_args_combination():
94 | # Only two of the three are allowed on any given Command and the third is derived from the equation stop_time = start_time + duration.
95 | with pytest.raises(openc2.exceptions.PropertyPresenceError):
96 | openc2.v10.Args(start_time=1, stop_time=1, duration=1)
97 |
98 | with pytest.raises(openc2.exceptions.InvalidValueError):
99 | openc2.v10.Args(start_time=1, stop_time=0)
100 |
101 |
102 | def test_args_allow_custom():
103 | a = openc2.v10.Args(duration=sys.maxsize)
104 | foo = openc2.v10.Args(allow_custom=True, what="who", item=a)
105 |
106 | with pytest.raises(AttributeError):
107 | openc2.utils.parse_args(foo.serialize())
108 |
109 | with pytest.raises(openc2.exceptions.CustomContentError):
110 | bar = openc2.utils.parse_args(json.loads(foo.serialize()))
111 |
112 | bar = openc2.utils.parse_args(json.loads(foo.serialize()), allow_custom=True)
113 | assert foo == bar
114 |
115 | bar = foo.clone(what="what")
116 | assert foo.what == "who" and bar.what == "what"
117 | assert bar.item == a and foo.item == a
118 |
119 | try:
120 | foo.clone(type="bad")
121 | except Exception as e:
122 | assert "These properties cannot be changed" in str(e)
123 |
124 |
125 | def test_args_custom():
126 | with pytest.raises(openc2.exceptions.PropertyPresenceError):
127 |
128 | @openc2.v10.CustomArgs(
129 | "custom-args", [("type", openc2.properties.StringProperty())]
130 | )
131 | class MyCustomArgs(object):
132 | pass
133 |
134 | with pytest.raises(TypeError):
135 |
136 | @openc2.v10.CustomArgs(
137 | "custom-args", ("type", openc2.properties.StringProperty())
138 | )
139 | class MyCustomArgs(object):
140 | pass
141 |
142 | with pytest.raises(ValueError):
143 |
144 | @openc2.v10.CustomArgs("custom-args", [])
145 | class MyCustomArgs(object):
146 | pass
147 |
148 | @openc2.v10.CustomArgs("custom-args", [("id", openc2.properties.StringProperty())])
149 | class MyCustomArgs(object):
150 | pass
151 |
152 | foo = MyCustomArgs(id="value")
153 | assert foo.id == "value"
154 |
155 | bar = openc2.utils.parse_args(json.loads(foo.serialize()), allow_custom=True)
156 | assert bar == foo
157 |
158 | args = {"custom-args": json.loads(foo.serialize())}
159 |
160 | bar = openc2.utils.parse_args(args, allow_custom=True)
161 | assert bar["custom-args"] == foo
162 |
163 | args = {"bad-custom-args": json.loads(foo.serialize())}
164 |
165 | with pytest.raises(openc2.exceptions.CustomContentError):
166 | bar = openc2.utils.parse_args(args)
167 |
168 | args = {"custom-args": MyCustomArgs(id="value")}
169 | bar = openc2.utils.parse_args(args, allow_custom=True)
170 |
171 | args = {"custom-args": openc2.v10.Args(id="value", allow_custom=True)}
172 | with pytest.raises(ValueError):
173 | openc2.utils.parse_args(args)
174 |
--------------------------------------------------------------------------------
/tests/test_cmd.py:
--------------------------------------------------------------------------------
1 | import openc2
2 | import pytest
3 | import json
4 | import sys
5 |
6 |
7 | def test_cmd_create():
8 | with pytest.raises(openc2.exceptions.MissingPropertiesError):
9 | openc2.v10.Command()
10 |
11 | with pytest.raises(openc2.exceptions.MissingPropertiesError):
12 | openc2.v10.Command(action="query")
13 |
14 | with pytest.raises(openc2.exceptions.MissingPropertiesError):
15 | openc2.v10.Command(target=openc2.v10.Features())
16 |
17 | foo = openc2.v10.Command(action="query", target=openc2.v10.Features())
18 | assert foo
19 | assert foo.action == "query"
20 | assert foo.target.features == []
21 | assert '"action": "query"' in foo.serialize()
22 | assert '"target": {"features": []}' in foo.serialize()
23 |
24 | bar = openc2.v10.Command(**json.loads(foo.serialize()))
25 | assert bar == foo
26 |
27 | bar = openc2.parse(foo.serialize())
28 | assert foo == bar
29 |
30 | d = json.loads(foo.serialize())
31 | foo = openc2.utils.dict_to_openc2(d)
32 | d["invalid"] = {"bad": "value"}
33 | with pytest.raises(openc2.exceptions.ExtraPropertiesError):
34 | openc2.utils.dict_to_openc2(d)
35 |
36 | with pytest.raises(openc2.exceptions.InvalidValueError):
37 | openc2.v10.Command(action="invalid", target=openc2.v10.Features())
38 |
39 | with pytest.raises(openc2.exceptions.InvalidValueError):
40 | openc2.v10.Command(action="query", target=openc2.v10.Args())
41 |
42 |
43 | def test_cmd_custom_actuator():
44 | @openc2.v10.CustomActuator(
45 | "x-acme-widget",
46 | [
47 | ("name", openc2.properties.StringProperty(required=True)),
48 | ("version", openc2.properties.FloatProperty()),
49 | ],
50 | )
51 | class AcmeWidgetActuator(object):
52 | def __init__(self, version=None, **kwargs):
53 | if version and version < 1.0:
54 | raise ValueError("'%f' is not a supported version." % version)
55 |
56 | widget = AcmeWidgetActuator(name="foo", version=1.1)
57 | foo = openc2.v10.Command(
58 | action="query", target=openc2.v10.Features(), actuator=widget
59 | )
60 | assert foo
61 | assert foo.action == "query"
62 | assert foo.actuator.name == "foo"
63 | assert foo.actuator.version == 1.1
64 |
65 | bar = openc2.v10.Command(**json.loads(foo.serialize()))
66 | assert bar == foo
67 |
68 | bar = openc2.parse(foo.serialize())
69 | assert foo == bar
70 |
71 |
72 | def test_cmd_slpf_actuator():
73 | widget = openc2.v10.SLPFActuator(hostname="localhost")
74 | foo = openc2.v10.Command(
75 | action="query", target=openc2.v10.Features(), actuator=widget
76 | )
77 | assert foo
78 | assert foo.action == "query"
79 | assert foo.actuator.hostname == "localhost"
80 | assert '"action": "query"' in foo.serialize()
81 | assert '"target": {"features": []}' in foo.serialize()
82 | assert '"actuator": {"slpf": {"hostname": "localhost"}}' in foo.serialize()
83 |
84 | bar = openc2.v10.Command(**json.loads(foo.serialize()))
85 | assert bar == foo
86 |
87 | bar = openc2.parse(foo.serialize())
88 | assert foo == bar
89 |
90 |
91 | def test_cmd_custom():
92 | @openc2.properties.CustomProperty(
93 | "x-thing",
94 | [
95 | ("uid", openc2.properties.StringProperty()),
96 | ("name", openc2.properties.StringProperty()),
97 | ("version", openc2.properties.StringProperty()),
98 | ],
99 | )
100 | class CustomTargetProperty(object):
101 | pass
102 |
103 | @openc2.v10.CustomTarget("x-thing:id", [("id", CustomTargetProperty())])
104 | class CustomTarget(object):
105 | pass
106 |
107 | @openc2.v10.CustomArgs(
108 | "whatever-who-cares", [("custom_args", CustomTargetProperty())]
109 | )
110 | class CustomArgs(object):
111 | pass
112 |
113 | @openc2.v10.CustomActuator(
114 | "x-acme-widget",
115 | [
116 | ("name", openc2.properties.StringProperty(required=True)),
117 | ("version", CustomTargetProperty()),
118 | ],
119 | )
120 | class AcmeWidgetActuator(object):
121 | pass
122 |
123 | tp = CustomTargetProperty(name="target")
124 | t = CustomTarget(id=tp)
125 | args = CustomArgs(custom_args=CustomTargetProperty(name="args"))
126 | act = AcmeWidgetActuator(
127 | name="hello", version=CustomTargetProperty(name="actuator")
128 | )
129 | cmd = openc2.v10.Command(action="query", target=t, args=args, actuator=act)
130 |
131 | bar = openc2.parse(cmd.serialize())
132 | assert cmd == bar
133 | bar = openc2.parse(json.loads(cmd.serialize()))
134 | assert cmd == bar
135 |
136 | bar = openc2.v10.Command(**json.loads(cmd.serialize()))
137 | assert cmd == bar
138 |
139 |
140 | def test_cmd_device():
141 | gen = """{
142 | "action": "allow",
143 | "target": {
144 | "device": {
145 | "hostname": "device hostname",
146 | "idn_hostname": "device idn hostname",
147 | "device_id": "Device id"
148 | }
149 | }
150 | }
151 | """
152 | foo = openc2.parse(gen)
153 | assert foo.action == "allow"
154 | assert isinstance(foo.target, openc2.v10.Device)
155 | assert foo.target.hostname == "device hostname"
156 | assert foo.target.idn_hostname == "device idn hostname"
157 | assert foo.target.device_id == "Device id"
158 |
159 | bar = openc2.parse(foo.serialize())
160 | assert foo == bar
161 | bar = openc2.parse(json.loads(foo.serialize()))
162 | assert foo == bar
163 | bar = openc2.v10.Command(**json.loads(foo.serialize()))
164 | assert foo == bar
165 |
166 |
167 | def test_cmd_domain():
168 | gen = """{
169 | "action": "cancel",
170 | "target": {
171 | "domain_name": "Domain name"
172 | }
173 | }"""
174 | foo = openc2.parse(gen)
175 | assert foo.action == "cancel"
176 | assert isinstance(foo.target, openc2.v10.DomainName)
177 | assert foo.target.domain_name == "Domain name"
178 |
179 | bar = openc2.parse(foo.serialize())
180 | assert foo == bar
181 | bar = openc2.parse(json.loads(foo.serialize()))
182 | assert foo == bar
183 | bar = openc2.v10.Command(**json.loads(foo.serialize()))
184 | assert foo == bar
185 |
186 |
187 | def test_cmd_email():
188 | gen = """{
189 | "action": "copy",
190 | "target": {
191 | "email_addr": "Email address"
192 | }
193 | }"""
194 | foo = openc2.parse(gen)
195 | assert foo.action == "copy"
196 | assert isinstance(foo.target, openc2.v10.EmailAddress)
197 | assert foo.target.email_addr == "Email address"
198 |
199 | bar = openc2.parse(foo.serialize())
200 | assert foo == bar
201 | bar = openc2.parse(json.loads(foo.serialize()))
202 | assert foo == bar
203 | bar = openc2.v10.Command(**json.loads(foo.serialize()))
204 | assert foo == bar
205 |
206 |
207 | def test_cmd_features():
208 | gen = """{
209 | "action": "create",
210 | "target": {
211 | "features": [
212 | "versions",
213 | "profiles",
214 | "pairs",
215 | "rate_limit"
216 | ]
217 | }
218 | }"""
219 | foo = openc2.parse(gen)
220 | assert foo.action == "create"
221 | assert isinstance(foo.target, openc2.v10.Features)
222 | assert foo.target.features == ["versions", "profiles", "pairs", "rate_limit"]
223 |
224 | bar = openc2.parse(foo.serialize())
225 | assert foo == bar
226 | bar = openc2.parse(json.loads(foo.serialize()))
227 | assert foo == bar
228 | bar = openc2.v10.Command(**json.loads(foo.serialize()))
229 | assert foo == bar
230 |
231 |
232 | def test_cmd_file():
233 | gen = """{
234 | "action": "delete",
235 | "target": {
236 | "file": {
237 | "name": "File name",
238 | "path": "File path",
239 | "hashes": {
240 | "sha1": "1234567890ABCDEF1234567890ABCDEF12345678",
241 | "sha256": "1234567890ABCDEF1234567890ABCDEF1234567890ABCDEF1234567890ABDEF1",
242 | "md5": "1234567890ABCDEF1234567890ABCDEF"
243 | }
244 | }
245 | }
246 | }"""
247 | foo = openc2.parse(gen)
248 | assert foo.action == "delete"
249 | assert isinstance(foo.target, openc2.v10.File)
250 | assert foo.target.name == "File name"
251 | assert foo.target.path == "File path"
252 | assert foo.target.hashes
253 |
254 | bar = openc2.parse(foo.serialize())
255 | assert foo == bar
256 | bar = openc2.parse(json.loads(foo.serialize()))
257 | assert foo == bar
258 | bar = openc2.v10.Command(**json.loads(foo.serialize()))
259 | assert foo == bar
260 |
261 |
262 | def test_cmd_idn():
263 | gen = """{
264 | "action": "deny",
265 | "target": {
266 | "idn_domain_name": "IDN Domain name"
267 | }
268 | }"""
269 | foo = openc2.parse(gen)
270 | assert foo.action == "deny"
271 | assert isinstance(foo.target, openc2.v10.InternationalizedDomainName)
272 | assert foo.target.idn_domain_name == "IDN Domain name"
273 |
274 | bar = openc2.parse(foo.serialize())
275 | assert foo == bar
276 | bar = openc2.parse(json.loads(foo.serialize()))
277 | assert foo == bar
278 | bar = openc2.v10.Command(**json.loads(foo.serialize()))
279 | assert foo == bar
280 |
281 | gen = """{
282 | "action": "detonate",
283 | "target": {
284 | "idn_email_addr": "IDN Email address"
285 | }
286 | }"""
287 | foo = openc2.parse(gen)
288 | assert foo.action == "detonate"
289 | assert isinstance(foo.target, openc2.v10.InternationalizedEmailAddress)
290 | assert foo.target.idn_email_addr == "IDN Email address"
291 |
292 | bar = openc2.parse(foo.serialize())
293 | assert foo == bar
294 | bar = openc2.parse(json.loads(foo.serialize()))
295 | assert foo == bar
296 | bar = openc2.v10.Command(**json.loads(foo.serialize()))
297 | assert foo == bar
298 |
299 |
300 | def test_cmd_ip():
301 | gen = """{
302 | "action": "investigate",
303 | "target": {
304 | "ipv4_connection": {
305 | "src_addr": "10.0.0.0/24",
306 | "src_port": 8443,
307 | "dst_addr": "10.0.0.0/24",
308 | "dst_port": 9443,
309 | "protocol": "tcp"
310 | }
311 | }
312 | }
313 | """
314 | foo = openc2.parse(gen)
315 | assert foo.action == "investigate"
316 | assert isinstance(foo.target, openc2.v10.IPv4Connection)
317 | assert foo.target.src_addr == "10.0.0.0/24"
318 | assert foo.target.src_port == 8443
319 | assert foo.target.dst_addr == "10.0.0.0/24"
320 | assert foo.target.dst_port == 9443
321 | assert foo.target.protocol == "tcp"
322 |
323 | bar = openc2.parse(foo.serialize())
324 | assert foo == bar
325 | bar = openc2.parse(json.loads(foo.serialize()))
326 | assert foo == bar
327 | bar = openc2.v10.Command(**json.loads(foo.serialize()))
328 | assert foo == bar
329 |
330 | gen = """{
331 | "action": "locate",
332 | "target": {
333 | "ipv4_net": "10.0.0.0/24"
334 | }
335 | }
336 |
337 | """
338 | foo = openc2.parse(gen)
339 | assert foo.action == "locate"
340 | assert isinstance(foo.target, openc2.v10.IPv4Address)
341 | assert foo.target.ipv4_net == "10.0.0.0/24"
342 |
343 | bar = openc2.parse(foo.serialize())
344 | assert foo == bar
345 | bar = openc2.parse(json.loads(foo.serialize()))
346 | assert foo == bar
347 | bar = openc2.v10.Command(**json.loads(foo.serialize()))
348 | assert foo == bar
349 |
350 | gen = """{
351 | "action": "query",
352 | "target": {
353 | "ipv6_connection": {
354 | "src_addr": "AE:00:E4:F1:04:65/24",
355 | "src_port": 8443,
356 | "dst_addr": "AE:00:E4:F1:04:65/24",
357 | "dst_port": 9443,
358 | "protocol": "tcp"
359 | }
360 | }
361 | }
362 | """
363 | foo = openc2.parse(gen)
364 | assert foo.action == "query"
365 | assert isinstance(foo.target, openc2.v10.IPv6Connection)
366 | assert foo.target.src_addr == "AE:00:E4:F1:04:65/24"
367 | assert foo.target.src_port == 8443
368 | assert foo.target.dst_addr == "AE:00:E4:F1:04:65/24"
369 | assert foo.target.dst_port == 9443
370 | assert foo.target.protocol == "tcp"
371 |
372 | bar = openc2.parse(foo.serialize())
373 | assert foo == bar
374 | bar = openc2.parse(json.loads(foo.serialize()))
375 | assert foo == bar
376 | bar = openc2.v10.Command(**json.loads(foo.serialize()))
377 | assert foo == bar
378 |
379 | gen = """{
380 | "action": "locate",
381 | "target": {
382 | "ipv6_net": "AE:00:E4:F1:04:65/24"
383 | }
384 | }
385 | """
386 | foo = openc2.parse(gen)
387 | assert foo.action == "locate"
388 | assert isinstance(foo.target, openc2.v10.IPv6Address)
389 | assert foo.target.ipv6_net == "AE:00:E4:F1:04:65/24"
390 |
391 | bar = openc2.parse(foo.serialize())
392 | assert foo == bar
393 | bar = openc2.parse(json.loads(foo.serialize()))
394 | assert foo == bar
395 | bar = openc2.v10.Command(**json.loads(foo.serialize()))
396 | assert foo == bar
397 |
398 |
399 | def test_cmd_iri():
400 | gen = """{
401 | "action": "remediate",
402 | "target": {
403 | "iri": "My IRI identifier"
404 | }
405 | }
406 | """
407 | foo = openc2.parse(gen)
408 | assert foo.action == "remediate"
409 | assert isinstance(foo.target, openc2.v10.IRI)
410 | assert foo.target.iri == "My IRI identifier"
411 |
412 | bar = openc2.parse(foo.serialize())
413 | assert foo == bar
414 | bar = openc2.parse(json.loads(foo.serialize()))
415 | assert foo == bar
416 | bar = openc2.v10.Command(**json.loads(foo.serialize()))
417 | assert foo == bar
418 |
419 |
420 | def test_cmd_mac():
421 | gen = """{
422 | "action": "restart",
423 | "target": {
424 | "mac_addr": "VGhpcyBpcyBteSBtYWMgYWRkcmVzcw=="
425 | }
426 | }
427 | """
428 | foo = openc2.parse(gen)
429 | assert foo.action == "restart"
430 | assert isinstance(foo.target, openc2.v10.MACAddress)
431 | assert foo.target.mac_addr == "VGhpcyBpcyBteSBtYWMgYWRkcmVzcw=="
432 |
433 | bar = openc2.parse(foo.serialize())
434 | assert foo == bar
435 | bar = openc2.parse(json.loads(foo.serialize()))
436 | assert foo == bar
437 | bar = openc2.v10.Command(**json.loads(foo.serialize()))
438 | assert foo == bar
439 |
440 |
441 | def test_cmd_process():
442 | gen = """{
443 | "action": "restore",
444 | "target": {
445 | "process": {
446 | "pid": 12354,
447 | "name": "Process name",
448 | "cwd": "Process CWD",
449 | "executable": {
450 | "name": "File name",
451 | "path": "File path",
452 | "hashes": {
453 | "sha1": "1234567890ABCDEF1234567890ABCDEF12345678",
454 | "sha256": "1234567890ABCDEF1234567890ABCDEF1234567890ABCDEF1234567890ABDEF1",
455 | "md5": "1234567890ABCDEF1234567890ABCDEF"
456 | }
457 | },
458 | "parent": {
459 | "pid": 43521,
460 | "name": "Process parent name",
461 | "cwd": "Process parent CWD"
462 | },
463 | "command_line": "Process command line statement"
464 | }
465 | }
466 | }
467 | """
468 | foo = openc2.parse(gen)
469 | assert foo.action == "restore"
470 | assert isinstance(foo.target, openc2.v10.Process)
471 | assert foo.target.pid == 12354
472 | assert foo.target.name == "Process name"
473 | assert foo.target.cwd == "Process CWD"
474 | assert foo.target.executable
475 | assert foo.target.parent
476 | assert foo.target.command_line == "Process command line statement"
477 |
478 | bar = openc2.parse(foo.serialize())
479 | assert foo == bar
480 | bar = openc2.parse(json.loads(foo.serialize()))
481 | assert foo == bar
482 | bar = openc2.v10.Command(**json.loads(foo.serialize()))
483 | assert foo == bar
484 |
485 |
486 | def test_cmd_properties():
487 | gen = """{
488 | "action": "scan",
489 | "target": {
490 | "properties": [
491 | "Tag1",
492 | "Tag2",
493 | "Tag3",
494 | "Tag4"
495 | ]
496 | }
497 | }
498 | """
499 | foo = openc2.parse(gen)
500 | assert foo.action == "scan"
501 | assert isinstance(foo.target, openc2.v10.Properties)
502 | assert foo.target.properties == ["Tag1", "Tag2", "Tag3", "Tag4"]
503 |
504 | bar = openc2.parse(foo.serialize())
505 | assert foo == bar
506 | bar = openc2.parse(json.loads(foo.serialize()))
507 | assert foo == bar
508 | bar = openc2.v10.Command(**json.loads(foo.serialize()))
509 | assert foo == bar
510 |
511 |
512 | def test_cmd_uri():
513 | gen = """{
514 | "action": "set",
515 | "target": {
516 | "uri": "www.myuri.com"
517 | }
518 | }"""
519 | foo = openc2.parse(gen)
520 | assert foo.action == "set"
521 | assert isinstance(foo.target, openc2.v10.URI)
522 | assert foo.target.uri == "www.myuri.com"
523 |
524 | bar = openc2.parse(foo.serialize())
525 | assert foo == bar
526 | bar = openc2.parse(json.loads(foo.serialize()))
527 | assert foo == bar
528 | bar = openc2.v10.Command(**json.loads(foo.serialize()))
529 | assert foo == bar
530 |
531 |
532 | def test_cmd_generated():
533 | generated = []
534 | generated.append(
535 | """{
536 | "action": "contain",
537 | "target": {
538 | "artifact": {
539 | "payload": {
540 | "url": "www.testurl.com"
541 | },
542 | "hashes": {
543 | "sha1": "1234567890ABCDEF1234567890ABCDEF12345678",
544 | "sha256": "1234567890ABCDEF1234567890ABCDEF1234567890ABCDEF1234567890ABDEF1",
545 | "md5": "1234567890ABCDEF1234567890ABCDEF"
546 | },
547 | "mime_type": "My MIME Type"
548 | }
549 | }
550 | }
551 | """
552 | )
553 | generated.append(
554 | """{
555 | "action": "start",
556 | "target": {
557 | "artifact": {
558 | "payload": {
559 | "url": "www.testurl.com"
560 | },
561 | "hashes": {
562 | "sha1": "1234567890ABCDEF1234567890ABCDEF12345678",
563 | "sha256": "1234567890ABCDEF1234567890ABCDEF1234567890ABCDEF1234567890ABDEF1",
564 | "md5": "1234567890ABCDEF1234567890ABCDEF"
565 | },
566 | "mime_type": "My MIME Type"
567 | }
568 | },
569 | "args": {
570 | "start_time": 1568209029693,
571 | "stop_time": 1568209059693,
572 | "response_requested": "complete"
573 | }
574 | }
575 | """
576 | )
577 | generated.append(
578 | """{
579 | "action": "start",
580 | "target": {
581 | "artifact": {
582 | "payload": {
583 | "url": "www.testurl.com"
584 | },
585 | "hashes": {
586 | "sha1": "1234567890ABCDEF1234567890ABCDEF12345678",
587 | "sha256": "1234567890ABCDEF1234567890ABCDEF1234567890ABCDEF1234567890ABDEF1",
588 | "md5": "1234567890ABCDEF1234567890ABCDEF"
589 | },
590 | "mime_type": "My MIME Type"
591 | }
592 | },
593 | "args": {
594 | "duration": 30000,
595 | "start_time": 1568209029693,
596 | "response_requested": "complete"
597 | }
598 | }
599 | """
600 | )
601 | generated.append(
602 | """{
603 | "action": "stop",
604 | "target": {
605 | "artifact": {
606 | "payload": {
607 | "bin": "YmluIGRhdGE="
608 | },
609 | "hashes": {
610 | "sha1": "1234567890ABCDEF1234567890ABCDEF12345678",
611 | "sha256": "1234567890ABCDEF1234567890ABCDEF1234567890ABCDEF1234567890ABDEF1",
612 | "md5": "1234567890ABCDEF1234567890ABCDEF"
613 | },
614 | "mime_type": "My MIME Type"
615 | }
616 | }
617 | }
618 | """
619 | )
620 | generated.append(
621 | """{
622 | "action": "update",
623 | "target": {
624 | "artifact": {
625 | "payload": {
626 | "url": "www.testurl.com"
627 | },
628 | "hashes": {
629 | "sha1": "1234567890ABCDEF1234567890ABCDEF12345678",
630 | "sha256": "1234567890ABCDEF1234567890ABCDEF1234567890ABCDEF1234567890ABDEF1",
631 | "md5": "1234567890ABCDEF1234567890ABCDEF"
632 | },
633 | "mime_type": "My MIME Type"
634 | }
635 | }
636 | }
637 | """
638 | )
639 | for gen in generated:
640 | foo = openc2.parse(gen)
641 |
642 | bar = openc2.parse(foo.serialize())
643 | assert foo == bar
644 | bar = openc2.parse(json.loads(foo.serialize()))
645 | assert foo == bar
646 | bar = openc2.v10.Command(**json.loads(foo.serialize()))
647 | assert foo == bar
648 |
--------------------------------------------------------------------------------
/tests/test_hashes_property.py:
--------------------------------------------------------------------------------
1 | import openc2
2 | import pytest
3 | import json
4 | import sys
5 |
6 |
7 | def test_hash_property():
8 | foo = openc2.properties.HashesProperty()
9 | with pytest.raises(ValueError):
10 | assert foo.clean({})
11 | assert foo.clean({"md5": "9AC6EC101ABD3F40A5C74B38038E1E20"})
12 | with pytest.raises(ValueError):
13 | foo.clean({"md5": "bad-hash"})
14 | with pytest.raises(ValueError):
15 | foo.clean({"bad": "value"})
16 |
--------------------------------------------------------------------------------
/tests/test_properties.py:
--------------------------------------------------------------------------------
1 | import openc2
2 | import pytest
3 | import json
4 | import sys
5 | import datetime
6 |
7 |
8 | def test_args_custom_invalid_property():
9 |
10 | with pytest.raises(openc2.exceptions.PropertyPresenceError):
11 |
12 | @openc2.properties.CustomProperty(
13 | "x-custom-property", [("type", openc2.properties.StringProperty())]
14 | )
15 | class MyCustomProp(object):
16 | pass
17 |
18 | with pytest.raises(TypeError):
19 |
20 | @openc2.properties.CustomProperty("x-custom-property", None)
21 | class MyCustomProp(object):
22 | pass
23 |
24 | with pytest.raises(ValueError):
25 |
26 | @openc2.properties.CustomProperty("x-custom-property", [])
27 | class MyCustomProp(object):
28 | pass
29 |
30 |
31 | def test_args_custom_embed_property():
32 | @openc2.properties.CustomProperty(
33 | "x-custom-property-inner", [("value", openc2.properties.StringProperty())]
34 | )
35 | class MyCustomPropInner(object):
36 | pass
37 |
38 | @openc2.properties.CustomProperty(
39 | "x-custom-property",
40 | [("value", openc2.properties.ListProperty(MyCustomPropInner))],
41 | )
42 | class MyCustomProp(object):
43 | pass
44 |
45 | foo = MyCustomProp(value=[{"value": "my_value"}])
46 | assert foo != None
47 | assert len(foo.value) > 0
48 | assert foo.value[0] != None
49 | assert foo.value[0].value == "my_value"
50 |
51 | bar = foo(_value=foo)
52 | assert bar == foo
53 |
54 | foo = MyCustomProp(value=[MyCustomPropInner(value="my_value")])
55 | assert foo != None
56 | assert len(foo.value) > 0
57 | assert foo.value[0] != None
58 | assert foo.value[0].value == "my_value"
59 |
60 | foo2 = MyCustomProp(value=[MyCustomPropInner(value="my_value2")])
61 | assert foo2 != None
62 | assert len(foo2.value) > 0
63 | assert foo2.value[0] != None
64 | assert foo2.value[0].value == "my_value2"
65 |
66 | assert foo != foo2
67 |
68 | @openc2.v10.CustomArgs(
69 | "x-custom", [("value", openc2.properties.ListProperty(MyCustomProp))]
70 | )
71 | class MyCustomArgs(object):
72 | pass
73 |
74 | bar = MyCustomArgs(value=[foo])
75 | assert bar != None
76 | assert len(foo.value) > 0
77 | assert bar.value[0] != None
78 | assert len(bar.value[0].value) > 0
79 | assert bar.value[0].value[0] != None
80 | assert bar.value[0].value[0].value == "my_value"
81 |
82 | bar = MyCustomArgs(value=[foo, foo2])
83 | print("bar", bar.serialize())
84 | assert bar != None
85 | assert len(foo.value) > 0
86 | assert bar.value[0] != None
87 | assert len(bar.value[0].value) > 0
88 | assert bar.value[0].value[0] != None
89 | assert bar.value[0].value[0].value == "my_value"
90 | assert bar.value[1].value[0] != None
91 | assert bar.value[1].value[0].value == "my_value2"
92 |
93 |
94 | def test_binary_property():
95 | foo = openc2.properties.BinaryProperty()
96 | assert foo.clean("RXZlcldhdGNo") == "RXZlcldhdGNo"
97 |
98 | with pytest.raises(ValueError):
99 | foo.clean("bad")
100 |
101 |
102 | def test_integer_property():
103 | foo = openc2.properties.IntegerProperty()
104 | assert foo.clean(1) == 1
105 |
106 | invalid = ["a"]
107 | for i in invalid:
108 | with pytest.raises(ValueError):
109 | foo.clean(i)
110 |
111 | foo = openc2.properties.IntegerProperty(min=1)
112 | with pytest.raises(ValueError):
113 | foo.clean(0)
114 |
115 | foo = openc2.properties.IntegerProperty(max=10)
116 | with pytest.raises(ValueError):
117 | foo.clean(11)
118 |
119 |
120 | def test_float_property():
121 | foo = openc2.properties.FloatProperty()
122 | assert foo.clean(1.0) == 1.0
123 | assert foo.clean(1) == 1.0
124 | with pytest.raises(ValueError):
125 | foo.clean("a")
126 |
127 | foo = openc2.properties.FloatProperty(min=1.0)
128 | with pytest.raises(ValueError):
129 | foo.clean(0.0)
130 |
131 | foo.clean(1.0)
132 |
133 | foo = openc2.properties.FloatProperty(max=10.0)
134 | with pytest.raises(ValueError):
135 | foo.clean(11.0)
136 |
137 |
138 | def test_dictionary_property():
139 | foo = openc2.properties.DictionaryProperty()
140 | assert foo.clean({"key": "value"}) == {"key": "value"}
141 |
142 | with pytest.raises(ValueError):
143 | foo.clean("bad")
144 |
145 | foo = openc2.properties.DictionaryProperty(allowed_keys=["key"])
146 | assert foo.clean({"key": "value"}) == {"key": "value"}
147 |
148 | try:
149 | foo.clean({"bad": "value"})
150 | except Exception as e:
151 | assert "Invalid dictionary key" in str(e)
152 |
153 | foo = openc2.properties.DictionaryProperty(allowed_key_regex=r"^[a-zA-Z0-9_-]+$")
154 | with pytest.raises(openc2.exceptions.DictionaryKeyError):
155 | foo.clean({"😈": "bad"})
156 |
157 |
158 | def test_file_property():
159 | foo = openc2.properties.FileProperty()
160 | assert foo.clean({"name": "file.txt"}) == openc2.v10.targets.File(name="file.txt")
161 |
162 | foo = openc2.properties.FileProperty(version="0.0")
163 | assert foo.clean("bad") == "bad"
164 |
165 |
166 | def test_component_property():
167 | foo = openc2.properties.ComponentProperty()
168 | with pytest.raises(ValueError):
169 | foo.clean({"key": "value"})
170 |
171 | foo = openc2.properties.ComponentProperty(component_type="bad_type")
172 | with pytest.raises(openc2.exceptions.CustomContentError):
173 | foo.clean({"key": "value"})
174 |
175 | with pytest.raises(ValueError):
176 | foo.clean("bad")
177 |
178 |
179 | def test_datetime_property():
180 | foo = openc2.properties.DateTimeProperty()
181 | assert foo.clean(1) == 1
182 | assert (
183 | foo.clean(
184 | datetime.datetime(1970, 1, 1, 0, 0, 0, 0, tzinfo=datetime.timezone.utc)
185 | )
186 | == 0
187 | )
188 | assert (
189 | foo.clean(
190 | datetime.datetime(1970, 1, 1, 1, 0, 0, 0, tzinfo=datetime.timezone.utc)
191 | )
192 | == 3.6e6
193 | )
194 | assert (
195 | foo.clean(
196 | datetime.datetime(1970, 1, 1, 0, 1, 0, 0, tzinfo=datetime.timezone.utc)
197 | )
198 | == 60000
199 | )
200 | assert (
201 | foo.clean(
202 | datetime.datetime(1970, 1, 1, 0, 0, 1, 0, tzinfo=datetime.timezone.utc)
203 | )
204 | == 1000
205 | )
206 | assert (
207 | foo.clean(
208 | datetime.datetime(1970, 1, 1, 0, 0, 0, 1000, tzinfo=datetime.timezone.utc)
209 | )
210 | == 1
211 | )
212 |
213 | assert (
214 | foo.clean(
215 | datetime.datetime(1960, 1, 1, 0, 0, 0, 1000, tzinfo=datetime.timezone.utc)
216 | )
217 | == -315619199999
218 | )
219 | assert (
220 | foo.clean(
221 | datetime.datetime(
222 | 1969, 12, 31, 23, 59, 59, 999000, tzinfo=datetime.timezone.utc
223 | )
224 | )
225 | == -1
226 | )
227 |
228 | assert foo.datetime(0) == datetime.datetime(1970, 1, 1, 0, 0, 0, 0)
229 |
230 | with pytest.raises(ValueError):
231 | foo.clean(sys.maxsize)
232 |
233 | with pytest.raises(ValueError):
234 | foo.datetime(sys.maxsize)
235 |
236 |
237 | def test_EnumProperty():
238 | foo = openc2.properties.EnumProperty(["good", "very_good"])
239 | assert foo.clean("good") == "good"
240 | assert foo.clean("very_good") == "very_good"
241 | with pytest.raises(ValueError):
242 | foo.clean("bad")
243 |
244 | with pytest.raises(ValueError):
245 | openc2.properties.EnumProperty("good")
246 |
247 |
248 | def test_StringProperty():
249 | foo = openc2.properties.StringProperty()
250 | assert foo.clean("Hello") == "Hello"
251 | assert foo.clean(1) == "1"
252 |
253 |
254 | def test_listproperty():
255 | foo = openc2.properties.ListProperty(openc2.properties.StringProperty)
256 | assert foo.clean("hello") == ["hello"]
257 |
258 | with pytest.raises(ValueError):
259 | foo.clean(1)
260 |
261 |
262 | def test_custom_property_fixed():
263 | @openc2.properties.CustomProperty(
264 | "x-custom-property", [("custom", openc2.properties.StringProperty())]
265 | )
266 | class MyCustomProp(object):
267 | pass
268 |
269 | foo = MyCustomProp(custom="custom_value")
270 | assert foo.serialize() == '{"custom": "custom_value"}'
271 |
272 | fixed = MyCustomProp(custom="fixed_value")
273 |
274 | foo = MyCustomProp(fixed=fixed)
275 | assert foo.serialize() == "{}" # not required so can be empty
276 |
277 | bar = openc2.properties.EmbeddedObjectProperty(foo)
278 |
279 | assert bar.clean({"custom": "fixed_value"}) == {"custom": "fixed_value"}
280 | with pytest.raises(ValueError):
281 | bar.clean({"custom": "bad"})
282 |
283 | with pytest.raises(ValueError):
284 | bar.clean({"bad": "fixed_value"})
285 |
286 | @openc2.properties.CustomProperty("x-custom", [("custom", bar)])
287 | class EmbedCustomFixed(object):
288 | pass
289 |
290 | foo = EmbedCustomFixed()
291 | assert foo != None
292 | assert foo.serialize() == "{}"
293 |
294 | with pytest.raises(openc2.exceptions.InvalidValueError):
295 | EmbedCustomFixed(custom=MyCustomProp(custom="bad"))
296 |
297 | foo = EmbedCustomFixed(custom=fixed)
298 | assert foo.serialize() == '{"custom": {"custom": "fixed_value"}}'
299 |
300 | foo = EmbedCustomFixed(custom={"custom": "fixed_value"})
301 | assert foo.serialize() == '{"custom": {"custom": "fixed_value"}}'
302 |
--------------------------------------------------------------------------------
/tests/test_readme.py:
--------------------------------------------------------------------------------
1 | def test_readme():
2 | import openc2
3 | import stix2
4 |
5 | # encode
6 | cmd = openc2.v10.Command(
7 | action="deny",
8 | target=openc2.v10.IPv4Address(ipv4_net="1.2.3.4"),
9 | args=openc2.v10.Args(response_requested="complete"),
10 | )
11 | msg = cmd.serialize()
12 |
13 | # decode
14 | cmd = openc2.parse(msg)
15 | if cmd.action == "deny" and cmd.target.type == "ipv4_net":
16 |
17 | if cmd.args.response_requested == "complete":
18 | resp = openc2.v10.Response(status=200)
19 | msg = resp.serialize()
20 |
21 | # custom actuator
22 | @openc2.v10.CustomActuator(
23 | "x-acme-widget",
24 | [
25 | ("name", openc2.properties.StringProperty(required=True)),
26 | ("version", stix2.properties.FloatProperty()),
27 | ],
28 | )
29 | class AcmeWidgetActuator(object):
30 | def __init__(self, version=None, **kwargs):
31 | if version and version < 1.0:
32 | raise ValueError("'%f' is not a supported version." % version)
33 |
34 | widget = AcmeWidgetActuator(name="foo", version=1.1)
35 | assert widget.serialize() == '{"x-acme-widget": {"name": "foo", "version": 1.1}}'
36 |
--------------------------------------------------------------------------------
/tests/test_response.py:
--------------------------------------------------------------------------------
1 | import openc2
2 | import pytest
3 | import json
4 | import sys
5 |
6 |
7 | def test_response_create():
8 | with pytest.raises(openc2.exceptions.MissingPropertiesError):
9 | openc2.v10.Response()
10 |
11 | with pytest.raises(openc2.exceptions.MissingPropertiesError):
12 | openc2.v10.Response(results={})
13 |
14 | with pytest.raises(openc2.exceptions.MissingPropertiesError):
15 | openc2.v10.Response(status_text="Ok.")
16 |
17 | foo = openc2.v10.Response(status=200)
18 | assert foo
19 | assert foo.status == 200
20 | assert foo.serialize() == '{"status": 200}'
21 |
22 | bar = openc2.v10.Response(**json.loads(foo.serialize()))
23 | assert bar == foo
24 |
25 | bar = openc2.parse({"status": 200})
26 | assert foo == bar
27 |
28 | bar = openc2.parse(foo.serialize())
29 | assert foo == bar
30 |
--------------------------------------------------------------------------------
/tests/test_slpf.py:
--------------------------------------------------------------------------------
1 | import openc2
2 | import pytest
3 | import json
4 | import sys
5 |
6 |
7 | def test_slpf_actuator():
8 | foo = openc2.v10.SLPFActuator(hostname="hostname")
9 | assert foo
10 | assert foo.hostname == "hostname"
11 |
12 | with pytest.raises(openc2.exceptions.ExtraPropertiesError):
13 | foo = openc2.v10.SLPFActuator(bad="bad")
14 |
15 | foo = openc2.v10.SLPFActuator(named_group="named_group")
16 | assert foo != None
17 | assert foo.named_group == "named_group"
18 |
19 | foo = openc2.v10.SLPFActuator(asset_id="asset_id")
20 | assert foo != None
21 | assert foo.asset_id == "asset_id"
22 |
23 | # with the current specification SLPFActuator does not need any elements
24 | # and an empty asset_tuple is None
25 | foo = openc2.v10.SLPFActuator(asset_tuple=[])
26 | assert foo != None
27 |
28 | with pytest.raises(AttributeError):
29 | foo.asset_tuple
30 |
31 | # check that there is less than 10
32 | for s in range(1, 10):
33 | values = list(map(lambda x: str(x), list(range(s))))
34 | f = openc2.v10.SLPFActuator(asset_tuple=values)
35 | assert f.asset_tuple == values
36 |
37 | # max 10 items
38 | with pytest.raises(openc2.exceptions.InvalidValueError):
39 | openc2.v10.SLPFActuator(
40 | asset_tuple=list(map(lambda x: str(x), list(range(11))))
41 | )
42 |
43 |
44 | def test_slpf_cmd():
45 | with pytest.raises(openc2.exceptions.MissingPropertiesError):
46 | openc2.v10.slpf.SLPF()
47 |
48 | with pytest.raises(openc2.exceptions.MissingPropertiesError):
49 | openc2.v10.slpf.SLPF(action="query")
50 |
51 | with pytest.raises(openc2.exceptions.MissingPropertiesError):
52 | openc2.v10.slpf.SLPF(target=openc2.v10.Features())
53 |
54 | foo = openc2.v10.slpf.SLPF(action="query", target=openc2.v10.Features())
55 | assert foo != None
56 | assert foo.action == "query"
57 |
58 | with pytest.raises(openc2.exceptions.InvalidValueError):
59 | openc2.v10.slpf.SLPF(action="create", target=openc2.v10.Features())
60 |
61 | with pytest.raises(ValueError):
62 | openc2.v10.slpf.SLPF(action="query", target=openc2.v10.targets.URI(uri="uri"))
63 |
64 | foo = openc2.v10.slpf.SLPF(
65 | action="query",
66 | target=openc2.v10.Features(),
67 | actuator=openc2.v10.SLPFActuator(hostname="hostname"),
68 | )
69 | assert foo != None
70 | assert foo.action == "query"
71 | assert foo.actuator.hostname == "hostname"
72 |
73 | @openc2.v10.CustomActuator("x-thing", [("id", openc2.properties.StringProperty())])
74 | class MyCustomActuator(object):
75 | pass
76 |
77 | with pytest.raises(ValueError):
78 | openc2.v10.slpf.SLPF(
79 | action="query",
80 | target=openc2.v10.Features(),
81 | actuator=MyCustomActuator(id="id"),
82 | )
83 |
--------------------------------------------------------------------------------
/tests/test_targets.py:
--------------------------------------------------------------------------------
1 | import openc2
2 | import pytest
3 | import json
4 |
5 |
6 | def test_ipv4_address_example():
7 | ip4 = openc2.v10.IPv4Address(ipv4_net="198.51.100.3")
8 | assert ip4.type == "ipv4_net"
9 | assert ip4["type"] == "ipv4_net"
10 | assert ip4.get("type") == "ipv4_net"
11 |
12 | with pytest.raises(AttributeError):
13 | ip4.bad
14 |
15 | with pytest.raises(openc2.exceptions.ImmutableError):
16 | ip4.type = "bad"
17 |
18 | try:
19 | ip4.type = "bad"
20 | except Exception as e:
21 | assert "Cannot modify" in str(e)
22 |
23 | assert ("%r" % ip4) == "IPv4Address(ipv4_net='198.51.100.3')"
24 |
25 | assert ip4.ipv4_net == "198.51.100.3"
26 | assert ip4["ipv4_net"] == "198.51.100.3"
27 | assert ip4.get("ipv4_net") == "198.51.100.3"
28 |
29 | assert len(ip4) == 1
30 |
31 | for prop in ip4:
32 | assert prop == "ipv4_net"
33 |
34 | ip4.ipv4_net = "198.51.100.32"
35 | assert ip4.ipv4_net == "198.51.100.32"
36 |
37 | assert ip4.serialize() == '{"ipv4_net": "198.51.100.3"}'
38 | assert ip4.serialize(pretty=True) == '{\n "ipv4_net": "198.51.100.3"\n}'
39 |
40 | ser_ipv4 = json.loads(ip4.serialize())
41 | ip4_2 = openc2.v10.IPv4Address(**ser_ipv4)
42 | assert ip4 == ip4_2
43 |
44 | ip4 = openc2.v10.IPv4Address(ipv4_net="198.51.100.0/24")
45 |
46 | assert ip4.ipv4_net == "198.51.100.0/24"
47 |
48 | with pytest.raises(openc2.exceptions.MissingPropertiesError) as excinfo:
49 | ip4 = openc2.v10.IPv4Address()
50 |
51 | assert excinfo.value.cls == openc2.v10.IPv4Address
52 |
53 | bar = openc2.utils.parse_target(json.loads(ip4.serialize()))
54 | assert ip4 == bar
55 |
56 |
57 | def test_custom_target():
58 | @openc2.v10.CustomTarget("x-thing:id", [("id", openc2.properties.StringProperty())])
59 | class CustomTarget(object):
60 | pass
61 |
62 | one = CustomTarget()
63 | assert one != None # for some reason `assert one` fails
64 |
65 | with pytest.raises(openc2.exceptions.ExtraPropertiesError):
66 | CustomTarget(bad="id")
67 |
68 | one = CustomTarget(id="uuid")
69 | assert one
70 | assert one.id == "uuid"
71 |
72 | two = CustomTarget(id=(json.loads(one.serialize())["x-thing:id"]))
73 | assert one == two
74 |
75 | with pytest.raises(openc2.exceptions.ParseError):
76 | openc2.parse(one.serialize())
77 |
78 | with pytest.raises(ValueError):
79 |
80 | @openc2.v10.CustomTarget(
81 | "x-invalid", [("id", openc2.properties.StringProperty())]
82 | )
83 | class CustomTargetInvalid(object):
84 | pass
85 |
86 | with pytest.raises(ValueError):
87 |
88 | @openc2.v10.CustomTarget(
89 | "invalid_target", [("id", openc2.properties.StringProperty())]
90 | )
91 | class CustomTargetInvalid(object):
92 | pass
93 |
94 | with pytest.raises(ValueError):
95 |
96 | @openc2.v10.CustomTarget(
97 | "over_16_chars_long_aaaaaaaaaaaaaaaaaaaa123",
98 | [("id", openc2.properties.StringProperty())],
99 | )
100 | class CustomTargetInvalid(object):
101 | pass
102 |
103 | with pytest.raises(TypeError):
104 |
105 | @openc2.v10.CustomTarget(
106 | "x-custom:id", ("id", openc2.properties.StringProperty()),
107 | )
108 | class CustomTargetInvalid(object):
109 | pass
110 |
111 | with pytest.raises(TypeError):
112 |
113 | @openc2.v10.CustomTarget("x-custom:id")
114 | class CustomTargetInvalid(object):
115 | pass
116 |
117 | with pytest.raises(ValueError):
118 |
119 | @openc2.v10.CustomTarget(
120 | "x-over_16_chars_long_aaaaaaaaaaaaaaaaaaaa:id",
121 | [("id", openc2.properties.StringProperty())],
122 | )
123 | class CustomTargetInvalid(object):
124 | pass
125 |
126 | with pytest.raises(ValueError):
127 |
128 | @openc2.v10.CustomTarget("x-thing:noprops", [])
129 | class CustomTargetInvalid(object):
130 | pass
131 |
132 | with pytest.raises(openc2.exceptions.InvalidValueError):
133 | v = """{ "target": {"x-custom": "value"}, "action":"query"}"""
134 | openc2.utils.parse(v)
135 |
136 | with pytest.raises(openc2.exceptions.InvalidValueError):
137 | v = """{ "target": {"x-custom:id": "value"}, "action":"query"}"""
138 | openc2.utils.parse(v)
139 |
140 | with pytest.raises(openc2.exceptions.InvalidValueError):
141 | v = """{ "target": {}, "action":"query"}"""
142 | openc2.utils.parse(v)
143 |
144 |
145 | def test_custom_target_embed():
146 | @openc2.properties.CustomProperty(
147 | "x-custom", [("custom", openc2.properties.StringProperty(fixed="custom"))]
148 | )
149 | class CustomProperty(object):
150 | pass
151 |
152 | @openc2.v10.CustomTarget(
153 | "x-thing:custom",
154 | [("custom", openc2.properties.EmbeddedObjectProperty(CustomProperty))],
155 | )
156 | class CustomTarget(object):
157 | pass
158 |
159 | foo = CustomTarget()
160 | assert foo != None
161 |
162 | with pytest.raises(openc2.exceptions.ExtraPropertiesError):
163 | CustomTarget(bad="id")
164 |
165 | with pytest.raises(openc2.exceptions.InvalidValueError):
166 | bar = CustomProperty(custom="bad")
167 |
168 | bar = CustomProperty(custom="custom")
169 | foo = CustomTarget(custom=bar)
170 | assert foo
171 |
172 | assert foo.serialize() == '{"x-thing:custom": {"custom": "custom"}}'
173 |
174 |
175 | def test_multiple_custom_targets():
176 | @openc2.v10.CustomTarget("x-thing:id", [("id", openc2.properties.StringProperty())])
177 | class CustomTarget(object):
178 | pass
179 |
180 | @openc2.v10.CustomTarget(
181 | "x-thing:name", [("name", openc2.properties.StringProperty())]
182 | )
183 | class CustomTarget2(object):
184 | pass
185 |
186 | one = CustomTarget()
187 | assert one != None # for some reason `assert one` fails
188 |
189 | with pytest.raises(openc2.exceptions.ExtraPropertiesError):
190 | CustomTarget(bad="id")
191 |
192 | one = CustomTarget(id="uuid")
193 | assert one
194 | assert one.id == "uuid"
195 |
196 | two = CustomTarget(id=(json.loads(one.serialize())["x-thing:id"]))
197 | assert one == two
198 |
199 | with pytest.raises(openc2.exceptions.ParseError):
200 | openc2.parse(one.serialize())
201 |
202 | foo = CustomTarget2()
203 | assert foo != None
204 |
205 | with pytest.raises(openc2.exceptions.ExtraPropertiesError):
206 | CustomTarget2(bad="id")
207 |
208 | one = CustomTarget2(name="name")
209 | assert one
210 | assert one.name == "name"
211 |
212 | two = CustomTarget2(name=(json.loads(one.serialize())["x-thing:name"]))
213 | assert one == two
214 |
215 | with pytest.raises(openc2.exceptions.ParseError):
216 | openc2.parse(one.serialize())
217 |
218 |
219 | def test_custom_target_required():
220 | @openc2.v10.CustomTarget(
221 | "x-thing:id", [("id", openc2.properties.StringProperty(required=True))]
222 | )
223 | class CustomTarget(object):
224 | pass
225 |
226 | with pytest.raises(openc2.exceptions.MissingPropertiesError):
227 | CustomTarget()
228 |
229 | with pytest.raises(openc2.exceptions.ExtraPropertiesError):
230 | CustomTarget(bad="id")
231 |
232 | one = CustomTarget(id="uuid")
233 | assert one
234 | assert one.id == "uuid"
235 |
236 | with pytest.raises(openc2.exceptions.ParseError):
237 | openc2.parse(one.serialize())
238 |
239 | two = CustomTarget(id=(json.loads(one.serialize())["x-thing:id"]))
240 | assert one == two
241 |
242 |
243 | def test_custom_target_with_custom_property():
244 | @openc2.properties.CustomProperty(
245 | "x-thing",
246 | [
247 | ("uid", openc2.properties.StringProperty()),
248 | ("name", openc2.properties.StringProperty()),
249 | ("version", openc2.properties.StringProperty()),
250 | ],
251 | )
252 | class CustomTargetProperty(object):
253 | pass
254 |
255 | @openc2.v10.CustomTarget(
256 | "x-thing:id", [("id", CustomTargetProperty(required=True, default=lambda: {}))]
257 | )
258 | class CustomTarget(object):
259 | pass
260 |
261 | # no property
262 | with pytest.raises(openc2.exceptions.MissingPropertiesError):
263 | CustomTarget()
264 |
265 | # empty property
266 |
267 | one = CustomTarget(id=CustomTargetProperty())
268 | assert one
269 |
270 | with pytest.raises(openc2.exceptions.ParseError):
271 | openc2.parse(one.serialize())
272 |
273 | two = CustomTarget(id=(json.loads(one.serialize())["x-thing:id"]))
274 | assert one == two
275 |
276 | # property with one value
277 |
278 | one = CustomTarget(id=CustomTargetProperty(name="name"))
279 | assert one != None
280 | assert one.id.name == "name"
281 |
282 | two = CustomTarget(id=(json.loads(one.serialize())["x-thing:id"]))
283 | assert one == two
284 |
285 | # property with multiple values
286 |
287 | one = CustomTarget(
288 | id=CustomTargetProperty(name="name", uid="uid", version="version")
289 | )
290 | assert one.id.name == "name"
291 | assert one.id.uid == "uid"
292 | assert one.id.version == "version"
293 |
294 | with pytest.raises(openc2.exceptions.ParseError):
295 | openc2.parse(one.serialize())
296 |
297 | two = CustomTarget(id=(json.loads(one.serialize())["x-thing:id"]))
298 | assert one == two
299 |
300 | @openc2.v10.CustomTarget(
301 | "x-thing:list",
302 | [("list", openc2.properties.ListProperty(CustomTargetProperty))],
303 | )
304 | class CustomTarget2(object):
305 | pass
306 |
307 | foo = CustomTarget2()
308 | assert foo != None
309 |
310 | foo = CustomTarget2(
311 | list=[CustomTargetProperty(name="name", uid="uid", version="version")]
312 | )
313 | assert foo != None
314 | assert foo.list != None
315 | assert foo.list[0] != None
316 | assert foo.list[0].name == "name"
317 | assert foo.list[0].uid == "uid"
318 | assert foo.list[0].version == "version"
319 |
--------------------------------------------------------------------------------
/tests/v10/test_data_types.py:
--------------------------------------------------------------------------------
1 | import openc2
2 | import pytest
3 |
4 |
5 | def test_payload():
6 | foo = openc2.v10.Payload(url="https://everwatchsolutions.com/payload.exe")
7 | assert foo.url == "https://everwatchsolutions.com/payload.exe"
8 |
9 | with pytest.raises(openc2.exceptions.MutuallyExclusivePropertiesError):
10 | openc2.v10.Payload(
11 | url="https://everwatchsolutions.com/payload.exe", bin="RXZlcldhdGNo"
12 | )
13 |
14 |
15 | def test_artifact():
16 | with pytest.raises(openc2.exceptions.AtLeastOnePropertyError):
17 | openc2.v10.Artifact()
18 |
--------------------------------------------------------------------------------
/tests/v10/test_features.py:
--------------------------------------------------------------------------------
1 | import openc2
2 | import pytest
3 | import json
4 |
5 |
6 | def test_features_empty():
7 | f = openc2.v10.Features()
8 | assert f.features == []
9 |
10 | f = openc2.v10.Features([])
11 | assert f.features == []
12 |
13 | f = openc2.v10.Features(None)
14 | assert f.features == []
15 |
16 | f = openc2.v10.Features(["pairs"])
17 | assert f.features
18 | assert f.features[0] == "pairs"
19 |
20 | f = openc2.v10.Features(["pairs", "versions"])
21 | assert f.features
22 | assert f.features[0] == "pairs"
23 | assert f.features[1] == "versions"
24 |
25 | with pytest.raises(openc2.exceptions.InvalidValueError):
26 | openc2.v10.Features(["invalid"])
27 |
28 |
29 | def test_features_unique():
30 | # A Producer MUST NOT send a list containing more than one instance of any Feature.
31 | # items must be unique
32 | with pytest.raises(openc2.exceptions.InvalidValueError):
33 | openc2.v10.Features(["versions"] * 2)
34 |
--------------------------------------------------------------------------------
/tests/v10/test_stix2.py:
--------------------------------------------------------------------------------
1 | import openc2
2 | import stix2
3 | import pytest
4 | import json
5 |
6 |
7 | def test_custom_target():
8 | @openc2.v10.CustomTarget("x-thing:id", [("id", stix2.properties.StringProperty())])
9 | class CustomTarget(object):
10 | pass
11 |
12 | one = CustomTarget()
13 | assert one != None # for some reason `assert one` fails
14 |
15 | with pytest.raises(openc2.exceptions.ExtraPropertiesError):
16 | CustomTarget(bad="id")
17 |
18 | one = CustomTarget(id="uuid")
19 | assert one
20 | assert one.id == "uuid"
21 |
22 | two = CustomTarget(id=(json.loads(one.serialize())["x-thing:id"]))
23 | assert one == two
24 |
25 | with pytest.raises(openc2.exceptions.ParseError):
26 | openc2.parse(one.serialize())
27 |
28 | with pytest.raises(ValueError):
29 |
30 | @openc2.v10.CustomTarget(
31 | "x-invalid", [("id", stix2.properties.StringProperty())]
32 | )
33 | class CustomTargetInvalid(object):
34 | pass
35 |
36 | with pytest.raises(ValueError):
37 |
38 | @openc2.v10.CustomTarget(
39 | "invalid_target", [("id", stix2.properties.StringProperty())]
40 | )
41 | class CustomTargetInvalid(object):
42 | pass
43 |
44 | with pytest.raises(ValueError):
45 |
46 | @openc2.v10.CustomTarget(
47 | "over_16_chars_long_aaaaaaaaaaaaaaaaaaaa123",
48 | [("id", openc2.properties.StringProperty())],
49 | )
50 | class CustomTargetInvalid(object):
51 | pass
52 |
53 | with pytest.raises(TypeError):
54 |
55 | @openc2.v10.CustomTarget(
56 | "x-custom:id", ("id", stix2.properties.StringProperty()),
57 | )
58 | class CustomTargetInvalid(object):
59 | pass
60 |
61 | with pytest.raises(TypeError):
62 |
63 | @openc2.v10.CustomTarget("x-custom:id")
64 | class CustomTargetInvalid(object):
65 | pass
66 |
67 | with pytest.raises(ValueError):
68 |
69 | @openc2.v10.CustomTarget(
70 | "x-over_16_chars_long_aaaaaaaaaaaaaaaaaaaa:id",
71 | [("id", openc2.properties.StringProperty())],
72 | )
73 | class CustomTargetInvalid(object):
74 | pass
75 |
76 | with pytest.raises(ValueError):
77 |
78 | @openc2.v10.CustomTarget("x-thing:noprops", [])
79 | class CustomTargetInvalid(object):
80 | pass
81 |
82 | with pytest.raises(openc2.exceptions.InvalidValueError):
83 | v = """{ "target": {"x-custom": "value"}, "action":"query"}"""
84 | openc2.utils.parse(v)
85 |
86 | with pytest.raises(openc2.exceptions.InvalidValueError):
87 | v = """{ "target": {"x-custom:id": "value"}, "action":"query"}"""
88 | openc2.utils.parse(v)
89 |
90 | with pytest.raises(openc2.exceptions.InvalidValueError):
91 | v = """{ "target": {}, "action":"query"}"""
92 | openc2.utils.parse(v)
93 |
94 |
95 | def test_stix2_indicator():
96 | indicator = stix2.Indicator(
97 | name="File hash for malware variant",
98 | labels=["malicious-activity"],
99 | pattern="[file:hashes.md5 = 'd41d8cd98f00b204e9800998ecf8427e']",
100 | )
101 |
102 | foo = openc2.v10.Args(indicator=indicator, allow_custom=True)
103 | assert '"name": "File hash for malware variant"' in foo.serialize()
104 | assert '"labels": ["malicious-activity"]' in foo.serialize()
105 | assert (
106 | '"pattern": "[file:hashes.md5 = \'d41d8cd98f00b204e9800998ecf8427e\']"'
107 | in foo.serialize()
108 | )
109 |
110 | # an indicator isn't a property so we have to embed it
111 | @openc2.v10.args.CustomArgs(
112 | "x-indicator-args",
113 | [("indicator", openc2.properties.EmbeddedObjectProperty(stix2.Indicator))],
114 | )
115 | class CustomIndicatorArgs(object):
116 | pass
117 |
118 | foo = CustomIndicatorArgs(indicator=indicator)
119 | assert '"name": "File hash for malware variant"' in foo.serialize()
120 | assert '"labels": ["malicious-activity"]' in foo.serialize()
121 | assert (
122 | '"pattern": "[file:hashes.md5 = \'d41d8cd98f00b204e9800998ecf8427e\']"'
123 | in foo.serialize()
124 | )
125 |
126 |
127 | def test_EmbeddedObjectProperty():
128 | foo = openc2.properties.EmbeddedObjectProperty(stix2.Indicator)
129 |
130 | indicator = stix2.Indicator(
131 | name="File hash for malware variant",
132 | labels=["malicious-activity"],
133 | pattern="[file:hashes.md5 = 'd41d8cd98f00b204e9800998ecf8427e']",
134 | )
135 |
136 | assert foo.clean(indicator).serialize() == indicator.serialize()
137 | assert (
138 | foo.clean(json.loads(indicator.serialize())).serialize()
139 | == indicator.serialize()
140 | )
141 |
142 | with pytest.raises(ValueError):
143 | foo.clean("bad").serialize()
144 |
--------------------------------------------------------------------------------
/tests/validate.py:
--------------------------------------------------------------------------------
1 | import sys, json
2 | from openc2 import parse
3 |
4 | # read in json file and attempt to parse
5 |
6 | with open(sys.argv[1], "r") as IN:
7 | msg = json.load(IN)
8 |
9 | cmd = parse(msg)
10 | print(cmd)
11 |
--------------------------------------------------------------------------------
/tox.ini:
--------------------------------------------------------------------------------
1 | [tox]
2 | envlist = py36,py38
3 |
4 | [testenv]
5 | commands =
6 | python -m pytest --cov=openc2 --cov-report term-missing
7 | deps =
8 | tox
9 | pytest
10 | pytest-cov
11 | coverage
12 | stix2
13 |
--------------------------------------------------------------------------------