From a4a4613a664cd717bcfa14c57b6fa93a6b7bae76 Mon Sep 17 00:00:00 2001
From: Mike Salvatore <mike.s.salvatore@gmail.com>
Date: Thu, 18 Aug 2022 08:49:51 -0400
Subject: [PATCH] Island: Add a Machine model

---
 monkey/monkey_island/cc/models/__init__.py    |   1 +
 monkey/monkey_island/cc/models/machine.py     |  17 ++
 .../monkey_island/cc/models/test_machine.py   | 153 ++++++++++++++++++
 vulture_allowlist.py                          |   1 +
 4 files changed, 172 insertions(+)
 create mode 100644 monkey/monkey_island/cc/models/machine.py
 create mode 100644 monkey/tests/unit_tests/monkey_island/cc/models/test_machine.py

diff --git a/monkey/monkey_island/cc/models/__init__.py b/monkey/monkey_island/cc/models/__init__.py
index 5ed812b7d..319b7b34f 100644
--- a/monkey/monkey_island/cc/models/__init__.py
+++ b/monkey/monkey_island/cc/models/__init__.py
@@ -9,3 +9,4 @@ from .pba_results import PbaResults
 from monkey_island.cc.models.report.report import Report
 from .simulation import Simulation, SimulationSchema, IslandMode
 from .user_credentials import UserCredentials
