Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions pyoaev/contracts/contract_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,53 @@ class ContractOutputType(str, Enum):
KerberoastableAccount: str = "kerberoastable_account"


class PrimitiveType(str, Enum):
"""The semantic type of a contract argument's value, independent of how the
field renders in the UI (see ``ContractFieldType``) and independent of
chaining specifically — an argument can carry a ``PrimitiveType`` and be
filled by hand, exactly as it can be auto-linked from a prior step's
matching output. Mirrors ``io.openaev.database.model.PrimitiveType``
(the openaev platform's own enum) label-for-label; every value here must
stay in sync with it.
"""

AccountWithPasswordNotRequired: str = "account_with_password_not_required"
ActionOutput: str = "action_output"
AdminUsername: str = "admin_username"
AsreproastableAccount: str = "asreproastable_account"
AssetGroupId: str = "asset_group_id"
AssetId: str = "asset_id"
ComputerName: str = "computer_name"
CVE: str = "cve"
DelegationAccount: str = "delegation_account"
Document: str = "document"
Domain: str = "domain"
FileName: str = "file_name"
FilePath: str = "file_path"
GroupName: str = "group_name"
Hash: str = "hash"
Host: str = "host"
IPv4: str = "ipv4"
IPv6: str = "ipv6"
IpSubnet: str = "ip_subnet"
KerberoastableAccount: str = "kerberoastable_account"
Key: str = "key"
Number: str = "number"
Password: str = "password"
Permissions: str = "permissions"
Port: str = "port"
Service: str = "service"
Severity: str = "severity"
ShareName: str = "share_name"
SID: str = "sid"
TargetedAsset: str = "targeted-asset"
Text: str = "text"
Username: str = "username"
Value: str = "value"
VulnerabilityName: str = "vulnerability_name"
VulnerabilityStatus: str = "vulnerability_status"


class ExpectationType(str, Enum):
text: str = "TEXT"
document: str = "DOCUMENT"
Expand Down Expand Up @@ -131,6 +178,15 @@ class ContractElement(ABC):
linkedFields: List[str] = field(default_factory=list)
mandatory: bool = False
readOnly: bool = False
# The argument's chaining/semantic type (``PrimitiveType``), e.g. "username"
# or "host" — matches the platform's own ``argumentType`` field
# (``io.openaev.database.model.ContractElement``) label-for-label so a
# contract pushed from here needs no translation on the other end. Left
# unset (``None``) for fields with no established type: the platform
# normalizes a missing/null/empty value to ``PrimitiveType.Text`` on its
# own, so omitting this is always safe and never a breaking change for
# existing contracts.
argumentType: Optional[PrimitiveType] = None

@property
@abstractmethod
Expand Down
41 changes: 41 additions & 0 deletions test/contracts/test_contract_element_argument_type.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import json
import unittest

from pyoaev import utils
from pyoaev.contracts.contract_config import ContractText, PrimitiveType


def _serialize(field):
return json.loads(json.dumps(field, cls=utils.EnhancedJSONEncoder))


class ContractElementArgumentTypeTest(unittest.TestCase):
def test_defaults_to_none_when_not_declared(self):
"""Existing contracts that never pass argumentType keep working
unchanged — the platform normalizes a null value to PrimitiveType.Text
on its own, so this is not a breaking change for anything already
deployed."""
untyped = ContractText(key="uri", label="URL")
self.assertIsNone(untyped.argumentType)
self.assertIsNone(_serialize(untyped)["argumentType"])

def test_explicit_argument_type_round_trips(self):
typed = ContractText(
key="basicUser", label="Username", argumentType=PrimitiveType.Username
)
self.assertEqual(typed.argumentType, PrimitiveType.Username)
self.assertEqual(_serialize(typed)["argumentType"], "username")

def test_argument_type_is_independent_of_widget_type(self):
"""A field's rendering (ContractFieldType, on `type`) and its chaining
semantics (PrimitiveType, on `argumentType`) are separate axes."""
typed = ContractText(
key="basicUser", label="Username", argumentType=PrimitiveType.Username
)
serialized = _serialize(typed)
self.assertEqual(serialized["type"], "text")
self.assertEqual(serialized["argumentType"], "username")


if __name__ == "__main__":
unittest.main()
77 changes: 77 additions & 0 deletions test/contracts/test_primitive_type.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import unittest

from pyoaev.contracts.contract_config import PrimitiveType


class PrimitiveTypeTest(unittest.TestCase):
def test_username_wire_label(self):
# The wire label is a public contract shared with the platform enum
# (io.openaev.database.model.PrimitiveType.Username); it must stay
# exactly "username".
self.assertEqual(PrimitiveType.Username.value, "username")
self.assertEqual(PrimitiveType.Username, "username")

def test_host_wire_label(self):
self.assertEqual(PrimitiveType.Host.value, "host")
self.assertEqual(PrimitiveType.Host, "host")

def test_text_wire_label(self):
# The platform's own normalizer defaults a missing argumentType to
# exactly this value — it must stay "text".
self.assertEqual(PrimitiveType.Text.value, "text")
self.assertEqual(PrimitiveType.Text, "text")

def test_action_output_wire_label(self):
# Shared with ContractOutputType.ActionOutput; both enums describe the
# same platform-side value from two different angles (an output
# producing it vs. an input typed to receive it) and must not drift
# apart.
self.assertEqual(PrimitiveType.ActionOutput.value, "action_output")

def test_every_value_matches_the_openaev_platform_enum(self):
# io.openaev.database.model.PrimitiveType, transcribed label-for-label.
# Keep this set in sync with that file, not just the four spot-checked
# above.
expected_labels = {
"account_with_password_not_required",
"action_output",
"admin_username",
"asreproastable_account",
"asset_group_id",
"asset_id",
"computer_name",
"cve",
"delegation_account",
"document",
"domain",
"file_name",
"file_path",
"group_name",
"hash",
"host",
"ipv4",
"ipv6",
"ip_subnet",
"kerberoastable_account",
"key",
"number",
"password",
"permissions",
"port",
"service",
"severity",
"share_name",
"sid",
"targeted-asset",
"text",
"username",
"value",
"vulnerability_name",
"vulnerability_status",
}
actual_labels = {member.value for member in PrimitiveType}
self.assertEqual(actual_labels, expected_labels)


if __name__ == "__main__":
unittest.main()
Loading