├── .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 |
5 |

Public Participation Invited

6 | 7 |

This OASIS TC Open Repository ( github.com/oasis-open/openc2-lycan-python ) is a community public repository that supports participation by anyone, whether affiliated with OASIS or not. Substantive contributions (repository "code") and related feedback is invited from all parties, following the common conventions for participation in GitHub public repository projects. Participation is expected to be consistent with the OASIS TC Open Repository Guidelines and Procedures, the LICENSE designated for this particular repository (MIT License), and the requirement for an Individual Contributor License Agreement. Please see the repository README document for other details.

8 |
9 | 10 | 11 |
12 |

Governance Distinct from OASIS TC Process

13 |

Content accepted as "contributions" to this TC Open Repository, as defined below, are distinct from any Contributions made to the associated OASIS Open Command and Control (OpenC2) TC itself. Participation in the associated Technical Committee is governed by the OASIS Bylaws, OASIS TC Process, IPR Policy, and related policies. This TC Open Repository is not subject to the OASIS TC-related policies. TC Open Repository governance is defined by separate participation and contribution guidelines as referenced in the OASIS TC Open Repositories Overview.

14 |
15 | 16 |
17 |

Licensing Distinct from OASIS IPR Policy

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 |

Contributions Subject to Individual CLA

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 |

Fork-and-Pull Collaboration Model

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 |

Feedback

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 | [![Python Support](https://img.shields.io/badge/python-3.6+-blue.svg)](https://www.python.org/) 3 | [![Build Status](https://travis-ci.org/oasis-open/openc2-lycan-python.svg)](https://travis-ci.org/open-oasis/openc2-lycan-python) 4 | [![Coverage Status](https://coveralls.io/repos/github/oasis-open/openc2-lycan-python/badge.svg)](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 |

OASIS TC Open Repository: openc2-lycan-python

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 |

Statement of Purpose

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 |

Additions to Statement of Purpose

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 |

Maintainers

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 |

About OASIS TC Open Repositories

102 | 103 |

110 | 111 |
112 | 113 |

Feedback

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 | --------------------------------------------------------------------------------