+from .machine import Machine
diff --git a/monkey/monkey_island/cc/models/machine.py b/monkey/monkey_island/cc/models/machine.py
new file mode 100644
index 000000000..c06c6b086
--- /dev/null
+++ b/monkey/monkey_island/cc/models/machine.py
@@ -0,0 +1,17 @@
+from ipaddress import IPv4Interface
+from typing import Optional, Sequence
+
+from pydantic import Field, PositiveInt
+
+from common import OperatingSystems
+
+from .base_models import MutableBaseModel
+
+
+class Machine(MutableBaseModel):
+    id: PositiveInt = Field(..., allow_mutation=False)
+    node_id: Optional[PositiveInt]
+    network_interfaces: Sequence[IPv4Interface]
+    operating_system: OperatingSystems
+    operating_system_version: str
+    hostname: str
diff --git a/monkey/tests/unit_tests/monkey_island/cc/models/test_machine.py b/monkey/tests/unit_tests/monkey_island/cc/models/test_machine.py
new file mode 100644
index 000000000..dc0ccd45d
--- /dev/null
+++ b/monkey/tests/unit_tests/monkey_island/cc/models/test_machine.py
@@ -0,0 +1,153 @@
+import uuid
+from ipaddress import IPv4Interface
+from types import MappingProxyType
+
+import pytest
+
+from common import OperatingSystems
+from monkey_island.cc.models import Machine
+
+MACHINE_OBJECT_DICT = MappingProxyType(
+    {
+        "id": 1,
+        "node_id": uuid.getnode(),
+        "network_interfaces": [IPv4Interface("10.0.0.1/24"), IPv4Interface("192.168.5.32/16")],
+        "operating_system": OperatingSystems.WINDOWS,
+        "operating_system_version": "eXtra Problems",
+        "hostname": "my.host",
+    }
+)
+
+MACHINE_SIMPLE_DICT = MappingProxyType(
+    {
+        "id": 1,
+        "node_id": uuid.getnode(),
+        "network_interfaces": ["10.0.0.1/24", "192.168.5.32/16"],
+        "operating_system": "windows",
+        "operating_system_version": "eXtra Problems",
+        "hostname": "my.host",
+    }
+)
+
+
+def test_constructor():
+    # Raises exception_on_failure
+    Machine(**MACHINE_OBJECT_DICT)
+
+
+def test_from_dict():
+    # Raises exception_on_failure
+    Machine(**MACHINE_SIMPLE_DICT)
+
+
+def test_to_dict():
+    m = Machine(**MACHINE_OBJECT_DICT)
+
+    assert m.dict(simplify=True) == dict(MACHINE_SIMPLE_DICT)
+
+
+@pytest.mark.parametrize(
+    "key, value",
+    [
+        ("id", "not-an-int"),
+        ("node_id", "not-an-int"),
+        ("network_interfaces", "not-a-list"),
+        ("operating_system", 2.1),
+        ("operating_system", "bsd"),
+        ("operating_system_version", {}),
+        ("hostname", []),
+    ],
+)
+def test_construct_invalid_field__type_error(key, value):
+    invalid_type_dict = MACHINE_SIMPLE_DICT.copy()
+    invalid_type_dict[key] = value
+
+    with pytest.raises(TypeError):
+        Machine(**invalid_type_dict)
+
+
+@pytest.mark.parametrize(
+    "key, value",
+    [
+        ("id", -1),
+        ("node_id", 0),
+        ("network_interfaces", [1, "stuff", 3]),
+        ("network_interfaces", ["10.0.0.1/16", 2, []]),
+    ],
+)
+def test_construct_invalid_field__value_error(key, value):
+    invalid_type_dict = MACHINE_SIMPLE_DICT.copy()
+    invalid_type_dict[key] = value
+
+    with pytest.raises(ValueError):
+        Machine(**invalid_type_dict)
+
+
+def test_construct__extra_fields_forbidden():
+    extra_field_dict = MACHINE_SIMPLE_DICT.copy()
+    extra_field_dict["extra_field"] = 99  # red balloons
+
+    with pytest.raises(ValueError):
+        Machine(**extra_field_dict)
+
+
+def test_id_immutable():
+    m = Machine(**MACHINE_OBJECT_DICT)
+    with pytest.raises(TypeError):
+        m.id = 2
+
+
+@pytest.mark.parametrize("node_id", [None, 1, 100])
+def test_node_id_set_valid_value(node_id):
+    m = Machine(**MACHINE_OBJECT_DICT)
+
+    # Raises exception_on_failure
+    m.node_id = node_id
+
+
+def test_node_id_validate_on_set():
+    m = Machine(**MACHINE_OBJECT_DICT)
+    with pytest.raises(ValueError):
+        m.node_id = -50
+
+
+def test_network_interfaces_set_valid_value():
+    m = Machine(**MACHINE_OBJECT_DICT)
+
+    # Raises exception_on_failure
+    m.network_interfaces = [IPv4Interface("172.1.2.3/24")]
+
+
+def test_network_interfaces_set_invalid_value():
+    m = Machine(**MACHINE_OBJECT_DICT)
+
+    with pytest.raises(ValueError):
+        m.network_interfaces = [IPv4Interface("172.1.2.3/24"), None]
+
+
+def test_operating_system_set_valid_value():
+    m = Machine(**MACHINE_OBJECT_DICT)
+
+    # Raises exception_on_failure
+    m.operating_system = OperatingSystems.LINUX
+
+
+def test_operating_system_set_invalid_value():
+    m = Machine(**MACHINE_OBJECT_DICT)
+
+    with pytest.raises(ValueError):
+        m.operating_system = "MacOS"
+
+
+def test_set_operating_system_version():
+    m = Machine(**MACHINE_OBJECT_DICT)
+
+    # Raises exception_on_failure
+    m.operating_system_version = "1234"
+
+
+def test_set_hostname():
+    m = Machine(**MACHINE_OBJECT_DICT)
+
+    # Raises exception_on_failure
+    m.operating_system_version = "wopr"
diff --git a/vulture_allowlist.py b/vulture_allowlist.py
index 70f620492..312022dd3 100644
--- a/vulture_allowlist.py
+++ b/vulture_allowlist.py
@@ -209,6 +209,7 @@ _serialize_credentials  # unused method (monkey/common/credentials/credentials:6
 
 # Models
 _make_simulation  # unused method (monkey/monkey_island/cc/models/simulation.py:19
+operating_system_version
 
 # TODO DELETE AFTER RESOURCE REFACTORING