diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 57811ab6..e411c25c 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -3,3 +3,5 @@ # Owners for a specific folder maltoolbox/language/compiler/ @nkakouros +maltoolbox/language/compiler/mal_compiler.py @nkakouros @sandorstormen +maltoolbox/language/language_graph_model_effect.py @sandorstormen diff --git a/.gitignore b/.gitignore index dde54e8f..0e66111c 100644 --- a/.gitignore +++ b/.gitignore @@ -66,4 +66,8 @@ nosetests.xml !examples/resources/* # uv lock file -uv.lock \ No newline at end of file +uv.lock + +# Claude +.claude +*.local.* \ No newline at end of file diff --git a/maltoolbox/language/compiler/mal_analyzer.py b/maltoolbox/language/compiler/mal_analyzer.py index 3c114bd2..1dbb03cb 100644 --- a/maltoolbox/language/compiler/mal_analyzer.py +++ b/maltoolbox/language/compiler/mal_analyzer.py @@ -584,12 +584,12 @@ def _read_steps(self, asset: str, parents: List) -> None: or attackStep_node_reaches.child_by_field_name( 'operator' ).text.decode() - == '->' + in ('->', 'A>', 'R>') ): # Valid step current_steps.append(attackStep) else: - # Step was defined using '+>', but there is nothing to inherit from + # Step was defined using '+>', '+A>', or '+R>', but there is nothing to inherit from error_msg = f"Cannot inherit attack step '{attackStep}' without previous definition" self._raise_analyzer_exception(error_msg) else: diff --git a/maltoolbox/language/compiler/mal_compiler.py b/maltoolbox/language/compiler/mal_compiler.py index 2ee31b6a..48458591 100644 --- a/maltoolbox/language/compiler/mal_compiler.py +++ b/maltoolbox/language/compiler/mal_compiler.py @@ -462,11 +462,6 @@ def visit_attack_step(self, cursor: TreeCursor) -> tuple[str, dict]: requires = self.visit(cursor) go_to_sibling(cursor) - reaches = None - if assert_node(cursor.node).type == 'reaching': - reaches = self.visit(cursor) - go_to_sibling(cursor) - ret = { 'name': name, 'meta': meta, @@ -477,9 +472,23 @@ def visit_attack_step(self, cursor: TreeCursor) -> tuple[str, dict]: 'risk': risk, 'ttc': ttc, 'requires': requires, - 'reaches': reaches, + 'reaches': None, + 'append_reaches': None, + 'remove_reaches': None } + reaching_types = ('reaching', 'append_reaching', 'remove_reaching') + + while assert_node(cursor.node).type in reaching_types: + if assert_node(cursor.node).type == 'reaching': + ret['reaches'] = self.visit(cursor) + elif assert_node(cursor.node).type == 'append_reaching': + ret['append_reaches'] = self.visit(cursor) + elif assert_node(cursor.node).type == 'remove_reaching': + ret['remove_reaches'] = self.visit(cursor) + if not go_to_sibling(cursor): + break + return ('step', ret) def visit_detector(self, cursor: TreeCursor): @@ -797,9 +806,76 @@ def visit_reaching(self, cursor: TreeCursor): ret['stepExpressions'].append(self.visit(cursor)) return ret + + def visit_append_reaching(self, cursor: TreeCursor): + ################################################# + # ( '+A>' | 'A>' ) (reaches) ( ',' (reaches) )* # + ################################################# + + ret: dict[str, Any] = {} + + # Get type of reaches + ret['overrides'] = assert_node(cursor.node).text == b'A>' + go_to_sibling(cursor) + + # Visit the steps + ret['stepExpressions'] = [self.visit(cursor)] + + while go_to_sibling(cursor): # check if we have a ',' + go_to_sibling(cursor) # ignore the ',' + ret['stepExpressions'].append(self.visit(cursor)) + + return ret + + def visit_remove_reaching(self, cursor: TreeCursor): + ################################################# + # ( '+R>' | 'R>' ) (reaches) ( ',' (reaches) )* # + ################################################# + + ret: dict[str, Any] = {} + + # Get type of reaches + ret['overrides'] = assert_node(cursor.node).text == b'R>' + go_to_sibling(cursor) + + # Visit the steps + ret['stepExpressions'] = [self.visit(cursor)] + + while go_to_sibling(cursor): # check if we have a ',' + go_to_sibling(cursor) # ignore the ',' + ret['stepExpressions'].append(self.visit(cursor)) + + return ret + + def visit_dyn_sentence(self, cursor: TreeCursor): + ######################################### + # (base) '/' (target) ( '^' (target) )* # + ######################################### + + base = self.visit(cursor) + go_to_sibling(cursor) + assert assert_node(cursor.node).type == 'dynamic_separator', 'Expected dynamic separator in dynamic sentence' + go_to_sibling(cursor) + targets = [self.visit(cursor)] + while go_to_sibling(cursor): + assert assert_node(cursor.node).type == 'hold', 'Expected hold between targets in dynamic sentence' + go_to_sibling(cursor) + targets.append(self.visit(cursor)) + + return {"type": "dyn_sentence", "base": base, "targets": targets} + + def visit_assoc_and_asset_expr(self, cursor: TreeCursor): + if assert_node(cursor.node).type == 'assoc_op': + assert cursor.goto_next_sibling(), "Expected next sibling after assoc_op" + result = {"type": "assoc_op", "operand": self.visit(cursor)} + assert result["operand"] is not None, "Expected operand in assoc_and_asset_expr" + else: + result = self.visit(cursor) + return result def visit_asset_expr(self, cursor: TreeCursor): - return self._visit_inline_asset_expr(cursor) + ret = self._visit_inline_asset_expr(cursor) + return ret def _visit_inline_asset_expr(self, cursor: TreeCursor): ############################################################################################################################################# diff --git a/maltoolbox/language/expression_chain.py b/maltoolbox/language/expression_chain.py index f67b2555..676846ce 100644 --- a/maltoolbox/language/expression_chain.py +++ b/maltoolbox/language/expression_chain.py @@ -32,6 +32,7 @@ class ExprType(str, Enum): FIELD = "field" TRANSITIVE = "transitive" SUBTYPE = "subType" + ASSOC_OP = "assoc_op" def is_binary(self) -> bool: return self in { @@ -74,6 +75,10 @@ def _validate(self) -> None: if self.type == ExprType.SUBTYPE: if not self.sub_link or not self.subtype: raise ValueError("SUBTYPE requires sub_link and subtype") + + if self.type == ExprType.ASSOC_OP: + if not self.sub_link: + raise ValueError("ASSOC_OP requires sub_link") def to_dict(self) -> dict: if self.type.is_binary(): @@ -87,6 +92,9 @@ def to_dict(self) -> dict: if self.type == ExprType.SUBTYPE: return self._subtype_to_dict() + + if self.type == ExprType.ASSOC_OP: + return self._assoc_op_to_dict() raise LanguageGraphAssociationError( f"Unknown expressions chain element {self.type}" @@ -116,6 +124,13 @@ def _subtype_to_dict(self) -> dict: "expression": self.sub_link.to_dict(), "type": self.type.value, } + + def _assoc_op_to_dict(self) -> dict: + assert self.sub_link, "ASSOC_OP expression requires sub_link" + return { + "operand": self.sub_link.to_dict(), + "type": self.type.value + } def _field_to_dict(self) -> dict: asset_type = self._resolve_field_asset_type() diff --git a/maltoolbox/language/language_graph_attack_step.py b/maltoolbox/language/language_graph_attack_step.py index ce63dbb1..d7b6124d 100644 --- a/maltoolbox/language/language_graph_attack_step.py +++ b/maltoolbox/language/language_graph_attack_step.py @@ -6,11 +6,12 @@ from dataclasses import dataclass, field from functools import cached_property + if TYPE_CHECKING: from maltoolbox.language.expression_chain import ExpressionsChain from maltoolbox.language.language_graph_asset import LanguageGraphAsset from maltoolbox.language.language_graph_detector import LanguageGraphDetector - + from maltoolbox.language.language_graph_model_effect import LanguageGraphModelEffect @dataclass class LanguageGraphAttackStep: @@ -26,6 +27,8 @@ class LanguageGraphAttackStep: own_children: dict[ LanguageGraphAttackStep, list[ExpressionsChain | None] ] = field(default_factory=dict) + own_additive_model_effects: list[LanguageGraphModelEffect] = field(default_factory=list) + own_subtractive_model_effects: list[LanguageGraphModelEffect] = field(default_factory=list) own_parents: dict[ LanguageGraphAttackStep, list[ExpressionsChain | None] ] = field(default_factory=dict) diff --git a/maltoolbox/language/language_graph_builder.py b/maltoolbox/language/language_graph_builder.py index 7f07c035..1062e62e 100644 --- a/maltoolbox/language/language_graph_builder.py +++ b/maltoolbox/language/language_graph_builder.py @@ -9,6 +9,7 @@ from maltoolbox.language.language_graph_asset import LanguageGraphAsset from maltoolbox.language.language_graph_assoc import LanguageGraphAssociation, LanguageGraphAssociationField, link_association_to_assets from maltoolbox.language.language_graph_attack_step import LanguageGraphAttackStep +from maltoolbox.language.language_graph_model_effect import build_model_effect from maltoolbox.language.step_expression_processor import process_step_expression, resolve_variable, reverse_expr_chain @@ -200,7 +201,7 @@ def _connect_attack_steps( lang_spec: dict[str, Any], attack_step_dicts: dict[str, dict] ) -> None: - """Connect attack steps based on the 'reaches' and 'requires' expressions in the language specification.""" + """Connect attack steps based on the 'reaches', 'append_reaches', 'remove_reaches' and 'requires' expressions in the language specification.""" for asset in assets.values(): for step in asset.attack_steps.values(): @@ -244,6 +245,37 @@ def _connect_attack_steps( f'Failed to find existence step requirement for:\n{expr}' ) step.own_requires.append(chain) + + # append_reaches = attack_step_dict.get('append_reaches') or {} + # for append_expression in append_reaches.get('stepExpressions', []): + # base_asset, base_expr, base_targets = proccess_dyn_sentence(assets, step.asset, append_expression, lang_spec) + +def _connect_model_effects( + assets: dict[str, LanguageGraphAsset], + lang_spec: dict[str, Any], + attack_step_dicts: dict[str, dict] +) -> None: + """Connect model effects to attack steps""" + + for asset in assets.values(): + for step in asset.attack_steps.values(): + logger.debug('Determining children for attack step %s', step.name) + + if step.full_name not in attack_step_dicts: + # Skip steps that are not in the lang spec (inherited steps) + continue + + attack_step_dict = attack_step_dicts[step.full_name] + + append_reaches = attack_step_dict.get('append_reaches') or {} + for expr in append_reaches.get('stepExpressions', []): + model_effect = build_model_effect(assets, step.asset, expr, lang_spec) + step.own_additive_model_effects.append(model_effect) + + remove_reaches = attack_step_dict.get('remove_reaches') or {} + for expr in remove_reaches.get('stepExpressions', []): + model_effect = build_model_effect(assets, step.asset, expr, lang_spec) + step.own_subtractive_model_effects.append(model_effect) def generate_attack_steps(assets: dict[str, LanguageGraphAsset], lang_spec: dict) -> None: @@ -257,6 +289,7 @@ def generate_attack_steps(assets: dict[str, LanguageGraphAsset], lang_spec: dict 2. Inherit attack steps from super-assets, respecting overrides. 3. Link attack steps via 'reaches' and evaluate 'exist'/'notExist' requirements. + 4. Link model effects via 'append_reaches' and 'remove_reaches'. Args: assets (dict): Mapping of asset names to asset objects. @@ -271,6 +304,7 @@ def generate_attack_steps(assets: dict[str, LanguageGraphAsset], lang_spec: dict attack_step_dicts = _create_lg_attack_step_nodes(assets, lang_spec) _inherit_attack_steps(assets) _connect_attack_steps(assets, lang_spec, attack_step_dicts) + _connect_model_effects(assets, lang_spec, attack_step_dicts) def create_associations_for_assets( diff --git a/maltoolbox/language/language_graph_model_effect.py b/maltoolbox/language/language_graph_model_effect.py new file mode 100644 index 00000000..19f3233a --- /dev/null +++ b/maltoolbox/language/language_graph_model_effect.py @@ -0,0 +1,103 @@ +"""LanguageGraphAttackStep functionality +- Represents a step (type) defined in a MAL language +""" +from __future__ import annotations +from copy import deepcopy +from typing import TYPE_CHECKING, NamedTuple, Optional +from dataclasses import dataclass + +from maltoolbox.language.expression_chain import ExprType +from maltoolbox.language.step_expression_processor import process_assoc_op_step_expression, process_step_expression + +if TYPE_CHECKING: + from maltoolbox.language.expression_chain import ExpressionsChain + from maltoolbox.language.language_graph_asset import LanguageGraphAsset + + +class AssocTraversal(NamedTuple): + field_name: str + asset_filter: Optional[LanguageGraphAsset] + quantity_filter: Optional[int] + +SELF_TRAVERSAL = AssocTraversal(field_name="self", asset_filter=None, quantity_filter=None) + +class DynTarget(NamedTuple): + """A target in a dynamic sentence""" + assoc_op: bool # Decides if we are adding/removing assets or connections to assets + assoc_traversal: list[AssocTraversal | tuple[AssocTraversal, AssocTraversal]] + +@dataclass +class LanguageGraphModelEffect: + """An effect on the model instance triggered by compromising the attack step.""" + + base: list[AssocTraversal | tuple[AssocTraversal, AssocTraversal]] + targets: list[DynTarget] # Assumes ^ between targets + + +def build_model_effect( + assets: dict[str, LanguageGraphAsset], + target_asset: LanguageGraphAsset, + step_expression: dict, + lang_spec + ) -> LanguageGraphModelEffect: + """Build a model effect from a step expression""" + + assert step_expression["type"] == "dyn_sentence", "Only dynamic sentences can be used to build model effects" + + def build_assoc_traversal(expr_chain: ExpressionsChain | None) -> list[AssocTraversal]: + """Create a list of AssocTraversal from an expression chain""" + + if expr_chain is None: + return [] + elif expr_chain.type == ExprType.COLLECT: + left = build_assoc_traversal(expr_chain.left_link) + right = build_assoc_traversal(expr_chain.right_link) + return left + right + elif expr_chain.type == ExprType.DIFFERENCE: + raise NotImplementedError() + elif expr_chain.type == ExprType.FIELD: + assert expr_chain.fieldname is not None, "Fieldname must be set for FIELD expression" + return [AssocTraversal(field_name=expr_chain.fieldname, asset_filter=None, quantity_filter=None)] + elif expr_chain.type == ExprType.INTERSECTION: + raise NotImplementedError() + elif expr_chain.type == ExprType.SUBTYPE: + ret = build_assoc_traversal(expr_chain.sub_link) + ret[-1] = ret[-1]._replace(asset_filter=expr_chain.subtype) + return ret + elif expr_chain.type == ExprType.TRANSITIVE: + raise NotImplementedError() + elif expr_chain.type == ExprType.UNION: + left = build_assoc_traversal(expr_chain.left_link) + right = build_assoc_traversal(expr_chain.right_link) + return (left, right) + + raise ValueError(f"Unexpected expression chain type: {expr_chain.type}") + + base_expr = step_expression["base"] + target_exprs = step_expression["targets"] + + base_target_asset, base_expr_chain, base_step = process_step_expression(assets, target_asset, None, base_expr, lang_spec) + assert not base_step, "Base can not refer to an attack step in a dynamic sentence" + base = build_assoc_traversal(base_expr_chain) + + targets = [] + for target_expr in target_exprs: + process_function = process_assoc_op_step_expression if target_expr["type"] == "assoc_op" else process_step_expression + try: + _, target_expr_chain, target_step = process_function(assets, target_asset, None, target_expr, lang_spec) + except LookupError: + _, target_expr_chain, target_step = process_function(assets, base_target_asset, None, target_expr, lang_spec) + assert not target_step, "Targets can not refer to an attack step in a dynamic sentence" + if target_expr_chain: + if target_expr_chain.type == ExprType.ASSOC_OP: + assoc_op = True + target_expr_chain = target_expr_chain.sub_link + else: + assoc_op = False + assoc_traversal = build_assoc_traversal(target_expr_chain) + else: + assoc_op = False + assoc_traversal = [deepcopy(SELF_TRAVERSAL)] + targets.append(DynTarget(assoc_op=assoc_op, assoc_traversal=assoc_traversal)) + + return LanguageGraphModelEffect(base=base, targets=targets) \ No newline at end of file diff --git a/maltoolbox/language/step_expression_processor.py b/maltoolbox/language/step_expression_processor.py index c42f6ffe..f1defbcb 100644 --- a/maltoolbox/language/step_expression_processor.py +++ b/maltoolbox/language/step_expression_processor.py @@ -19,6 +19,12 @@ Optional[str], ] +# DynStepResult = tuple[ +# LanguageGraphAsset, +# Optional[ExpressionsChain], +# list[tuple[LanguageGraphAsset, Optional[ExpressionsChain]]], +# ] + def process_attack_step_expression( target_asset: LanguageGraphAsset, step_expression: dict[str, Any] @@ -152,6 +158,15 @@ def process_field_step_expression( ), None ) + + if fieldname == 'self': + # TODO: See if this needs an expression chain of some kind + # Possibly implement another ExprType for self references + return ( + target_asset, + None, + None, + ) raise LookupError( f'Failed to find field {fieldname} on asset {target_asset.name}!', ) @@ -462,3 +477,59 @@ def resolve_variable( ) return (target_asset, expr_chain) return asset.variables[var_name] + +# def proccess_dyn_sentence( +# assets: dict[str, LanguageGraphAsset], +# target_asset: LanguageGraphAsset, +# step_expression: dict[str, Any], +# lang_spec +# ) -> DynStepResult: +# base_asset, base_expr, base_step = process_step_expression(assets, target_asset, None, step_expression["base"], lang_spec) +# assert not base_step, "A base in a dynamic sentence can not end with an attack step" + +# assoc_op_or_not = lambda step_expr: process_assoc_op_step_expression( +# assets, target_asset, None, step_expr, lang_spec +# ) if step_expr["type"] == "assoc_op" else process_step_expression( +# assets, +# target_asset, +# None, +# step_expr, +# lang_spec +# ) + +# target_results = [assoc_op_or_not(target) for target in step_expression["targets"]] +# assert all(not target_result[2] for target_result in target_results) +# # for target in step_expression["targets"]: +# # base2target_asset, base2target_expr, base2target_step = assoc_op_or_not(target) +# # assert not base2target_step, "A target in a dynamic sentence can not end with an attack step" + +# return (base_asset, base_expr, target_results) + +def process_assoc_op_step_expression( + assets: dict[str, LanguageGraphAsset], + target_asset: LanguageGraphAsset, + expr_chain: ExpressionsChain | None, + step_expression: dict[str, Any], + lang_spec +) -> StepResult: + + result_target_asset, result_expr_chain, _result_step_name = ( + process_step_expression( + assets, + target_asset, + expr_chain, + step_expression['operand'], + lang_spec + ) + ) + assert not _result_step_name, "ASSOC_OP can not have an attack step as operand" + + new_expr_chain = ExpressionsChain( + type=ExprType.ASSOC_OP, + sub_link=result_expr_chain, + ) + return ( + result_target_asset, + new_expr_chain, + None + ) \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 3538b7ed..abb8b6dc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,7 +20,7 @@ dependencies = [ "PyYAML", "py2neo", "networkx", - "tree-sitter-mal>=1.2.3, <2.0.0", + "tree-sitter-mal @ git+https://github.com/sandorstormen/tree-sitter-mal.git@dynamal-grammar", "tree-sitter", ] license = {text = "Apache Software License"} @@ -42,6 +42,9 @@ classifiers = [ [project.optional-dependencies] dev = [ "pytest", + "ruff", + "mypy", + "types-PyYAML" ] [project.scripts] diff --git a/tests/language/test_compiler.py b/tests/language/test_compiler.py index f4367719..cbfa2f7b 100644 --- a/tests/language/test_compiler.py +++ b/tests/language/test_compiler.py @@ -1,8 +1,30 @@ +from pathlib import Path + from maltoolbox.language.compiler import MalCompiler +from maltoolbox.language.compiler.lang import PARSER from maltoolbox.language import LanguageGraph import pytest +def format_ast(lang_file: Path) -> str: + """Return a pretty-printed tree-sitter AST for a .mal file""" + tree = PARSER.parse(lang_file.read_bytes()) + + lines = [] + + def visit(node, depth): + prefix = " " * depth + if node.child_count == 0: + lines.append(f"{prefix}{node.type} {node.text!r}") + else: + lines.append(f"{prefix}{node.type}") + for child in node.children: + visit(child, depth + 1) + + visit(tree.root_node, 0) + return "\n".join(lines) + + def compile_lang(tmp_path, src: str): lang_file = tmp_path / "test.mal" lang_file.write_text(src) @@ -69,3 +91,78 @@ def test_compile_actions_effects(): assert lang_graph.assets['AssetB'].attack_steps['hack'].causal_mode == 'effect' assert lang_graph.assets['AssetB'].attack_steps['attack'].causal_mode == 'action' assert lang_graph.assets['AssetB'].attack_steps['test'].causal_mode is None + +def test_compile_wiperlang(): + """Test that we can compile the wiperlang.mal language""" + result = MalCompiler().compile("tests/testdata/wiperLang.mal") + + try: + malware_asset = next(asset for asset in result["assets"] if asset["name"] == "Malware") + except StopIteration: + pytest.fail("Malware asset not found") + try: + activate_step = next(step for step in malware_asset["attackSteps"] if step["name"] == "activate") + except StopIteration: + pytest.fail("activate attack step not found in Device asset") + assert activate_step["reaches"]["stepExpressions"][0]["name"] == "trigger" + + try: + device_asset = next(asset for asset in result["assets"] if asset["name"] == "Device") + except StopIteration: + pytest.fail("Device asset not found") + try: + infect_step = next(step for step in device_asset["attackSteps"] if step["name"] == "infect") + except StopIteration: + pytest.fail("infect attack step not found in Device asset") + + assert infect_step["reaches"]["stepExpressions"][0]["lhs"]["name"] == "malware" + assert infect_step["reaches"]["stepExpressions"][0]["rhs"]["name"] == "activate" + + assert infect_step["append_reaches"]["stepExpressions"][0]["base"]["name"] == "self" + assert infect_step["append_reaches"]["stepExpressions"][0]["targets"][0]["stepExpression"]["name"] == "malware" + assert infect_step["append_reaches"]["stepExpressions"][0]["targets"][0]["subType"] == "Wiper" + + wiper_lang = LanguageGraph(result) + + device = wiper_lang.assets["Device"] + wiper = wiper_lang.assets["Wiper"] + assert wiper.attack_steps["exfiltrate"] in wiper.attack_steps["activate"].own_children + assert wiper.attack_steps["propagate"] in wiper.attack_steps["activate"].own_children + assert wiper.super_assets[1].attack_steps["trigger"] in wiper.super_assets[1].attack_steps["activate"].own_children + assert device.attack_steps["infect"] in wiper.super_assets[1].attack_steps["activate"].own_parents + + device_infect = device.attack_steps["infect"] + assert device_infect.own_additive_model_effects[0].base == [], "Device:infect A> base should be self" + assert not device_infect.own_additive_model_effects[0].targets[0].assoc_op, "Device:infect A> target should not be assoc_op" + assert device_infect.own_additive_model_effects[0].targets[0].assoc_traversal[0].field_name == "malware" + assert device_infect.own_additive_model_effects[0].targets[0].assoc_traversal[0].asset_filter == wiper_lang.assets["Wiper"] + + wiper_exfiltrate = wiper.attack_steps["exfiltrate"] + assert wiper_exfiltrate.own_additive_model_effects[0].base[0].field_name == "victim" + assert wiper_exfiltrate.own_additive_model_effects[0].base[1].field_name == "data" + assert wiper_exfiltrate.own_additive_model_effects[0].targets[0].assoc_op + assert wiper_exfiltrate.own_additive_model_effects[0].targets[0].assoc_traversal[0].field_name == "victim" + assert wiper_exfiltrate.own_additive_model_effects[0].targets[0].assoc_traversal[1].field_name == "inet" + assert wiper_exfiltrate.own_additive_model_effects[0].targets[0].assoc_traversal[2].field_name == "hosts" + assert wiper_exfiltrate.own_additive_model_effects[0].targets[0].assoc_traversal[2].asset_filter == wiper_lang.assets["C2Server"] + assert wiper_exfiltrate.own_additive_model_effects[0].targets[0].assoc_traversal[3].field_name == "data" + +def test_compile_basic_dynamal_languages(): + dynamal_test_langs_dir = Path("tests/testdata/dynamal_test_langs/basic") + for lang_file in dynamal_test_langs_dir.glob("*.mal"): + lang = LanguageGraph.from_mal_spec(lang_file) + +def test_compile_intermediate_dynamal_languages(): + dynamal_test_langs_dir = Path("tests/testdata/dynamal_test_langs/intermediate") + for lang_file in dynamal_test_langs_dir.glob("*.mal"): + lang = LanguageGraph.from_mal_spec(lang_file) + +def test_compile_advanced_dynamal_languages(): + dynamal_test_langs_dir = Path("tests/testdata/dynamal_test_langs/advanced") + for lang_file in dynamal_test_langs_dir.glob("*.mal"): + try: + lang = LanguageGraph.from_mal_spec(lang_file) + except Exception as e: + pytest.fail( + f"Failed to compile {lang_file}: {e}\n\nAST:\n{format_ast(lang_file)}" + ) \ No newline at end of file diff --git a/tests/testdata/dynamal_test_langs/advanced/diceLang.mal b/tests/testdata/dynamal_test_langs/advanced/diceLang.mal new file mode 100644 index 00000000..a62bc008 --- /dev/null +++ b/tests/testdata/dynamal_test_langs/advanced/diceLang.mal @@ -0,0 +1,190 @@ +/** + * Created by Viktor Engström + * Repo: https://gitlab.com/kth-ssas/dynamal-group/dynamaltestlangs.git + * Converted by Sandor Berglund + */ + +#id: "diceLang" +#version: "0.1.0" +category diceLang { + // A contrived language for checking various dice configurations. + + abstract asset Die { + | even + | odd + | all + | prime + | betterPrime + | evenPrime + | altEvenPrime + | notPrime + | noPrime + | noEven + | noOdd + | onlyPrime + } + + asset One extends Die {} + asset Two extends Die {} + asset Three extends Die {} + asset Four extends Die {} + asset Five extends Die {} + asset Six extends Die {} + + asset Dice { + let even = ((die[Two] \/ die[Four]) \/ die[Six]) + let odd = (die[One] \/ (die[Three] \/ die[Five])) + let all = (even() \/ odd()) + let primes = (all() /\ (die[Two] \/ die[Three] \/ die[Five])) + let betterPrimes = (die[Two] \/ die[Three] \/ die[Five]) + let evenPrimes = (all() /\ die[Two]) + let altEvenPrimes = (die[Two] /\ all()) + let notPrimes = (die[Four] \/ die[Six] \/ die[One]) + let noPrimes = (all() - primes()) + let noEven = (all() - even()) + let noOdd = (all() - odd()) + let onlyPrimes = (all() - primes()) + + let noOnes = (die[Two] \/ die[Three] \/ die[Four] \/ die[Five] \/ die[Six]) + let noTwos = (die[One] \/ die[Three] \/ die[Four] \/ die[Five] \/ die[Six]) + let noThrees = (die[Two] \/ die[One] \/ die[Four] \/ die[Five] \/ die[Six]) + let noFours = (die[Two] \/ die[Three] \/ die[One] \/ die[Five] \/ die[Six]) + let noFives = (die[Two] \/ die[Three] \/ die[Four] \/ die[One] \/ die[Six]) + let noSixes = (die[Two] \/ die[Three] \/ die[Four] \/ die[Five] \/ die[One]) + + | startingPoint + modeler info: "Start your agents here." + -> allOnes, + ((die[Two] \/ die[Four]) \/ die[Six]).even, // Testing syntax + (die[One] \/ (die[Three] \/ die[Five])).odd, // Testing syntax + allTwos, + allThrees, + allFours, + allFives, + allSixes, + oneExists, + twoExists, + threeExists, + fourExists, + fiveExists, + sixExists, + even().even, + odd().odd, + all().all, + primes().prime, + betterPrimes().betterPrime, + evenPrimes().evenPrime, + altEvenPrimes().altEvenPrime, + notPrimes().notPrime, + noPrimes().noPrime, + noEven().noEven, + noOdd().noOdd, + onlyPrimes().onlyPrime + + + & allOnes + -> yahtzee + + & allTwos + -> yahtzee + + & allThrees + -> yahtzee + + & allFours + -> yahtzee + + & allFives + -> yahtzee + + & allSixes + -> yahtzee + + | yahtzee + user info: "All show the same number, usually six dice." + + & oneExists + -> simpleSequence, + oneTwoThree, + allOnes + + & twoExists + -> simpleSequence, + oneTwoThree, + allTwos + + & threeExists + -> simpleSequence, + oneTwoThree, + allThrees + + & fourExists + -> simpleSequence, + allFours + + & fiveExists + -> simpleSequence, + allFives + + & sixExists + -> simpleSequence, + allSixes + + & oneTwoThree + & simpleSequence + + E ifOneExists + <- die[One] + -> oneExists + + E ifTwoExists + <- die[Two] + -> twoExists + + E ifThreeExists + <- die[Three] + -> threeExists + + E ifFourExists + <- die[Four] + -> fourExists + + E ifFiveExists + <- die[Five] + -> fiveExists + + E ifSixExists + <- die[Six] + -> sixExists + + !E oneComplement + <- noOnes() + -> allOnes + + !E twoComplement + <- noTwos() + -> allTwos + + !E threeComplement + <- noThrees() + -> allThrees + + !E fourComplement + <- noFours() + -> allFours + + !E fiveComplement + <- noFives() + -> allFives + + !E sixComplement + <- noSixes() + -> allSixes + + } + +} + +associations{ + Die [die] * <-- collection --> 1 [collection] Dice +} diff --git a/tests/testdata/dynamal_test_langs/basic/baseDynamicTestLang1.mal b/tests/testdata/dynamal_test_langs/basic/baseDynamicTestLang1.mal new file mode 100644 index 00000000..6853af9c --- /dev/null +++ b/tests/testdata/dynamal_test_langs/basic/baseDynamicTestLang1.mal @@ -0,0 +1,25 @@ +/** + * Created by Viktor Engström + * Repo: https://gitlab.com/kth-ssas/dynamal-group/dynamaltestlangs.git + * Converted by Sandor Berglund + */ + +#id: "baseDynamicTestLang1" +#version: "0.1.0" +category baseDynamicTestLang1 { + asset Bowl { + | access + -> addApple + + | addApple + A> self / apples + } + + asset Apple {} + +} + +associations { + Bowl [container] 1 <-- contains --> * [apples] Apple + +} diff --git a/tests/testdata/dynamal_test_langs/basic/baseDynamicTestLang10.mal b/tests/testdata/dynamal_test_langs/basic/baseDynamicTestLang10.mal new file mode 100644 index 00000000..93fd1165 --- /dev/null +++ b/tests/testdata/dynamal_test_langs/basic/baseDynamicTestLang10.mal @@ -0,0 +1,29 @@ +/** + * Created by Viktor Engström + * Repo: https://gitlab.com/kth-ssas/dynamal-group/dynamaltestlangs.git + * Converted by Sandor Berglund + */ + +#id: "baseDynamicTestLang10" +#version: "0.0.1" + + +category Business { + asset Plantation { + | access + -> pilferBananas + + | pilferBananas + developer info: "Burn bridge, eat bananas." + R> trees / ~plant ^ bananas + + } + + asset BananaTree{} + asset Banana {} +} + +associations { + Plantation [plant] 1 <-- Cultivate --> * [trees] BananaTree + BananaTree [tree] 1 <-- Contains --> * [bananas] Banana +} diff --git a/tests/testdata/dynamal_test_langs/basic/baseDynamicTestLang2.mal b/tests/testdata/dynamal_test_langs/basic/baseDynamicTestLang2.mal new file mode 100644 index 00000000..a9936804 --- /dev/null +++ b/tests/testdata/dynamal_test_langs/basic/baseDynamicTestLang2.mal @@ -0,0 +1,24 @@ +/** + * Created by Viktor Engström + * Repo: https://gitlab.com/kth-ssas/dynamal-group/dynamaltestlangs.git + * Converted by Sandor Berglund + */ + +#id: "baseDynamicTestLang2" +#version: "0.1.0" +category baseDynamicTestLang2 { + asset Bowl { + | access + -> eatApple + + | eatApple + R> self / apples + } + + asset Apple {} + +} + +associations { + Bowl [container] 1 <-- contains --> * [apples] Apple +} diff --git a/tests/testdata/dynamal_test_langs/basic/baseDynamicTestLang3.mal b/tests/testdata/dynamal_test_langs/basic/baseDynamicTestLang3.mal new file mode 100644 index 00000000..fc832b40 --- /dev/null +++ b/tests/testdata/dynamal_test_langs/basic/baseDynamicTestLang3.mal @@ -0,0 +1,28 @@ +/** + * Created by Viktor Engström + * Repo: https://gitlab.com/kth-ssas/dynamal-group/dynamaltestlangs.git + * Converted by Sandor Berglund + */ + +#id: "baseDynamicTestLang3" +#version: "0.1.0" +category baseDynamicTestLang3 { + asset Bowl { + | access + -> addApple, removeApple + + | addApple + A> self / apples + + | removeApple + R> self / apples + } + + asset Apple {} + +} + +associations { + Bowl [container] 1 <-- contains --> * [apples] Apple + +} diff --git a/tests/testdata/dynamal_test_langs/basic/baseDynamicTestLang4.mal b/tests/testdata/dynamal_test_langs/basic/baseDynamicTestLang4.mal new file mode 100644 index 00000000..633e2d45 --- /dev/null +++ b/tests/testdata/dynamal_test_langs/basic/baseDynamicTestLang4.mal @@ -0,0 +1,26 @@ +/** + * Created by Viktor Engström + * Repo: https://gitlab.com/kth-ssas/dynamal-group/dynamaltestlangs.git + * Converted by Sandor Berglund + */ + +#id: "baseDynamicTestLang4" +#version: "0.1.0" +category baseDynamicTestLang4 { + asset Bowl { + | access + -> tamper + + | tamper + A> self / apples + R> self / apples + } + + asset Apple {} + +} + +associations { + Bowl [container] 1 <-- contains --> * [apples] Apple + +} diff --git a/tests/testdata/dynamal_test_langs/basic/baseDynamicTestLang5.mal b/tests/testdata/dynamal_test_langs/basic/baseDynamicTestLang5.mal new file mode 100644 index 00000000..6268b30f --- /dev/null +++ b/tests/testdata/dynamal_test_langs/basic/baseDynamicTestLang5.mal @@ -0,0 +1,29 @@ +/** + * Created by Viktor Engström + * Repo: https://gitlab.com/kth-ssas/dynamal-group/dynamaltestlangs.git + * Converted by Sandor Berglund + */ + +#id: "baseDynamicTestLang5" +#version: "0.0.1" + +category Business { + asset Monkey {} + asset Typewriter {} + + asset Storage { + | access + -> giveTypewriter + + | giveTypewriter + user info: "With enough monkeys and typewriters, we can create the complete works of Shakespeare." + A> monkeys / ~typewriters.writer + } +} + +associations { + Storage [storage] 1 <-- contains --> * [typewriters] Typewriter + Storage [storage] * <-- provision --> * [monkeys] Monkey + Monkey [writer] * <-- instrument --> * [instrument] Typewriter + +} diff --git a/tests/testdata/dynamal_test_langs/basic/baseDynamicTestLang6.mal b/tests/testdata/dynamal_test_langs/basic/baseDynamicTestLang6.mal new file mode 100644 index 00000000..e2748c40 --- /dev/null +++ b/tests/testdata/dynamal_test_langs/basic/baseDynamicTestLang6.mal @@ -0,0 +1,27 @@ +/** + * Created by Viktor Engström + * Repo: https://gitlab.com/kth-ssas/dynamal-group/dynamaltestlangs.git + * Converted by Sandor Berglund + */ + +#id: "baseDynamicTestLang6" +#version: "0.0.1" + +category Business { + asset Monkey {} + + asset Typewriter { + | access + -> hideTypewriter + + | hideTypewriter + user info: "Monkeys without typewriters cannot work." + developer info: "Only removes the association. The typewriter still exists in the model." + R> self / ~writer + } +} + +associations { + Typewriter [typewriter] 0..1 <-- operatedBy --> 1 [writer] Monkey + +} diff --git a/tests/testdata/dynamal_test_langs/basic/baseDynamicTestLang7.mal b/tests/testdata/dynamal_test_langs/basic/baseDynamicTestLang7.mal new file mode 100644 index 00000000..03d6b068 --- /dev/null +++ b/tests/testdata/dynamal_test_langs/basic/baseDynamicTestLang7.mal @@ -0,0 +1,34 @@ +/** + * Created by Viktor Engström + * Repo: https://gitlab.com/kth-ssas/dynamal-group/dynamaltestlangs.git + * Converted by Sandor Berglund + */ + +#id: "baseDynamicTestLang7" +#version: "0.0.1" + + +category Business { + asset Plantation { + | access + -> plantTree + + | plantTree + A> self / trees + + } + + asset MagicPlantation extends Plantation + { + | plantTree + +A> trees / bananas + } + + asset BananaTree{} + asset Banana {} +} + +associations { + Plantation [plant] 1 <-- cultivate --> * [trees] BananaTree + BananaTree [tree] 1 <-- contains --> * [bananas] Banana +} diff --git a/tests/testdata/dynamal_test_langs/basic/baseDynamicTestLang8.mal b/tests/testdata/dynamal_test_langs/basic/baseDynamicTestLang8.mal new file mode 100644 index 00000000..43281859 --- /dev/null +++ b/tests/testdata/dynamal_test_langs/basic/baseDynamicTestLang8.mal @@ -0,0 +1,34 @@ +/** + * Created by Viktor Engström + * Repo: https://gitlab.com/kth-ssas/dynamal-group/dynamaltestlangs.git + * Converted by Sandor Berglund + */ + +#id: "baseDynamicTestLang8" +#version: "0.0.1" + + +category Business { + asset Plantation { + | access + -> pilferBananas + + | pilferBananas + R> trees / bananas + + } + + asset PlantationWithSmallTrees extends Plantation + { + | pilferBananas + +R> trees / self + } + + asset BananaTree{} + asset Banana {} +} + +associations { + Plantation [plant] 1 <-- Cultivate --> * [trees] BananaTree + BananaTree [tree] 1 <-- Contains --> * [bananas] Banana +} diff --git a/tests/testdata/dynamal_test_langs/basic/baseDynamicTestLang9.mal b/tests/testdata/dynamal_test_langs/basic/baseDynamicTestLang9.mal new file mode 100644 index 00000000..042c7ca9 --- /dev/null +++ b/tests/testdata/dynamal_test_langs/basic/baseDynamicTestLang9.mal @@ -0,0 +1,28 @@ +/** + * Created by Viktor Engström + * Repo: https://gitlab.com/kth-ssas/dynamal-group/dynamaltestlangs.git + * Converted by Sandor Berglund + */ + +#id: "baseDynamicTestLang9" +#version: "0.0.1" + + +category Business { + asset Plantation { + | access + -> plantTree + + | plantTree + A> self / trees ^ trees.bananas + + } + + asset BananaTree{} + asset Banana {} +} + +associations { + Plantation [plant] 1 <-- cultivate --> * [trees] BananaTree + BananaTree [tree] 1 <-- contains --> * [bananas] Banana +} diff --git a/tests/testdata/dynamal_test_langs/basic/doorProblemLang.mal b/tests/testdata/dynamal_test_langs/basic/doorProblemLang.mal new file mode 100644 index 00000000..97a691fe --- /dev/null +++ b/tests/testdata/dynamal_test_langs/basic/doorProblemLang.mal @@ -0,0 +1,24 @@ +/** + * Created by Viktor Engström + * Repo: https://gitlab.com/kth-ssas/dynamal-group/dynamaltestlangs.git + * Converted by Sandor Berglund + */ + +#id: "doorProblemLang" +#version: "0.1.0" +category doorProblemLang { + asset Room { + | leave + -> entrance.leave // Leave the room + R> self / ~entrance // Remove link between two rooms, i.e., close the door. + + # deathLasers + -> leave // Prevent attackers in rooms. + + } + +} + +associations { + Room [exit] * <-- Connection --> * [entrance] Room +} diff --git a/tests/testdata/dynamal_test_langs/basic/easyRansomwareLang.mal b/tests/testdata/dynamal_test_langs/basic/easyRansomwareLang.mal new file mode 100644 index 00000000..cccc3601 --- /dev/null +++ b/tests/testdata/dynamal_test_langs/basic/easyRansomwareLang.mal @@ -0,0 +1,33 @@ +/** + * Created by Viktor Engström + * Repo: https://gitlab.com/kth-ssas/dynamal-group/dynamaltestlangs.git + * Converted by Sandor Berglund + */ + +#id: "easyRansomwareLang" +#version: "0.1.0" +category easyRansomwareLang { + asset Data {} + + asset LockedData extends Data {} + + asset Host { + | connect + -> ransomware.attack + } + + asset Ransomware { + | attack + A> host / data.locked, + host / ~host.data.locked.victim + R> host / data + } + +} + +associations { + Host [host] * <-- containment --> * [data] Data + Host [host] 1 <-- infection --> * [ransomware] Ransomware + Data [plain] 1 <-- encryption --> 0..1 [locked] LockedData + Host [victim] 1 <-- victim --> * [infected] LockedData +} diff --git a/tests/testdata/dynamal_test_langs/basic/glider.mal b/tests/testdata/dynamal_test_langs/basic/glider.mal new file mode 100644 index 00000000..22e3758c --- /dev/null +++ b/tests/testdata/dynamal_test_langs/basic/glider.mal @@ -0,0 +1,21 @@ +/** + * Created by Viktor Engström + * Repo: https://gitlab.com/kth-ssas/dynamal-group/dynamaltestlangs.git + * Converted by Sandor Berglund + */ + +#id: "glider" +#version: "0.1.0" +category glider { + asset Glider { + | glide + A> self / next + R> self / previous + -> next.glide + } + +} + +associations { + Glider [previous] 0..1 <-- link --> 0..1 [next] Glider +} diff --git a/tests/testdata/dynamal_test_langs/basic/looper.mal b/tests/testdata/dynamal_test_langs/basic/looper.mal new file mode 100644 index 00000000..1b8057c4 --- /dev/null +++ b/tests/testdata/dynamal_test_langs/basic/looper.mal @@ -0,0 +1,20 @@ +/** + * Created by Viktor Engström + * Repo: https://gitlab.com/kth-ssas/dynamal-group/dynamaltestlangs.git + * Converted by Sandor Berglund + */ + +#id: "looper" +#version: "0.1.0" +category looper { + asset Looper { + | loop + A> self / next + -> next.loop + } + +} + +associations { + Looper [previous] 0..1 <-- link --> 0..1 [next] Looper +} diff --git a/tests/testdata/dynamal_test_langs/basic/posterLang.mal b/tests/testdata/dynamal_test_langs/basic/posterLang.mal new file mode 100644 index 00000000..cd80f9c3 --- /dev/null +++ b/tests/testdata/dynamal_test_langs/basic/posterLang.mal @@ -0,0 +1,28 @@ +/** + * Created by Viktor Engström + * Repo: https://gitlab.com/kth-ssas/dynamal-group/dynamaltestlangs.git + * Converted by Sandor Berglund + */ + +#id: "posterLang" +#version: "0.1.0" +category posterLang { + asset Host { + | infect // Start by infecting a host. + A> self / malware // Add the data wiping malware. + -> malware.execute // Prepare to execute the malware. + } + asset WiperMalware { + | execute // Awaken the malware. + -> victim.sendTo.infect // Propagate then... + R> victim / data // ... wipe system data. + } + asset Data {} + +} + +associations { + Host [receiveFrom] * <-- communicate --> * [sendTo] Host + Host [host] 1 <-- contains --> * [data] Data + Host [victim] 1 <-- infects --> * [malware] WiperMalware +} diff --git a/tests/testdata/dynamal_test_langs/intermediate/intDynamicTestLang1.mal b/tests/testdata/dynamal_test_langs/intermediate/intDynamicTestLang1.mal new file mode 100644 index 00000000..b0436147 --- /dev/null +++ b/tests/testdata/dynamal_test_langs/intermediate/intDynamicTestLang1.mal @@ -0,0 +1,32 @@ +/** + * Created by Viktor Engström + * Repo: https://gitlab.com/kth-ssas/dynamal-group/dynamaltestlangs.git + * Converted by Sandor Berglund + */ + +#id: "intDynamicTestLang1" +#version: "0.0.1" + +category Business { + asset Apples { + | access + -> plantBadApple + + | plantBadApple + A> self / badApple + -> badApple.apples.spoil + + | spoil + } + + asset BadApple + user info: "One bad apple spoils the bunch!" + { + + } +} + +associations { + Apples [apples] 1 <-- spoiledBy --> 0..1 [badApple] BadApple + +} diff --git a/tests/testdata/dynamal_test_langs/intermediate/intDynamicTestLang10.mal b/tests/testdata/dynamal_test_langs/intermediate/intDynamicTestLang10.mal new file mode 100644 index 00000000..cad226a6 --- /dev/null +++ b/tests/testdata/dynamal_test_langs/intermediate/intDynamicTestLang10.mal @@ -0,0 +1,24 @@ +/** + * Created by Viktor Engström + * Repo: https://gitlab.com/kth-ssas/dynamal-group/dynamaltestlangs.git + * Converted by Sandor Berglund + */ + +#id: "intDynamicTestLang10" +#version: "0.1.0" +category intDynamicTestLang10 { + asset Table { + | delegateFruit + A> bowls / apples, + bowls.apples / ~bowls.apples + R> bowls / ~apples + } + asset Bowl {} + asset Apple {} + +} + +associations { + Bowl [bowls] * <-- placement --> 1 [table] Table + Bowl [container] * <-- contains --> * [apples] Apple +} diff --git a/tests/testdata/dynamal_test_langs/intermediate/intDynamicTestLang11.mal b/tests/testdata/dynamal_test_langs/intermediate/intDynamicTestLang11.mal new file mode 100644 index 00000000..5b665f49 --- /dev/null +++ b/tests/testdata/dynamal_test_langs/intermediate/intDynamicTestLang11.mal @@ -0,0 +1,58 @@ +/** + * Created by Viktor Engström + * Repo: https://gitlab.com/kth-ssas/dynamal-group/dynamaltestlangs.git + * Converted by Sandor Berglund + */ + +#id: "intDynamicTestLang11" +#version: "0.1.0" +category intDynamicTestLang11 { + asset Table { + | access + -> testLeftUnionRemove, + testRightUnionRemove, + testDoubleUnionRemove, + testLeftUnionAdd, + testRightUnionAdd, + testDoubleUnionAdd + + | testLeftUnionRemove + R> (bowls \/ baskets) / apples + + | testRightUnionRemove + R> bowls / (apples \/ oranges) + + | testDoubleUnionRemove + R> (bowls \/ baskets) / (apples \/ oranges) + + | testLeftUnionAdd + A> (bowls \/ baskets) / apples + + | testRightUnionAdd + A> bowls / (apples \/ oranges) + + | testDoubleUnionAdd + A> (bowls \/ baskets) / (apples \/ oranges) + } + + abstract asset Container {} + + abstract asset Fruit {} + + asset Bowl extends Container {} + + asset Basket extends Container {} + + asset Apple extends Fruit {} + + asset Orange extends Fruit {} + + +} + +associations{ + Table [table] 1 <-- BowlPlacement --> * [bowls] Bowl + Table [table] 1 <-- BasketPlacement --> * [baskets] Basket + Container [container] 1 <-- AppleContain --> * [apples] Apple + Container [container] 1 <-- OrangeContain --> * [oranges] Orange +} diff --git a/tests/testdata/dynamal_test_langs/intermediate/intDynamicTestLang12.mal b/tests/testdata/dynamal_test_langs/intermediate/intDynamicTestLang12.mal new file mode 100644 index 00000000..55c2d37c --- /dev/null +++ b/tests/testdata/dynamal_test_langs/intermediate/intDynamicTestLang12.mal @@ -0,0 +1,425 @@ +/** + * Created by Viktor Engström + * Repo: https://gitlab.com/kth-ssas/dynamal-group/dynamaltestlangs.git + * Converted by Sandor Berglund + */ + +#id: "intDynamicTestLang12" +#version: "0.1.0" +category intDynamicTestLang12 { + asset Table { + | access + -> testNodeAdditions, + testNodeRemovals, + testLinkAdditions, + testLinkRemovals, + testCaretOperations, + testEmptySetCases + + | testNodeAdditions + -> testAddLeftUnionTwo, + testAddLeftUnionThree, + testLeftUnionAddFour, + testLeftUnionAddThreeLeftNested, + testLeftUnionAddFourLeftNested, + testLeftUnionAddThreeRightNested, + testLeftUnionAddFourRightNested, + testRightUnionAddTwo, + testRightUnionAddThree, + testRightUnionAddFour, + testRightUnionAddThreeLeftNested, + testRightUnionAddFourLeftNested, + testDoubleUnionAddTwo, + testDoubleUnionAddThree, + testDoubleUnionAddFour + + | testLinkAdditions + -> testLinkLeftUnionTwo, + testLinkLeftUnionThree, + testLinkLeftUnionFour, + testLinkLeftUnionThreeLeftNested, + testLinkLeftUnionFourLeftNested, + testLinkRightUnionTwo, + testLinkRightUnionThree, + testLinkRightUnionFour, + testLinkRightUnionThreeLeftNested, + testLinkRightUnionFourLeftNested, + testLinkRightUnionThreeRightNested, + testLinkRightUnionFourRightNested, + testLinkDoubleUnionTwo, + testLinkDoubleUnionThree, + testLinkDoubleUnionFour + + | testNodeRemovals + -> testLeftUnionRemoveTwo, + testLeftUnionRemoveTwoSelf, + testLeftUnionRemoveThree, + testLeftUnionRemoveFour, + testLeftUnionRemoveThreeLeftNested, + testLeftUnionRemoveFourLeftNested, + testLeftUnionRemoveThreeRightNested, + testLeftUnionRemoveFourRightNested, + testRightUnionRemoveTwo, + testRightUnionRemoveThree, + testRightUnionRemoveFour, + testRightUnionRemoveThreeLeftNested, + testRightUnionRemoveFourLeftNested, + testDoubleUnionRemoveTwo, + testDoubleUnionRemoveThree, + testDoubleUnionRemoveFour + + | testLinkRemovals + -> testUnlinkLeftUnionRemoveTwo, + testUnlinkLeftUnionRemoveThree, + testUnlinkLeftUnionRemoveFour, + testUnlinkLeftUnionRemoveThreeLeftNested, + testUnlinkLeftUnionRemoveFourLeftNested, + testUnlinkLeftUnionRemoveThreeRightNested, + testUnlinkLeftUnionRemoveFourRightNested, + testUnlinkRightUnionRemoveTwo, + testUnlinkRightUnionRemoveThree, + testUnlinkRightUnionRemoveFour, + testUnlinkRightUnionRemoveThreeLeftNested, + testUnlinkRightUnionRemoveFourLeftNested, + testUnlinkDoubleUnionRemoveTwo, + testUnlinkDoubleUnionRemoveThree, + testUnlinkDoubleUnionRemoveFour + + | testCaretOperations + -> testAddLeftUnionAddWithCaret, + testAddRightUnionAddWithCaret, + testAddDoubleUnionWithCaret, + testAddDoubleUnionNestedWithCaret, + testAddWithBackAndForthInCaret, + testAddToTwoOthersFromBowlInCaret, + testRemoveLeftUnionRemoveWithCaret, + testRemoveRightUnionAddWithCaret, + testRemoveDoubleUnionWithCaret, + testRemoveDoubleUnionNestedWithCaret, + testRemoveWithBackAndForthInCaret, + testRemoveFromTwoOthersFromBowlInCaret + + | testEmptySetCases + -> testAddEmptyBaseIntersectOne, + testAddEmptyBaseIntersectTwo, + testAddEmptyBaseDifferenceOne, + testAddEmptyBaseDifferenceTwo, + testAddEmptyTarget, + testAddEmptyTargetInCaretOne, + testAddEmptyTargetInCaretTwo, + testRemoveEmptyBaseIntersectOne, + testRemoveEmptyBaseIntersectTwo, + testRemoveEmptyBaseDifferenceOne, + testRemoveEmptyBaseDifferenceTwo, + testRemoveEmptyTargetInCaretOne, + testRemoveEmptyTargetInCaretTwo + + + // Add with union combinations + | testAddLeftUnionTwo + A> (bowls \/ baskets) / apples + + | testAddLeftUnionThree + A> (bowls \/ baskets \/ plates) / apples + + | testLeftUnionAddFour + A> (bowls \/ baskets \/ plates \/ buckets) / apples + + | testLeftUnionAddThreeLeftNested + A> ((bowls \/ baskets) \/ plates) / apples + + | testLeftUnionAddFourLeftNested + A> (((bowls \/ baskets) \/ plates) \/ buckets) / apples + + | testLeftUnionAddThreeRightNested + A> (bowls \/ (baskets \/ plates)) / apples + + | testLeftUnionAddFourRightNested + A> (bowls \/ (baskets \/ (plates \/ buckets))) / apples + + | testRightUnionAddTwo + A> bowls / (apples \/ oranges) + + | testRightUnionAddThree + A> bowls / (apples \/ oranges \/ bananas) + + | testRightUnionAddFour + A> bowls / (apples \/ oranges \/ bananas \/ durians) + + | testRightUnionAddThreeLeftNested + A> bowls / ((apples \/ oranges) \/ bananas) + + | testRightUnionAddFourLeftNested + A> bowls / (((apples \/ oranges) \/ bananas) \/ durians) + + | testDoubleUnionAddTwo + A> (bowls \/ baskets) / (apples \/ oranges) + + | testDoubleUnionAddThree + A> (bowls \/ baskets \/ plates) / (apples \/ oranges \/ bananas) + + | testDoubleUnionAddFour + A> (bowls \/ baskets \/ plates \/ buckets) / (apples \/ oranges \/ bananas \/ durians) + + + + // Link add with union combinations + | testLinkLeftUnionTwo + A> (bowls \/ baskets) / ~plates.apples.container + + | testLinkLeftUnionThree + A> (bowls \/ baskets \/ plates) / ~buckets.apples.container + + | testLinkLeftUnionFour + A> (bowls \/ baskets \/ plates \/ buckets) / ~bowls.apples.container + + | testLinkLeftUnionThreeLeftNested + A> ((bowls \/ baskets) \/ plates) / ~buckets.apples.container + + | testLinkLeftUnionFourLeftNested + A> (((bowls \/ baskets) \/ plates) \/ buckets) / ~bowls.apples.container + + | testLinkRightUnionTwo + A> bowls / ~((bowls \/ baskets).apples).container + + | testLinkRightUnionThree + A> bowls / ~((bowls \/ baskets \/ plates).apples).container + + | testLinkRightUnionFour + A> bowls / ~((bowls \/ baskets \/ plates \/ buckets).apples).container + + | testLinkRightUnionThreeLeftNested + A> bowls / ~(((bowls \/ baskets) \/ plates).apples).container + + | testLinkRightUnionFourLeftNested + A> bowls / ~((((bowls \/ baskets) \/ plates) \/ buckets).apples).container + + | testLinkRightUnionThreeRightNested + A> bowls / ~(bowls \/ (baskets \/ plates)).apples.container + + | testLinkRightUnionFourRightNested + A> bowls / ~(bowls \/ (baskets \/ (plates \/ buckets))).apples.container + + | testLinkDoubleUnionTwo + A> (bowls \/ baskets) / ~(bowls \/ baskets).apples.container + + | testLinkDoubleUnionThree + A> (bowls \/ baskets \/ plates) / ~(bowls \/ baskets \/ plates).apples.container + + | testLinkDoubleUnionFour + A> (bowls \/ baskets \/ plates \/ buckets) / ~(bowls \/ baskets \/ plates \/ buckets).apples.container + + + + // Remove with union combinations + | testLeftUnionRemoveTwo + R> (bowls \/ baskets) / apples + + | testLeftUnionRemoveTwoSelf + R> (bowls \/ baskets).apples / self + + | testLeftUnionRemoveThree + R> (bowls \/ baskets \/ plates) / apples + + | testLeftUnionRemoveFour + R> (bowls \/ baskets \/ plates \/ buckets) / apples + + | testLeftUnionRemoveThreeLeftNested + R> ((bowls \/ baskets) \/ plates) / apples + + | testLeftUnionRemoveFourLeftNested + R> (((bowls \/ baskets) \/ plates) \/ buckets) / apples + + | testLeftUnionRemoveThreeRightNested + R> (bowls \/ (baskets \/ plates)) / apples + + | testLeftUnionRemoveFourRightNested + R> (bowls \/ (baskets \/ (plates \/ buckets))) / apples + + | testRightUnionRemoveTwo + R> bowls / (apples \/ oranges) + + | testRightUnionRemoveThree + R> bowls / (apples \/ oranges \/ bananas) + + | testRightUnionRemoveFour + R> bowls / (apples \/ oranges \/ bananas \/ durians) + + | testRightUnionRemoveThreeLeftNested + R> bowls / ((apples \/ oranges) \/ bananas) + + | testRightUnionRemoveFourLeftNested + R> bowls / (((apples \/ oranges) \/ bananas) \/ durians) + + | testDoubleUnionRemoveTwo + R> (bowls \/ baskets) / (apples \/ oranges) + + | testDoubleUnionRemoveThree + R> (bowls \/ baskets \/ plates) / (apples \/ oranges \/ bananas) + + | testDoubleUnionRemoveFour + R> (bowls \/ baskets \/ plates \/ buckets) / (apples \/ oranges \/ bananas \/ durians) + + + + // Link remove with union combinations + | testUnlinkLeftUnionRemoveTwo + R> (bowls \/ baskets) / ~apples + + | testUnlinkLeftUnionRemoveThree + R> (bowls \/ baskets \/ plates) / ~apples + + | testUnlinkLeftUnionRemoveFour + R> (bowls \/ baskets \/ plates \/ buckets) / ~apples + + | testUnlinkLeftUnionRemoveThreeLeftNested + R> ((bowls \/ baskets) \/ plates) / ~apples + + | testUnlinkLeftUnionRemoveFourLeftNested + R> (((bowls \/ baskets) \/ plates) \/ buckets) / ~apples + + | testUnlinkLeftUnionRemoveThreeRightNested + R> (bowls \/ (baskets \/ plates)) / ~apples + + | testUnlinkLeftUnionRemoveFourRightNested + R> (bowls \/ (baskets \/ (plates \/ buckets))) / ~apples + + | testUnlinkRightUnionRemoveTwo + R> bowls / ~(apples \/ oranges) + + | testUnlinkRightUnionRemoveThree + R> bowls / ~(apples \/ oranges \/ bananas) + + | testUnlinkRightUnionRemoveFour + R> bowls / ~(apples \/ oranges \/ bananas \/ durians) + + | testUnlinkRightUnionRemoveThreeLeftNested + R> bowls / ~((apples \/ oranges) \/ bananas) + + | testUnlinkRightUnionRemoveFourLeftNested + R> bowls / ~(((apples \/ oranges) \/ bananas) \/ durians) + + | testUnlinkDoubleUnionRemoveTwo + R> (bowls \/ baskets) / ~(apples \/ oranges) + + | testUnlinkDoubleUnionRemoveThree + R> (bowls \/ baskets \/ plates) / ~(apples \/ oranges \/ bananas) + + | testUnlinkDoubleUnionRemoveFour + R> (bowls \/ baskets \/ plates \/ buckets) / ~(apples \/ oranges \/ bananas \/ durians) + + + + // Tests with caret + | testAddLeftUnionAddWithCaret + A> bowls / (apples \/ oranges) ^ bananas + + | testAddRightUnionAddWithCaret + A> bowls / apples ^ (oranges \/ bananas) + + | testAddDoubleUnionWithCaret + A> bowls / (apples \/ oranges) ^ (bananas \/ durians) + + | testAddDoubleUnionNestedWithCaret + A> bowls / ((apples \/ oranges) \/ bananas) ^ (oranges \/ (bananas \/ durians)) + + | testAddWithBackAndForthInCaret + A> bowls / table.bowls.apples ^ table.bowls.oranges + + | testAddToTwoOthersFromBowlInCaret + A> bowls / (table.baskets \/ table.plates).apples ^ (table.baskets \/ table.plates).bananas + + | testRemoveLeftUnionRemoveWithCaret + R> bowls / (apples \/ oranges) ^ bananas + + | testRemoveRightUnionAddWithCaret + R> bowls / apples ^ (oranges \/ bananas) + + | testRemoveDoubleUnionWithCaret + R> bowls / (apples \/ oranges) ^ (bananas \/ durians) + + | testRemoveDoubleUnionNestedWithCaret + R> bowls / ((apples \/ oranges) \/ bananas) ^ (oranges \/ (bananas \/ durians)) + + | testRemoveWithBackAndForthInCaret + R> bowls / table.bowls.apples ^ table.bowls.oranges + + | testRemoveFromTwoOthersFromBowlInCaret + R> bowls / (table.baskets \/ table.plates).apples ^ (table.baskets \/ table.plates).bananas + + + // Empty set cases + | testAddEmptyBaseIntersectOne + A> (bowls /\ baskets) / apples + + | testAddEmptyBaseIntersectTwo + A> (bowls \/ baskets) /\ (plates \/ buckets) / apples + + | testAddEmptyBaseDifferenceOne + A> (bowls - bowls) / apples + + | testAddEmptyBaseDifferenceTwo + A> (bowls \/ baskets) - (bowls \/ baskets) / apples + + | testAddEmptyTarget + A> bowls / (apples /\ oranges) + + | testAddEmptyTargetInCaretOne + A> bowls / (apples /\ oranges) ^ apples + + | testAddEmptyTargetInCaretTwo + A> bowls / apples ^ (apples /\ oranges) + + + | testRemoveEmptyBaseIntersectOne + R> (bowls /\ baskets) / apples + + | testRemoveEmptyBaseIntersectTwo + R> (bowls \/ baskets) /\ (plates \/ buckets) / apples + + | testRemoveEmptyBaseDifferenceOne + R> (bowls - bowls) / apples + + | testRemoveEmptyBaseDifferenceTwo + R> (bowls \/ baskets) - (bowls \/ baskets) / apples + + | testRemoveEmptyTargetInCaretOne + R> bowls / (apples /\ oranges) ^ apples + + | testRemoveEmptyTargetInCaretTwo + R> bowls / apples ^ (apples /\ oranges) + } + + abstract asset Container {} + + abstract asset Fruit {} + + asset Bowl extends Container {} + + asset Basket extends Container {} + + asset Plate extends Container {} + + asset Bucket extends Container {} + + asset Apple extends Fruit {} + + asset Orange extends Fruit {} + + asset Banana extends Fruit {} + + asset Durian extends Fruit {} + + +} + +associations{ + Table [table] 1 <-- BowlPlacement --> * [bowls] Bowl + Table [table] 1 <-- BasketPlacement --> * [baskets] Basket + Table [table] 1 <-- PlatePlacement --> * [plates] Plate + Table [table] 1 <-- BucketPlacement --> * [buckets] Bucket + Container [container] 1 <-- AppleContain --> * [apples] Apple + Container [container] 1 <-- OrangeContain --> * [oranges] Orange + Container [container] 1 <-- BananaContain --> * [bananas] Banana + Container [container] 1 <-- DurianContain --> * [durians] Durian +} diff --git a/tests/testdata/dynamal_test_langs/intermediate/intDynamicTestLang13.mal b/tests/testdata/dynamal_test_langs/intermediate/intDynamicTestLang13.mal new file mode 100644 index 00000000..39f3acc7 --- /dev/null +++ b/tests/testdata/dynamal_test_langs/intermediate/intDynamicTestLang13.mal @@ -0,0 +1,260 @@ +/** + * Created by Viktor Engström + * Repo: https://gitlab.com/kth-ssas/dynamal-group/dynamaltestlangs.git + * Converted by Sandor Berglund + */ + +#id: "intDynamicTestLang13" +#version: "0.1.0" +category intDynamicTestLang13 { + asset Directory { + // Use at own risk + | testAll + -> moveTopToBottom, + moveTopToNothing, + moveTopToAll, + addTopToBottomLeft, + removeTopToBottomLeft, + addTopToBottomRight, + removeTopToBottomRight, + moveBackAndForth, + moveMiddleToBottom, + moveTopThenBottom, + moveMiddleToAll, + moveMiddleToAllAlternate, + addMiddleToTopLeft, + addMiddleToBottomLeft, + addMiddleToTopRight, + addMiddleToBottomRight, + removeMiddleToTopLeft, + removeMiddleToBottomLeft, + removeMiddleToTopRight, + removeMiddleToBottomRight, + addMiddleToAll, + removeMiddleToAll, + moveBottomToTop, + moveBottomToAll, + moveBottomToNothing, + addBottomToNothing, + addBottomToTopLeft, + addBottomToTopRight, + addBottomToAll, + removeBottomToNothingLeft, + removeBottomToNothingRight, + removeBottomToTopLeft, + removeBottomToTopRight, + removeBottomToAll + + + // Use on top directories + | testTop + -> moveTopToBottom, + moveTopToNothing, + moveTopToAll, + addTopToBottomLeft, + removeTopToBottomLeft, + addTopToBottomRight, + removeTopToBottomRight, + moveBackAndForth + + + // Use on middle directories + | testMiddle + -> addMiddleToTopLeft, + addMiddleToBottomLeft, + addMiddleToTopRight, + addMiddleToBottomRight, + removeMiddleToTopLeft, + removeMiddleToBottomLeft, + removeMiddleToTopRight, + removeMiddleToBottomRight, + addMiddleToAll, + removeMiddleToAll + + + // Use on bottom directories + | testBottom + -> moveBottomToTop, + moveTopThenBottom, + moveBottomToAll, + moveBottomToNothing, + addBottomToNothing, + addBottomToTopLeft, + addBottomToTopRight, + addBottomToAll, + removeBottomToNothingLeft, + removeBottomToNothingRight, + removeBottomToTopLeft, + removeBottomToTopRight, + removeBottomToAll + + + + //********************************************************** + + // Tests from the top + + // {middle, bottom} -> reached + | moveTopToBottom + -> *sub.files.reached + + // Empty set + | moveTopToNothing + -> *super.files.reached + + // Joins {Top} and {middle, bottom} + | moveTopToAll + -> (files \/ *sub.files).reached + + // Add from {middle, bottom}, LH + | addTopToBottomLeft + A> *sub / files + + // Remove from {middle, bottom}, LH + | removeTopToBottomLeft + R> *sub.files / self + + // Add to {middle, bottom}, RH + | addTopToBottomRight + A> self / *sub.files + + // Remove from {middle, bottom}, RH + | removeTopToBottomRight + R> self / *sub.files + + // {middle, bottom} -> {middle, top} -> {middle, bottom} -> reached + | moveBackAndForth + -> *sub*super*sub.reached + + + // Tests from the middle + + // {bottom} -> reached + | moveMiddleToBottom + -> *sub.files.reached + + // {top} -> {middle, bottom} -> reached + | moveTopThenBottom + -> super*sub.files.reached + + // Joins {middle}, {top}, and {bottom} + | moveMiddleToAll + -> (files \/ *super.files \/ *sub.files).reached + + // Joins {top} and {middle, bottom} + | moveMiddleToAllAlternate + -> (*super \/ *super*sub).files.reached + + // {top} + | addMiddleToTopLeft + A> *super / files + + // {bottom} + | addMiddleToBottomLeft + A> *sub / files + + // {top} + | addMiddleToTopRight + A> self / *super.files + + // {bottom} + | addMiddleToBottomRight + A> self / *super.files + + // {top} + | removeMiddleToTopLeft + R> *super.files / self + + // {bottom} + | removeMiddleToBottomLeft + R> *sub.files / self + + // {top} + | removeMiddleToTopRight + R> self / *super.files + + // {top} + | removeMiddleToBottomRight + R> self / *sub.files + + // {top} and {middle, bottom} + | addMiddleToAll + A> (*super \/ *super*sub) / files + + // {top} and {middle, bottom} + | removeMiddleToAll + R> (*super \/ *super*sub) / files + + + + // Test from the bottom + + // {middle, top} -> reached + | moveBottomToTop + -> *super.files.reached + + // {middle, top} -> {middle, bottom} + | moveTopThenBottom + -> *super*sub.files.reached + + // Joins {bottom}, {middle, top}, and {middle, bottom} + | moveBottomToAll + -> (files \/ *super.files \/ *super*sub.files).reached + + // Empty set + | moveBottomToNothing + -> *sub.files.reached + + // Empty set + | addBottomToNothing + A> *sub / files + + // {middle, top} + | addBottomToTopLeft + A> *super / files + + // {middle, top} + | addBottomToTopRight + A> self / *super.files + + // {middle, top} + | addBottomToTopRight + A> self / *super.files + + // {middle, top} and {middle, bottom} + | addBottomToAll + A> (*super \/ *super*sub) / files + + // Empty set + | removeBottomToNothingLeft + R> *sub.files / self + + // Empty set + | removeBottomToNothingRight + R> self / *sub.files + + // {middle, top} + | removeBottomToTopLeft + R> *super.files / self + + // {middle, top} + | removeBottomToTopRight + R> self / *super.files + + // {middle, top} and {middle, bottom} + | removeBottomToAll + R> (*super \/ *super*sub) / files + } + + asset File { + | reached + + } + + +} + +associations { + Directory [super] 1 <-- nesting --> * [sub] Directory + Directory [dirs] * <-- contain --> * [files] File +} diff --git a/tests/testdata/dynamal_test_langs/intermediate/intDynamicTestLang2.mal b/tests/testdata/dynamal_test_langs/intermediate/intDynamicTestLang2.mal new file mode 100644 index 00000000..1a5c6d0a --- /dev/null +++ b/tests/testdata/dynamal_test_langs/intermediate/intDynamicTestLang2.mal @@ -0,0 +1,36 @@ +/** + * Created by Viktor Engström + * Repo: https://gitlab.com/kth-ssas/dynamal-group/dynamaltestlangs.git + * Converted by Sandor Berglund + */ + +#id: "intDynamicTestLang2" +#version: "0.0.1" + +category Business { + asset AccessPolicy {} + + asset Banana {} + + asset BananaStorage { + | access + -> pilfer + | pilfer + R> self / goods + } + + asset Monkey { + let allowedAccess = (storage /\ allowPolicies.facilities) + | attemptTheft + -> allowedAccess().access + | rewritePolicy + A> self / ~storage.policy.allowedEmployees + } +} + +associations { + Monkey [agent] * <-- Access --> * [storage] BananaStorage + BananaStorage [facilities] * <-- Govern --> 1 [policy] AccessPolicy + AccessPolicy [allowPolicies] * <-- Control --> * [allowedEmployees] Monkey + BananaStorage [container] 1 <-- Contain --> * [goods] Banana +} diff --git a/tests/testdata/dynamal_test_langs/intermediate/intDynamicTestLang3.mal b/tests/testdata/dynamal_test_langs/intermediate/intDynamicTestLang3.mal new file mode 100644 index 00000000..49a8ce24 --- /dev/null +++ b/tests/testdata/dynamal_test_langs/intermediate/intDynamicTestLang3.mal @@ -0,0 +1,36 @@ +/** + * Created by Viktor Engström + * Repo: https://gitlab.com/kth-ssas/dynamal-group/dynamaltestlangs.git + * Converted by Sandor Berglund + */ + +#id: "intDynamicTestLang3" +#version: "0.0.1" + +category Business { + asset Alarm {} + asset Poison {} + + asset BananaTree { + | access + -> contaminateBananas + + | contaminateBananas + user info: "Deactivate alarms, then poison reachable bananas." + developer info: "Order of operations!" + R> self / ~alarm + A> bananas / poison + -> bananas.poison.banana.poisoned + } + + asset Banana { + | poisoned + user info: "Flag this banana as poisoned!" + } +} + +associations { + Alarm [alarm] 1 <-- protects --> * [trees] BananaTree + BananaTree [tree] 1 <-- contains --> * [bananas] Banana + Banana [banana] 1 <-- contamination --> 0..1 [poison] Poison +} diff --git a/tests/testdata/dynamal_test_langs/intermediate/intDynamicTestLang4.mal b/tests/testdata/dynamal_test_langs/intermediate/intDynamicTestLang4.mal new file mode 100644 index 00000000..6bda4089 --- /dev/null +++ b/tests/testdata/dynamal_test_langs/intermediate/intDynamicTestLang4.mal @@ -0,0 +1,51 @@ +/** + * Created by Viktor Engström + * Repo: https://gitlab.com/kth-ssas/dynamal-group/dynamaltestlangs.git + * Converted by Sandor Berglund + */ + +#id: "intDynamicTestLang4" +#version: "0.0.1" + +category Business { + asset Apples { + | access + -> plantBadApple, plantInfestedApple + + | plantBadApple + A> self / badApple + -> badApple.spoil + + | plantInfestedApple + A> self / badApple[InfestedApple] + -> badApple.spoil + + | spoil +} + + asset BadApple + user info: "One bad apple spoils the bunch!" + { + | spoil + -> apples.spoil + + } + + asset InfestedApple extends BadApple + user info: "Ew!" + { + | spoil + +A> apples / pest + } + + asset Worm + user info: "Wiggly." + { + } +} + +associations { + Apples [apples] 1 <-- spoiledBy --> 0..1 [badApple] BadApple + Apples [apple] 1 <-- infestedBy --> 0..1 [pest] Worm + +} diff --git a/tests/testdata/dynamal_test_langs/intermediate/intDynamicTestLang5.mal b/tests/testdata/dynamal_test_langs/intermediate/intDynamicTestLang5.mal new file mode 100644 index 00000000..c57b25a8 --- /dev/null +++ b/tests/testdata/dynamal_test_langs/intermediate/intDynamicTestLang5.mal @@ -0,0 +1,45 @@ +/** + * Created by Viktor Engström + * Repo: https://gitlab.com/kth-ssas/dynamal-group/dynamaltestlangs.git + * Converted by Sandor Berglund + */ + +#id: "intDynamicTestLang5" +#version: "0.1.0" +category intDynamicTestLang5 { + asset Business {} + + abstract asset BusinessResource {} + + abstract asset Perishable extends BusinessResource { + | consume + R> self / self + } + + abstract asset NonPerishable extends BusinessResource {} + + asset Typewriter extends NonPerishable {} + asset Apple extends Perishable {} + asset Banana extends Perishable {} + + asset Monkey { + | eatPerishables + -> business.goodies[Perishable].consume + + | eatApples + -> business.goodies[Apple].consume + + | eatBananas + -> business.goodies[Banana].consume + + | breakTypewriters + R> business.goodies[Typewriter] / self + + } + +} + +associations { + Business [organization] 1 <-- trades --> * [goodies] BusinessResource + Business [business] 1 <-- employs --> * [monkeys] Monkey +} diff --git a/tests/testdata/dynamal_test_langs/intermediate/intDynamicTestLang6.mal b/tests/testdata/dynamal_test_langs/intermediate/intDynamicTestLang6.mal new file mode 100644 index 00000000..84335dd3 --- /dev/null +++ b/tests/testdata/dynamal_test_langs/intermediate/intDynamicTestLang6.mal @@ -0,0 +1,30 @@ +/** + * Created by Viktor Engström + * Repo: https://gitlab.com/kth-ssas/dynamal-group/dynamaltestlangs.git + * Converted by Sandor Berglund + */ + +#id: "intDynamicTestLang6" +#version: "0.1.0" +category intDynamicTestLang6 { + asset Table { + | access + -> bowls.addApple, bowls.transferApple + } + asset Bowl { + | addApple + A> self / apples + + | transferApple + A> apples / ~table.bowls.apples + R> self / ~apples + } + + asset Apple {} + +} + +associations { + Bowl [bowls] * <-- placement --> 1 [table] Table + Bowl [container] * <-- contains --> * [apples] Apple +} diff --git a/tests/testdata/dynamal_test_langs/intermediate/intDynamicTestLang7.mal b/tests/testdata/dynamal_test_langs/intermediate/intDynamicTestLang7.mal new file mode 100644 index 00000000..999921bc --- /dev/null +++ b/tests/testdata/dynamal_test_langs/intermediate/intDynamicTestLang7.mal @@ -0,0 +1,32 @@ +/** + * Created by Viktor Engström + * Repo: https://gitlab.com/kth-ssas/dynamal-group/dynamaltestlangs.git + * Converted by Sandor Berglund + */ + +#id: "intDynamicTestLang7" +#version: "0.1.0" +category intDynamicTestLang7 { + asset Table { + | access + -> bowls.appleToOrange, bowls.orangeToApple + } + asset Bowl { + | appleToOrange + R> self / fruit[Apple] + A> self / fruit[Orange] + | orangeToApple + R> self / fruit[Orange] + A> self / fruit[Apple] + } + + abstract asset Fruit {} + asset Apple extends Fruit {} + asset Orange extends Fruit {} + +} + +associations { + Bowl [bowls] * <-- placement --> 1 [table] Table + Bowl [container] * <-- acontains --> * [fruit] Fruit +} diff --git a/tests/testdata/dynamal_test_langs/intermediate/intDynamicTestLang8.mal b/tests/testdata/dynamal_test_langs/intermediate/intDynamicTestLang8.mal new file mode 100644 index 00000000..24cdbd9e --- /dev/null +++ b/tests/testdata/dynamal_test_langs/intermediate/intDynamicTestLang8.mal @@ -0,0 +1,25 @@ +/** + * Created by Viktor Engström + * Repo: https://gitlab.com/kth-ssas/dynamal-group/dynamaltestlangs.git + * Converted by Sandor Berglund + */ + +#id: "intDynamicTestLang8" +#version: "0.1.0" +category intDynamicTestLang8 { + asset Table { + | assembleFruitBowl + A> self / bowls, + bowls / fruit[Apple] ^ fruit[Orange] + } + asset Bowl {} + abstract asset Fruit {} + asset Apple extends Fruit {} + asset Orange extends Fruit {} + +} + +associations { + Bowl [bowls] * <-- placement --> 1 [table] Table + Bowl [container] * <-- contains --> * [fruit] Fruit +} diff --git a/tests/testdata/dynamal_test_langs/intermediate/intDynamicTestLang9.mal b/tests/testdata/dynamal_test_langs/intermediate/intDynamicTestLang9.mal new file mode 100644 index 00000000..79893e33 --- /dev/null +++ b/tests/testdata/dynamal_test_langs/intermediate/intDynamicTestLang9.mal @@ -0,0 +1,26 @@ +/** + * Created by Viktor Engström + * Repo: https://gitlab.com/kth-ssas/dynamal-group/dynamaltestlangs.git + * Converted by Sandor Berglund + */ + +#id: "intDynamicTestLang9" +#version: "0.1.0" +category intDynamicTestLang9 { + asset Room { + | addChairs + A> self / chairs + | installTable + A> self / tables, + tables / ~chairs.table + } + asset Table {} + asset Chair {} + +} + +associations { + Room [room] 1 <-- tables --> 0..* [tables] Table + Room [room] 1 <-- chairs --> 0..* [chairs] Chair + Table [table] 1 <-- arrange --> 0..* [chair] Chair +} diff --git a/tests/testdata/get_dynamal_test_langs.py b/tests/testdata/get_dynamal_test_langs.py new file mode 100644 index 00000000..65fbc8e5 --- /dev/null +++ b/tests/testdata/get_dynamal_test_langs.py @@ -0,0 +1,202 @@ +"""Fetch the DynaMAL test language specifications into tests/testdata. + +Clones https://gitlab.com/kth-ssas/dynamal-group/dynamaltestlangs and copies +every ``*.dmal`` file it contains into this directory (flattened by +filename), so they can be committed as test fixtures. + +Usage: + python tests/testdata/get_dynamal_test_langs.py +""" +import re +import shutil +from pathlib import Path + +from maltoolbox.file_utils import download_git_repo +from maltoolbox.language.languagegraph import LanguageGraph + +DYNAMAL_TEST_LANGS_URL = ( + "https://gitlab.com/kth-ssas/dynamal-group/dynamaltestlangs.git" +) +TESTDATA_DIR = Path(__file__).resolve().parent +test_lang_dir = TESTDATA_DIR / "dynamal_test_langs" +test_lang_dir.mkdir(exist_ok=True) + +version_pattern = re.compile(r"#version:\s*") +id_pattern = re.compile(r"#id:\s*") +def fix_version_id(lang_path: Path): + lang_text = lang_path.read_text() + prepend_text = "" + id_match = bool(id_pattern.search(lang_text)) + version_match = bool(version_pattern.search(lang_text)) + if not id_match: + prepend_text += "#id: \"" + lang_path.stem + "\"\n" + + if not version_match: + prepend_text += "#version: \"0.1.0\"\n" + + if len(prepend_text) > 0: + if id_match and version_match: + prepend_text += "\n" + lang_path.write_text(prepend_text + lang_text) + + +def add_credit_disclaimer(lang_path: Path, source_path: Path) -> None: + lang_text = lang_path.read_text() + disclaimer = ( + "/**\n" + f" * Created by Viktor Engström\n" + f" * Repo: {DYNAMAL_TEST_LANGS_URL}\n" + f" * Converted by Sandor Berglund\n" + " */\n\n" + ) + lang_path.write_text(disclaimer + lang_text) + + +def fix_category(lang_path: Path): + lang_text = lang_path.read_text() + has_category = "category" in lang_text + if not has_category: + lines = lang_text.splitlines(keepends=True) + + insert_idx = 0 + while insert_idx < len(lines) and lines[insert_idx].startswith("#"): + insert_idx += 1 + lines.insert(insert_idx, f"category {lang_path.stem} {{\n") + + assoc_idx = next( + (i for i, line in enumerate(lines) + if line.lstrip().startswith("associations")), + None, + ) + body_end = assoc_idx if assoc_idx is not None else len(lines) + for i in range(insert_idx + 1, body_end): + if lines[i].strip(): + lines[i] = "\t" + lines[i] + + if assoc_idx is not None: + lines.insert(assoc_idx, "}\n\n") + else: + lines.append("}\n") + + lang_path.write_text("".join(lines)) + +step_start_pattern = re.compile(r"^\s*(\||&|#|!E(?![A-Za-z0-9_])|E(?![A-Za-z0-9_]))") +stray_step_arrow_pattern = re.compile( + r"^(\s*[|&#]\s*[A-Za-z_][A-Za-z0-9_]*)\s*>(\s*(//.*)?)$" +) +def fix_stray_step_arrow(lang_path: Path): + """Drop the bare trailing `>` some step declarations have (e.g. + `| plantTree >`) — it isn't part of the current attack_step grammar + and causes a syntax error.""" + lines = lang_path.read_text().splitlines(keepends=True) + + fixed_lines = [] + for line in lines: + eol = "\n" if line.endswith("\n") else "" + content = line[:-1] if eol else line + match = stray_step_arrow_pattern.match(content) + if match: + line = f"{match.group(1)}{match.group(2)}{eol}" + fixed_lines.append(line) + + lang_path.write_text("".join(fixed_lines)) + + +reaches_pattern = re.compile(r"^(\s*)->\s*(.*)$") +def fix_double_reaching(lang_path: Path): + """Merge repeated `-> target` lines within a single attack step into + one comma-separated clause, keeping only the first `->` and dropping + any exact-duplicate targets.""" + lines = lang_path.read_text().splitlines(keepends=True) + + seen_targets: set[str] = set() + last_line_idx: int | None = None + kept_lines: list[str] = [] + for line in lines: + if step_start_pattern.match(line): + seen_targets = set() + last_line_idx = None + + eol = "\n" if line.endswith("\n") else "" + content = line[:-1] if eol else line + match = reaches_pattern.match(content) + if match: + indent, rest = match.groups() + target = re.sub(r"//.*", "", rest).strip().rstrip(",") + if target in seen_targets: + continue + seen_targets.add(target) + + if last_line_idx is None: + kept_lines.append(line) + else: + prev_content = kept_lines[last_line_idx].rstrip("\n") + if not prev_content.endswith(","): + prev_content += "," + kept_lines[last_line_idx] = prev_content + "\n" + kept_lines.append(f"{indent}{rest}{eol}") + last_line_idx = len(kept_lines) - 1 + continue + + kept_lines.append(line) + + lang_path.write_text("".join(kept_lines)) + + +dynamic_op_pattern = re.compile(r"^(\s*)(\+?[AR]>)\s*(.*)$") +def fix_double_dynamic_operations(lang_path: Path): + """Merge repeated `A>`/`R>`/`+A>`/`+R>` clauses within a single attack + step into one comma-separated clause, keeping only the first operator.""" + lines = lang_path.read_text().splitlines(keepends=True) + + last_line_idx: dict[str, int] = {} + kept_lines: list[str] = [] + for line in lines: + if step_start_pattern.match(line): + last_line_idx = {} + + eol = "\n" if line.endswith("\n") else "" + content = line[:-1] if eol else line + match = dynamic_op_pattern.match(content) + if match: + indent, op, rest = match.groups() + if op in last_line_idx: + prev_idx = last_line_idx[op] + prev_content = kept_lines[prev_idx].rstrip("\n") + if not prev_content.endswith(","): + prev_content += "," + kept_lines[prev_idx] = prev_content + "\n" + kept_lines.append(f"{indent}{rest}{eol}") + else: + kept_lines.append(line) + last_line_idx[op] = len(kept_lines) - 1 + continue + + kept_lines.append(line) + + lang_path.write_text("".join(kept_lines)) + + +def copy_test_langs(dir_name: str, repo_dir: Path) -> None: + sub_dir = test_lang_dir / dir_name + sub_dir.mkdir(exist_ok=True) + for copy_path in (repo_dir / dir_name).rglob("*.dmal"): + lang_path = (sub_dir / copy_path.name).with_suffix(".mal") + shutil.copy(copy_path, lang_path) + fix_version_id(lang_path) + fix_category(lang_path) + fix_double_reaching(lang_path) + fix_double_dynamic_operations(lang_path) + fix_stray_step_arrow(lang_path) + add_credit_disclaimer(lang_path, copy_path.relative_to(repo_dir)) + +def main() -> None: + repo_dir = download_git_repo(DYNAMAL_TEST_LANGS_URL) + + copy_test_langs("basic", repo_dir) + copy_test_langs("intermediate", repo_dir) + copy_test_langs("advanced", repo_dir) + + +if __name__ == "__main__": + main() diff --git a/tests/testdata/wiperLang.mal b/tests/testdata/wiperLang.mal new file mode 100644 index 00000000..812ec4b3 --- /dev/null +++ b/tests/testdata/wiperLang.mal @@ -0,0 +1,57 @@ +/** + * @file wiperLang.mal + * @brief WiperLang example model + * @author Viktor Engström + + source: https://doi.org/10.1145/3664476.3664508 +**/ + + +#id: "org.mal-lang.wiperLang" +#version: "1.0.0" + +category WiperLang { + asset Internet {} + abstract asset Host {} + asset C2Server extends Host {} + asset Device extends Host { + | infect + A> self / malware[Wiper] // Add Malware vertex + -> malware.activate + } + + abstract asset Malware { + | activate + -> trigger + | trigger + + | test + A> trigger + } + + asset Wiper extends Malware { + | activate + +> exfiltrate, propagate + | exfiltrate + A> victim.data / // Link data + ~victim.inet.hosts[C2Server].data // to C2 + | propagate + A> victim.sendTo / malware[Wiper] + -> victim.sendTo.malware[Wiper].activate + | trigger + R> victim / malware ^ ~data + // Retain victim, then self-destruct + wipe + + | test + +A> activate + } + + asset Data {} +} + +associations { + Internet [inet] 1 <-- contains --> * [hosts] Host + Host [receiveFrom] * <-- neighbor --> * [sendTo] Host + Host [node] 1 <-- store --> * [data] Data + Host [victim] 1 <-- infect --> * [malware] Malware +}