Skip to content

Commit

Permalink
Merge pull request #12 from ScreamBun/master
Browse files Browse the repository at this point in the history
Python 3.10 compatibility and unimplemented command handling
  • Loading branch information
ScreamBun authored Jul 21, 2022
2 parents dcb1366 + 8738631 commit a1effcb
Show file tree
Hide file tree
Showing 27 changed files with 79 additions and 57 deletions.
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,12 +89,12 @@ Serialization(name='json', deserialize=json.loads, serialize=json.dumps)

Using Python3.8+, install with venv and pip:
```sh
mkdir yuuki
cd yuuki
mkdir oc2_arch
cd oc2_arch
python3 -m venv venv
source venv/bin/activate
git clone THIS_REPO
pip install ./openc2-yuuki
pip install ./openc2-oc2_arch
```


Expand Down Expand Up @@ -132,7 +132,7 @@ After sending the Command, the Producer should receive a Response similar to the
"request_id": "f81d4fae-7dec-11d0-a765-00a0c91e6bf6",
"created": 1619554273604,
"to": "Producer1",
"from": "yuuki"
"from": "oc2_arch"
},
"body": {
"openc2": {
Expand Down
29 changes: 22 additions & 7 deletions examples/actuators/slpf.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,24 @@

from ipaddress import ip_network

from yuuki import Actuator, OpenC2CmdFields, OpenC2RspFields, StatusCode
from oc2_arch import Actuator, OpenC2CmdFields, OpenC2RspFields, StatusCode

slpf = Actuator(nsid='slpf')


@slpf.pair('deny', 'ipv4_connection', implemented=False)
def deny_ipv4_connection(oc2_cmd: OpenC2CmdFields) -> OpenC2RspFields:
pass

status_text = f'Command Not Implemented'
return OpenC2RspFields(status=StatusCode.NOT_IMPLEMENTED, status_text=status_text)


@slpf.pair('allow', 'ipv4_connection', implemented=False)
def allow_ipv4_connection(oc2_cmd: OpenC2CmdFields) -> OpenC2RspFields:
pass

status_text = f'Command Not Implemented'
return OpenC2RspFields(status=StatusCode.NOT_IMPLEMENTED, status_text=status_text)



@slpf.pair('deny', 'ipv6_connection')
Expand Down Expand Up @@ -76,12 +81,17 @@ def allow_ipv6_connection(oc2_cmd: OpenC2CmdFields) -> OpenC2RspFields:

@slpf.pair('deny', 'ipv4_net', implemented=False)
def deny_ipv4_net(oc2_cmd: OpenC2CmdFields) -> OpenC2RspFields:
pass

status_text = f'Command Not Implemented'
return OpenC2RspFields(status=StatusCode.NOT_IMPLEMENTED, status_text=status_text)


@slpf.pair('allow', 'ipv4_net', implemented=False)
def allow_ipv4_net(oc2_cmd: OpenC2CmdFields) -> OpenC2RspFields:
pass

status_text = f'Command Not Implemented'
return OpenC2RspFields(status=StatusCode.NOT_IMPLEMENTED, status_text=status_text)



@slpf.pair('deny', 'ipv6_net')
Expand Down Expand Up @@ -124,9 +134,14 @@ def allow_ipv6_net(oc2_cmd: OpenC2CmdFields) -> OpenC2RspFields:

@slpf.pair('update', 'file', implemented=False)
def update_file(oc2_cmd: OpenC2CmdFields) -> OpenC2RspFields:
pass

status_text = f'Command Not Implemented'
return OpenC2RspFields(status=StatusCode.NOT_IMPLEMENTED, status_text=status_text)


@slpf.pair('delete', 'slpf:rule_number', implemented=False)
def delete_rule_number(oc2_cmd: OpenC2CmdFields) -> OpenC2RspFields:
pass

status_text = f'Command Not Implemented'
return OpenC2RspFields(status=StatusCode.NOT_IMPLEMENTED, status_text=status_text)

2 changes: 1 addition & 1 deletion examples/consumer_example.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
OpenC2 Consumer Example
"""

from yuuki import Consumer
from oc2_arch import Consumer
from actuators.slpf import slpf


Expand Down
2 changes: 1 addition & 1 deletion examples/http_example.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""
Example Implementation of an OpenC2 HTTP Consumer
"""
from yuuki.transports import HttpTransport, HttpConfig
from oc2_arch.transports import HttpTransport, HttpConfig
from consumer_example import consumer


Expand Down
2 changes: 1 addition & 1 deletion examples/mqtt_example.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"""
import argparse

from yuuki.transports import (
from oc2_arch.transports import (
MqttTransport,
MqttConfig,
MQTTAuthorization,
Expand Down
2 changes: 1 addition & 1 deletion examples/opendxl_example.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import argparse

from yuuki.transports import OpenDxlTransport, OpenDxlConfig
from oc2_arch.transports import OpenDxlTransport, OpenDxlConfig

from consumer_example import consumer

Expand Down
20 changes: 0 additions & 20 deletions examples/producers/openc2_command_report.py

This file was deleted.

File renamed without changes.
18 changes: 10 additions & 8 deletions yuuki/actuator.py → oc2_arch/actuator.py
Original file line number Diff line number Diff line change
@@ -1,25 +1,22 @@
"""OpenC2 Actuator
https://docs.oasis-open.org/openc2/oc2ls/v1.0/oc2ls-v1.0.html
"""
from collections import defaultdict
from typing import Callable, Dict, List, NoReturn
from .openc2_types import OpenC2CmdFields, OpenC2RspFields

OpenC2Function = Callable[[OpenC2CmdFields], OpenC2RspFields]


def unimplemented_command() -> NoReturn:
raise NotImplementedError


class Actuator:
dispatch: Dict[str, Dict[str, Dict[str, Callable]]]
pairs: Dict[str, List[str]]
understood: Dict[str, List[str]]
nsid: str

def __init__(self, nsid: str):
self.dispatch = {}
self.pairs = {}
self.understood = {}
self.nsid = nsid

def pair(self, action: str, target: str, implemented: bool = True) -> Callable:
Expand All @@ -45,7 +42,7 @@ def a_function(oc2_cmd):
"""
def decorator(function: OpenC2Function) -> OpenC2Function:
self.register_pair(function, action, target, implemented)
print("Added "+action+" "+target)

return function
return decorator

Expand All @@ -58,9 +55,14 @@ def register_pair(self, function: OpenC2Function, action: str, target: str, impl
:param target: Name of the Target of the Action
:param implemented: Indicates whether the Command specified in the Actuator profile is supported or not
"""
if implemented:
if implemented is True:
self.dispatch.setdefault(action, {}).setdefault(target, {})[self.nsid] = function
self.pairs.setdefault(action, []).append(target)
self.understood.setdefault(action, []).append(target)
print("Added " + action + " " + target)
else:
self.dispatch[action][target][self.nsid] = unimplemented_command
self.understood.setdefault(action, []).append(target)
print("Unimplemented Command"+action+" "+target)
pass


37 changes: 31 additions & 6 deletions yuuki/consumer.py → oc2_arch/consumer.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
from pprint import pformat
from concurrent.futures import ThreadPoolExecutor
from functools import partial
from collections import defaultdict
from typing import Any, Callable, Dict, List, Union

from pydantic import ValidationError
Expand All @@ -24,6 +23,7 @@ class Consumer:
"""
dispatch: Dict[str, Dict[str, Dict[str, Callable]]]
pairs: Dict[str, List[str]]
understood: Dict[str, List[str]]
profiles: List[str]
rate_limit: int
versions: List[str]
Expand All @@ -40,6 +40,9 @@ def __init__(self, rate_limit: int, versions: List[str], actuators: List[Actuato
self.pairs = {
'query': ['features']
}
self.understood = {
'query': ['features']
}
self.profiles = []
self.rate_limit = rate_limit
self.versions = versions
Expand Down Expand Up @@ -86,16 +89,26 @@ def process_command(self, command, encode: str) -> Union[str, bytes, None]:
try:
openc2_msg = OpenC2Msg(**message)
except ValidationError as e:
logging.error(e)
#logging.error(e)
openc2_rsp = OpenC2RspFields(status=StatusCode.BAD_REQUEST, status_text='Malformed OpenC2 message')
return self.create_response_msg(openc2_rsp, encode=encode)
except KeyError as e:
#logging.error(e)
openc2_rsp = OpenC2RspFields(status=StatusCode.BAD_REQUEST, status_text='Malformed OpenC2 message')
return self.create_response_msg(openc2_rsp, encode=encode)

try:
actuator_callable = self._get_actuator_callable(openc2_msg)
except TypeError as e:
logging.error(e)
openc2_rsp = OpenC2RspFields(status=StatusCode.NOT_FOUND, status_text='No matching Actuator found')
return self.create_response_msg(openc2_rsp, headers=openc2_msg.headers, encode=encode)
#logging.error(e)
#sorting logic to differentiate the unknown actuator commmands from known but unimplemented ones
if openc2_msg.body.openc2.request.action in self.understood \
and openc2_msg.body.openc2.request.target_name in self.understood:
openc2_rsp = OpenC2RspFields(status=StatusCode.NOT_IMPLEMENTED, status_text='Command Not Supported')
return self.create_response_msg(openc2_rsp, headers=openc2_msg.headers, encode=encode)
else:
openc2_rsp = OpenC2RspFields(status=StatusCode.NOT_FOUND, status_text='No matching Actuator found')
return self.create_response_msg(openc2_rsp, headers=openc2_msg.headers, encode=encode)

if openc2_msg.body.openc2.request.args and openc2_msg.body.openc2.request.args.response_requested:
response_requested = openc2_msg.body.openc2.request.args.response_requested
Expand Down Expand Up @@ -168,6 +181,7 @@ def _get_actuator_callable(self, oc2_msg: OpenC2Msg) -> Callable[[], OpenC2RspFi
oc2_cmd = oc2_msg.body.openc2.request
print(f"{oc2_cmd.action} {oc2_cmd.target_name} {oc2_cmd.actuator_name}")
print(self.dispatch)
print(self.understood)
if oc2_cmd.action == 'query' and oc2_cmd.target_name == 'features':
function = self.query_features
elif oc2_cmd.action in self.dispatch and oc2_cmd.target_name in self.dispatch[oc2_cmd.action]:
Expand All @@ -180,10 +194,12 @@ def _get_actuator_callable(self, oc2_msg: OpenC2Msg) -> Callable[[], OpenC2RspFi
function = self.dispatch[oc2_cmd.action][oc2_cmd.target_name][oc2_cmd.actuator_name]
else:
raise TypeError(f'No Actuator: {oc2_cmd.actuator_name}')
elif oc2_cmd.action in self.understood and oc2_cmd.target_name in self.understood[oc2_cmd.action]:
function = self.unimplemented_command_function
else:
raise TypeError(f'No Action-Target pair for {oc2_cmd.action} {oc2_cmd.target_name}')

logging.debug(f'Will call a function named: {function.__name__}')
#logging.debug(f'Will call a function named: {function.__name__}')
return partial(function, oc2_cmd)

def query_features(self, oc2_cmd: OpenC2CmdFields) -> OpenC2RspFields:
Expand Down Expand Up @@ -229,6 +245,14 @@ def query_features(self, oc2_cmd: OpenC2CmdFields) -> OpenC2RspFields:
else:
return OpenC2RspFields(status=StatusCode.OK)

def unimplemented_command_function(self, oc2_cmd: OpenC2CmdFields) -> OpenC2RspFields:
"""
Handles instances of get_actuator_callable that find a command that is understood by an actuator
but not implemented. Returns OpenC2 RSP with a 501 error.
"""
print("Command Not Implemented")
return OpenC2RspFields(status=StatusCode.NOT_IMPLEMENTED, status_text='Command Not Supported')

def add_actuator_profile(self, actuator: Actuator) -> None:
"""
Adds the Actuator's functions to the Consumer and adds the Actuator's namespace identifier (nsid) to the
Expand All @@ -240,6 +264,7 @@ def add_actuator_profile(self, actuator: Actuator) -> None:
else:
self.profiles.append(actuator.nsid)
self.pairs.update(actuator.pairs)
self.understood.update(actuator.understood)
self._update_dispatch_rec(self.dispatch, actuator.dispatch)

def add_serialization(self, serialization: Serialization) -> None:
Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
from flask import Flask, request, make_response
from werkzeug.http import parse_options_header
from waitress import serve
from yuuki.consumer import Consumer
from yuuki.openc2_types import StatusCode, OpenC2RspFields
from oc2_arch.consumer import Consumer
from oc2_arch.openc2_types import StatusCode, OpenC2RspFields
from .config import HttpConfig


Expand All @@ -23,7 +23,7 @@ class HttpTransport:
def __init__(self, consumer: Consumer, config: HttpConfig):
self.consumer = consumer
self.config = config
self.app = Flask('yuuki')
self.app = Flask('oc2_arch')
self.app.add_url_rule('/', view_func=self.receive, methods=['POST'])

def receive(self):
Expand Down
File renamed without changes.
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@
from typing import Any
from paho.mqtt.packettypes import PacketTypes
from paho.mqtt.properties import Properties
from yuuki.consumer import Consumer
from yuuki.openc2_types import StatusCode, OpenC2RspFields
from oc2_arch.consumer import Consumer
from oc2_arch.openc2_types import StatusCode, OpenC2RspFields
from .config import MqttConfig


Expand Down
File renamed without changes.
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@
from dxlclient.message import Event, Request, Response
from dxlclient.service import ServiceRegistrationInfo

from yuuki.consumer import Consumer
from yuuki.openc2_types import StatusCode, OpenC2RspFields
from oc2_arch.consumer import Consumer
from oc2_arch.openc2_types import StatusCode, OpenC2RspFields
from .config import OpenDxlConfig


Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ def get_requirements():
setup(
name='Yuuki',
package_data={
'Yuuki': ['./yuuki/*']
'Yuuki': ['./oc2_arch/*']
},
install_requires=get_requirements()
)

0 comments on commit a1effcb

Please sign in to comment.