diff --git a/.azure/azure-ci-build-pipeline.yaml b/.azure/azure-ci-build-pipeline.yaml index 01111290..2375a391 100644 --- a/.azure/azure-ci-build-pipeline.yaml +++ b/.azure/azure-ci-build-pipeline.yaml @@ -102,9 +102,7 @@ stages: version: '2.1.x' - script: | - coverage run -m --source=primaite pytest -v -o junit_family=xunit2 --junitxml=junit/test-results.xml --cov-fail-under=80 - coverage xml -o coverage.xml -i - coverage html -d htmlcov -i + python run_test_and_coverage.py displayName: 'Run tests and code coverage' # Run the notebooks diff --git a/CHANGELOG.md b/CHANGELOG.md index b5996f98..4b9ca8e6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,37 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [3.3.0] - 2024-09-04 + +### Added +- Random Number Generator Seeding by specifying a random number seed in the config file. +- Implemented Terminal service class, providing a generic terminal simulation. +- Added `User`, `UserManager` and `UserSessionManager` to enable the creation of user accounts and login on Nodes. +- Added actions to establish SSH connections, send commands remotely and terminate SSH connections. +- Added actions to change users' passwords. +- Added a `listen_on_ports` set in the `IOSoftware` class to enable software listening on ports in addition to the + main port they're assigned. +- Added two new red applications: ``C2Beacon`` and ``C2Server`` which aim to simulate malicious network infrastructure. + Refer to the ``Command and Control Application Suite E2E Demonstration`` notebook for more information. +- Added reward calculation details to AgentHistoryItem. +- Added a new Privilege-Escalation-and Data-Loss-Example.ipynb notebook with a realistic cyber scenario focusing on + internal privilege escalation and data loss through the manipulation of SSH access and Access Control Lists (ACLs). + +### Changed +- File and folder observations can now be configured to always show the true health status, or require scanning like before. +- It's now possible to disable stickiness on reward components, meaning their value returns to 0 during timesteps where agent don't issue the corresponding action. Affects `GreenAdminDatabaseUnreachablePenalty`, `WebpageUnavailablePenalty`, `WebServer404Penalty` +- Node observations can now be configured to show the number of active local and remote logins. + +### Fixed +- Folder observations showing the true health state without scanning (the old behaviour can be reenabled via config) +- Updated `SoftwareManager` `install` and `uninstall` to handle all functionality that was being done at the `install` + and `uninstall` methods in the `Node` class. +- Updated the `receive_payload_from_session_manager` method in `SoftwareManager` so that it now sends a copy of the + payload to any software listening on the destination port of the `Frame`. + +### Removed +- Removed the `install` and `uninstall` methods in the `Node` class. + ## [3.2.0] - 2024-07-18 @@ -17,7 +48,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Tests to verify that airspace bandwidth is applied correctly and can be configured via YAML - Agent logging for agents' internal decision logic - Action masking in all PrimAITE environments - ### Changed - Application registry was moved to the `Application` class and now updates automatically when Application is subclassed - Databases can no longer respond to request while performing a backup @@ -27,6 +57,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Frame `size` attribute now includes both core size and payload size in bytes - The `speed` attribute of `NetworkInterface` has been changed from `int` to `float` - Tidied up CHANGELOG +- Enhanced `AirSpace` logic to block transmissions that would exceed the available capacity. +- Updated `_can_transmit` function in `Link` to account for current load and total bandwidth capacity, ensuring transmissions do not exceed limits. ### Fixed - Links and airspaces can no longer transmit data if this would exceed their bandwidth diff --git a/README.md b/README.md index 137852f5..c8f644be 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,8 @@ PrimAITE presents the following features: - Support for multiple agents, each having their own customisable observation space, action space, and reward function definition, and either deterministic or RL-directed behaviour +Whilst PrimAITE ships with a number of example modelled scenarios (a.k.a. Use Cases), it has not been developed to mandate the solving of a single cyber challenge, and instead provides a highly flexible environment application that can be extended and reconfigured by the user to suit their specific cyber defence training and evaluation needs. PrimAITE provides default networks, red agent and green agent behaviour, reward functions, and action / observation space configuration, all of which can be utilised out of the box, but which ultimately can (and in some instances should) be built upon and / or reconfigured to meet the needs of different defensive agent developers. The PrimAITE user guide provides comprehensive instruction on all PrimAITE features, functionality and components, and can be consulted in order to help guide users in any reconfiguration or enhancements they wish to undertake; a library of example Jupyter notebooks are also provided to support such work. + ## Getting Started with PrimAITE ### 💫 Installation diff --git a/benchmark/primaite_benchmark.py b/benchmark/primaite_benchmark.py index 0e6c2acc..86ed22a9 100644 --- a/benchmark/primaite_benchmark.py +++ b/benchmark/primaite_benchmark.py @@ -5,7 +5,7 @@ from datetime import datetime from pathlib import Path from typing import Any, Dict, Final, Tuple -from report import build_benchmark_md_report +from report import build_benchmark_md_report, md2pdf from stable_baselines3 import PPO import primaite @@ -159,6 +159,13 @@ def run( learning_rate: float = 3e-4, ) -> None: """Run the PrimAITE benchmark.""" + # generate report folder + v_str = f"v{primaite.__version__}" + + version_result_dir = _RESULTS_ROOT / v_str + version_result_dir.mkdir(exist_ok=True, parents=True) + output_path = version_result_dir / f"PrimAITE {v_str} Benchmark Report.md" + benchmark_start_time = datetime.now() session_metadata_dict = {} @@ -193,6 +200,12 @@ def run( session_metadata=session_metadata_dict, config_path=data_manipulation_config_path(), results_root_path=_RESULTS_ROOT, + output_path=output_path, + ) + md2pdf( + md_path=output_path, + pdf_path=str(output_path).replace(".md", ".pdf"), + css_path="static/styles.css", ) diff --git a/benchmark/report.py b/benchmark/report.py index 5eaaab9f..4035ceca 100644 --- a/benchmark/report.py +++ b/benchmark/report.py @@ -2,6 +2,7 @@ import json import sys from datetime import datetime +from os import PathLike from pathlib import Path from typing import Dict, Optional @@ -14,7 +15,7 @@ from utils import _get_system_info import primaite PLOT_CONFIG = { - "size": {"auto_size": False, "width": 1500, "height": 900}, + "size": {"auto_size": False, "width": 800, "height": 640}, "template": "plotly_white", "range_slider": False, } @@ -144,6 +145,20 @@ def _plot_benchmark_metadata( yaxis={"title": "Total Reward"}, title=title, ) + fig.update_layout( + legend=dict( + yanchor="top", + y=0.99, + xanchor="left", + x=0.01, + bgcolor="rgba(255,255,255,0.3)", + ) + ) + for trace in fig["data"]: + if trace["name"].startswith("Session"): + trace["showlegend"] = False + fig["data"][0]["name"] = "Individual Sessions" + fig["data"][0]["showlegend"] = True return fig @@ -194,6 +209,7 @@ def _plot_all_benchmarks_combined_session_av(results_directory: Path) -> Figure: title=title, ) fig["data"][0]["showlegend"] = True + fig.update_layout(legend=dict(yanchor="top", y=-0.2, xanchor="left", x=0.01, orientation="h")) return fig @@ -234,10 +250,7 @@ def _plot_av_s_per_100_steps_10_nodes( """ major_v = primaite.__version__.split(".")[0] title = f"Performance of Minor and Bugfix Releases for Major Version {major_v}" - subtitle = ( - f"Average Training Time per 100 Steps on 10 Nodes " - f"(target: <= {PLOT_CONFIG['av_s_per_100_steps_10_nodes_benchmark_threshold']} seconds)" - ) + subtitle = "Average Training Time per 100 Steps on 10 Nodes " title = f"{title}
{subtitle}" layout = go.Layout( @@ -250,24 +263,12 @@ def _plot_av_s_per_100_steps_10_nodes( versions = sorted(list(version_times_dict.keys())) times = [version_times_dict[version] for version in versions] - av_s_per_100_steps_10_nodes_benchmark_threshold = PLOT_CONFIG["av_s_per_100_steps_10_nodes_benchmark_threshold"] - # Calculate the appropriate maximum y-axis value - max_y_axis_value = max(max(times), av_s_per_100_steps_10_nodes_benchmark_threshold) + 1 - - fig.add_trace( - go.Bar( - x=versions, - y=times, - text=times, - textposition="auto", - ) - ) + fig.add_trace(go.Bar(x=versions, y=times, text=times, textposition="auto", texttemplate="%{y:.3f}")) fig.update_layout( xaxis_title="PrimAITE Version", yaxis_title="Avg Time per 100 Steps on 10 Nodes (seconds)", - yaxis=dict(range=[0, max_y_axis_value]), title=title, ) @@ -275,7 +276,11 @@ def _plot_av_s_per_100_steps_10_nodes( def build_benchmark_md_report( - benchmark_start_time: datetime, session_metadata: Dict, config_path: Path, results_root_path: Path + benchmark_start_time: datetime, + session_metadata: Dict, + config_path: Path, + results_root_path: Path, + output_path: PathLike, ) -> None: """ Generates a Markdown report for a benchmarking session, documenting performance metrics and graphs. @@ -327,7 +332,7 @@ def build_benchmark_md_report( data = benchmark_metadata_dict primaite_version = data["primaite_version"] - with open(version_result_dir / f"PrimAITE v{primaite_version} Benchmark Report.md", "w") as file: + with open(output_path, "w") as file: # Title file.write(f"# PrimAITE v{primaite_version} Learning Benchmark\n") file.write("## PrimAITE Dev Team\n") @@ -401,3 +406,15 @@ def build_benchmark_md_report( f"![Performance of Minor and Bugfix Releases for Major Version {major_v}]" f"({performance_benchmark_plot_path.name})\n" ) + + +def md2pdf(md_path: PathLike, pdf_path: PathLike, css_path: PathLike) -> None: + """Generate PDF version of Markdown report.""" + from md2pdf.core import md2pdf + + md2pdf( + pdf_file_path=pdf_path, + md_file_path=md_path, + base_url=Path(md_path).parent, + css_file_path=css_path, + ) diff --git a/benchmark/results/v3/PrimAITE Versions Learning Benchmark.png b/benchmark/results/v3/PrimAITE Versions Learning Benchmark.png new file mode 100644 index 00000000..9884e2ec Binary files /dev/null and b/benchmark/results/v3/PrimAITE Versions Learning Benchmark.png differ diff --git a/benchmark/results/v3/v3.0.0/PrimAITE Learning Benchmark of Minor and Bugfix Releases for Major Version 3.png b/benchmark/results/v3/v3.0.0/PrimAITE Learning Benchmark of Minor and Bugfix Releases for Major Version 3.png deleted file mode 100644 index 542f8f56..00000000 Binary files a/benchmark/results/v3/v3.0.0/PrimAITE Learning Benchmark of Minor and Bugfix Releases for Major Version 3.png and /dev/null differ diff --git a/benchmark/results/v3/v3.0.0/PrimAITE Performance of Minor and Bugfix Releases for Major Version 3.png b/benchmark/results/v3/v3.0.0/PrimAITE Performance of Minor and Bugfix Releases for Major Version 3.png deleted file mode 100644 index 05fa4f15..00000000 Binary files a/benchmark/results/v3/v3.0.0/PrimAITE Performance of Minor and Bugfix Releases for Major Version 3.png and /dev/null differ diff --git a/benchmark/results/v3/v3.0.0/PrimAITE v3.0.0 Learning Benchmark.pdf b/benchmark/results/v3/v3.0.0/PrimAITE v3.0.0 Learning Benchmark.pdf new file mode 100644 index 00000000..fceba624 Binary files /dev/null and b/benchmark/results/v3/v3.0.0/PrimAITE v3.0.0 Learning Benchmark.pdf differ diff --git a/benchmark/results/v3/v3.0.0/PrimAITE v3.0.0 Learning Benchmark.png b/benchmark/results/v3/v3.0.0/PrimAITE v3.0.0 Learning Benchmark.png index b61c706a..c54dc354 100644 Binary files a/benchmark/results/v3/v3.0.0/PrimAITE v3.0.0 Learning Benchmark.png and b/benchmark/results/v3/v3.0.0/PrimAITE v3.0.0 Learning Benchmark.png differ diff --git a/benchmark/results/v3/v3.3.0/PrimAITE Learning Benchmark of Minor and Bugfix Releases for Major Version 3.png b/benchmark/results/v3/v3.3.0/PrimAITE Learning Benchmark of Minor and Bugfix Releases for Major Version 3.png new file mode 100644 index 00000000..123c9774 Binary files /dev/null and b/benchmark/results/v3/v3.3.0/PrimAITE Learning Benchmark of Minor and Bugfix Releases for Major Version 3.png differ diff --git a/benchmark/results/v3/v3.3.0/PrimAITE Performance of Minor and Bugfix Releases for Major Version 3.png b/benchmark/results/v3/v3.3.0/PrimAITE Performance of Minor and Bugfix Releases for Major Version 3.png new file mode 100644 index 00000000..43f661ea Binary files /dev/null and b/benchmark/results/v3/v3.3.0/PrimAITE Performance of Minor and Bugfix Releases for Major Version 3.png differ diff --git a/benchmark/results/v3/v3.0.0/PrimAITE v3.0.0 Benchmark Report.md b/benchmark/results/v3/v3.3.0/PrimAITE v3.3.0 Benchmark Report.md similarity index 81% rename from benchmark/results/v3/v3.0.0/PrimAITE v3.0.0 Benchmark Report.md rename to benchmark/results/v3/v3.3.0/PrimAITE v3.3.0 Benchmark Report.md index c2cd6e78..da71ede3 100644 --- a/benchmark/results/v3/v3.0.0/PrimAITE v3.0.0 Benchmark Report.md +++ b/benchmark/results/v3/v3.3.0/PrimAITE v3.3.0 Benchmark Report.md @@ -1,10 +1,10 @@ -# PrimAITE v3.0.0 Learning Benchmark +# PrimAITE v3.3.0 Learning Benchmark ## PrimAITE Dev Team -### 2024-07-20 +### 2024-09-02 --- ## 1 Introduction -PrimAITE v3.0.0 was benchmarked automatically upon release. Learning rate metrics were captured to be referenced during system-level testing and user acceptance testing (UAT). +PrimAITE v3.3.0 was benchmarked automatically upon release. Learning rate metrics were captured to be referenced during system-level testing and user acceptance testing (UAT). The benchmarking process consists of running 5 training session using the same config file. Each session trains an agent for 1000 episodes, with each episode consisting of 128 steps. The total reward per episode from each session is captured. This is then used to calculate an caverage total reward per episode from the 5 individual sessions for smoothing. Finally, a 25-widow rolling average of the average total reward per session is calculated for further smoothing. ## 2 System Information @@ -26,12 +26,12 @@ The total reward per episode from each session is captured. This is then used to - **Total Sessions:** 5 - **Total Episodes:** 5005 - **Total Steps:** 640000 -- **Av Session Duration (s):** 1452.5910 -- **Av Step Duration (s):** 0.0454 -- **Av Duration per 100 Steps per 10 Nodes (s):** 4.5393 +- **Av Session Duration (s):** 1458.2831 +- **Av Step Duration (s):** 0.0456 +- **Av Duration per 100 Steps per 10 Nodes (s):** 4.5571 ## 4 Graphs -### 4.1 v3.0.0 Learning Benchmark Plot -![PrimAITE 3.0.0 Learning Benchmark Plot](PrimAITE v3.0.0 Learning Benchmark.png) +### 4.1 v3.3.0 Learning Benchmark Plot +![PrimAITE 3.3.0 Learning Benchmark Plot](PrimAITE v3.3.0 Learning Benchmark.png) ### 4.2 Learning Benchmark of Minor and Bugfix Releases for Major Version 3 ![Learning Benchmark of Minor and Bugfix Releases for Major Version 3](PrimAITE Learning Benchmark of Minor and Bugfix Releases for Major Version 3.png) ### 4.3 Performance of Minor and Bugfix Releases for Major Version 3 diff --git a/benchmark/results/v3/v3.3.0/PrimAITE v3.3.0 Benchmark Report.pdf b/benchmark/results/v3/v3.3.0/PrimAITE v3.3.0 Benchmark Report.pdf new file mode 100644 index 00000000..10a55a10 Binary files /dev/null and b/benchmark/results/v3/v3.3.0/PrimAITE v3.3.0 Benchmark Report.pdf differ diff --git a/benchmark/results/v3/v3.3.0/PrimAITE v3.3.0 Learning Benchmark.png b/benchmark/results/v3/v3.3.0/PrimAITE v3.3.0 Learning Benchmark.png new file mode 100644 index 00000000..fb0709a4 Binary files /dev/null and b/benchmark/results/v3/v3.3.0/PrimAITE v3.3.0 Learning Benchmark.png differ diff --git a/benchmark/results/v3/v3.3.0/session_metadata/1.json b/benchmark/results/v3/v3.3.0/session_metadata/1.json new file mode 100644 index 00000000..c2a234ec --- /dev/null +++ b/benchmark/results/v3/v3.3.0/session_metadata/1.json @@ -0,0 +1,1009 @@ +{ + "total_episodes": 1001, + "total_time_steps": 128000, + "total_s": 1507.500278, + "s_per_step": 0.0471093836875, + "s_per_100_steps_10_nodes": 4.71093836875, + "total_reward_per_episode": { + "1": -22.899999999999963, + "2": -11.84999999999998, + "3": -45.15000000000006, + "4": -11.449999999999983, + "5": -22.449999999999953, + "6": -14.549999999999981, + "7": -69.80000000000005, + "8": -23.149999999999963, + "9": -92.95, + "10": -1.6499999999999995, + "11": -41.85000000000005, + "12": -20.199999999999953, + "13": -2.049999999999983, + "14": -23.34999999999995, + "15": -66.40000000000009, + "16": -60.350000000000094, + "17": -12.69999999999998, + "18": -19.599999999999987, + "19": -13.349999999999982, + "20": -23.24999999999995, + "21": -14.399999999999986, + "22": -45.600000000000065, + "23": -49.10000000000007, + "24": -21.649999999999956, + "25": -95.7000000000001, + "26": -45.55000000000019, + "27": -21.749999999999957, + "28": -39.05, + "29": -42.900000000000105, + "30": -19.849999999999966, + "31": 7.00000000000002, + "32": -52.75000000000008, + "33": -28.799999999999976, + "34": -4.099999999999985, + "35": -34.749999999999986, + "36": -21.09999999999996, + "37": -37.00000000000011, + "38": -16.24999999999998, + "39": -15.299999999999986, + "40": -12.499999999999995, + "41": -83.54999999999981, + "42": -22.2, + "43": -84.75000000000009, + "44": -16.89999999999997, + "45": -25.49999999999999, + "46": -18.89999999999997, + "47": -11.349999999999987, + "48": -21.049999999999958, + "49": -22.99999999999995, + "50": -22.499999999999954, + "51": -70.44999999999999, + "52": -62.300000000000104, + "53": 3.049999999999968, + "54": -7.399999999999997, + "55": -16.799999999999972, + "56": -73.75, + "57": -33.30000000000002, + "58": -3.0000000000000067, + "59": -16.74999999999997, + "60": -21.699999999999957, + "61": -69.05000000000005, + "62": -98.54999999999998, + "63": -7.099999999999993, + "64": -3.749999999999984, + "65": -98.19999999999999, + "66": -60.90000000000017, + "67": -97.2, + "68": -22.199999999999953, + "69": -14.549999999999965, + "70": -20.999999999999957, + "71": -20.399999999999963, + "72": -5.599999999999977, + "73": -13.300000000000004, + "74": -14.649999999999979, + "75": -11.399999999999993, + "76": -6.699999999999988, + "77": -43.300000000000125, + "78": -30.449999999999992, + "79": -23.29999999999995, + "80": -75.85, + "81": 11.55, + "82": -37.24999999999999, + "83": -94.24999999999997, + "84": -18.74999999999999, + "85": -89.3, + "86": -27.350000000000026, + "87": -103.15000000000006, + "88": -73.15000000000002, + "89": -16.999999999999975, + "90": -31.54999999999993, + "91": -16.699999999999974, + "92": -22.699999999999953, + "93": -91.19999999999999, + "94": -18.949999999999967, + "95": -87.8, + "96": -17.89999999999997, + "97": -65.3, + "98": -16.24999999999998, + "99": -12.749999999999995, + "100": -2.199999999999976, + "101": -30.199999999999978, + "102": -69.74999999999997, + "103": -75.4, + "104": -63.35000000000011, + "105": -21.749999999999957, + "106": -15.04999999999998, + "107": -11.149999999999993, + "108": -95.4, + "109": -9.299999999999994, + "110": -7.399999999999994, + "111": -67.90000000000006, + "112": -66.00000000000001, + "113": -88.4, + "114": -14.949999999999976, + "115": 0.20000000000001994, + "116": -7.80000000000002, + "117": -10.3, + "118": 4.550000000000024, + "119": -42.250000000000114, + "120": -23.89999999999997, + "121": 8.499999999999986, + "122": -73.14999999999999, + "123": -18.749999999999968, + "124": -18.14999999999997, + "125": -2.8999999999999737, + "126": -6.199999999999982, + "127": -13.09999999999999, + "128": -11.849999999999987, + "129": 14.849999999999994, + "130": -14.749999999999977, + "131": -50.60000000000007, + "132": -39.65000000000005, + "133": 14.300000000000018, + "134": -9.399999999999993, + "135": -21.949999999999953, + "136": -16.69999999999997, + "137": 29.599999999999888, + "138": -19.99999999999996, + "139": 0.7500000000000469, + "140": 25.200000000000024, + "141": -18.399999999999967, + "142": -97.19999999999999, + "143": -90.15, + "144": 20.800000000000008, + "145": -7.900000000000008, + "146": -56.750000000000014, + "147": -81.70000000000005, + "148": -91.45, + "149": -31.10000000000001, + "150": -64.35, + "151": -59.49999999999999, + "152": -15.89999999999998, + "153": 8.65000000000002, + "154": -80.35000000000001, + "155": -84.64999999999996, + "156": -20.79999999999996, + "157": 1.900000000000028, + "158": -53.599999999999994, + "159": -86.80000000000001, + "160": -93.6, + "161": -92.14999999999995, + "162": -66.75000000000001, + "163": -78.65000000000002, + "164": -8.049999999999995, + "165": -87.99999999999997, + "166": 27.249999999999893, + "167": -35.30000000000001, + "168": -0.7999999999999727, + "169": -96.44999999999999, + "170": -53.09999999999996, + "171": -7.750000000000002, + "172": -1.2499999999999776, + "173": -63.39999999999997, + "174": -36.79999999999995, + "175": -10.09999999999999, + "176": -9.699999999999998, + "177": -48.2, + "178": -76.7, + "179": -73.59999999999995, + "180": -76.2, + "181": -88.39999999999999, + "182": -15.649999999999977, + "183": -91.14999999999998, + "184": -11.499999999999995, + "185": -21.949999999999953, + "186": -30.85000000000002, + "187": 40.64999999999981, + "188": 8.850000000000062, + "189": -77.00000000000004, + "190": -75.45, + "191": -0.8999999999999571, + "192": -47.25, + "193": -61.69999999999993, + "194": 7.100000000000066, + "195": -7.099999999999988, + "196": -4.050000000000007, + "197": -6.499999999999984, + "198": -82.9, + "199": 1.300000000000014, + "200": 9.849999999999971, + "201": -3.7499999999999805, + "202": 84.9000000000002, + "203": 8.45000000000005, + "204": -32.749999999999964, + "205": -36.44999999999997, + "206": -90.1, + "207": -84.05, + "208": -12.199999999999989, + "209": 13.94999999999997, + "210": -18.849999999999994, + "211": 16.80000000000005, + "212": 26.599999999999895, + "213": -22.84999999999995, + "214": -74.05, + "215": -8.149999999999993, + "216": -28.949999999999967, + "217": -61.29999999999995, + "218": -3.8000000000000043, + "219": -56.799999999999976, + "220": 25.85000000000001, + "221": -87.0, + "222": -64.14999999999999, + "223": -40.10000000000003, + "224": 5.250000000000007, + "225": -11.449999999999992, + "226": -0.39999999999999414, + "227": -65.19999999999999, + "228": -34.400000000000006, + "229": -5.9499999999999895, + "230": -19.349999999999966, + "231": 32.99999999999977, + "232": 6.500000000000082, + "233": -1.399999999999994, + "234": -46.099999999999966, + "235": 51.249999999999815, + "236": -68.25000000000001, + "237": -74.30000000000001, + "238": -4.049999999999976, + "239": -82.25, + "240": -28.799999999999937, + "241": 5.90000000000005, + "242": -1.949999999999961, + "243": -80.85, + "244": -12.649999999999988, + "245": -1.5999999999999868, + "246": -53.999999999999986, + "247": -65.85, + "248": -25.799999999999994, + "249": 0.8000000000000482, + "250": 8.250000000000032, + "251": 8.55000000000004, + "252": 7.000000000000038, + "253": -30.549999999999972, + "254": -49.400000000000034, + "255": 2.2000000000000446, + "256": 2.550000000000025, + "257": -17.399999999999984, + "258": -71.35, + "259": 13.550000000000004, + "260": -80.0, + "261": -10.74999999999999, + "262": 27.84999999999992, + "263": -10.95, + "264": -57.65000000000002, + "265": 25.99999999999989, + "266": 31.899999999999963, + "267": 2.4000000000000163, + "268": -71.5, + "269": -63.45000000000001, + "270": 78.64999999999993, + "271": -78.9, + "272": -13.149999999999956, + "273": -17.599999999999973, + "274": -14.24999999999999, + "275": -0.19999999999996576, + "276": -34.44999999999999, + "277": -1.999999999999969, + "278": -16.700000000000017, + "279": -55.699999999999996, + "280": -63.64999999999999, + "281": -0.04999999999998295, + "282": -35.45, + "283": -31.89999999999997, + "284": -69.44999999999997, + "285": -78.5, + "286": -1.1000000000000014, + "287": -74.20000000000002, + "288": -78.35000000000002, + "289": -81.80000000000001, + "290": -32.50000000000001, + "291": 8.750000000000028, + "292": -22.49999999999997, + "293": 6.8500000000000005, + "294": -91.6, + "295": 36.099999999999795, + "296": -81.25, + "297": 5.149999999999975, + "298": 7.249999999999992, + "299": -10.149999999999983, + "300": -68.54999999999993, + "301": -61.24999999999994, + "302": -13.749999999999988, + "303": -66.64999999999995, + "304": -72.1, + "305": -53.400000000000034, + "306": -41.95000000000002, + "307": 22.650000000000034, + "308": -78.69999999999999, + "309": -62.0, + "310": -72.04999999999997, + "311": -60.74999999999993, + "312": -77.45, + "313": -51.69999999999997, + "314": -78.50000000000001, + "315": -44.65000000000001, + "316": 15.80000000000004, + "317": 39.44999999999979, + "318": -43.999999999999964, + "319": -48.29999999999999, + "320": 42.99999999999991, + "321": -23.049999999999983, + "322": -4.899999999999984, + "323": 34.099999999999795, + "324": -62.24999999999992, + "325": -76.95, + "326": 7.3000000000000504, + "327": -101.30000000000013, + "328": -16.95000000000004, + "329": -50.199999999999996, + "330": -41.8, + "331": -60.84999999999993, + "332": 13.500000000000007, + "333": -53.04999999999998, + "334": 0.7500000000000511, + "335": 60.79999999999987, + "336": 6.50000000000005, + "337": 8.10000000000003, + "338": -63.7, + "339": -22.79999999999995, + "340": -82.69999999999999, + "341": -39.10000000000001, + "342": 39.599999999999795, + "343": -32.35000000000003, + "344": -65.24999999999994, + "345": 85.15000000000003, + "346": 18.34999999999998, + "347": -86.14999999999999, + "348": 30.99999999999976, + "349": -79.75, + "350": 43.44999999999984, + "351": -78.65000000000003, + "352": 34.799999999999834, + "353": -4.249999999999974, + "354": -39.35, + "355": -75.14999999999999, + "356": -67.94999999999999, + "357": -64.94999999999996, + "358": -54.19999999999996, + "359": -68.19999999999996, + "360": -38.10000000000001, + "361": 10.249999999999986, + "362": -2.0999999999999925, + "363": -10.299999999999955, + "364": -70.75, + "365": -59.25000000000002, + "366": -46.25000000000003, + "367": -61.64999999999998, + "368": 5.250000000000063, + "369": -24.54999999999994, + "370": -32.00000000000002, + "371": 25.10000000000001, + "372": -92.89999999999998, + "373": 26.450000000000102, + "374": -49.60000000000004, + "375": 13.300000000000011, + "376": -17.49999999999998, + "377": 7.600000000000042, + "378": -66.69999999999993, + "379": -25.049999999999994, + "380": -64.74999999999997, + "381": -64.34999999999998, + "382": -38.20000000000001, + "383": 59.04999999999991, + "384": 0.6000000000000636, + "385": 21.85000000000011, + "386": 14.049999999999986, + "387": -28.49999999999998, + "388": -65.89999999999996, + "389": 31.79999999999977, + "390": -54.74999999999997, + "391": -58.699999999999946, + "392": -73.99999999999999, + "393": 7.249999999999879, + "394": -62.55000000000001, + "395": -64.75000000000003, + "396": -64.69999999999992, + "397": -72.95, + "398": -57.300000000000026, + "399": 17.350000000000023, + "400": -77.60000000000005, + "401": 49.599999999999916, + "402": -78.75000000000009, + "403": -32.750000000000036, + "404": -13.849999999999985, + "405": -57.54999999999998, + "406": -67.64999999999996, + "407": -14.549999999999986, + "408": -38.69999999999999, + "409": -42.34999999999999, + "410": -75.05000000000001, + "411": -73.25000000000001, + "412": 36.849999999999795, + "413": -43.14999999999998, + "414": 50.84999999999989, + "415": -64.3999999999999, + "416": -17.599999999999984, + "417": -3.6999999999999673, + "418": -65.64999999999998, + "419": -11.450000000000015, + "420": -57.24999999999999, + "421": -65.54999999999995, + "422": -59.34999999999998, + "423": -64.79999999999997, + "424": -8.000000000000071, + "425": -12.900000000000041, + "426": -18.499999999999975, + "427": -24.499999999999975, + "428": -55.39999999999993, + "429": -30.89999999999997, + "430": -28.44999999999996, + "431": -12.949999999999976, + "432": -65.84999999999995, + "433": -50.99999999999996, + "434": -19.099999999999973, + "435": -68.4, + "436": -60.800000000000004, + "437": -3.9499999999999735, + "438": -10.999999999999922, + "439": -62.49999999999996, + "440": -57.299999999999976, + "441": -61.749999999999936, + "442": -46.04999999999999, + "443": -67.99999999999994, + "444": -62.64999999999992, + "445": 25.599999999999856, + "446": -55.09999999999995, + "447": -68.19999999999993, + "448": 3.499999999999991, + "449": -34.300000000000004, + "450": -29.700000000000006, + "451": 34.25000000000014, + "452": -12.100000000000064, + "453": -42.04999999999998, + "454": -29.10000000000001, + "455": -23.09999999999998, + "456": -26.79999999999995, + "457": -20.24999999999999, + "458": -52.2, + "459": -72.34999999999995, + "460": -65.34999999999991, + "461": -55.79999999999996, + "462": -41.65000000000001, + "463": -33.04999999999998, + "464": 22.450000000000095, + "465": 95.45000000000006, + "466": 41.64999999999985, + "467": -50.09999999999998, + "468": 6.800000000000042, + "469": 37.55, + "470": -13.75000000000001, + "471": 10.600000000000062, + "472": -62.24999999999993, + "473": -16.699999999999964, + "474": -56.79999999999995, + "475": -27.550000000000004, + "476": 84.70000000000005, + "477": -59.54999999999993, + "478": -74.04999999999998, + "479": -74.99999999999987, + "480": 28.04999999999995, + "481": -25.19999999999999, + "482": -96.60000000000001, + "483": -20.700000000000014, + "484": 77.85000000000011, + "485": -50.849999999999945, + "486": 28.700000000000042, + "487": -58.649999999999935, + "488": -35.949999999999974, + "489": -24.349999999999934, + "490": -54.0, + "491": -14.149999999999975, + "492": -44.8499999999999, + "493": -66.24999999999991, + "494": -10.149999999999965, + "495": 1.499999999999983, + "496": 26.350000000000072, + "497": -57.949999999999974, + "498": -54.799999999999955, + "499": 33.00000000000012, + "500": 7.250000000000083, + "501": 74.15000000000002, + "502": 21.699999999999974, + "503": -73.0999999999999, + "504": -15.34999999999998, + "505": 70.99999999999991, + "506": -66.84999999999991, + "507": -61.649999999999935, + "508": -58.94999999999998, + "509": -41.09999999999997, + "510": -26.349999999999994, + "511": -65.6999999999999, + "512": 44.44999999999985, + "513": -70.19999999999989, + "514": 65.74999999999986, + "515": -50.59999999999994, + "516": -66.84999999999997, + "517": -64.04999999999998, + "518": -25.099999999999998, + "519": -59.99999999999991, + "520": -67.19999999999997, + "521": -21.99999999999999, + "522": -49.89999999999998, + "523": -27.19999999999997, + "524": -51.04999999999998, + "525": -30.54999999999997, + "526": -38.25, + "527": 67.24999999999982, + "528": -63.899999999999935, + "529": -62.44999999999999, + "530": -36.05000000000003, + "531": -67.59999999999995, + "532": -67.89999999999992, + "533": -64.14999999999999, + "534": -22.099999999999984, + "535": -62.64999999999992, + "536": -52.49999999999997, + "537": -76.15000000000006, + "538": -15.750000000000046, + "539": -59.89999999999995, + "540": -1.1999999999999718, + "541": -63.65000000000005, + "542": -65.94999999999992, + "543": -5.55000000000003, + "544": -59.99999999999997, + "545": -66.7499999999999, + "546": 15.80000000000001, + "547": 87.35000000000007, + "548": -72.19999999999989, + "549": -64.09999999999992, + "550": -52.69999999999996, + "551": -9.40000000000001, + "552": -19.750000000000057, + "553": -62.94999999999993, + "554": -60.09999999999995, + "555": -39.7, + "556": 27.00000000000004, + "557": -52.04999999999994, + "558": -30.59999999999998, + "559": -86.75, + "560": -51.39999999999996, + "561": -61.20000000000003, + "562": 0.5499999999999772, + "563": -37.59999999999999, + "564": -18.650000000000027, + "565": -58.349999999999945, + "566": 55.750000000000014, + "567": -15.649999999999968, + "568": -27.250000000000007, + "569": -47.499999999999986, + "570": 100.40000000000032, + "571": -43.05, + "572": -62.24999999999993, + "573": 28.100000000000087, + "574": -65.99999999999996, + "575": 28.39999999999995, + "576": -2.0499999999999177, + "577": -58.399999999999935, + "578": -57.19999999999993, + "579": -24.6, + "580": -63.69999999999992, + "581": -4.249999999999938, + "582": 13.300000000000011, + "583": -51.749999999999964, + "584": -49.64999999999997, + "585": 50.100000000000136, + "586": 82.85000000000016, + "587": -34.00000000000001, + "588": -26.950000000000024, + "589": 102.25000000000016, + "590": -33.900000000000034, + "591": -1.549999999999984, + "592": -61.99999999999995, + "593": -56.95, + "594": 14.499999999999964, + "595": -66.7499999999999, + "596": 52.29999999999995, + "597": -50.99999999999997, + "598": 88.75, + "599": -23.750000000000014, + "600": 68.1499999999999, + "601": -47.39999999999999, + "602": -68.29999999999997, + "603": 62.750000000000156, + "604": -65.84999999999991, + "605": -3.4000000000000314, + "606": -23.75000000000003, + "607": 3.1499999999999764, + "608": -52.29999999999997, + "609": -13.599999999999982, + "610": -51.59999999999997, + "611": -37.8, + "612": -19.049999999999997, + "613": -55.84999999999996, + "614": -7.299999999999946, + "615": -79.05000000000001, + "616": 29.05000000000002, + "617": 6.500000000000016, + "618": -26.70000000000005, + "619": 79.24999999999993, + "620": -34.80000000000003, + "621": 47.85000000000002, + "622": 32.150000000000006, + "623": 88.59999999999998, + "624": -19.449999999999946, + "625": 49.79999999999995, + "626": 15.09999999999998, + "627": 38.949999999999996, + "628": 19.950000000000063, + "629": -12.799999999999974, + "630": 10.050000000000054, + "631": 67.65000000000006, + "632": -1.949999999999986, + "633": 21.60000000000012, + "634": 92.8000000000001, + "635": 25.64999999999996, + "636": 73.35000000000002, + "637": 3.9999999999999902, + "638": 42.89999999999986, + "639": -49.499999999999964, + "640": -18.200000000000045, + "641": 63.14999999999993, + "642": -16.550000000000015, + "643": 64.95000000000006, + "644": -41.099999999999966, + "645": 24.10000000000002, + "646": 34.84999999999997, + "647": 42.84999999999999, + "648": 62.49999999999998, + "649": 45.649999999999906, + "650": 72.89999999999999, + "651": 32.30000000000004, + "652": -26.04999999999997, + "653": 68.10000000000008, + "654": 70.24999999999977, + "655": 90.7000000000001, + "656": 88.55000000000003, + "657": 24.04999999999996, + "658": 57.899999999999956, + "659": 33.700000000000045, + "660": 59.29999999999987, + "661": 101.15000000000005, + "662": 41.899999999999935, + "663": 36.14999999999995, + "664": -51.19999999999997, + "665": 81.8500000000001, + "666": 88.4000000000001, + "667": 61.69999999999994, + "668": 19.299999999999983, + "669": 70.64999999999995, + "670": 46.49999999999997, + "671": 40.69999999999995, + "672": -27.34999999999998, + "673": 107.95, + "674": 44.99999999999982, + "675": 11.40000000000002, + "676": 90.50000000000016, + "677": 38.84999999999977, + "678": 56.59999999999999, + "679": 93.05000000000005, + "680": 57.399999999999956, + "681": 41.05000000000001, + "682": 90.94999999999996, + "683": 64.69999999999997, + "684": -54.09999999999994, + "685": 101.75000000000003, + "686": 53.74999999999992, + "687": 100.40000000000002, + "688": 35.8999999999998, + "689": 47.50000000000003, + "690": 32.59999999999986, + "691": 42.99999999999987, + "692": 76.00000000000003, + "693": -5.800000000000042, + "694": 3.199999999999882, + "695": 24.200000000000006, + "696": 43.40000000000005, + "697": 91.05000000000003, + "698": 84.25000000000014, + "699": 37.04999999999994, + "700": 30.149999999999984, + "701": 94.55000000000007, + "702": 94.60000000000008, + "703": 24.45, + "704": 30.49999999999995, + "705": -24.300000000000033, + "706": 82.0, + "707": 55.3499999999999, + "708": 76.55000000000014, + "709": 40.09999999999989, + "710": -10.999999999999964, + "711": 75.35000000000007, + "712": 62.09999999999993, + "713": 82.65000000000018, + "714": 8.700000000000028, + "715": 87.75000000000017, + "716": 84.55000000000001, + "717": 12.949999999999957, + "718": 73.14999999999998, + "719": 50.79999999999997, + "720": 60.599999999999994, + "721": 91.55000000000008, + "722": 93.15000000000006, + "723": 42.74999999999994, + "724": 77.49999999999997, + "725": 86.10000000000015, + "726": 69.45000000000003, + "727": 63.299999999999926, + "728": 86.40000000000002, + "729": 78.8000000000001, + "730": 92.50000000000004, + "731": 75.10000000000008, + "732": 50.99999999999997, + "733": 91.25000000000016, + "734": 85.25000000000003, + "735": 93.4500000000001, + "736": 65.05, + "737": 76.20000000000003, + "738": 57.95000000000003, + "739": 48.85, + "740": 66.79999999999995, + "741": 66.65000000000003, + "742": 76.25000000000011, + "743": 73.75000000000004, + "744": 76.15000000000006, + "745": 5.349999999999966, + "746": 45.9500000000001, + "747": 72.94999999999999, + "748": 104.60000000000005, + "749": 78.95000000000002, + "750": 67.34999999999998, + "751": 32.549999999999926, + "752": 48.449999999999825, + "753": 84.25000000000004, + "754": 53.54999999999998, + "755": 79.40000000000008, + "756": 103.10000000000002, + "757": 83.95, + "758": 92.45000000000007, + "759": 100.00000000000006, + "760": 85.30000000000001, + "761": -24.09999999999999, + "762": 53.000000000000014, + "763": 42.849999999999916, + "764": 85.10000000000004, + "765": 72.24999999999999, + "766": -34.849999999999994, + "767": 61.199999999999974, + "768": 90.8000000000001, + "769": 61.14999999999984, + "770": 81.09999999999995, + "771": 53.55000000000009, + "772": 60.849999999999895, + "773": 63.05000000000005, + "774": 53.400000000000055, + "775": 77.84999999999997, + "776": 94.4, + "777": 66.94999999999996, + "778": 67.34999999999995, + "779": 52.44999999999994, + "780": 101.69999999999999, + "781": 78.05000000000007, + "782": 46.29999999999982, + "783": 100.85000000000001, + "784": 73.85000000000004, + "785": 53.25, + "786": 84.30000000000004, + "787": 76.89999999999998, + "788": 77.30000000000008, + "789": 68.04999999999997, + "790": 80.60000000000011, + "791": 86.50000000000018, + "792": 52.39999999999994, + "793": 95.65000000000006, + "794": 88.14999999999999, + "795": 87.40000000000009, + "796": 56.29999999999998, + "797": 93.30000000000011, + "798": 85.0500000000001, + "799": 85.30000000000011, + "800": 72.05000000000001, + "801": 69.79999999999998, + "802": 76.30000000000007, + "803": 56.150000000000006, + "804": 65.74999999999997, + "805": 73.30000000000004, + "806": 76.89999999999998, + "807": 86.79999999999986, + "808": 84.99999999999997, + "809": 76.80000000000005, + "810": 86.0, + "811": 62.39999999999998, + "812": 88.30000000000003, + "813": 91.55000000000001, + "814": 75.59999999999994, + "815": 76.5, + "816": 65.29999999999993, + "817": 29.899999999999984, + "818": 77.55000000000007, + "819": 90.95000000000009, + "820": 73.30000000000004, + "821": 58.69999999999999, + "822": 87.59999999999997, + "823": 89.94999999999996, + "824": 68.29999999999997, + "825": 91.89999999999998, + "826": 74.89999999999995, + "827": 71.24999999999996, + "828": 70.69999999999999, + "829": 93.04999999999998, + "830": 88.30000000000003, + "831": 102.65000000000009, + "832": 23.799999999999955, + "833": 96.55000000000001, + "834": 89.35000000000002, + "835": 74.05000000000005, + "836": 90.3000000000001, + "837": 75.65, + "838": 81.5, + "839": 29.04999999999999, + "840": 78.9, + "841": 61.69999999999996, + "842": 46.19999999999999, + "843": 65.54999999999998, + "844": 60.95000000000003, + "845": 100.65000000000019, + "846": 73.50000000000003, + "847": 96.75000000000001, + "848": 57.24999999999997, + "849": 64.3, + "850": 59.04999999999996, + "851": 103.14999999999996, + "852": 86.50000000000007, + "853": 63.150000000000034, + "854": 67.30000000000008, + "855": 69.74999999999997, + "856": 89.69999999999999, + "857": 73.50000000000014, + "858": 58.80000000000003, + "859": 93.35000000000008, + "860": 98.75000000000001, + "861": 80.49999999999999, + "862": 78.50000000000006, + "863": 68.25000000000003, + "864": 102.9000000000001, + "865": 94.05000000000001, + "866": 46.65000000000003, + "867": 96.39999999999999, + "868": 100.6000000000001, + "869": 48.44999999999997, + "870": 88.05, + "871": 68.70000000000006, + "872": 75.75000000000001, + "873": 100.00000000000003, + "874": 102.50000000000006, + "875": 85.50000000000001, + "876": 21.64999999999999, + "877": 59.69999999999999, + "878": 70.19999999999999, + "879": 85.15000000000003, + "880": 88.3, + "881": 70.00000000000009, + "882": 92.64999999999999, + "883": 96.00000000000004, + "884": 86.60000000000002, + "885": 70.70000000000005, + "886": 53.69999999999994, + "887": 104.5, + "888": 63.85000000000002, + "889": 86.85000000000004, + "890": 81.45000000000003, + "891": 73.30000000000003, + "892": 94.95000000000003, + "893": 42.05, + "894": 93.99999999999999, + "895": 94.80000000000018, + "896": 91.7, + "897": 62.349999999999945, + "898": 66.35000000000001, + "899": 86.85000000000002, + "900": 37.30000000000004, + "901": 74.94999999999997, + "902": 92.05000000000008, + "903": 92.34999999999998, + "904": 61.80000000000008, + "905": 85.14999999999999, + "906": 84.49999999999993, + "907": 66.30000000000004, + "908": 88.05000000000001, + "909": 81.55000000000001, + "910": 99.35000000000001, + "911": 71.79999999999994, + "912": 87.15, + "913": 82.39999999999998, + "914": 38.949999999999974, + "915": 91.35000000000002, + "916": 69.45000000000007, + "917": 73.04999999999998, + "918": 72.00000000000003, + "919": 62.45000000000006, + "920": 46.89999999999998, + "921": 66.95000000000003, + "922": 77.94999999999997, + "923": 84.45, + "924": 75.39999999999998, + "925": 91.70000000000013, + "926": 80.74999999999997, + "927": 77.20000000000005, + "928": 79.20000000000005, + "929": 59.05, + "930": 66.14999999999992, + "931": 47.899999999999935, + "932": 89.64999999999996, + "933": 78.3499999999999, + "934": 91.60000000000005, + "935": 70.89999999999996, + "936": 85.45, + "937": 84.65000000000003, + "938": 82.84999999999997, + "939": 102.19999999999999, + "940": 53.80000000000001, + "941": 50.199999999999974, + "942": 72.7, + "943": 63.90000000000002, + "944": 80.15000000000003, + "945": 92.15, + "946": 13.999999999999988, + "947": 62.400000000000034, + "948": 73.60000000000005, + "949": 56.29999999999998, + "950": 84.25000000000003, + "951": 80.85000000000001, + "952": 84.45000000000005, + "953": 86.70000000000002, + "954": 87.04999999999995, + "955": 30.700000000000024, + "956": 82.05, + "957": 78.55000000000007, + "958": 83.95000000000006, + "959": 57.44999999999996, + "960": 83.45000000000006, + "961": 72.25000000000001, + "962": 73.05000000000001, + "963": 79.30000000000001, + "964": 81.55, + "965": 69.99999999999997, + "966": 67.20000000000003, + "967": 92.80000000000003, + "968": 72.10000000000002, + "969": 48.64999999999985, + "970": 71.94999999999997, + "971": 15.949999999999934, + "972": 61.44999999999984, + "973": 90.85000000000004, + "974": 96.55000000000003, + "975": 78.15000000000003, + "976": 84.40000000000009, + "977": 84.75000000000003, + "978": 52.95000000000003, + "979": 84.84999999999995, + "980": 52.20000000000008, + "981": 67.1, + "982": 84.00000000000001, + "983": 87.8500000000001, + "984": 76.8000000000001, + "985": 91.4499999999999, + "986": 80.74999999999999, + "987": 83.09999999999998, + "988": 92.9000000000001, + "989": 63.34999999999997, + "990": 66.49999999999997, + "991": 96.65000000000002, + "992": 101.85000000000002, + "993": 84.79999999999993, + "994": 91.65000000000003, + "995": 77.25000000000009, + "996": 64.0, + "997": 59.04999999999998, + "998": 72.10000000000002, + "999": 85.40000000000005, + "1000": 38.94999999999991 + } +} diff --git a/benchmark/results/v3/v3.3.0/session_metadata/2.json b/benchmark/results/v3/v3.3.0/session_metadata/2.json new file mode 100644 index 00000000..bc5243d2 --- /dev/null +++ b/benchmark/results/v3/v3.3.0/session_metadata/2.json @@ -0,0 +1,1009 @@ +{ + "total_episodes": 1001, + "total_time_steps": 128000, + "total_s": 1437.777365, + "s_per_step": 0.044930542656249996, + "s_per_100_steps_10_nodes": 4.493054265625, + "total_reward_per_episode": { + "1": -11.099999999999989, + "2": -32.05000000000004, + "3": -58.200000000000095, + "4": -8.599999999999987, + "5": -81.89999999999999, + "6": -63.89999999999999, + "7": 2.050000000000006, + "8": -25.199999999999996, + "9": -21.249999999999957, + "10": -31.65000000000001, + "11": -65.34999999999998, + "12": -19.39999999999996, + "13": -81.10000000000001, + "14": -12.099999999999989, + "15": -27.799999999999933, + "16": -97.5, + "17": -13.399999999999984, + "18": -66.80000000000008, + "19": -18.29999999999997, + "20": -11.59999999999998, + "21": -71.05000000000005, + "22": -15.149999999999983, + "23": -18.54999999999997, + "24": -51.90000000000001, + "25": -19.54999999999996, + "26": -64.8500000000001, + "27": -78.94999999999996, + "28": -24.649999999999935, + "29": -63.800000000000104, + "30": -15.949999999999978, + "31": -0.24999999999996536, + "32": -7.449999999999997, + "33": -13.29999999999999, + "34": -18.64999999999997, + "35": -22.499999999999954, + "36": -36.20000000000002, + "37": -15.999999999999979, + "38": -23.04999999999995, + "39": -20.099999999999966, + "40": -14.149999999999977, + "41": -13.849999999999989, + "42": -20.69999999999996, + "43": -15.349999999999977, + "44": -11.650000000000004, + "45": -19.74999999999996, + "46": -11.849999999999993, + "47": -10.049999999999992, + "48": -78.7999999999999, + "49": -23.29999999999995, + "50": -16.199999999999967, + "51": -22.399999999999952, + "52": -12.14999999999999, + "53": -20.849999999999962, + "54": -46.05000000000007, + "55": -19.199999999999964, + "56": -17.249999999999975, + "57": -19.649999999999963, + "58": -10.7, + "59": -41.35000000000013, + "60": -35.35000000000003, + "61": -19.899999999999963, + "62": -18.949999999999964, + "63": -1.7999999999999938, + "64": -18.19999999999997, + "65": -7.05000000000001, + "66": -5.949999999999997, + "67": -2.649999999999964, + "68": -14.55, + "69": -33.54999999999995, + "70": -104.4, + "71": -62.25000000000008, + "72": -14.099999999999989, + "73": -8.1, + "74": -15.299999999999986, + "75": -7.699999999999998, + "76": -14.649999999999984, + "77": 0.20000000000002705, + "78": -4.3999999999999995, + "79": -8.850000000000009, + "80": -1.399999999999962, + "81": -94.55, + "82": -26.200000000000006, + "83": -5.899999999999989, + "84": -10.299999999999997, + "85": 14.200000000000006, + "86": -67.09999999999997, + "87": -23.849999999999948, + "88": -19.74999999999996, + "89": -19.899999999999963, + "90": 2.4999999999999614, + "91": -15.899999999999983, + "92": -21.899999999999956, + "93": 8.400000000000063, + "94": -47.25000000000005, + "95": -11.949999999999987, + "96": -3.649999999999981, + "97": 3.550000000000037, + "98": -10.849999999999996, + "99": -17.74999999999997, + "100": -17.89999999999997, + "101": -6.999999999999993, + "102": -14.49999999999999, + "103": -31.800000000000008, + "104": -21.199999999999957, + "105": -14.39999999999998, + "106": -5.749999999999986, + "107": -2.4499999999999744, + "108": -25.14999999999999, + "109": 2.0000000000000373, + "110": -28.29999999999995, + "111": -14.14999999999999, + "112": -83.15, + "113": -1.4000000000000008, + "114": -2.14999999999997, + "115": -49.30000000000006, + "116": -9.449999999999987, + "117": 9.500000000000043, + "118": 13.65000000000003, + "119": -5.350000000000008, + "120": -10.849999999999982, + "121": -6.64999999999998, + "122": -18.74999999999999, + "123": 0.9500000000000433, + "124": -7.499999999999983, + "125": -18.09999999999997, + "126": -22.499999999999975, + "127": 7.350000000000016, + "128": 6.75000000000004, + "129": 14.700000000000049, + "130": 16.899999999999995, + "131": -52.54999999999996, + "132": -89.35, + "133": -17.34999999999997, + "134": 33.99999999999979, + "135": 8.900000000000048, + "136": 0.5499999999999989, + "137": -11.349999999999998, + "138": 14.650000000000023, + "139": -5.5000000000000036, + "140": -9.500000000000004, + "141": -40.30000000000011, + "142": 20.849999999999973, + "143": -2.049999999999981, + "144": 39.899999999999736, + "145": -87.6, + "146": 37.74999999999996, + "147": 14.400000000000034, + "148": 9.50000000000004, + "149": 10.95000000000001, + "150": -2.549999999999983, + "151": -0.8499999999999777, + "152": -8.100000000000007, + "153": 23.649999999999913, + "154": -4.599999999999987, + "155": -70.4, + "156": 39.649999999999956, + "157": -80.55000000000001, + "158": 8.550000000000013, + "159": 41.49999999999975, + "160": -28.099999999999977, + "161": -17.599999999999973, + "162": 42.6499999999998, + "163": -36.35, + "164": -41.80000000000015, + "165": -4.450000000000005, + "166": 6.150000000000029, + "167": 23.65000000000003, + "168": 36.250000000000014, + "169": 16.650000000000045, + "170": 15.200000000000077, + "171": 33.14999999999989, + "172": 63.14999999999998, + "173": -10.550000000000002, + "174": -23.29999999999995, + "175": 16.749999999999893, + "176": -74.75000000000003, + "177": 31.75000000000006, + "178": 14.049999999999985, + "179": 61.94999999999975, + "180": 20.499999999999943, + "181": 39.69999999999988, + "182": 6.100000000000064, + "183": -7.250000000000073, + "184": -6.950000000000007, + "185": -69.10000000000001, + "186": 29.150000000000073, + "187": -76.60000000000002, + "188": 45.299999999999905, + "189": -50.74999999999996, + "190": -48.10000000000014, + "191": 43.74999999999974, + "192": 10.300000000000058, + "193": 54.9499999999999, + "194": 38.9, + "195": 8.150000000000055, + "196": 7.00000000000001, + "197": 44.59999999999982, + "198": 72.34999999999984, + "199": -47.9, + "200": -49.45000000000003, + "201": 46.29999999999976, + "202": -46.099999999999994, + "203": -2.8499999999999783, + "204": -9.450000000000045, + "205": 2.600000000000059, + "206": 31.550000000000026, + "207": -65.50000000000004, + "208": 55.29999999999975, + "209": -39.000000000000014, + "210": 72.29999999999986, + "211": 64.34999999999982, + "212": 42.44999999999975, + "213": 13.750000000000027, + "214": -21.300000000000033, + "215": 21.99999999999996, + "216": -12.299999999999985, + "217": 12.149999999999999, + "218": -67.85000000000007, + "219": 9.400000000000013, + "220": 50.24999999999981, + "221": -7.300000000000061, + "222": 99.84999999999995, + "223": 53.84999999999977, + "224": 27.100000000000044, + "225": -50.69999999999999, + "226": 17.0, + "227": 62.94999999999987, + "228": 61.14999999999988, + "229": -33.30000000000001, + "230": -56.850000000000016, + "231": 7.650000000000029, + "232": 75.10000000000004, + "233": 10.900000000000059, + "234": 41.2, + "235": 20.80000000000005, + "236": 65.34999999999977, + "237": 92.25000000000003, + "238": -34.149999999999984, + "239": 55.24999999999974, + "240": 24.999999999999925, + "241": 50.74999999999988, + "242": 59.09999999999977, + "243": -67.4, + "244": 66.49999999999979, + "245": 10.300000000000068, + "246": 69.09999999999977, + "247": 98.94999999999973, + "248": -65.04999999999994, + "249": -79.70000000000003, + "250": 104.89999999999986, + "251": 71.6999999999999, + "252": 87.69999999999986, + "253": -50.74999999999998, + "254": -56.29999999999996, + "255": 91.54999999999977, + "256": 48.89999999999978, + "257": 34.55000000000007, + "258": 20.249999999999936, + "259": 68.99999999999976, + "260": 46.94999999999985, + "261": 68.79999999999974, + "262": 38.44999999999996, + "263": 84.59999999999978, + "264": -74.45000000000006, + "265": 44.349999999999866, + "266": 70.54999999999973, + "267": 29.450000000000028, + "268": 83.2499999999998, + "269": 3.2499999999999343, + "270": -70.75, + "271": -6.750000000000014, + "272": 66.94999999999982, + "273": 59.899999999999835, + "274": 83.99999999999979, + "275": -65.85000000000002, + "276": 40.94999999999991, + "277": 81.09999999999972, + "278": 7.450000000000036, + "279": 28.399999999999917, + "280": -59.750000000000064, + "281": 58.849999999999795, + "282": 68.55, + "283": 22.59999999999995, + "284": 0.4000000000000341, + "285": -93.34999999999998, + "286": -1.4999999999999711, + "287": 22.900000000000023, + "288": 11.349999999999937, + "289": 79.09999999999978, + "290": 91.54999999999995, + "291": 58.19999999999978, + "292": 36.849999999999994, + "293": 78.14999999999985, + "294": 15.799999999999962, + "295": 58.599999999999774, + "296": 7.149999999999958, + "297": 39.99999999999988, + "298": 34.80000000000001, + "299": 86.79999999999978, + "300": 54.09999999999981, + "301": 91.29999999999973, + "302": 61.09999999999973, + "303": -11.449999999999982, + "304": 75.79999999999986, + "305": 33.04999999999986, + "306": -17.800000000000043, + "307": 89.59999999999977, + "308": 68.39999999999988, + "309": -55.85000000000005, + "310": 69.39999999999975, + "311": 88.19999999999987, + "312": 57.09999999999975, + "313": 23.20000000000005, + "314": 94.19999999999975, + "315": 91.14999999999979, + "316": 33.54999999999974, + "317": 94.79999999999976, + "318": 98.44999999999976, + "319": 53.449999999999726, + "320": 81.09999999999974, + "321": -12.450000000000038, + "322": 95.29999999999973, + "323": -6.65000000000002, + "324": 88.59999999999977, + "325": 101.29999999999987, + "326": 107.70000000000005, + "327": 101.29999999999995, + "328": 102.39999999999979, + "329": 15.799999999999955, + "330": 38.05000000000004, + "331": 67.99999999999989, + "332": 74.34999999999978, + "333": -19.399999999999967, + "334": 96.14999999999979, + "335": -5.7500000000000036, + "336": -9.849999999999985, + "337": 87.19999999999975, + "338": 97.5499999999998, + "339": 27.20000000000004, + "340": 43.799999999999976, + "341": 92.39999999999976, + "342": 92.79999999999976, + "343": 90.64999999999978, + "344": 90.10000000000004, + "345": 20.650000000000023, + "346": 96.4999999999999, + "347": -85.49999999999997, + "348": 38.299999999999955, + "349": 99.84999999999987, + "350": 93.09999999999981, + "351": 59.24999999999997, + "352": 66.74999999999984, + "353": 89.54999999999976, + "354": 60.39999999999989, + "355": 13.699999999999973, + "356": 99.69999999999985, + "357": 25.949999999999886, + "358": 79.24999999999976, + "359": -9.149999999999986, + "360": 94.29999999999974, + "361": 103.09999999999992, + "362": 99.3499999999999, + "363": 95.34999999999974, + "364": -43.89999999999998, + "365": 103.39999999999976, + "366": 102.34999999999978, + "367": 106.49999999999972, + "368": 101.34999999999975, + "369": 103.99999999999991, + "370": -72.85000000000001, + "371": 86.74999999999973, + "372": -9.499999999999982, + "373": 97.89999999999976, + "374": 100.44999999999976, + "375": -84.24999999999997, + "376": 101.34999999999975, + "377": 78.59999999999984, + "378": 100.59999999999978, + "379": -28.049999999999972, + "380": 9.80000000000001, + "381": 104.94999999999978, + "382": 102.79999999999977, + "383": 93.59999999999994, + "384": 64.25000000000009, + "385": 77.09999999999992, + "386": 92.24999999999979, + "387": 98.04999999999973, + "388": -76.55, + "389": 94.49999999999979, + "390": 89.34999999999977, + "391": 26.050000000000026, + "392": 27.999999999999975, + "393": -24.05000000000006, + "394": 106.04999999999976, + "395": 105.99999999999974, + "396": 102.39999999999975, + "397": 59.6499999999999, + "398": 97.99999999999977, + "399": 101.49999999999973, + "400": -18.7, + "401": 105.6000000000001, + "402": 95.99999999999977, + "403": 103.69999999999982, + "404": 90.14999999999976, + "405": 96.34999999999987, + "406": 87.99999999999982, + "407": 93.29999999999971, + "408": 98.89999999999986, + "409": 104.39999999999972, + "410": 97.2499999999999, + "411": 100.04999999999974, + "412": 86.5499999999998, + "413": -60.74999999999996, + "414": 99.79999999999977, + "415": 82.84999999999978, + "416": 101.19999999999975, + "417": 1.5000000000000357, + "418": 102.49999999999976, + "419": 65.89999999999989, + "420": 103.89999999999974, + "421": 96.84999999999974, + "422": 101.39999999999976, + "423": 102.14999999999976, + "424": 101.99999999999972, + "425": 101.74999999999976, + "426": 102.39999999999976, + "427": 106.99999999999977, + "428": -73.45, + "429": 104.14999999999974, + "430": 100.09999999999984, + "431": 102.49999999999979, + "432": 101.19999999999975, + "433": 101.24999999999973, + "434": 102.44999999999976, + "435": 102.59999999999977, + "436": 98.84999999999977, + "437": 85.04999999999976, + "438": -77.7, + "439": -89.75, + "440": 33.79999999999973, + "441": 94.74999999999979, + "442": 99.84999999999975, + "443": 99.64999999999978, + "444": 103.6, + "445": 101.64999999999976, + "446": 52.39999999999978, + "447": 100.19999999999976, + "448": 80.09999999999977, + "449": 103.1999999999998, + "450": 97.54999999999977, + "451": 87.94999999999976, + "452": 103.49999999999974, + "453": 75.04999999999977, + "454": 94.59999999999977, + "455": 84.64999999999982, + "456": 99.49999999999977, + "457": -0.9500000000000004, + "458": 82.89999999999972, + "459": 103.79999999999977, + "460": 102.39999999999974, + "461": 106.64999999999976, + "462": 95.24999999999979, + "463": 97.79999999999977, + "464": 84.49999999999982, + "465": -11.800000000000033, + "466": 101.04999999999978, + "467": 106.29999999999974, + "468": 18.449999999999903, + "469": 105.19999999999975, + "470": 105.59999999999972, + "471": 82.29999999999977, + "472": 103.44999999999976, + "473": 104.19999999999978, + "474": 104.94999999999975, + "475": 106.19999999999972, + "476": 101.19999999999976, + "477": 106.44999999999973, + "478": -66.74999999999999, + "479": 98.14999999999978, + "480": 102.29999999999976, + "481": 102.44999999999976, + "482": 85.79999999999973, + "483": -77.75, + "484": 95.94999999999976, + "485": 101.19999999999976, + "486": 97.69999999999975, + "487": 104.59999999999975, + "488": 102.24999999999977, + "489": 103.8499999999998, + "490": 103.74999999999977, + "491": 104.39999999999971, + "492": 100.64999999999974, + "493": 105.04999999999976, + "494": -41.75000000000008, + "495": 105.84999999999972, + "496": 106.59999999999972, + "497": 99.04999999999977, + "498": 86.34999999999974, + "499": 104.54999999999976, + "500": 102.44999999999978, + "501": 104.69999999999979, + "502": 104.39999999999976, + "503": 107.34999999999974, + "504": 94.84999999999981, + "505": 104.34999999999977, + "506": 100.34999999999975, + "507": 104.14999999999976, + "508": 81.09999999999985, + "509": 97.69999999999976, + "510": -71.14999999999999, + "511": 101.94999999999973, + "512": 98.79999999999978, + "513": 104.79999999999976, + "514": 103.84999999999977, + "515": 103.94999999999973, + "516": 100.04999999999976, + "517": 104.74999999999974, + "518": 101.99999999999977, + "519": 105.14999999999975, + "520": 106.69999999999973, + "521": -84.45, + "522": 101.99999999999974, + "523": 60.84999999999993, + "524": 68.99999999999977, + "525": 84.59999999999974, + "526": 97.84999999999977, + "527": 104.34999999999977, + "528": 104.64999999999975, + "529": 104.89999999999975, + "530": 104.09999999999977, + "531": 103.44999999999976, + "532": 105.94999999999986, + "533": 104.24999999999977, + "534": 73.69999999999986, + "535": 106.49999999999973, + "536": 107.44999999999973, + "537": 85.34999999999978, + "538": 97.04999999999977, + "539": 103.79999999999977, + "540": 97.24999999999976, + "541": 42.84999999999979, + "542": 54.34999999999977, + "543": 67.29999999999974, + "544": 80.19999999999973, + "545": 19.350000000000026, + "546": 90.84999999999975, + "547": 49.69999999999974, + "548": 68.64999999999976, + "549": 82.89999999999972, + "550": 105.49999999999973, + "551": 4.50000000000003, + "552": 103.19999999999978, + "553": 98.44999999999976, + "554": 48.04999999999981, + "555": 58.39999999999973, + "556": 105.39999999999974, + "557": 84.39999999999976, + "558": 55.44999999999979, + "559": -74.25, + "560": 7.249999999999989, + "561": 103.24999999999976, + "562": 101.64999999999986, + "563": 105.40000000000003, + "564": -83.85000000000001, + "565": 20.699999999999964, + "566": 61.54999999999973, + "567": 108.19999999999976, + "568": 70.19999999999975, + "569": 40.29999999999995, + "570": 69.89999999999972, + "571": 35.2, + "572": 72.19999999999983, + "573": 112.84999999999992, + "574": 100.04999999999973, + "575": 30.000000000000053, + "576": 102.49999999999977, + "577": 103.89999999999976, + "578": 51.749999999999716, + "579": 74.24999999999977, + "580": 49.34999999999983, + "581": 1.2000000000000497, + "582": 103.49999999999976, + "583": 79.14999999999988, + "584": 36.14999999999989, + "585": 104.04999999999973, + "586": -4.849999999999975, + "587": 106.94999999999973, + "588": 48.39999999999986, + "589": 73.79999999999973, + "590": 71.19999999999976, + "591": 106.14999999999974, + "592": 90.19999999999982, + "593": 102.44999999999975, + "594": -26.200000000000017, + "595": 104.79999999999973, + "596": 54.59999999999989, + "597": 75.1999999999998, + "598": 88.89999999999976, + "599": 100.14999999999976, + "600": 99.24999999999977, + "601": 55.299999999999756, + "602": 68.29999999999973, + "603": 102.04999999999977, + "604": 101.69999999999978, + "605": 76.04999999999977, + "606": 105.39999999999972, + "607": 102.49999999999977, + "608": 102.59999999999977, + "609": 102.14999999999976, + "610": 105.94999999999972, + "611": 65.59999999999975, + "612": 104.44999999999975, + "613": 107.2499999999998, + "614": -82.45, + "615": -5.00000000000005, + "616": 80.24999999999972, + "617": 98.04999999999977, + "618": 67.89999999999984, + "619": 99.74999999999976, + "620": 103.09999999999977, + "621": 103.19999999999973, + "622": -49.15000000000008, + "623": 98.69999999999976, + "624": 56.09999999999976, + "625": 107.44999999999975, + "626": 103.59999999999977, + "627": 38.049999999999756, + "628": -41.949999999999996, + "629": -88.2, + "630": 104.7999999999998, + "631": 107.59999999999972, + "632": -9.949999999999976, + "633": 86.69999999999972, + "634": 104.49999999999977, + "635": 86.89999999999984, + "636": 61.24999999999979, + "637": 73.19999999999973, + "638": -31.000000000000007, + "639": 76.99999999999979, + "640": 80.19999999999976, + "641": 74.8999999999998, + "642": 103.79999999999976, + "643": 97.39999999999974, + "644": 107.44999999999976, + "645": 97.94999999999979, + "646": 104.74999999999976, + "647": 102.64999999999972, + "648": 104.89999999999972, + "649": 104.59999999999974, + "650": 102.74999999999976, + "651": 102.89999999999976, + "652": 105.34999999999972, + "653": 105.59999999999975, + "654": 101.99999999999977, + "655": 101.19999999999978, + "656": 106.59999999999972, + "657": 105.04999999999974, + "658": 76.14999999999978, + "659": 104.69999999999976, + "660": 103.04999999999977, + "661": -3.800000000000068, + "662": 103.94999999999976, + "663": 103.99999999999976, + "664": 101.04999999999976, + "665": 103.44999999999976, + "666": 98.09999999999977, + "667": 90.65, + "668": 69.89999999999976, + "669": 103.59999999999977, + "670": 105.09999999999975, + "671": 104.19999999999976, + "672": 104.79999999999974, + "673": 66.19999999999978, + "674": -6.449999999999992, + "675": 104.34999999999977, + "676": 79.89999999999976, + "677": 97.49999999999977, + "678": 81.64999999999975, + "679": 48.19999999999988, + "680": 89.09999999999975, + "681": 108.29999999999974, + "682": 105.74999999999973, + "683": 102.54999999999977, + "684": 38.79999999999977, + "685": 103.69999999999976, + "686": 80.2999999999998, + "687": 103.49999999999977, + "688": 107.7999999999998, + "689": 104.84999999999975, + "690": 100.39999999999976, + "691": -71.64999999999999, + "692": 104.24999999999976, + "693": 102.64999999999976, + "694": 104.09999999999977, + "695": 105.09999999999978, + "696": 93.24999999999976, + "697": 70.94999999999972, + "698": 104.84999999999975, + "699": 65.79999999999981, + "700": 108.39999999999974, + "701": 100.54999999999971, + "702": 104.79999999999976, + "703": 102.84999999999977, + "704": 103.49999999999977, + "705": 104.89999999999974, + "706": 101.14999999999975, + "707": 104.89999999999972, + "708": 103.94999999999975, + "709": 102.49999999999977, + "710": 62.44999999999977, + "711": 102.39999999999976, + "712": 105.99999999999973, + "713": 104.49999999999974, + "714": 105.34999999999974, + "715": 106.69999999999972, + "716": 38.44999999999985, + "717": 103.64999999999976, + "718": 103.09999999999977, + "719": 102.89999999999978, + "720": 35.2499999999998, + "721": 103.99999999999976, + "722": 105.04999999999974, + "723": 103.39999999999976, + "724": 104.54999999999977, + "725": -81.7, + "726": 104.54999999999976, + "727": 100.89999999999975, + "728": 105.44999999999975, + "729": 111.64999999999979, + "730": 104.69999999999976, + "731": 99.89999999999978, + "732": 2.8999999999999346, + "733": 104.39999999999976, + "734": 103.14999999999976, + "735": 102.99999999999977, + "736": 103.39999999999976, + "737": 105.69999999999975, + "738": -85.25, + "739": 41.99999999999972, + "740": 103.99999999999976, + "741": 17.600000000000016, + "742": 65.44999999999976, + "743": 102.24999999999977, + "744": 102.49999999999977, + "745": 105.89999999999972, + "746": 102.64999999999978, + "747": 104.69999999999976, + "748": 102.79999999999977, + "749": 102.19999999999978, + "750": 104.49999999999973, + "751": 102.64999999999978, + "752": 104.74999999999977, + "753": 104.54999999999976, + "754": 99.79999999999978, + "755": 103.94999999999976, + "756": 66.09999999999991, + "757": 103.99999999999976, + "758": -85.1, + "759": 103.29999999999977, + "760": 106.04999999999974, + "761": 99.29999999999973, + "762": 104.89999999999975, + "763": 104.34999999999977, + "764": 103.69999999999976, + "765": 102.14999999999978, + "766": 104.84999999999974, + "767": 103.09999999999977, + "768": 104.04999999999974, + "769": 104.69999999999975, + "770": 104.49999999999976, + "771": 108.84999999999974, + "772": 101.49999999999976, + "773": 103.69999999999976, + "774": 60.79999999999977, + "775": 103.29999999999977, + "776": 104.84999999999975, + "777": 104.29999999999976, + "778": 102.84999999999977, + "779": 103.89999999999976, + "780": 104.54999999999977, + "781": 103.79999999999976, + "782": 105.59999999999974, + "783": 102.84999999999977, + "784": 104.69999999999976, + "785": 101.59999999999977, + "786": 96.09999999999974, + "787": 105.99999999999972, + "788": 104.34999999999977, + "789": 103.79999999999977, + "790": 103.24999999999977, + "791": 102.89999999999976, + "792": 96.19999999999976, + "793": 105.09999999999978, + "794": 52.8499999999999, + "795": 105.24999999999974, + "796": 107.49999999999972, + "797": 111.64999999999986, + "798": 104.59999999999975, + "799": 73.74999999999973, + "800": 104.69999999999975, + "801": 105.49999999999974, + "802": -69.5, + "803": 105.69999999999975, + "804": 103.89999999999976, + "805": 105.19999999999973, + "806": 103.89999999999976, + "807": 107.24999999999974, + "808": 105.39999999999974, + "809": 106.69999999999972, + "810": 104.59999999999975, + "811": 81.64999999999984, + "812": 103.84999999999977, + "813": -63.90000000000002, + "814": 106.59999999999972, + "815": -68.5, + "816": 103.59999999999977, + "817": 104.99999999999974, + "818": 104.29999999999977, + "819": -9.799999999999994, + "820": 104.19999999999976, + "821": 109.09999999999977, + "822": 103.04999999999977, + "823": 108.49999999999974, + "824": 105.24999999999972, + "825": 103.94999999999976, + "826": 107.69999999999972, + "827": 103.49999999999977, + "828": 59.39999999999974, + "829": -74.35000000000001, + "830": 103.84999999999977, + "831": 91.04999999999978, + "832": 103.54999999999977, + "833": 105.19999999999979, + "834": 102.84999999999974, + "835": 106.09999999999972, + "836": 104.04999999999976, + "837": 104.59999999999977, + "838": 109.09999999999977, + "839": 103.29999999999977, + "840": 104.14999999999976, + "841": 103.34999999999977, + "842": 106.19999999999972, + "843": 103.59999999999977, + "844": 100.54999999999977, + "845": 103.84999999999977, + "846": 104.44999999999976, + "847": 103.89999999999978, + "848": 105.84999999999974, + "849": -61.300000000000004, + "850": 103.79999999999976, + "851": 105.59999999999974, + "852": 103.64999999999976, + "853": 105.74999999999974, + "854": 106.0999999999998, + "855": 109.14999999999975, + "856": 106.79999999999971, + "857": 105.39999999999975, + "858": 101.14999999999976, + "859": 104.24999999999973, + "860": 104.19999999999976, + "861": 106.94999999999973, + "862": 102.94999999999978, + "863": 104.84999999999975, + "864": 103.49999999999976, + "865": 103.04999999999977, + "866": 105.94999999999973, + "867": 102.34999999999978, + "868": 107.29999999999974, + "869": 104.14999999999976, + "870": 103.34999999999977, + "871": 103.89999999999974, + "872": -77.69999999999999, + "873": -86.14999999999999, + "874": 103.19999999999976, + "875": 108.6499999999998, + "876": 105.44999999999975, + "877": 105.89999999999974, + "878": 105.14999999999972, + "879": 103.64999999999976, + "880": 70.64999999999974, + "881": 103.09999999999977, + "882": 105.24999999999984, + "883": 103.69999999999976, + "884": 107.29999999999973, + "885": 103.59999999999975, + "886": 105.59999999999974, + "887": 104.69999999999978, + "888": 102.34999999999978, + "889": 102.99999999999977, + "890": 107.69999999999972, + "891": 104.24999999999976, + "892": 100.89999999999978, + "893": 103.69999999999978, + "894": 106.34999999999972, + "895": 107.39999999999974, + "896": 103.44999999999978, + "897": 103.54999999999977, + "898": 101.14999999999975, + "899": 104.14999999999976, + "900": 105.84999999999972, + "901": 69.94999999999983, + "902": -37.80000000000014, + "903": 9.05000000000001, + "904": -22.399999999999988, + "905": 49.749999999999915, + "906": 22.449999999999854, + "907": 66.74999999999989, + "908": 69.29999999999987, + "909": 30.299999999999937, + "910": 83.39999999999984, + "911": 23.899999999999878, + "912": -70.00000000000003, + "913": 43.499999999999865, + "914": 51.34999999999983, + "915": 40.50000000000001, + "916": 55.599999999999795, + "917": -5.299999999999992, + "918": 99.69999999999979, + "919": 24.89999999999985, + "920": 32.19999999999985, + "921": 94.69999999999978, + "922": 18.75000000000001, + "923": -1.2500000000000309, + "924": 49.799999999999876, + "925": 6.649999999999977, + "926": 92.04999999999981, + "927": 38.100000000000016, + "928": 52.099999999999895, + "929": 61.399999999999984, + "930": 53.399999999999935, + "931": 79.69999999999986, + "932": 66.04999999999977, + "933": -14.500000000000021, + "934": 73.94999999999979, + "935": 71.44999999999979, + "936": 104.69999999999982, + "937": 93.74999999999982, + "938": 21.799999999999986, + "939": 107.49999999999973, + "940": 83.3499999999999, + "941": 77.09999999999995, + "942": 92.24999999999986, + "943": 103.34999999999977, + "944": 104.64999999999979, + "945": 103.59999999999975, + "946": -20.79999999999999, + "947": 64.84999999999978, + "948": 104.14999999999976, + "949": 55.599999999999916, + "950": 97.54999999999976, + "951": 103.24999999999976, + "952": 107.04999999999974, + "953": 104.79999999999976, + "954": 103.04999999999977, + "955": 89.69999999999976, + "956": 86.39999999999988, + "957": 104.04999999999977, + "958": 103.89999999999976, + "959": 87.29999999999978, + "960": 95.09999999999981, + "961": 104.09999999999975, + "962": 103.49999999999977, + "963": 87.14999999999976, + "964": 101.9999999999998, + "965": 103.49999999999977, + "966": 82.94999999999978, + "967": 108.14999999999974, + "968": 77.59999999999977, + "969": 103.89999999999976, + "970": 109.74999999999976, + "971": 108.89999999999976, + "972": 103.59999999999977, + "973": 108.09999999999974, + "974": 103.24999999999977, + "975": 105.39999999999975, + "976": 105.04999999999976, + "977": 107.74999999999977, + "978": 103.74999999999977, + "979": 103.49999999999976, + "980": -77.8, + "981": 108.69999999999973, + "982": 105.54999999999973, + "983": 103.49999999999976, + "984": 106.99999999999973, + "985": 103.89999999999976, + "986": -63.9, + "987": 102.89999999999978, + "988": 109.24999999999977, + "989": 111.94999999999995, + "990": 106.79999999999974, + "991": -64.75000000000001, + "992": 107.59999999999972, + "993": 98.29999999999976, + "994": 103.39999999999976, + "995": 104.49999999999976, + "996": 88.94999999999982, + "997": 103.24999999999977, + "998": -62.95, + "999": -70.9, + "1000": 103.34999999999977 + } +} diff --git a/benchmark/results/v3/v3.3.0/session_metadata/3.json b/benchmark/results/v3/v3.3.0/session_metadata/3.json new file mode 100644 index 00000000..fb81d2b1 --- /dev/null +++ b/benchmark/results/v3/v3.3.0/session_metadata/3.json @@ -0,0 +1,1009 @@ +{ + "total_episodes": 1001, + "total_time_steps": 128000, + "total_s": 1478.051265, + "s_per_step": 0.04618910203125, + "s_per_100_steps_10_nodes": 4.618910203125, + "total_reward_per_episode": { + "1": -64.2500000000001, + "2": -10.899999999999991, + "3": -30.800000000000004, + "4": -14.649999999999977, + "5": -75.69999999999999, + "6": -60.350000000000094, + "7": -21.8, + "8": -93.69999999999996, + "9": -19.499999999999964, + "10": -34.64999999999998, + "11": -17.999999999999968, + "12": -38.15000000000004, + "13": -15.749999999999979, + "14": -15.34999999999998, + "15": -21.599999999999955, + "16": -55.05000000000011, + "17": -10.049999999999995, + "18": -20.949999999999957, + "19": -53.30000000000008, + "20": -13.199999999999989, + "21": -19.29999999999997, + "22": -10.65000000000001, + "23": -9.000000000000002, + "24": -103.89999999999996, + "25": -12.64999999999999, + "26": -1.8999999999999888, + "27": -26.54999999999997, + "28": -34.600000000000044, + "29": -29.650000000000013, + "30": -64.30000000000015, + "31": -43.50000000000005, + "32": -29.600000000000023, + "33": -18.999999999999993, + "34": -101.0, + "35": -21.499999999999957, + "36": -21.499999999999957, + "37": -8.699999999999983, + "38": -6.550000000000001, + "39": -20.74999999999996, + "40": -17.999999999999968, + "41": -64.0500000000001, + "42": -17.349999999999977, + "43": -17.099999999999973, + "44": -14.899999999999965, + "45": -10.499999999999995, + "46": -13.849999999999985, + "47": -54.05000000000008, + "48": -16.79999999999998, + "49": -16.849999999999973, + "50": -61.650000000000155, + "51": -15.699999999999987, + "52": -47.80000000000007, + "53": -75.5, + "54": -25.049999999999944, + "55": -95.6, + "56": -7.65, + "57": -3.150000000000033, + "58": -16.649999999999977, + "59": -15.199999999999985, + "60": -17.099999999999977, + "61": -1.149999999999972, + "62": -93.65, + "63": -20.349999999999962, + "64": -7.749999999999991, + "65": -21.049999999999958, + "66": -23.19999999999995, + "67": -40.60000000000015, + "68": -18.699999999999967, + "69": -76.6999999999999, + "70": 5.15000000000003, + "71": -14.299999999999981, + "72": -8.399999999999997, + "73": -23.29999999999995, + "74": -21.550000000000004, + "75": -11.699999999999982, + "76": -66.05000000000005, + "77": -93.85, + "78": -15.749999999999982, + "79": -101.05000000000001, + "80": -11.600000000000007, + "81": -85.0000000000001, + "82": -6.999999999999995, + "83": 22.04999999999996, + "84": -47.15000000000007, + "85": -3.4999999999999805, + "86": -18.049999999999972, + "87": -97.4, + "88": -77.79999999999995, + "89": 9.00000000000001, + "90": -15.049999999999983, + "91": -4.350000000000004, + "92": -21.499999999999954, + "93": -3.6999999999999797, + "94": -39.69999999999998, + "95": -57.45000000000009, + "96": -17.349999999999973, + "97": -7.249999999999995, + "98": -14.199999999999978, + "99": -11.699999999999987, + "100": 2.5000000000000444, + "101": -12.649999999999984, + "102": -3.750000000000001, + "103": -20.349999999999962, + "104": -90.05, + "105": -18.299999999999972, + "106": -0.9000000000000015, + "107": -57.05, + "108": -2.399999999999965, + "109": -49.15000000000007, + "110": -20.49999999999996, + "111": -8.749999999999996, + "112": -79.10000000000001, + "113": -17.14999999999997, + "114": -3.0499999999999785, + "115": -64.35, + "116": -47.39999999999996, + "117": -10.999999999999996, + "118": -12.199999999999989, + "119": -16.89999999999998, + "120": -64.85000000000001, + "121": -6.749999999999996, + "122": 7.750000000000069, + "123": 13.75, + "124": -3.0999999999999863, + "125": -27.09999999999994, + "126": -16.649999999999977, + "127": 19.349999999999955, + "128": -49.350000000000044, + "129": -21.2, + "130": -39.49999999999999, + "131": -74.44999999999999, + "132": -5.449999999999989, + "133": -0.3499999999999986, + "134": -14.499999999999979, + "135": -21.699999999999953, + "136": -1.7499999999999736, + "137": -12.149999999999993, + "138": 23.949999999999946, + "139": -48.3, + "140": -11.49999999999999, + "141": -43.150000000000006, + "142": -11.04999999999999, + "143": -18.09999999999997, + "144": -11.1, + "145": 19.249999999999986, + "146": -90.4, + "147": 35.69999999999979, + "148": -78.85, + "149": -39.95000000000009, + "150": -6.799999999999987, + "151": -10.35, + "152": 16.45000000000007, + "153": 8.500000000000078, + "154": -18.199999999999967, + "155": -1.0999999999999819, + "156": 12.350000000000023, + "157": -36.69999999999998, + "158": 14.750000000000039, + "159": -13.999999999999991, + "160": -2.399999999999996, + "161": 14.250000000000043, + "162": -80.15000000000002, + "163": -19.499999999999964, + "164": 18.00000000000006, + "165": -41.499999999999964, + "166": 6.550000000000042, + "167": 5.700000000000033, + "168": -15.95000000000001, + "169": -10.549999999999978, + "170": -89.14999999999996, + "171": -0.2999999999999714, + "172": -17.70000000000004, + "173": -9.450000000000001, + "174": 14.849999999999959, + "175": -90.44999999999999, + "176": -11.799999999999978, + "177": -56.5, + "178": -13.249999999999984, + "179": -55.35, + "180": -17.699999999999974, + "181": -17.85000000000002, + "182": -7.799999999999989, + "183": -49.900000000000006, + "184": 27.400000000000055, + "185": 31.449999999999942, + "186": -49.59999999999996, + "187": 16.20000000000001, + "188": 2.5500000000000336, + "189": 27.44999999999993, + "190": -3.049999999999991, + "191": -60.84999999999995, + "192": 5.850000000000024, + "193": -7.199999999999984, + "194": -48.800000000000004, + "195": -69.60000000000004, + "196": 25.200000000000067, + "197": -37.649999999999956, + "198": -64.1500000000001, + "199": -48.59999999999997, + "200": -71.94999999999993, + "201": -18.249999999999968, + "202": -14.450000000000003, + "203": 4.750000000000047, + "204": 30.049999999999923, + "205": -5.549999999999984, + "206": -32.64999999999997, + "207": 16.450000000000045, + "208": -46.25, + "209": 11.549999999999937, + "210": 15.100000000000076, + "211": 23.450000000000063, + "212": -6.50000000000001, + "213": -35.00000000000002, + "214": 16.199999999999992, + "215": 46.099999999999845, + "216": -11.49999999999999, + "217": 5.550000000000029, + "218": 23.749999999999908, + "219": -56.75000000000003, + "220": 2.400000000000005, + "221": -9.299999999999951, + "222": 83.19999999999989, + "223": 46.249999999999915, + "224": 16.449999999999932, + "225": 34.49999999999993, + "226": -86.15, + "227": -8.049999999999992, + "228": -39.1, + "229": 15.749999999999899, + "230": -53.80000000000006, + "231": -24.649999999999956, + "232": 6.149999999999947, + "233": -27.50000000000003, + "234": 10.249999999999982, + "235": -9.850000000000056, + "236": -49.05, + "237": -25.099999999999987, + "238": 1.4500000000000328, + "239": 44.749999999999794, + "240": -23.800000000000022, + "241": 49.34999999999976, + "242": 26.250000000000018, + "243": 12.250000000000032, + "244": -5.773159728050814e-15, + "245": -15.94999999999996, + "246": 5.600000000000033, + "247": -12.049999999999978, + "248": 36.699999999999775, + "249": 27.94999999999998, + "250": -0.34999999999997033, + "251": -46.449999999999996, + "252": -21.749999999999957, + "253": 35.649999999999984, + "254": 47.79999999999981, + "255": 2.3000000000000114, + "256": 49.75000000000003, + "257": 48.54999999999982, + "258": 18.55000000000003, + "259": 25.85000000000007, + "260": -0.9500000000000135, + "261": 35.8999999999999, + "262": 62.64999999999988, + "263": -6.200000000000021, + "264": 41.94999999999994, + "265": 49.94999999999991, + "266": 49.49999999999995, + "267": -13.349999999999987, + "268": 67.94999999999983, + "269": 41.39999999999988, + "270": 15.000000000000068, + "271": -47.39999999999999, + "272": -82.35, + "273": 13.600000000000065, + "274": 43.84999999999982, + "275": 36.19999999999991, + "276": 39.64999999999994, + "277": 40.99999999999974, + "278": 11.800000000000047, + "279": 32.94999999999998, + "280": 81.80000000000007, + "281": 58.499999999999936, + "282": -15.399999999999983, + "283": 8.40000000000001, + "284": 30.95, + "285": 14.400000000000006, + "286": -10.149999999999995, + "287": 44.84999999999989, + "288": 49.5999999999999, + "289": 69.59999999999988, + "290": 63.049999999999784, + "291": 86.50000000000001, + "292": 47.64999999999981, + "293": 71.1499999999998, + "294": -7.049999999999991, + "295": 47.34999999999976, + "296": 102.65000000000008, + "297": 66.04999999999983, + "298": 63.899999999999935, + "299": 1.5000000000000955, + "300": 24.95000000000004, + "301": 54.74999999999998, + "302": -13.150000000000004, + "303": 52.64999999999996, + "304": 40.2999999999998, + "305": 83.10000000000001, + "306": -0.29999999999999305, + "307": -7.599999999999988, + "308": 58.74999999999989, + "309": 47.24999999999983, + "310": 70.59999999999985, + "311": 26.299999999999976, + "312": 38.4999999999999, + "313": 0.2999999999999834, + "314": 41.84999999999984, + "315": 91.40000000000015, + "316": -50.15000000000002, + "317": 56.24999999999983, + "318": 38.24999999999996, + "319": 5.849999999999965, + "320": 32.30000000000001, + "321": 47.35000000000001, + "322": 58.45000000000001, + "323": 11.04999999999999, + "324": -0.04999999999999771, + "325": 53.2999999999998, + "326": 84.10000000000014, + "327": 18.20000000000004, + "328": 68.14999999999982, + "329": 96.35000000000022, + "330": 64.09999999999994, + "331": 56.850000000000044, + "332": 95.80000000000014, + "333": 64.24999999999976, + "334": 13.299999999999962, + "335": 78.4499999999999, + "336": 55.099999999999945, + "337": 93.25, + "338": -9.449999999999983, + "339": 46.64999999999997, + "340": 82.04999999999998, + "341": 41.94999999999985, + "342": 94.45000000000003, + "343": 28.599999999999973, + "344": -11.29999999999999, + "345": 83.59999999999975, + "346": 12.250000000000037, + "347": 43.54999999999998, + "348": 85.2000000000001, + "349": 46.2999999999999, + "350": 48.49999999999998, + "351": 75.39999999999988, + "352": -18.69999999999997, + "353": 55.399999999999935, + "354": 97.35000000000015, + "355": 29.99999999999998, + "356": 87.15000000000005, + "357": 103.3500000000002, + "358": 16.800000000000047, + "359": 88.55000000000007, + "360": 36.15, + "361": 11.800000000000036, + "362": 58.44999999999989, + "363": 69.04999999999995, + "364": 20.099999999999994, + "365": 44.09999999999997, + "366": 82.40000000000019, + "367": 99.25000000000024, + "368": 74.4, + "369": 71.5000000000001, + "370": 105.35000000000024, + "371": 99.60000000000014, + "372": 66.64999999999998, + "373": 84.60000000000004, + "374": 81.05000000000011, + "375": 80.60000000000004, + "376": 59.999999999999915, + "377": 48.59999999999975, + "378": 63.6999999999998, + "379": 83.6, + "380": 82.09999999999994, + "381": -15.900000000000022, + "382": 59.14999999999999, + "383": 38.09999999999995, + "384": 98.75000000000016, + "385": 15.499999999999963, + "386": 102.85000000000022, + "387": 55.10000000000002, + "388": 68.54999999999988, + "389": 38.649999999999864, + "390": 105.55000000000022, + "391": 58.999999999999915, + "392": 67.6999999999999, + "393": 57.949999999999946, + "394": 94.20000000000017, + "395": 104.05000000000017, + "396": 96.35000000000018, + "397": 51.54999999999997, + "398": 105.35000000000015, + "399": 1.1499999999999722, + "400": 79.45000000000003, + "401": 101.05000000000004, + "402": 100.25000000000023, + "403": 76.55000000000001, + "404": 109.10000000000022, + "405": 65.04999999999995, + "406": 23.99999999999995, + "407": 52.29999999999998, + "408": 63.89999999999994, + "409": 97.10000000000015, + "410": 96.30000000000014, + "411": 54.949999999999825, + "412": 102.79999999999993, + "413": 60.199999999999996, + "414": 112.80000000000018, + "415": 41.849999999999945, + "416": 101.45, + "417": 61.75000000000006, + "418": 95.55000000000017, + "419": 98.70000000000019, + "420": 27.899999999999984, + "421": 86.70000000000005, + "422": 54.19999999999976, + "423": 95.19999999999978, + "424": 104.40000000000023, + "425": 102.59999999999984, + "426": 31.499999999999872, + "427": 12.299999999999995, + "428": 86.5000000000001, + "429": 22.599999999999987, + "430": 80.25000000000006, + "431": 101.30000000000011, + "432": 90.4999999999998, + "433": 98.25000000000006, + "434": 36.59999999999995, + "435": 56.649999999999906, + "436": 3.4499999999999584, + "437": -12.400000000000013, + "438": 80.20000000000003, + "439": 78.24999999999987, + "440": 85.50000000000007, + "441": 69.74999999999997, + "442": 108.8000000000003, + "443": 97.45000000000016, + "444": 90.2000000000002, + "445": 92.35000000000018, + "446": 50.95000000000005, + "447": 91.25000000000017, + "448": 82.90000000000016, + "449": 102.20000000000023, + "450": 59.39999999999996, + "451": 95.95000000000019, + "452": 29.04999999999998, + "453": 90.85000000000004, + "454": 78.85000000000001, + "455": 65.79999999999993, + "456": 52.50000000000005, + "457": 105.95000000000009, + "458": 91.10000000000024, + "459": 72.14999999999995, + "460": 97.20000000000013, + "461": 95.50000000000003, + "462": 102.59999999999997, + "463": 84.1499999999998, + "464": 35.199999999999925, + "465": 92.90000000000018, + "466": 60.79999999999991, + "467": 55.84999999999985, + "468": 81.05000000000011, + "469": 70.69999999999986, + "470": 100.45000000000017, + "471": 74.04999999999991, + "472": 104.45000000000017, + "473": 62.149999999999984, + "474": 54.949999999999996, + "475": 93.70000000000016, + "476": 100.90000000000025, + "477": 17.750000000000092, + "478": 59.64999999999989, + "479": 73.59999999999997, + "480": 65.99999999999987, + "481": 38.74999999999993, + "482": 102.6500000000002, + "483": 46.249999999999886, + "484": 63.749999999999936, + "485": 55.399999999999956, + "486": 109.40000000000022, + "487": 29.250000000000014, + "488": 58.700000000000045, + "489": 104.35000000000022, + "490": 59.49999999999989, + "491": 101.25000000000007, + "492": 53.249999999999915, + "493": 53.24999999999991, + "494": 79.4000000000001, + "495": 88.95000000000007, + "496": 20.14999999999995, + "497": 88.15000000000012, + "498": 66.89999999999999, + "499": 97.7000000000002, + "500": 94.10000000000011, + "501": 105.90000000000026, + "502": 41.849999999999945, + "503": 51.449999999999925, + "504": 50.84999999999991, + "505": 105.50000000000021, + "506": 67.54999999999993, + "507": 103.3500000000002, + "508": 97.4500000000002, + "509": 61.2000000000001, + "510": 69.74999999999993, + "511": 70.64999999999992, + "512": 96.05000000000014, + "513": 53.84999999999992, + "514": 63.29999999999977, + "515": -5.150000000000013, + "516": 102.50000000000018, + "517": 73.14999999999993, + "518": 74.95000000000002, + "519": 101.80000000000008, + "520": 102.05000000000004, + "521": 32.34999999999997, + "522": 47.699999999999946, + "523": 47.24999999999991, + "524": 59.1999999999999, + "525": 51.69999999999993, + "526": 38.35000000000001, + "527": 58.29999999999991, + "528": 28.94999999999998, + "529": 42.69999999999996, + "530": 34.19999999999997, + "531": 66.7999999999999, + "532": 105.80000000000021, + "533": 112.15000000000025, + "534": 103.3000000000002, + "535": 108.55000000000024, + "536": 80.05000000000005, + "537": 72.74999999999999, + "538": 54.64999999999994, + "539": 96.65000000000012, + "540": 63.64999999999992, + "541": 44.54999999999993, + "542": 104.45000000000023, + "543": 28.499999999999957, + "544": 82.9, + "545": 75.54999999999997, + "546": 96.45000000000013, + "547": 93.70000000000014, + "548": 97.45000000000013, + "549": 34.199999999999996, + "550": 112.20000000000017, + "551": 50.54999999999987, + "552": 108.80000000000024, + "553": 91.20000000000006, + "554": 4.450000000000015, + "555": 101.75000000000018, + "556": 53.84999999999991, + "557": 75.0, + "558": 100.8000000000002, + "559": 23.44999999999999, + "560": 12.899999999999986, + "561": 40.34999999999995, + "562": 95.30000000000011, + "563": 84.00000000000014, + "564": 96.35000000000014, + "565": 97.75000000000014, + "566": 97.79999999999995, + "567": 81.44999999999987, + "568": 94.65, + "569": 59.7499999999999, + "570": 71.39999999999993, + "571": 22.75, + "572": -5.800000000000004, + "573": 36.89999999999998, + "574": 63.29999999999989, + "575": 22.34999999999997, + "576": 70.19999999999992, + "577": 81.7499999999999, + "578": 61.94999999999994, + "579": 106.95000000000016, + "580": 29.20000000000001, + "581": 55.499999999999936, + "582": 79.25000000000004, + "583": 36.849999999999916, + "584": 32.69999999999993, + "585": 4.449999999999999, + "586": 38.7499999999999, + "587": 51.69999999999991, + "588": -0.6000000000000152, + "589": 60.84999999999991, + "590": 51.749999999999886, + "591": 66.49999999999993, + "592": 94.25000000000007, + "593": 91.25000000000014, + "594": 96.19999999999993, + "595": 64.29999999999988, + "596": 104.9000000000002, + "597": 59.299999999999955, + "598": 80.25, + "599": 68.84999999999981, + "600": 101.30000000000014, + "601": 98.95000000000012, + "602": 101.20000000000014, + "603": 93.45000000000005, + "604": 43.89999999999987, + "605": 52.349999999999945, + "606": 101.3500000000002, + "607": 98.20000000000019, + "608": 99.45000000000022, + "609": 91.50000000000013, + "610": 22.7, + "611": 103.40000000000013, + "612": 101.95000000000016, + "613": 84.0500000000001, + "614": 92.95000000000012, + "615": 38.39999999999995, + "616": -16.799999999999983, + "617": 80.15000000000002, + "618": 93.05000000000014, + "619": 98.45000000000016, + "620": 100.30000000000017, + "621": 95.14999999999998, + "622": 69.69999999999993, + "623": 60.04999999999989, + "624": 98.75000000000018, + "625": 108.40000000000026, + "626": 103.45000000000017, + "627": 89.34999999999988, + "628": 92.7000000000001, + "629": 19.199999999999925, + "630": 100.40000000000019, + "631": 89.89999999999999, + "632": 87.00000000000013, + "633": 102.20000000000017, + "634": 54.24999999999986, + "635": 41.19999999999994, + "636": 25.700000000000006, + "637": 79.35000000000005, + "638": 109.90000000000006, + "639": 103.00000000000016, + "640": 83.40000000000008, + "641": 110.35000000000025, + "642": 8.400000000000011, + "643": 88.65000000000009, + "644": 108.50000000000023, + "645": 109.2500000000002, + "646": 28.749999999999982, + "647": 71.59999999999991, + "648": 103.80000000000018, + "649": 115.35000000000026, + "650": 91.30000000000011, + "651": 62.799999999999926, + "652": 102.5000000000002, + "653": 100.7500000000001, + "654": 24.099999999999888, + "655": 107.95000000000019, + "656": 61.89999999999989, + "657": 105.65000000000018, + "658": 106.8000000000002, + "659": 104.90000000000023, + "660": 92.70000000000013, + "661": 101.45000000000024, + "662": 70.69999999999997, + "663": 24.700000000000003, + "664": 88.95000000000003, + "665": 91.95000000000012, + "666": 15.100000000000007, + "667": 52.64999999999994, + "668": 103.85000000000014, + "669": 56.999999999999936, + "670": 37.09999999999996, + "671": 101.85000000000021, + "672": 100.30000000000014, + "673": 100.15000000000013, + "674": 71.89999999999995, + "675": 77.95000000000006, + "676": 103.20000000000019, + "677": -4.400000000000001, + "678": 73.95, + "679": 68.2999999999999, + "680": 102.7000000000002, + "681": 106.95000000000019, + "682": 109.4500000000001, + "683": 74.64999999999996, + "684": 113.95000000000024, + "685": 102.95000000000017, + "686": 100.69999999999996, + "687": 90.15000000000008, + "688": 102.20000000000019, + "689": 78.3, + "690": 51.59999999999995, + "691": 76.60000000000001, + "692": 105.4000000000001, + "693": 77.90000000000003, + "694": 54.34999999999986, + "695": 108.60000000000022, + "696": 65.49999999999993, + "697": 45.899999999999956, + "698": 101.00000000000016, + "699": 105.85000000000022, + "700": 108.15000000000025, + "701": 35.54999999999997, + "702": 13.60000000000002, + "703": 112.25000000000024, + "704": 88.1, + "705": 102.3500000000002, + "706": 107.4500000000002, + "707": 36.5999999999999, + "708": 35.34999999999992, + "709": 63.49999999999995, + "710": 50.199999999999946, + "711": 108.15000000000015, + "712": 49.24999999999996, + "713": 47.39999999999993, + "714": 87.30000000000008, + "715": 101.80000000000017, + "716": 104.0000000000002, + "717": 97.80000000000007, + "718": 105.10000000000024, + "719": 109.20000000000024, + "720": 99.25000000000023, + "721": 98.30000000000001, + "722": 57.199999999999946, + "723": -32.70000000000001, + "724": 23.69999999999999, + "725": 101.44999999999997, + "726": 70.14999999999998, + "727": 102.90000000000022, + "728": 102.10000000000011, + "729": 104.35000000000022, + "730": 36.10000000000001, + "731": 91.05000000000008, + "732": 79.79999999999995, + "733": 92.29999999999995, + "734": 30.250000000000036, + "735": 89.5499999999999, + "736": 65.69999999999995, + "737": 102.5000000000002, + "738": 76.39999999999999, + "739": 63.44999999999994, + "740": 46.549999999999955, + "741": 37.09999999999995, + "742": 106.40000000000013, + "743": 64.94999999999993, + "744": 83.25000000000004, + "745": 99.8000000000001, + "746": 56.99999999999983, + "747": 94.70000000000017, + "748": 50.199999999999946, + "749": 100.70000000000024, + "750": 105.55000000000021, + "751": 50.64999999999995, + "752": 18.3, + "753": 36.55, + "754": 78.45000000000003, + "755": 18.149999999999995, + "756": 60.099999999999966, + "757": 82.99999999999997, + "758": 98.75000000000007, + "759": 51.099999999999945, + "760": 98.75000000000017, + "761": 42.049999999999955, + "762": 110.5500000000003, + "763": 105.80000000000021, + "764": 92.55000000000001, + "765": 101.35000000000015, + "766": 60.899999999999935, + "767": 10.500000000000057, + "768": 8.450000000000008, + "769": 110.70000000000024, + "770": 60.09999999999993, + "771": 51.69999999999993, + "772": 28.699999999999974, + "773": 110.35000000000022, + "774": 107.55000000000024, + "775": 62.44999999999991, + "776": 94.85000000000012, + "777": 73.54999999999998, + "778": 17.700000000000067, + "779": 109.95000000000007, + "780": 19.799999999999972, + "781": 35.35000000000003, + "782": 105.30000000000024, + "783": 75.94999999999999, + "784": 34.59999999999997, + "785": 88.55000000000004, + "786": 57.64999999999992, + "787": 34.850000000000115, + "788": 59.749999999999794, + "789": 67.45, + "790": 66.25, + "791": 55.449999999999925, + "792": 99.69999999999993, + "793": 80.75000000000009, + "794": 36.849999999999945, + "795": 62.74999999999996, + "796": 82.40000000000008, + "797": 88.10000000000005, + "798": 67.79999999999998, + "799": 58.04999999999991, + "800": 96.19999999999999, + "801": 49.09999999999993, + "802": 58.85000000000005, + "803": 101.30000000000005, + "804": 83.24999999999996, + "805": 58.04999999999985, + "806": 97.40000000000013, + "807": 60.15000000000003, + "808": -56.99999999999997, + "809": 99.75000000000014, + "810": 94.50000000000017, + "811": 95.45000000000016, + "812": 99.60000000000007, + "813": 100.25000000000016, + "814": 93.60000000000012, + "815": 93.95000000000016, + "816": 62.64999999999991, + "817": 58.64999999999985, + "818": 28.59999999999999, + "819": 82.50000000000004, + "820": 84.30000000000005, + "821": 75.60000000000001, + "822": 90.90000000000005, + "823": 95.15000000000015, + "824": 92.60000000000016, + "825": 78.80000000000008, + "826": 30.89999999999997, + "827": 93.09999999999997, + "828": 87.70000000000007, + "829": 105.30000000000027, + "830": 107.85000000000022, + "831": 94.24999999999984, + "832": 76.39999999999998, + "833": 96.20000000000017, + "834": 10.149999999999993, + "835": 94.25000000000001, + "836": 94.1500000000001, + "837": 69.84999999999997, + "838": 37.799999999999955, + "839": 101.1000000000002, + "840": 17.549999999999983, + "841": 78.10000000000001, + "842": 83.4999999999998, + "843": 79.54999999999986, + "844": 32.15, + "845": 51.249999999999915, + "846": 78.75000000000003, + "847": 91.60000000000011, + "848": 80.65000000000008, + "849": 88.9000000000001, + "850": 73.89999999999996, + "851": 109.00000000000018, + "852": 91.94999999999992, + "853": 107.10000000000028, + "854": 90.10000000000014, + "855": 88.9500000000001, + "856": 62.399999999999935, + "857": 61.299999999999905, + "858": 58.099999999999824, + "859": 99.55000000000021, + "860": 98.74999999999979, + "861": 106.45000000000005, + "862": 28.99999999999993, + "863": 59.599999999999866, + "864": -28.099999999999998, + "865": 73.35000000000002, + "866": 94.64999999999974, + "867": 52.94999999999982, + "868": 73.04999999999986, + "869": 82.5, + "870": 88.90000000000008, + "871": 104.40000000000013, + "872": 84.2, + "873": 7.800000000000001, + "874": 55.79999999999995, + "875": 88.45000000000012, + "876": -12.30000000000002, + "877": 48.399999999999885, + "878": 81.39999999999979, + "879": 102.65000000000012, + "880": 72.39999999999986, + "881": 92.04999999999976, + "882": 29.199999999999967, + "883": 98.70000000000014, + "884": 23.949999999999985, + "885": 10.499999999999943, + "886": 73.0, + "887": 67.35000000000001, + "888": 63.29999999999993, + "889": 106.95000000000012, + "890": 47.49999999999989, + "891": 66.39999999999988, + "892": 78.29999999999978, + "893": 96.19999999999997, + "894": 98.85000000000016, + "895": 44.44999999999992, + "896": 103.10000000000024, + "897": 97.55000000000008, + "898": 30.54999999999996, + "899": 88.09999999999991, + "900": 77.29999999999995, + "901": 94.39999999999988, + "902": 6.749999999999973, + "903": 64.29999999999995, + "904": 91.94999999999978, + "905": 10.450000000000053, + "906": 72.39999999999985, + "907": 96.35000000000005, + "908": 97.50000000000006, + "909": 60.69999999999993, + "910": -19.55, + "911": 80.24999999999983, + "912": 43.44999999999997, + "913": 82.79999999999981, + "914": 46.99999999999979, + "915": 92.09999999999978, + "916": 77.7999999999999, + "917": 98.40000000000003, + "918": 98.70000000000009, + "919": 62.74999999999975, + "920": 49.949999999999754, + "921": 21.599999999999998, + "922": 80.09999999999981, + "923": 70.69999999999979, + "924": 86.4000000000001, + "925": 95.00000000000009, + "926": 68.14999999999975, + "927": 63.04999999999991, + "928": 102.95000000000005, + "929": 78.94999999999983, + "930": 36.20000000000001, + "931": 73.89999999999999, + "932": 49.39999999999978, + "933": 77.75000000000007, + "934": 80.99999999999997, + "935": 77.85000000000004, + "936": 101.80000000000021, + "937": 69.29999999999987, + "938": 67.29999999999994, + "939": 90.7999999999999, + "940": 99.30000000000014, + "941": 40.79999999999979, + "942": 63.2499999999999, + "943": 96.79999999999977, + "944": 99.85000000000015, + "945": 62.499999999999886, + "946": 98.1000000000001, + "947": 87.44999999999999, + "948": 101.54999999999977, + "949": 81.39999999999984, + "950": 53.09999999999992, + "951": 80.09999999999975, + "952": 94.94999999999978, + "953": 92.14999999999978, + "954": 97.79999999999974, + "955": 87.79999999999983, + "956": 96.94999999999972, + "957": 94.20000000000003, + "958": 70.79999999999978, + "959": 65.5499999999998, + "960": 100.24999999999979, + "961": 102.64999999999993, + "962": 89.14999999999975, + "963": 21.099999999999984, + "964": 69.24999999999987, + "965": 93.15000000000018, + "966": 22.149999999999956, + "967": 87.44999999999975, + "968": 69.29999999999981, + "969": 91.24999999999977, + "970": 51.74999999999979, + "971": 78.74999999999993, + "972": 30.599999999999845, + "973": 94.5999999999999, + "974": 46.84999999999975, + "975": 64.99999999999977, + "976": 53.299999999999756, + "977": 87.60000000000004, + "978": 92.59999999999972, + "979": 93.34999999999977, + "980": 48.94999999999978, + "981": 97.70000000000009, + "982": 97.14999999999975, + "983": 48.74999999999975, + "984": 91.74999999999982, + "985": 101.14999999999986, + "986": 81.0499999999998, + "987": 63.1999999999999, + "988": 106.24999999999982, + "989": 66.89999999999992, + "990": 74.19999999999976, + "991": 92.29999999999974, + "992": 91.79999999999977, + "993": 81.50000000000001, + "994": 88.85000000000001, + "995": 99.09999999999972, + "996": 108.34999999999977, + "997": 54.049999999999926, + "998": 44.44999999999992, + "999": 101.74999999999973, + "1000": -39.899999999999984 + } +} diff --git a/benchmark/results/v3/v3.3.0/session_metadata/4.json b/benchmark/results/v3/v3.3.0/session_metadata/4.json new file mode 100644 index 00000000..49d8728b --- /dev/null +++ b/benchmark/results/v3/v3.3.0/session_metadata/4.json @@ -0,0 +1,1009 @@ +{ + "total_episodes": 1001, + "total_time_steps": 128000, + "total_s": 1435.848728, + "s_per_step": 0.044870272749999995, + "s_per_100_steps_10_nodes": 4.487027275, + "total_reward_per_episode": { + "1": -53.10000000000009, + "2": -17.299999999999972, + "3": -51.25000000000008, + "4": -48.30000000000006, + "5": -29.899999999999956, + "6": -23.449999999999964, + "7": -16.149999999999984, + "8": -38.750000000000036, + "9": -22.449999999999953, + "10": -48.40000000000015, + "11": -27.99999999999999, + "12": -7.199999999999988, + "13": -31.100000000000016, + "14": -25.95000000000002, + "15": -12.349999999999994, + "16": -17.799999999999976, + "17": -98.6, + "18": -43.65000000000011, + "19": -21.449999999999957, + "20": -52.95000000000008, + "21": -66.30000000000008, + "22": -39.55000000000012, + "23": -42.600000000000044, + "24": -81.64999999999998, + "25": -21.999999999999954, + "26": -15.499999999999979, + "27": -63.50000000000011, + "28": -20.249999999999982, + "29": -20.799999999999958, + "30": -13.249999999999982, + "31": -18.34999999999997, + "32": -53.20000000000015, + "33": -7.799999999999997, + "34": 5.850000000000034, + "35": -0.6999999999999571, + "36": -6.050000000000013, + "37": -20.19999999999996, + "38": -20.54999999999996, + "39": -13.349999999999985, + "40": -7.3499999999999925, + "41": -66.85000000000004, + "42": 8.750000000000043, + "43": -27.30000000000002, + "44": -12.34999999999999, + "45": -18.499999999999964, + "46": -33.24999999999999, + "47": -86.95, + "48": -16.8, + "49": -64.25000000000006, + "50": 3.5000000000000275, + "51": -7.499999999999999, + "52": -15.299999999999978, + "53": -23.94999999999995, + "54": -34.59999999999999, + "55": -11.35000000000001, + "56": -10.599999999999987, + "57": -31.75000000000003, + "58": -107.1, + "59": -30.550000000000022, + "60": -50.90000000000005, + "61": -103.75, + "62": -27.749999999999936, + "63": -13.699999999999983, + "64": 4.0500000000000576, + "65": -80.45000000000002, + "66": -18.549999999999965, + "67": -55.40000000000009, + "68": 21.0, + "69": 0.10000000000001108, + "70": -85.60000000000002, + "71": -67.35000000000008, + "72": -48.90000000000001, + "73": -13.649999999999986, + "74": -47.75000000000005, + "75": -15.749999999999979, + "76": -45.75000000000005, + "77": -63.7000000000001, + "78": -12.949999999999987, + "79": 21.599999999999916, + "80": -100.49999999999999, + "81": -56.700000000000095, + "82": -4.249999999999967, + "83": -19.099999999999966, + "84": -46.19999999999993, + "85": -21.299999999999965, + "86": 10.850000000000058, + "87": -17.94999999999997, + "88": -93.25, + "89": 11.400000000000013, + "90": -90.45, + "91": -19.299999999999965, + "92": -23.29999999999995, + "93": -60.950000000000095, + "94": -19.999999999999964, + "95": -16.699999999999974, + "96": -16.49999999999998, + "97": -18.24999999999997, + "98": -61.20000000000009, + "99": -86.45000000000002, + "100": -14.000000000000007, + "101": -7.049999999999984, + "102": -18.199999999999967, + "103": -28.550000000000004, + "104": -10.149999999999991, + "105": 5.500000000000013, + "106": -36.200000000000045, + "107": 31.4499999999998, + "108": -14.249999999999984, + "109": -69.05000000000005, + "110": 13.29999999999998, + "111": -81.05, + "112": 5.85000000000002, + "113": 2.7500000000000577, + "114": 2.05000000000003, + "115": 4.0500000000000504, + "116": -10.849999999999996, + "117": -90.05000000000001, + "118": -17.00000000000003, + "119": 4.550000000000038, + "120": -9.449999999999985, + "121": -11.75000000000004, + "122": -10.299999999999981, + "123": -18.949999999999967, + "124": 13.150000000000013, + "125": -63.050000000000104, + "126": -14.649999999999984, + "127": -22.59999999999995, + "128": 5.7499999999999005, + "129": 27.45000000000002, + "130": 5.699999999999998, + "131": -14.199999999999964, + "132": 25.299999999999972, + "133": -45.70000000000005, + "134": -5.649999999999976, + "135": -18.100000000000044, + "136": -13.150000000000006, + "137": 4.9499999999999655, + "138": 35.549999999999876, + "139": -3.3000000000001, + "140": 14.70000000000002, + "141": -9.150000000000004, + "142": -44.44999999999999, + "143": -14.649999999999977, + "144": -67.49999999999997, + "145": -67.69999999999997, + "146": -81.55000000000001, + "147": -34.65000000000004, + "148": 12.049999999999867, + "149": -7.5999999999999845, + "150": -8.249999999999984, + "151": 24.850000000000065, + "152": -15.649999999999979, + "153": 3.350000000000044, + "154": 40.74999999999999, + "155": -46.250000000000014, + "156": -6.149999999999986, + "157": 37.29999999999989, + "158": -15.699999999999973, + "159": -10.100000000000007, + "160": 7.750000000000041, + "161": 7.600000000000026, + "162": -13.49999999999997, + "163": -26.54999999999995, + "164": 30.449999999999932, + "165": -87.79999999999998, + "166": -68.70000000000002, + "167": -13.14999999999999, + "168": -7.299999999999983, + "169": 38.149999999999814, + "170": -79.34999999999998, + "171": -17.149999999999956, + "172": -16.649999999999974, + "173": 19.750000000000025, + "174": -63.00000000000011, + "175": 62.44999999999998, + "176": -0.44999999999996, + "177": -22.899999999999984, + "178": -7.2999999999999865, + "179": -94.5, + "180": -33.549999999999955, + "181": -94.2, + "182": -2.7499999999999885, + "183": -8.149999999999988, + "184": 5.450000000000016, + "185": 5.150000000000009, + "186": -4.550000000000033, + "187": 26.00000000000004, + "188": -38.09999999999999, + "189": -71.90000000000003, + "190": -73.1, + "191": 21.55000000000002, + "192": -31.149999999999963, + "193": -81.7, + "194": 50.449999999999854, + "195": -13.750000000000012, + "196": -41.70000000000003, + "197": -56.850000000000094, + "198": -5.399999999999981, + "199": 57.49999999999982, + "200": -52.40000000000001, + "201": -37.000000000000064, + "202": -73.34999999999994, + "203": 16.79999999999994, + "204": 44.499999999999744, + "205": 14.450000000000077, + "206": 48.34999999999985, + "207": 71.29999999999991, + "208": 23.250000000000014, + "209": -21.499999999999957, + "210": -21.34999999999996, + "211": 22.599999999999966, + "212": 34.799999999999805, + "213": 67.64999999999989, + "214": 73.19999999999978, + "215": -70.24999999999999, + "216": -48.74999999999998, + "217": -37.49999999999997, + "218": 82.90000000000016, + "219": -75.79999999999998, + "220": -89.75, + "221": 61.34999999999975, + "222": 30.749999999999762, + "223": -65.69999999999993, + "224": 20.500000000000025, + "225": 19.40000000000006, + "226": 14.349999999999914, + "227": 25.150000000000055, + "228": -50.04999999999994, + "229": 28.75000000000007, + "230": 51.749999999999815, + "231": 71.89999999999988, + "232": 73.29999999999981, + "233": -10.599999999999962, + "234": 69.34999999999987, + "235": -10.749999999999964, + "236": 44.24999999999973, + "237": 31.649999999999945, + "238": -16.09999999999998, + "239": -20.59999999999999, + "240": 21.25000000000007, + "241": 0.600000000000027, + "242": -2.7999999999999785, + "243": 38.84999999999985, + "244": 6.450000000000021, + "245": -63.199999999999974, + "246": 58.8999999999998, + "247": 38.30000000000006, + "248": 92.59999999999974, + "249": -9.850000000000032, + "250": -57.20000000000011, + "251": 57.74999999999977, + "252": 83.04999999999974, + "253": 64.39999999999992, + "254": -10.049999999999992, + "255": -57.05000000000007, + "256": 30.000000000000007, + "257": -0.8999999999999915, + "258": 21.20000000000008, + "259": 55.5999999999998, + "260": 83.64999999999976, + "261": 21.499999999999982, + "262": 32.65000000000008, + "263": -84.10000000000001, + "264": 98.24999999999974, + "265": 48.29999999999984, + "266": 60.7999999999999, + "267": 87.09999999999975, + "268": 33.10000000000005, + "269": -48.59999999999994, + "270": 78.24999999999987, + "271": 60.6499999999999, + "272": 34.199999999999896, + "273": 77.79999999999973, + "274": 26.800000000000033, + "275": -12.200000000000014, + "276": 88.24999999999977, + "277": 9.700000000000067, + "278": 101.29999999999971, + "279": 64.9999999999998, + "280": 17.199999999999896, + "281": 95.39999999999976, + "282": 84.74999999999977, + "283": 71.14999999999989, + "284": 34.29999999999986, + "285": 36.90000000000001, + "286": 61.74999999999984, + "287": 99.34999999999972, + "288": 86.09999999999981, + "289": 74.09999999999977, + "290": 74.9499999999998, + "291": 63.29999999999985, + "292": 80.34999999999977, + "293": 24.899999999999956, + "294": 82.44999999999978, + "295": -32.3, + "296": 77.89999999999985, + "297": -12.899999999999988, + "298": 54.99999999999979, + "299": 50.74999999999978, + "300": 77.74999999999984, + "301": 82.19999999999979, + "302": 98.29999999999978, + "303": 38.049999999999976, + "304": 78.04999999999977, + "305": -23.89999999999995, + "306": 85.59999999999984, + "307": 9.450000000000026, + "308": 39.24999999999997, + "309": 61.19999999999976, + "310": 49.149999999999764, + "311": 89.84999999999982, + "312": 11.500000000000043, + "313": 85.79999999999976, + "314": 54.1499999999998, + "315": 93.39999999999976, + "316": 91.59999999999977, + "317": 99.94999999999978, + "318": 80.19999999999978, + "319": 99.44999999999976, + "320": -71.65000000000002, + "321": 87.69999999999978, + "322": 94.69999999999979, + "323": 92.19999999999979, + "324": 77.49999999999987, + "325": 68.59999999999977, + "326": 99.74999999999976, + "327": 82.59999999999982, + "328": 94.94999999999973, + "329": 88.49999999999979, + "330": 85.94999999999976, + "331": 28.299999999999844, + "332": 85.24999999999977, + "333": 89.9499999999998, + "334": -50.50000000000008, + "335": 85.99999999999984, + "336": -1.0999999999999823, + "337": 92.64999999999978, + "338": 99.99999999999976, + "339": 29.99999999999981, + "340": 87.29999999999976, + "341": 81.7499999999998, + "342": 95.59999999999975, + "343": 91.99999999999974, + "344": 97.39999999999978, + "345": 96.09999999999978, + "346": 98.94999999999975, + "347": 99.6999999999998, + "348": 98.89999999999975, + "349": 84.34999999999975, + "350": 91.24999999999977, + "351": 83.09999999999978, + "352": 80.39999999999984, + "353": -86.1, + "354": 69.79999999999976, + "355": 82.64999999999972, + "356": 100.24999999999979, + "357": 91.74999999999977, + "358": 100.14999999999978, + "359": 88.5999999999998, + "360": 102.74999999999976, + "361": 21.94999999999991, + "362": 89.49999999999979, + "363": 95.79999999999976, + "364": 83.89999999999979, + "365": 83.2999999999998, + "366": 100.69999999999978, + "367": -84.44999999999999, + "368": 10.150000000000038, + "369": -11.899999999999991, + "370": 100.19999999999976, + "371": 102.44999999999973, + "372": 76.44999999999979, + "373": 101.89999999999976, + "374": 99.94999999999979, + "375": 8.650000000000025, + "376": 32.750000000000014, + "377": 82.79999999999984, + "378": 73.24999999999972, + "379": 103.49999999999976, + "380": 92.99999999999977, + "381": 103.09999999999977, + "382": 103.84999999999975, + "383": 96.04999999999974, + "384": 103.09999999999977, + "385": 95.09999999999977, + "386": 83.4999999999998, + "387": 103.39999999999976, + "388": -80.0, + "389": 90.1499999999998, + "390": 97.59999999999972, + "391": 83.09999999999978, + "392": 94.44999999999976, + "393": 100.39999999999976, + "394": 97.99999999999979, + "395": 90.99999999999982, + "396": 82.79999999999977, + "397": 99.09999999999977, + "398": 105.24999999999974, + "399": 65.94999999999976, + "400": 98.39999999999976, + "401": 103.09999999999977, + "402": 100.69999999999976, + "403": 98.74999999999976, + "404": -44.05000000000007, + "405": 91.44999999999976, + "406": 27.000000000000046, + "407": 98.64999999999976, + "408": 96.44999999999978, + "409": 104.99999999999983, + "410": 90.99999999999979, + "411": 101.9999999999998, + "412": 103.34999999999988, + "413": 98.29999999999974, + "414": 78.59999999999987, + "415": 99.34999999999987, + "416": 101.69999999999978, + "417": 77.8999999999998, + "418": 101.14999999999978, + "419": 103.09999999999977, + "420": 103.49999999999976, + "421": 100.69999999999978, + "422": 91.44999999999978, + "423": 100.54999999999977, + "424": -23.149999999999952, + "425": 99.79999999999976, + "426": -9.349999999999985, + "427": 106.89999999999979, + "428": 101.99999999999977, + "429": 84.2499999999998, + "430": 93.34999999999981, + "431": 104.69999999999982, + "432": 83.09999999999977, + "433": 59.49999999999984, + "434": 101.59999999999975, + "435": 98.99999999999977, + "436": 96.89999999999976, + "437": 105.84999999999984, + "438": 97.19999999999976, + "439": -11.799999999999985, + "440": 102.49999999999976, + "441": 98.44999999999979, + "442": 108.44999999999986, + "443": 103.54999999999977, + "444": 101.14999999999976, + "445": 104.74999999999976, + "446": 100.69999999999976, + "447": 87.79999999999976, + "448": 96.5499999999998, + "449": 75.94999999999978, + "450": 103.49999999999977, + "451": 84.84999999999974, + "452": 101.14999999999976, + "453": 23.049999999999898, + "454": 98.69999999999978, + "455": 102.39999999999978, + "456": 62.19999999999973, + "457": 102.74999999999976, + "458": 105.44999999999975, + "459": 65.6499999999999, + "460": -38.89999999999997, + "461": 62.44999999999979, + "462": 97.44999999999978, + "463": 102.69999999999976, + "464": 107.89999999999979, + "465": 103.59999999999977, + "466": 104.99999999999974, + "467": 103.04999999999977, + "468": 102.84999999999977, + "469": 104.79999999999974, + "470": 100.19999999999978, + "471": 104.59999999999977, + "472": 102.79999999999977, + "473": 104.94999999999975, + "474": -80.69999999999999, + "475": 102.34999999999977, + "476": -84.00000000000003, + "477": 96.09999999999975, + "478": 80.79999999999973, + "479": 102.24999999999977, + "480": 98.30000000000008, + "481": 103.39999999999975, + "482": 56.09999999999979, + "483": 103.54999999999977, + "484": 103.74999999999972, + "485": 67.74999999999976, + "486": 62.94999999999975, + "487": 101.99999999999977, + "488": 103.24999999999974, + "489": 104.49999999999974, + "490": 75.29999999999983, + "491": 72.84999999999977, + "492": 77.44999999999978, + "493": 102.69999999999978, + "494": 96.14999999999976, + "495": 94.8499999999998, + "496": 106.64999999999972, + "497": 80.89999999999989, + "498": 84.44999999999976, + "499": 101.94999999999976, + "500": 99.89999999999978, + "501": 105.89999999999974, + "502": -35.199999999999996, + "503": 106.69999999999973, + "504": 94.59999999999981, + "505": 101.49999999999977, + "506": 103.19999999999976, + "507": 103.99999999999972, + "508": 96.74999999999982, + "509": 97.8499999999998, + "510": 104.59999999999974, + "511": 102.74999999999977, + "512": 103.64999999999976, + "513": 100.39999999999975, + "514": 99.19999999999978, + "515": 71.64999999999976, + "516": 104.09999999999974, + "517": 104.34999999999975, + "518": 102.94999999999978, + "519": 97.54999999999977, + "520": 106.24999999999973, + "521": -75.3, + "522": 4.75, + "523": 100.34999999999975, + "524": 106.69999999999975, + "525": 56.699999999999854, + "526": 16.30000000000001, + "527": 101.29999999999977, + "528": 93.09999999999977, + "529": 103.54999999999977, + "530": 66.2999999999999, + "531": 102.59999999999977, + "532": 102.74999999999977, + "533": 102.84999999999977, + "534": 102.84999999999977, + "535": 104.59999999999977, + "536": 107.89999999999974, + "537": 104.04999999999974, + "538": 75.74999999999977, + "539": 102.29999999999977, + "540": 81.34999999999978, + "541": 102.09999999999977, + "542": 105.89999999999974, + "543": 99.54999999999974, + "544": 102.84999999999975, + "545": 105.84999999999972, + "546": 105.59999999999972, + "547": 103.54999999999977, + "548": 98.64999999999978, + "549": 103.39999999999976, + "550": 106.34999999999975, + "551": 84.14999999999976, + "552": 108.59999999999974, + "553": 104.34999999999975, + "554": -78.9, + "555": 91.19999999999978, + "556": 101.54999999999977, + "557": 103.49999999999977, + "558": 104.79999999999974, + "559": 48.14999999999982, + "560": 40.99999999999998, + "561": 93.69999999999982, + "562": 104.44999999999976, + "563": 105.49999999999974, + "564": 102.24999999999977, + "565": 93.59999999999978, + "566": 105.74999999999974, + "567": 99.74999999999976, + "568": 62.39999999999994, + "569": 100.64999999999974, + "570": 104.39999999999972, + "571": 103.89999999999976, + "572": 103.34999999999977, + "573": 85.79999999999977, + "574": 3.1499999999999915, + "575": 102.04999999999977, + "576": 104.64999999999978, + "577": 59.09999999999975, + "578": -81.00000000000001, + "579": 103.59999999999977, + "580": 105.69999999999972, + "581": 53.79999999999983, + "582": 104.79999999999974, + "583": 102.84999999999977, + "584": 104.44999999999978, + "585": 104.39999999999975, + "586": 104.54999999999976, + "587": 103.79999999999977, + "588": 105.59999999999972, + "589": 102.54999999999976, + "590": 103.54999999999977, + "591": 83.54999999999977, + "592": -75.8, + "593": 105.89999999999972, + "594": 102.09999999999977, + "595": 105.74999999999973, + "596": 103.19999999999976, + "597": 102.94999999999978, + "598": 107.04999999999974, + "599": 103.89999999999976, + "600": 104.39999999999976, + "601": 100.99999999999976, + "602": 106.09999999999974, + "603": 105.34999999999975, + "604": 105.09999999999974, + "605": 103.74999999999977, + "606": 102.89999999999976, + "607": -78.65, + "608": 102.89999999999978, + "609": 107.24999999999973, + "610": 102.64999999999976, + "611": 106.94999999999973, + "612": -82.80000000000001, + "613": 104.09999999999977, + "614": 104.39999999999976, + "615": 104.14999999999976, + "616": 43.74999999999976, + "617": 104.49999999999976, + "618": 60.74999999999977, + "619": 105.39999999999975, + "620": 103.29999999999977, + "621": 106.49999999999993, + "622": 107.54999999999974, + "623": 107.99999999999974, + "624": 76.69999999999978, + "625": 108.29999999999974, + "626": 102.99999999999977, + "627": 104.54999999999976, + "628": 103.74999999999977, + "629": 105.54999999999973, + "630": 104.64999999999975, + "631": 102.89999999999976, + "632": 105.39999999999974, + "633": 104.14999999999976, + "634": 104.59999999999975, + "635": 104.29999999999977, + "636": 103.94999999999976, + "637": 97.84999999999977, + "638": -79.04999999999998, + "639": 103.04999999999977, + "640": 100.74999999999979, + "641": 102.74999999999977, + "642": 104.09999999999977, + "643": 106.04999999999971, + "644": 106.64999999999989, + "645": 104.09999999999977, + "646": 103.24999999999976, + "647": 103.04999999999977, + "648": 103.99999999999976, + "649": 81.19999999999976, + "650": 102.79999999999977, + "651": 102.99999999999977, + "652": 101.94999999999978, + "653": 39.1, + "654": 105.84999999999972, + "655": 60.34999999999975, + "656": 96.39999999999979, + "657": 62.69999999999998, + "658": 105.24999999999974, + "659": 92.44999999999975, + "660": 103.69999999999976, + "661": 101.39999999999978, + "662": 103.09999999999977, + "663": 103.24999999999976, + "664": 94.4499999999998, + "665": 89.79999999999987, + "666": 103.54999999999976, + "667": 103.99999999999976, + "668": 73.64999999999976, + "669": 103.69999999999976, + "670": -14.500000000000007, + "671": 105.04999999999976, + "672": 104.94999999999975, + "673": 103.34999999999977, + "674": 90.89999999999982, + "675": 100.99999999999977, + "676": 105.09999999999981, + "677": 103.64999999999976, + "678": 71.59999999999977, + "679": 107.94999999999973, + "680": 73.14999999999976, + "681": 103.24999999999977, + "682": 103.29999999999977, + "683": 54.29999999999975, + "684": 98.69999999999975, + "685": 104.54999999999973, + "686": 105.6499999999998, + "687": 103.09999999999977, + "688": 103.84999999999977, + "689": 104.34999999999974, + "690": 83.54999999999977, + "691": 84.8499999999998, + "692": 105.44999999999982, + "693": 106.54999999999973, + "694": 106.24999999999983, + "695": 103.94999999999976, + "696": 105.94999999999973, + "697": 12.799999999999969, + "698": 103.29999999999984, + "699": 109.09999999999975, + "700": 101.99999999999974, + "701": 104.79999999999977, + "702": 103.79999999999976, + "703": 102.64999999999976, + "704": 103.29999999999977, + "705": 106.94999999999973, + "706": 104.69999999999976, + "707": 103.09999999999977, + "708": 103.74999999999976, + "709": 103.14999999999978, + "710": 102.79999999999974, + "711": 99.24999999999977, + "712": 103.04999999999977, + "713": 102.69999999999978, + "714": 103.74999999999976, + "715": 102.74999999999976, + "716": 83.99999999999983, + "717": 104.39999999999975, + "718": 104.84999999999975, + "719": 103.59999999999977, + "720": 103.39999999999976, + "721": 102.74999999999977, + "722": 104.84999999999974, + "723": 104.49999999999976, + "724": 105.64999999999974, + "725": 92.49999999999977, + "726": 102.49999999999976, + "727": 104.34999999999988, + "728": 104.39999999999975, + "729": 103.44999999999976, + "730": 106.94999999999979, + "731": 103.14999999999978, + "732": 103.69999999999976, + "733": 111.44999999999993, + "734": 102.94999999999978, + "735": 100.39999999999976, + "736": 99.54999999999978, + "737": 104.89999999999975, + "738": 104.19999999999976, + "739": 95.89999999999995, + "740": 105.29999999999994, + "741": 105.59999999999972, + "742": 104.19999999999976, + "743": 105.44999999999972, + "744": 105.84999999999974, + "745": 106.94999999999973, + "746": 107.84999999999972, + "747": 94.49999999999974, + "748": 104.84999999999975, + "749": 107.29999999999973, + "750": 104.04999999999976, + "751": 103.99999999999976, + "752": 62.34999999999977, + "753": 107.54999999999973, + "754": -84.6, + "755": 106.64999999999972, + "756": 85.69999999999983, + "757": 103.04999999999977, + "758": 57.54999999999978, + "759": 104.79999999999976, + "760": 96.04999999999976, + "761": 3.80000000000006, + "762": 102.79999999999977, + "763": -65.80000000000001, + "764": 106.64999999999974, + "765": 31.64999999999985, + "766": -68.35000000000001, + "767": 103.54999999999976, + "768": 104.84999999999977, + "769": 58.199999999999754, + "770": 103.89999999999976, + "771": 49.699999999999775, + "772": 109.94999999999985, + "773": 104.74999999999976, + "774": 104.59999999999975, + "775": 105.79999999999981, + "776": 31.39999999999987, + "777": 103.64999999999976, + "778": 103.34999999999977, + "779": 105.04999999999974, + "780": -68.1, + "781": 107.39999999999975, + "782": 109.50000000000018, + "783": -19.25, + "784": 108.39999999999989, + "785": 107.59999999999982, + "786": 102.39999999999978, + "787": 104.29999999999977, + "788": -84.00000000000003, + "789": 112.49999999999994, + "790": -77.4, + "791": 104.19999999999975, + "792": 92.74999999999983, + "793": 104.19999999999976, + "794": 104.29999999999976, + "795": -84.54999999999998, + "796": 103.89999999999979, + "797": 109.0499999999998, + "798": 105.29999999999977, + "799": 105.89999999999972, + "800": 103.14999999999976, + "801": 101.99999999999977, + "802": 105.19999999999973, + "803": 105.04999999999974, + "804": 105.74999999999972, + "805": 104.89999999999993, + "806": 105.34999999999974, + "807": 104.39999999999976, + "808": 103.64999999999976, + "809": 105.74999999999973, + "810": 49.74999999999981, + "811": 108.69999999999995, + "812": 106.39999999999971, + "813": -36.95000000000001, + "814": 105.59999999999975, + "815": 105.94999999999973, + "816": 103.34999999999977, + "817": 103.39999999999976, + "818": -76.6, + "819": 111.94999999999992, + "820": 103.34999999999977, + "821": 104.14999999999976, + "822": 106.59999999999972, + "823": 104.29999999999976, + "824": -3.8000000000000043, + "825": 103.29999999999977, + "826": 65.14999999999979, + "827": 103.99999999999976, + "828": 100.14999999999978, + "829": 104.44999999999976, + "830": 104.99999999999973, + "831": 78.94999999999978, + "832": -75.35, + "833": 102.49999999999977, + "834": -86.45000000000013, + "835": 116.35000000000032, + "836": 103.14999999999976, + "837": 105.34999999999972, + "838": 105.79999999999974, + "839": 108.69999999999975, + "840": 105.44999999999973, + "841": -88.44999999999999, + "842": 104.59999999999975, + "843": 104.24999999999976, + "844": 105.24999999999973, + "845": 113.80000000000021, + "846": 104.79999999999974, + "847": 104.94999999999982, + "848": 104.59999999999975, + "849": 103.39999999999976, + "850": 107.94999999999979, + "851": 105.69999999999972, + "852": 109.09999999999977, + "853": 106.29999999999971, + "854": 82.74999999999974, + "855": 71.29999999999978, + "856": -68.34999999999998, + "857": 106.49999999999996, + "858": 107.69999999999975, + "859": 105.39999999999972, + "860": 103.34999999999977, + "861": 107.74999999999974, + "862": 103.74999999999976, + "863": 100.79999999999973, + "864": 106.19999999999973, + "865": 100.79999999999976, + "866": -81.0, + "867": 105.69999999999986, + "868": 103.09999999999977, + "869": 104.09999999999977, + "870": 102.69999999999978, + "871": 103.94999999999976, + "872": 105.09999999999975, + "873": 103.94999999999973, + "874": 46.699999999999896, + "875": 94.54999999999978, + "876": 103.79999999999977, + "877": 106.24999999999973, + "878": 104.14999999999975, + "879": -73.69999999999997, + "880": 104.59999999999982, + "881": -77.44999999999999, + "882": -15.000000000000014, + "883": 104.84999999999975, + "884": -81.95, + "885": 105.14999999999975, + "886": 109.24999999999979, + "887": -77.85, + "888": 104.19999999999976, + "889": 113.75, + "890": -38.10000000000001, + "891": 104.69999999999976, + "892": -72.05000000000001, + "893": -73.80000000000001, + "894": 113.64999999999988, + "895": 104.19999999999976, + "896": 107.64999999999974, + "897": 109.29999999999978, + "898": 109.04999999999981, + "899": 109.24999999999976, + "900": 104.84999999999977, + "901": 104.24999999999976, + "902": 105.79999999999974, + "903": 104.04999999999977, + "904": 104.54999999999974, + "905": 104.94999999999975, + "906": 105.09999999999975, + "907": 101.49999999999972, + "908": -79.94999999999999, + "909": 103.29999999999977, + "910": 105.89999999999972, + "911": 102.64999999999976, + "912": 85.34999999999981, + "913": 104.69999999999976, + "914": 106.59999999999972, + "915": 106.44999999999972, + "916": 106.59999999999974, + "917": 107.64999999999974, + "918": 116.70000000000027, + "919": 59.150000000000034, + "920": 102.74999999999977, + "921": 104.89999999999972, + "922": 104.89999999999974, + "923": 107.19999999999972, + "924": 106.19999999999975, + "925": 104.79999999999976, + "926": 111.64999999999999, + "927": 109.04999999999976, + "928": 104.39999999999975, + "929": 105.34999999999975, + "930": 115.10000000000018, + "931": 108.39999999999975, + "932": 60.249999999999766, + "933": 40.69999999999997, + "934": 97.94999999999975, + "935": 105.39999999999974, + "936": 108.44999999999976, + "937": 105.89999999999974, + "938": 106.14999999999972, + "939": 106.09999999999972, + "940": 105.29999999999973, + "941": 104.44999999999978, + "942": 108.59999999999977, + "943": 105.79999999999973, + "944": 71.04999999999976, + "945": 106.94999999999973, + "946": 75.59999999999977, + "947": 103.14999999999978, + "948": 102.74999999999977, + "949": 106.99999999999973, + "950": 103.24999999999976, + "951": 110.54999999999983, + "952": 110.44999999999989, + "953": 104.49999999999974, + "954": 39.849999999999824, + "955": 104.94999999999975, + "956": -63.350000000000016, + "957": 104.04999999999977, + "958": -88.25, + "959": 103.24999999999977, + "960": 102.44999999999976, + "961": 83.34999999999975, + "962": -69.80000000000001, + "963": 108.59999999999974, + "964": 103.94999999999976, + "965": 105.39999999999974, + "966": 107.39999999999974, + "967": -45.80000000000007, + "968": 105.10000000000008, + "969": 103.69999999999973, + "970": 105.59999999999985, + "971": -79.0, + "972": 102.84999999999977, + "973": 103.44999999999976, + "974": 104.74999999999973, + "975": 103.29999999999977, + "976": -82.45000000000002, + "977": 105.59999999999974, + "978": 104.49999999999983, + "979": -83.35, + "980": 106.89999999999974, + "981": -83.85, + "982": -81.3, + "983": 103.49999999999972, + "984": 56.149999999999764, + "985": 106.19999999999978, + "986": 110.19999999999976, + "987": 108.69999999999976, + "988": 108.39999999999975, + "989": -41.05000000000001, + "990": 107.40000000000003, + "991": 46.79999999999997, + "992": 110.34999999999987, + "993": 106.39999999999972, + "994": 104.39999999999975, + "995": 106.09999999999972, + "996": 104.19999999999976, + "997": 107.14999999999974, + "998": 105.64999999999972, + "999": 103.14999999999976, + "1000": 107.24999999999979 + } +} diff --git a/benchmark/results/v3/v3.3.0/session_metadata/5.json b/benchmark/results/v3/v3.3.0/session_metadata/5.json new file mode 100644 index 00000000..018d05a9 --- /dev/null +++ b/benchmark/results/v3/v3.3.0/session_metadata/5.json @@ -0,0 +1,1009 @@ +{ + "total_episodes": 1001, + "total_time_steps": 128000, + "total_s": 1432.237888, + "s_per_step": 0.044757434000000006, + "s_per_100_steps_10_nodes": 4.475743400000001, + "total_reward_per_episode": { + "1": -4.399999999999995, + "2": -48.50000000000004, + "3": -109.5, + "4": -54.500000000000085, + "5": -15.949999999999978, + "6": -80.89999999999992, + "7": -15.349999999999982, + "8": -23.29999999999995, + "9": -34.350000000000016, + "10": -49.800000000000054, + "11": -46.95000000000006, + "12": -22.699999999999953, + "13": -32.35000000000003, + "14": -24.199999999999942, + "15": -51.150000000000176, + "16": -52.20000000000008, + "17": -68.60000000000007, + "18": -30.400000000000006, + "19": -19.99999999999996, + "20": -73.15000000000002, + "21": -17.949999999999974, + "22": -12.949999999999987, + "23": -61.25, + "24": -35.19999999999998, + "25": -70.70000000000005, + "26": -96.6, + "27": -48.550000000000146, + "28": -4.599999999999975, + "29": -7.4, + "30": -44.050000000000175, + "31": -4.29999999999998, + "32": -21.999999999999957, + "33": -78.30000000000004, + "34": -15.099999999999985, + "35": -55.60000000000003, + "36": -51.800000000000075, + "37": -20.39999999999996, + "38": -22.499999999999954, + "39": -104.3, + "40": -45.75000000000005, + "41": 2.100000000000044, + "42": -21.099999999999987, + "43": -14.99999999999998, + "44": -94.15, + "45": -45.70000000000012, + "46": -17.399999999999974, + "47": -29.099999999999948, + "48": -13.749999999999986, + "49": -87.25, + "50": -47.04999999999999, + "51": -23.89999999999995, + "52": -47.75000000000007, + "53": -13.699999999999976, + "54": -17.74999999999997, + "55": -23.799999999999972, + "56": -16.49999999999998, + "57": -21.299999999999958, + "58": -13.099999999999985, + "59": -96.69999999999997, + "60": -23.44999999999995, + "61": -8.399999999999995, + "62": -37.65000000000005, + "63": -20.349999999999962, + "64": -19.049999999999958, + "65": -17.24999999999998, + "66": -8.550000000000006, + "67": -18.14999999999997, + "68": -69.45000000000005, + "69": -16.999999999999975, + "70": -72.19999999999999, + "71": -29.599999999999994, + "72": -19.049999999999965, + "73": -7.249999999999993, + "74": -16.049999999999983, + "75": -17.49999999999997, + "76": -18.29999999999997, + "77": -15.799999999999976, + "78": -6.299999999999986, + "79": -17.24999999999997, + "80": -20.999999999999957, + "81": -12.84999999999996, + "82": -77.1, + "83": -18.34999999999997, + "84": -16.24999999999998, + "85": -51.75000000000008, + "86": -19.649999999999963, + "87": -0.5999999999999621, + "88": 4.700000000000018, + "89": -39.45000000000005, + "90": -19.79999999999996, + "91": -15.999999999999979, + "92": -45.39999999999999, + "93": 9.800000000000018, + "94": -81.9, + "95": 0.5499999999999933, + "96": -3.149999999999971, + "97": -16.899999999999974, + "98": -3.899999999999987, + "99": -36.55000000000004, + "100": -63.0000000000001, + "101": 13.300000000000004, + "102": -51.25000000000008, + "103": -1.849999999999997, + "104": -42.54999999999998, + "105": -84.10000000000001, + "106": -97.94999999999999, + "107": -17.65, + "108": -18.44999999999997, + "109": -15.399999999999975, + "110": 24.44999999999998, + "111": -40.20000000000011, + "112": -6.250000000000002, + "113": -22.74999999999996, + "114": -5.699999999999991, + "115": -18.64999999999996, + "116": -1.3500000000000008, + "117": 14.250000000000053, + "118": -53.49999999999998, + "119": -71.89999999999999, + "120": -29.299999999999994, + "121": 24.949999999999925, + "122": 17.000000000000025, + "123": -26.649999999999945, + "124": -50.449999999999974, + "125": 27.949999999999967, + "126": -71.39999999999999, + "127": -21.999999999999954, + "128": -15.499999999999979, + "129": -19.799999999999965, + "130": 10.100000000000012, + "131": -56.80000000000011, + "132": 19.60000000000007, + "133": -1.2499999999999811, + "134": -16.149999999999945, + "135": 1.1000000000000354, + "136": -8.749999999999972, + "137": -9.65, + "138": -16.649999999999977, + "139": -14.499999999999984, + "140": -9.949999999999998, + "141": 3.2500000000000187, + "142": 32.10000000000004, + "143": -3.199999999999998, + "144": 9.300000000000034, + "145": -26.29999999999997, + "146": 11.149999999999995, + "147": -3.199999999999984, + "148": -26.599999999999973, + "149": -12.699999999999967, + "150": -0.19999999999997642, + "151": -18.649999999999967, + "152": -42.80000000000001, + "153": 14.649999999999956, + "154": 5.300000000000017, + "155": -9.89999999999999, + "156": -0.4499999999999653, + "157": -40.65000000000006, + "158": 0.2000000000000146, + "159": -2.250000000000001, + "160": -85.30000000000001, + "161": 2.050000000000021, + "162": 3.450000000000025, + "163": -85.69999999999999, + "164": 2.5000000000000036, + "165": -0.5999999999999849, + "166": -10.249999999999996, + "167": -24.849999999999977, + "168": -8.5, + "169": -25.899999999999984, + "170": 18.2, + "171": -94.3, + "172": 5.500000000000007, + "173": 17.050000000000065, + "174": -19.39999999999999, + "175": -8.04999999999999, + "176": -9.949999999999987, + "177": -42.550000000000054, + "178": 27.35000000000007, + "179": -0.19999999999995866, + "180": 23.549999999999894, + "181": 44.899999999999885, + "182": 32.14999999999996, + "183": -15.999999999999975, + "184": 57.5999999999998, + "185": 16.999999999999996, + "186": -10.549999999999995, + "187": 10.550000000000061, + "188": -90.0, + "189": -9.900000000000002, + "190": 20.500000000000007, + "191": 19.15000000000004, + "192": -2.2999999999999963, + "193": -1.799999999999986, + "194": 22.149999999999963, + "195": -14.949999999999976, + "196": 17.100000000000044, + "197": -13.999999999999964, + "198": -0.6499999999999884, + "199": 46.599999999999746, + "200": 43.94999999999977, + "201": 27.95000000000003, + "202": 20.10000000000007, + "203": 51.59999999999976, + "204": 5.600000000000055, + "205": 40.349999999999866, + "206": -56.4, + "207": -7.200000000000007, + "208": 10.650000000000082, + "209": 53.599999999999795, + "210": -49.849999999999994, + "211": 46.29999999999992, + "212": -30.24999999999998, + "213": 52.39999999999978, + "214": 87.45000000000012, + "215": -84.95, + "216": 67.34999999999988, + "217": 46.09999999999985, + "218": 77.1499999999999, + "219": 1.9499999999999933, + "220": 46.24999999999981, + "221": 21.699999999999953, + "222": 34.3499999999999, + "223": -7.899999999999994, + "224": 41.84999999999974, + "225": 7.3500000000000085, + "226": 66.79999999999977, + "227": -2.6999999999999664, + "228": 11.500000000000039, + "229": 0.800000000000008, + "230": -7.500000000000008, + "231": -87.85, + "232": 62.39999999999993, + "233": -1.3500000000000272, + "234": 36.59999999999988, + "235": 91.64999999999998, + "236": 8.9, + "237": -81.39999999999996, + "238": 47.749999999999886, + "239": -9.55000000000004, + "240": 28.299999999999844, + "241": 83.0500000000001, + "242": 40.69999999999978, + "243": 28.649999999999892, + "244": -62.500000000000014, + "245": 72.35000000000011, + "246": -23.900000000000006, + "247": 81.30000000000008, + "248": 63.649999999999764, + "249": 25.249999999999947, + "250": 12.100000000000067, + "251": 58.84999999999993, + "252": 14.199999999999969, + "253": 86.15000000000022, + "254": -10.150000000000007, + "255": 69.09999999999984, + "256": 42.04999999999995, + "257": 36.2, + "258": 56.79999999999995, + "259": 64.35000000000014, + "260": 68.44999999999978, + "261": 81.19999999999982, + "262": 35.15000000000003, + "263": -4.699999999999989, + "264": 106.55000000000028, + "265": 48.44999999999992, + "266": 24.25000000000001, + "267": -54.64999999999994, + "268": 59.049999999999926, + "269": 46.24999999999976, + "270": 32.99999999999999, + "271": 89.09999999999981, + "272": 65.14999999999978, + "273": 89.64999999999995, + "274": 43.44999999999994, + "275": -33.30000000000004, + "276": 103.50000000000017, + "277": -56.74999999999996, + "278": 42.0999999999999, + "279": 92.80000000000008, + "280": -25.349999999999973, + "281": -38.799999999999976, + "282": -83.99999999999997, + "283": 85.84999999999978, + "284": 25.099999999999923, + "285": 42.24999999999992, + "286": 19.150000000000002, + "287": 72.99999999999984, + "288": -71.55000000000001, + "289": 26.99999999999999, + "290": 41.49999999999989, + "291": 31.899999999999864, + "292": -70.84999999999997, + "293": 94.0500000000001, + "294": 36.04999999999999, + "295": -25.900000000000027, + "296": 107.00000000000024, + "297": 59.099999999999866, + "298": 106.05000000000018, + "299": -21.799999999999986, + "300": 31.29999999999999, + "301": 47.9499999999999, + "302": 67.9499999999999, + "303": -39.30000000000008, + "304": 87.04999999999998, + "305": -16.950000000000028, + "306": 57.3499999999999, + "307": 106.35000000000026, + "308": 62.04999999999991, + "309": -21.999999999999982, + "310": 60.59999999999983, + "311": -3.1500000000000057, + "312": 94.70000000000009, + "313": 102.45000000000014, + "314": 92.20000000000016, + "315": -74.55, + "316": 89.00000000000003, + "317": 9.649999999999999, + "318": -70.24999999999997, + "319": -43.899999999999984, + "320": -54.94999999999995, + "321": -13.600000000000005, + "322": 26.799999999999976, + "323": 66.69999999999987, + "324": -12.59999999999998, + "325": -16.349999999999984, + "326": -64.74999999999986, + "327": 61.29999999999982, + "328": 29.900000000000013, + "329": 59.699999999999875, + "330": 67.79999999999981, + "331": -45.15000000000005, + "332": -68.54999999999997, + "333": 21.650000000000002, + "334": 1.5999999999999868, + "335": 48.249999999999915, + "336": 84.09999999999981, + "337": 7.899999999999989, + "338": 78.59999999999984, + "339": -9.949999999999996, + "340": 75.14999999999996, + "341": -44.34999999999996, + "342": 91.85000000000001, + "343": 94.60000000000016, + "344": 73.64999999999999, + "345": 33.3, + "346": 13.299999999999997, + "347": 107.25000000000024, + "348": 40.049999999999976, + "349": -63.89999999999992, + "350": 102.9500000000002, + "351": 51.54999999999981, + "352": 77.19999999999999, + "353": 95.70000000000012, + "354": 47.54999999999994, + "355": 28.650000000000055, + "356": 6.55, + "357": 22.949999999999967, + "358": 103.40000000000018, + "359": 51.34999999999995, + "360": 93.05000000000014, + "361": 95.5000000000001, + "362": 31.199999999999985, + "363": 98.0500000000002, + "364": 52.69999999999979, + "365": -7.450000000000001, + "366": 37.69999999999999, + "367": 64.69999999999985, + "368": 66.89999999999988, + "369": 89.24999999999979, + "370": -78.4, + "371": 93.25000000000013, + "372": 94.99999999999991, + "373": -2.849999999999964, + "374": 75.34999999999977, + "375": 85.70000000000003, + "376": 98.55000000000013, + "377": 90.14999999999976, + "378": -72.09999999999992, + "379": 97.10000000000015, + "380": 24.199999999999978, + "381": 57.94999999999993, + "382": 72.89999999999978, + "383": 49.799999999999926, + "384": 86.60000000000001, + "385": 78.34999999999981, + "386": 86.70000000000007, + "387": 31.84999999999996, + "388": 51.24999999999995, + "389": 50.29999999999987, + "390": 65.79999999999991, + "391": 83.45, + "392": 61.59999999999988, + "393": 89.7500000000001, + "394": 81.09999999999984, + "395": 95.04999999999974, + "396": 70.14999999999976, + "397": 67.84999999999984, + "398": 0.1999999999999731, + "399": 66.84999999999991, + "400": 55.949999999999775, + "401": 91.90000000000013, + "402": 92.0000000000001, + "403": 81.7999999999998, + "404": 96.0999999999998, + "405": 35.499999999999794, + "406": 48.799999999999955, + "407": 40.050000000000004, + "408": 92.00000000000006, + "409": 104.35, + "410": 86.29999999999978, + "411": -5.849999999999988, + "412": 51.099999999999746, + "413": 64.79999999999974, + "414": 15.150000000000034, + "415": 77.00000000000006, + "416": 68.79999999999991, + "417": 59.64999999999974, + "418": 75.04999999999976, + "419": 38.39999999999998, + "420": 84.29999999999978, + "421": 51.8499999999998, + "422": 37.84999999999993, + "423": 92.69999999999979, + "424": 71.39999999999985, + "425": 75.04999999999986, + "426": 77.64999999999975, + "427": 15.799999999999992, + "428": 25.150000000000013, + "429": 96.44999999999975, + "430": 85.69999999999978, + "431": 78.09999999999994, + "432": 82.39999999999974, + "433": 103.0000000000002, + "434": 95.99999999999973, + "435": 15.200000000000014, + "436": 80.79999999999978, + "437": 63.09999999999979, + "438": 90.84999999999981, + "439": 58.799999999999876, + "440": 75.24999999999987, + "441": 99.05000000000011, + "442": 72.39999999999982, + "443": 94.69999999999992, + "444": 62.14999999999993, + "445": 8.450000000000015, + "446": 93.79999999999976, + "447": 75.60000000000014, + "448": 96.74999999999976, + "449": 44.54999999999987, + "450": -3.7999999999999967, + "451": -43.19999999999999, + "452": 97.99999999999974, + "453": 75.25000000000003, + "454": 90.29999999999978, + "455": 86.69999999999976, + "456": 94.09999999999978, + "457": 79.04999999999977, + "458": 66.74999999999987, + "459": 76.2499999999999, + "460": 101.99999999999974, + "461": 104.69999999999983, + "462": 85.34999999999978, + "463": 97.39999999999975, + "464": 32.35, + "465": 98.29999999999976, + "466": 79.89999999999982, + "467": 101.00000000000016, + "468": 84.14999999999975, + "469": 92.69999999999979, + "470": 93.59999999999977, + "471": 36.899999999999956, + "472": 86.34999999999977, + "473": 105.54999999999991, + "474": 66.39999999999993, + "475": 101.09999999999975, + "476": -5.149999999999967, + "477": 98.49999999999977, + "478": 104.34999999999987, + "479": 104.29999999999991, + "480": 99.94999999999979, + "481": 99.59999999999974, + "482": 95.54999999999986, + "483": 97.19999999999982, + "484": 93.89999999999976, + "485": 75.14999999999996, + "486": 98.19999999999982, + "487": 45.299999999999784, + "488": 92.29999999999977, + "489": 103.09999999999977, + "490": 94.74999999999977, + "491": 101.34999999999977, + "492": 99.19999999999976, + "493": 91.89999999999979, + "494": 86.89999999999976, + "495": 103.54999999999976, + "496": 41.999999999999964, + "497": 100.49999999999977, + "498": 95.24999999999977, + "499": 103.89999999999974, + "500": 96.69999999999976, + "501": 96.59999999999977, + "502": 69.49999999999974, + "503": 81.34999999999978, + "504": 101.54999999999977, + "505": 82.44999999999978, + "506": 97.84999999999975, + "507": 93.29999999999977, + "508": 61.34999999999976, + "509": 94.99999999999979, + "510": 68.74999999999973, + "511": 104.84999999999975, + "512": 102.64999999999974, + "513": 102.84999999999975, + "514": -2.3000000000000753, + "515": 102.99999999999974, + "516": 99.1999999999998, + "517": 103.54999999999978, + "518": 98.59999999999978, + "519": 104.39999999999974, + "520": -76.1, + "521": 50.99999999999976, + "522": 103.04999999999976, + "523": -25.400000000000066, + "524": 93.29999999999983, + "525": 71.04999999999976, + "526": 94.49999999999976, + "527": 98.24999999999976, + "528": 103.99999999999986, + "529": 99.59999999999977, + "530": 99.89999999999976, + "531": 106.84999999999974, + "532": 102.74999999999976, + "533": 103.14999999999976, + "534": 97.79999999999974, + "535": 98.84999999999975, + "536": 102.79999999999977, + "537": 26.35000000000006, + "538": 102.39999999999985, + "539": 96.04999999999976, + "540": 83.19999999999983, + "541": 105.34999999999974, + "542": 102.94999999999975, + "543": 98.44999999999975, + "544": 92.8999999999998, + "545": 101.44999999999975, + "546": 107.34999999999982, + "547": 103.99999999999973, + "548": 103.54999999999974, + "549": 22.50000000000004, + "550": 103.94999999999972, + "551": -46.300000000000004, + "552": 101.94999999999975, + "553": 108.94999999999999, + "554": 108.19999999999978, + "555": 105.94999999999972, + "556": 95.14999999999975, + "557": 102.74999999999973, + "558": 103.89999999999972, + "559": 101.54999999999978, + "560": 100.49999999999977, + "561": 87.5499999999998, + "562": 94.84999999999977, + "563": -74.69999999999999, + "564": 105.59999999999975, + "565": 102.74999999999976, + "566": 94.29999999999977, + "567": 96.29999999999978, + "568": 97.69999999999978, + "569": 99.24999999999977, + "570": 103.24999999999973, + "571": 100.19999999999973, + "572": 97.44999999999979, + "573": 102.44999999999972, + "574": 93.74999999999999, + "575": 90.54999999999976, + "576": 101.24999999999977, + "577": 95.69999999999978, + "578": 101.24999999999974, + "579": 78.54999999999987, + "580": 101.94999999999972, + "581": 103.34999999999975, + "582": 109.10000000000002, + "583": 103.49999999999976, + "584": 104.04999999999971, + "585": 106.39999999999972, + "586": 104.74999999999973, + "587": 103.04999999999977, + "588": 13.099999999999978, + "589": 105.45000000000007, + "590": 14.149999999999906, + "591": 104.39999999999975, + "592": 104.3999999999998, + "593": 111.14999999999989, + "594": 102.44999999999976, + "595": 104.99999999999976, + "596": 76.74999999999986, + "597": 102.49999999999976, + "598": 76.04999999999984, + "599": 99.79999999999977, + "600": 49.79999999999981, + "601": 102.39999999999976, + "602": 101.24999999999977, + "603": 78.44999999999993, + "604": 39.39999999999976, + "605": 45.299999999999955, + "606": -85.6, + "607": 103.74999999999976, + "608": 104.19999999999995, + "609": 98.99999999999976, + "610": 95.99999999999974, + "611": 102.59999999999985, + "612": 97.64999999999979, + "613": -56.74999999999997, + "614": 27.650000000000027, + "615": 63.849999999999724, + "616": 103.99999999999976, + "617": 105.04999999999991, + "618": 101.79999999999977, + "619": 101.84999999999977, + "620": 82.54999999999974, + "621": 99.34999999999972, + "622": -76.30000000000007, + "623": 104.09999999999975, + "624": 103.39999999999975, + "625": 101.04999999999976, + "626": 96.29999999999986, + "627": -87.0, + "628": 101.74999999999977, + "629": 104.04999999999977, + "630": 106.84999999999974, + "631": 86.64999999999978, + "632": 102.49999999999974, + "633": 100.34999999999974, + "634": 55.89999999999977, + "635": 102.14999999999975, + "636": 104.34999999999974, + "637": -74.10000000000002, + "638": 105.44999999999972, + "639": 104.09999999999977, + "640": 105.1499999999998, + "641": 82.19999999999985, + "642": -68.75, + "643": 87.99999999999983, + "644": 104.09999999999977, + "645": 105.24999999999974, + "646": 100.54999999999974, + "647": 105.39999999999974, + "648": 103.19999999999976, + "649": 102.29999999999977, + "650": 102.94999999999976, + "651": 103.59999999999977, + "652": 102.04999999999976, + "653": 102.44999999999976, + "654": 99.94999999999976, + "655": 105.44999999999973, + "656": 42.85000000000001, + "657": 103.99999999999976, + "658": 104.19999999999976, + "659": 103.74999999999974, + "660": 53.79999999999975, + "661": 104.19999999999976, + "662": 109.49999999999976, + "663": 87.64999999999975, + "664": 102.49999999999973, + "665": -44.65000000000006, + "666": 104.14999999999976, + "667": 49.89999999999995, + "668": 105.79999999999973, + "669": 105.14999999999974, + "670": 73.8999999999998, + "671": 85.89999999999984, + "672": 97.04999999999978, + "673": 104.59999999999975, + "674": 103.99999999999976, + "675": 101.59999999999977, + "676": 106.14999999999982, + "677": 98.69999999999978, + "678": 106.69999999999975, + "679": 99.94999999999978, + "680": 99.24999999999976, + "681": 104.19999999999976, + "682": 104.14999999999976, + "683": 81.3999999999999, + "684": 98.69999999999976, + "685": 101.99999999999972, + "686": 105.24999999999974, + "687": 99.84999999999977, + "688": 103.49999999999976, + "689": 103.69999999999975, + "690": 104.09999999999972, + "691": 101.89999999999976, + "692": 106.94999999999976, + "693": 103.84999999999974, + "694": 104.44999999999973, + "695": 104.74999999999973, + "696": 87.49999999999972, + "697": 102.79999999999974, + "698": 103.69999999999976, + "699": 79.29999999999987, + "700": 108.04999999999973, + "701": 57.749999999999766, + "702": 106.29999999999973, + "703": 103.79999999999977, + "704": 107.54999999999983, + "705": -0.600000000000033, + "706": -80.14999999999999, + "707": 99.44999999999975, + "708": 107.99999999999976, + "709": 97.29999999999976, + "710": 89.29999999999977, + "711": 102.24999999999973, + "712": -38.75000000000003, + "713": -34.04999999999999, + "714": 103.69999999999973, + "715": 102.09999999999974, + "716": -43.19999999999999, + "717": 104.39999999999975, + "718": 0.5999999999999659, + "719": 99.69999999999976, + "720": 105.79999999999973, + "721": 103.09999999999977, + "722": 105.39999999999976, + "723": 105.14999999999976, + "724": 104.34999999999977, + "725": 104.04999999999976, + "726": -65.4, + "727": -42.550000000000054, + "728": 104.29999999999984, + "729": 103.44999999999976, + "730": -78.75, + "731": 103.84999999999975, + "732": 105.54999999999973, + "733": 91.7499999999998, + "734": 109.29999999999976, + "735": 110.54999999999991, + "736": 103.84999999999975, + "737": 108.04999999999973, + "738": -72.50000000000001, + "739": 109.09999999999977, + "740": 89.24999999999977, + "741": 103.29999999999976, + "742": 109.34999999999975, + "743": 102.84999999999977, + "744": 108.84999999999975, + "745": 105.59999999999974, + "746": 81.19999999999978, + "747": 100.99999999999977, + "748": 105.19999999999972, + "749": 58.29999999999976, + "750": 46.79999999999987, + "751": 67.84999999999987, + "752": 103.54999999999974, + "753": 88.74999999999976, + "754": 105.14999999999974, + "755": 109.55, + "756": 70.69999999999975, + "757": 103.94999999999975, + "758": 101.74999999999972, + "759": 105.14999999999972, + "760": 103.99999999999974, + "761": 102.69999999999978, + "762": 104.19999999999975, + "763": 104.39999999999975, + "764": -77.95, + "765": 25.599999999999895, + "766": 108.89999999999975, + "767": 106.34999999999977, + "768": 96.54999999999981, + "769": 104.24999999999976, + "770": 106.89999999999972, + "771": 105.19999999999973, + "772": 103.24999999999977, + "773": 103.14999999999976, + "774": 97.49999999999976, + "775": 104.24999999999977, + "776": 105.34999999999978, + "777": 84.54999999999984, + "778": 104.84999999999975, + "779": 104.04999999999973, + "780": 103.84999999999977, + "781": 106.94999999999973, + "782": 100.54999999999976, + "783": 80.19999999999978, + "784": 105.39999999999972, + "785": 103.74999999999977, + "786": 104.79999999999976, + "787": 107.49999999999973, + "788": 106.54999999999973, + "789": -39.34999999999994, + "790": 107.29999999999974, + "791": -74.0, + "792": 107.14999999999974, + "793": 102.84999999999977, + "794": 93.24999999999972, + "795": 108.14999999999974, + "796": -67.60000000000002, + "797": 103.89999999999976, + "798": 105.39999999999972, + "799": 104.99999999999973, + "800": 102.39999999999975, + "801": 106.74999999999977, + "802": 103.09999999999972, + "803": 105.39999999999974, + "804": 100.54999999999977, + "805": 109.39999999999976, + "806": 111.59999999999977, + "807": 104.84999999999974, + "808": 104.09999999999977, + "809": -102.30000000000001, + "810": 104.74999999999974, + "811": 106.19999999999973, + "812": 104.89999999999974, + "813": -72.50000000000001, + "814": 104.94999999999975, + "815": 103.84999999999977, + "816": 103.99999999999974, + "817": -64.64999999999999, + "818": 105.09999999999974, + "819": 105.99999999999972, + "820": 24.24999999999989, + "821": 102.74999999999977, + "822": 100.09999999999977, + "823": 104.19999999999976, + "824": 109.59999999999981, + "825": 105.09999999999972, + "826": 102.59999999999977, + "827": 102.94999999999976, + "828": -76.85000000000002, + "829": 106.19999999999975, + "830": 90.54999999999974, + "831": 41.94999999999977, + "832": -87.19999999999999, + "833": 106.49999999999974, + "834": 103.29999999999977, + "835": 106.24999999999972, + "836": 106.24999999999973, + "837": 104.84999999999975, + "838": 105.29999999999974, + "839": 103.59999999999977, + "840": 91.04999999999981, + "841": 103.59999999999975, + "842": 103.99999999999976, + "843": 106.24999999999973, + "844": 74.99999999999987, + "845": 103.29999999999977, + "846": 104.04999999999976, + "847": 106.99999999999973, + "848": -83.75000000000001, + "849": 105.79999999999973, + "850": -76.30000000000001, + "851": 105.24999999999972, + "852": 105.79999999999973, + "853": 100.84999999999977, + "854": 104.99999999999976, + "855": 105.09999999999972, + "856": 83.89999999999976, + "857": 107.24999999999983, + "858": 103.54999999999977, + "859": -72.05, + "860": 104.09999999999972, + "861": 103.59999999999977, + "862": 104.84999999999975, + "863": -74.89999999999999, + "864": 103.04999999999977, + "865": 104.29999999999977, + "866": 99.69999999999975, + "867": 104.24999999999974, + "868": 95.74999999999976, + "869": 104.59999999999975, + "870": 100.24999999999977, + "871": 104.04999999999976, + "872": 102.64999999999976, + "873": 104.59999999999975, + "874": 102.74999999999977, + "875": 104.39999999999975, + "876": 102.89999999999978, + "877": 104.54999999999977, + "878": 103.74999999999977, + "879": -79.10000000000001, + "880": 104.24999999999976, + "881": 103.49999999999977, + "882": -86.35000000000002, + "883": 103.39999999999976, + "884": 105.39999999999975, + "885": 100.3499999999998, + "886": 107.29999999999973, + "887": 104.09999999999975, + "888": 102.69999999999978, + "889": 101.84999999999977, + "890": 105.29999999999974, + "891": 103.54999999999977, + "892": 102.24999999999976, + "893": 105.29999999999973, + "894": 102.24999999999977, + "895": 97.74999999999973, + "896": 105.94999999999972, + "897": 103.84999999999977, + "898": 68.49999999999977, + "899": 97.24999999999977, + "900": -85.44999999999999, + "901": 103.79999999999977, + "902": 101.29999999999978, + "903": 101.94999999999976, + "904": 105.84999999999981, + "905": 105.44999999999982, + "906": 104.09999999999975, + "907": 109.04999999999977, + "908": 105.44999999999975, + "909": 103.19999999999978, + "910": 105.14999999999979, + "911": -85.65, + "912": 101.89999999999976, + "913": 108.25000000000006, + "914": 107.04999999999971, + "915": 107.29999999999977, + "916": 104.89999999999978, + "917": 104.24999999999976, + "918": 104.69999999999975, + "919": 105.4499999999998, + "920": 108.49999999999986, + "921": 108.34999999999987, + "922": 99.99999999999974, + "923": 2.0499999999999616, + "924": 103.64999999999975, + "925": 104.49999999999974, + "926": 103.84999999999977, + "927": 107.3, + "928": 104.59999999999977, + "929": 103.84999999999977, + "930": 10.249999999999922, + "931": 103.24999999999976, + "932": 105.59999999999974, + "933": 105.59999999999977, + "934": 97.09999999999977, + "935": 105.44999999999973, + "936": 104.09999999999972, + "937": 103.69999999999976, + "938": 105.29999999999974, + "939": 25.800000000000054, + "940": 105.59999999999975, + "941": 105.49999999999974, + "942": -74.49999999999999, + "943": 105.54999999999974, + "944": 104.14999999999976, + "945": 103.39999999999976, + "946": 104.79999999999976, + "947": 103.09999999999977, + "948": 54.35, + "949": 87.79999999999978, + "950": 104.19999999999976, + "951": 105.64999999999974, + "952": 104.49999999999974, + "953": 103.44999999999976, + "954": 61.849999999999945, + "955": 104.64999999999974, + "956": 103.54999999999977, + "957": 104.39999999999976, + "958": 102.69999999999978, + "959": 103.19999999999976, + "960": 103.04999999999977, + "961": 104.39999999999976, + "962": 82.99999999999973, + "963": 105.14999999999974, + "964": 104.04999999999974, + "965": 105.29999999999981, + "966": 105.04999999999974, + "967": -91.4, + "968": 105.09999999999974, + "969": 106.94999999999995, + "970": -50.55, + "971": 104.24999999999976, + "972": 104.09999999999981, + "973": -86.0, + "974": -32.00000000000002, + "975": 108.34999999999977, + "976": 106.34999999999984, + "977": -85.35, + "978": -45.9, + "979": 110.29999999999991, + "980": 108.49999999999976, + "981": 105.74999999999972, + "982": 104.59999999999977, + "983": 106.44999999999972, + "984": 105.59999999999974, + "985": -87.0, + "986": 106.69999999999978, + "987": 104.34999999999975, + "988": -53.199999999999974, + "989": 112.05000000000018, + "990": 104.34999999999972, + "991": 102.64999999999976, + "992": -84.9, + "993": -39.350000000000044, + "994": 103.94999999999976, + "995": 102.04999999999977, + "996": 103.64999999999976, + "997": 100.3499999999998, + "998": 84.7999999999998, + "999": 105.09999999999974, + "1000": 106.89999999999974 + } +} diff --git a/benchmark/results/v3/v3.3.0/v3.3.0_benchmark_metadata.json b/benchmark/results/v3/v3.3.0/v3.3.0_benchmark_metadata.json new file mode 100644 index 00000000..5aa47d95 --- /dev/null +++ b/benchmark/results/v3/v3.3.0/v3.3.0_benchmark_metadata.json @@ -0,0 +1,7445 @@ +{ + "start_timestamp": "2024-09-02T07:51:23.135859", + "end_datetime": "2024-09-02T09:52:55.690035", + "primaite_version": "3.3.0", + "system_info": { + "System": { + "OS": "Linux", + "OS Version": "#76~20.04.1-Ubuntu SMP Thu Jun 13 18:00:23 UTC 2024", + "Machine": "x86_64", + "Processor": "x86_64" + }, + "CPU": { + "Physical Cores": 2, + "Total Cores": 4, + "Max Frequency": "0.00Mhz" + }, + "Memory": { + "Total": "15.62GB", + "Swap Total": "0.00B" + }, + "GPU": [] + }, + "total_sessions": 5, + "total_episodes": 5005, + "total_time_steps": 640000, + "av_s_per_session": 1458.2831048, + "av_s_per_step": 0.045571347025, + "av_s_per_100_steps_10_nodes": 4.557134702499999, + "combined_total_reward_per_episode": { + "1": -31.150000000000027, + "2": -24.120000000000005, + "3": -58.980000000000054, + "4": -27.500000000000018, + "5": -45.17999999999997, + "6": -48.62999999999999, + "7": -24.21, + "8": -40.81999999999998, + "9": -38.09999999999998, + "10": -33.23000000000004, + "11": -40.03000000000001, + "12": -21.52999999999998, + "13": -32.470000000000006, + "14": -20.189999999999976, + "15": -35.86000000000003, + "16": -56.580000000000055, + "17": -40.67, + "18": -36.28000000000003, + "19": -25.27999999999999, + "20": -34.830000000000005, + "21": -37.80000000000001, + "22": -24.780000000000037, + "23": -36.100000000000016, + "24": -58.85999999999998, + "25": -44.12000000000001, + "26": -44.88000000000005, + "27": -47.86000000000003, + "28": -24.62999999999999, + "29": -32.91000000000004, + "30": -31.480000000000054, + "31": -11.87999999999999, + "32": -33.00000000000004, + "33": -29.439999999999998, + "34": -26.599999999999984, + "35": -27.00999999999998, + "36": -27.330000000000002, + "37": -20.46, + "38": -17.77999999999997, + "39": -34.75999999999998, + "40": -19.549999999999994, + "41": -45.23999999999998, + "42": -14.519999999999976, + "43": -31.900000000000006, + "44": -29.989999999999988, + "45": -23.990000000000006, + "46": -19.049999999999983, + "47": -38.3, + "48": -29.439999999999962, + "49": -42.929999999999986, + "50": -28.78000000000001, + "51": -27.989999999999974, + "52": -37.06000000000004, + "53": -26.189999999999987, + "54": -26.169999999999995, + "55": -33.34999999999999, + "56": -25.149999999999988, + "57": -21.830000000000002, + "58": -30.109999999999996, + "59": -40.110000000000014, + "60": -29.699999999999996, + "61": -40.45, + "62": -55.30999999999999, + "63": -12.659999999999979, + "64": -8.93999999999997, + "65": -44.79999999999999, + "66": -23.430000000000017, + "67": -42.80000000000003, + "68": -20.779999999999994, + "69": -28.339999999999957, + "70": -55.60999999999999, + "71": -38.780000000000015, + "72": -19.209999999999987, + "73": -13.119999999999987, + "74": -23.060000000000002, + "75": -12.809999999999985, + "76": -30.29000000000001, + "77": -43.290000000000035, + "78": -13.96999999999999, + "79": -25.77000000000001, + "80": -42.06999999999998, + "81": -47.51000000000003, + "82": -30.359999999999992, + "83": -23.109999999999985, + "84": -27.729999999999997, + "85": -30.330000000000005, + "86": -24.25999999999997, + "87": -48.58999999999999, + "88": -51.84999999999998, + "89": -11.189999999999992, + "90": -30.869999999999983, + "91": -14.449999999999983, + "92": -26.95999999999996, + "93": -27.529999999999994, + "94": -41.559999999999995, + "95": -34.67000000000001, + "96": -11.709999999999974, + "97": -20.82999999999998, + "98": -21.28000000000001, + "99": -33.040000000000006, + "100": -18.92, + "101": -8.719999999999988, + "102": -31.490000000000002, + "103": -31.589999999999996, + "104": -45.46000000000001, + "105": -26.609999999999978, + "106": -31.169999999999998, + "107": -11.370000000000031, + "108": -31.129999999999978, + "109": -28.180000000000014, + "110": -3.689999999999988, + "111": -42.41000000000003, + "112": -45.730000000000004, + "113": -25.38999999999998, + "114": -4.759999999999978, + "115": -25.609999999999992, + "116": -15.369999999999994, + "117": -17.519999999999982, + "118": -12.899999999999988, + "119": -26.37000000000001, + "120": -27.669999999999987, + "121": 1.6599999999999788, + "122": -15.489999999999972, + "123": -9.929999999999968, + "124": -13.20999999999998, + "125": -16.640000000000004, + "126": -26.279999999999983, + "127": -6.199999999999985, + "128": -12.840000000000014, + "129": 3.2000000000000206, + "130": -4.3099999999999925, + "131": -49.72000000000001, + "132": -17.910000000000004, + "133": -10.069999999999997, + "134": -2.340000000000021, + "135": -10.349999999999973, + "136": -7.959999999999985, + "137": 0.2799999999999724, + "138": 7.499999999999981, + "139": -14.170000000000007, + "140": 1.790000000000011, + "141": -21.550000000000015, + "142": -19.949999999999992, + "143": -25.629999999999985, + "144": -1.7200000000000384, + "145": -34.04999999999999, + "146": -35.960000000000015, + "147": -13.890000000000049, + "148": -35.07000000000001, + "149": -16.080000000000005, + "150": -16.429999999999986, + "151": -12.899999999999974, + "152": -13.199999999999983, + "153": 11.760000000000002, + "154": -11.419999999999987, + "155": -42.459999999999994, + "156": 4.920000000000014, + "157": -23.740000000000027, + "158": -9.159999999999979, + "159": -14.330000000000052, + "160": -40.329999999999984, + "161": -17.169999999999966, + "162": -22.860000000000035, + "163": -49.34999999999999, + "164": 0.21999999999996903, + "165": -44.469999999999985, + "166": -7.800000000000011, + "167": -8.789999999999983, + "168": 0.7400000000000091, + "169": -15.620000000000019, + "170": -37.639999999999965, + "171": -17.270000000000007, + "172": 6.609999999999999, + "173": -9.319999999999975, + "174": -25.53000000000001, + "175": -5.8800000000000185, + "176": -21.32999999999999, + "177": -27.679999999999996, + "178": -11.169999999999984, + "179": -32.34000000000003, + "180": -16.680000000000017, + "181": -23.170000000000044, + "182": 2.4100000000000144, + "183": -34.49, + "184": 14.399999999999974, + "185": -7.49, + "186": -13.279999999999987, + "187": 3.359999999999979, + "188": -14.279999999999998, + "189": -36.42000000000002, + "190": -35.84000000000003, + "191": 4.539999999999978, + "192": -12.909999999999977, + "193": -19.490000000000002, + "194": 13.959999999999974, + "195": -19.449999999999992, + "196": 0.7100000000000165, + "197": -14.080000000000036, + "198": -16.15000000000005, + "199": 1.7799999999999216, + "200": -24.000000000000046, + "201": 3.0499999999999567, + "202": -5.779999999999932, + "203": 15.749999999999963, + "204": 7.589999999999941, + "205": 3.0800000000000103, + "206": -19.850000000000016, + "207": -13.800000000000017, + "208": 6.149999999999972, + "209": 3.7199999999999465, + "210": -0.5300000000000026, + "211": 34.69999999999997, + "212": 13.419999999999892, + "213": 15.189999999999946, + "214": 16.299999999999972, + "215": -19.050000000000033, + "216": -6.830000000000007, + "217": -7.000000000000009, + "218": 22.429999999999986, + "219": -35.6, + "220": 6.999999999999927, + "221": -4.110000000000063, + "222": 36.799999999999905, + "223": -2.720000000000055, + "224": 22.22999999999995, + "225": -0.17999999999999616, + "226": 2.3199999999999377, + "227": 2.4299999999999953, + "228": -10.180000000000007, + "229": 1.209999999999996, + "230": -17.150000000000052, + "231": 0.009999999999945431, + "232": 44.68999999999996, + "233": -5.98999999999999, + "234": 22.259999999999955, + "235": 28.619999999999965, + "236": 0.23999999999989718, + "237": -11.379999999999999, + "238": -1.0200000000000045, + "239": -2.4800000000001, + "240": 4.389999999999977, + "241": 37.929999999999964, + "242": 24.259999999999923, + "243": -13.700000000000045, + "244": -0.44000000000004036, + "245": 0.3800000000000523, + "246": 11.13999999999992, + "247": 28.129999999999985, + "248": 20.419999999999867, + "249": -7.110000000000016, + "250": 13.539999999999978, + "251": 30.079999999999934, + "252": 34.03999999999993, + "253": 20.980000000000036, + "254": -15.620000000000037, + "255": 21.61999999999992, + "256": 34.64999999999996, + "257": 20.19999999999998, + "258": 9.09, + "259": 45.66999999999995, + "260": 23.619999999999873, + "261": 39.32999999999989, + "262": 39.34999999999998, + "263": -4.2700000000000475, + "264": 22.929999999999975, + "265": 43.40999999999989, + "266": 47.399999999999906, + "267": 10.189999999999973, + "268": 34.369999999999926, + "269": -4.230000000000075, + "270": 26.829999999999973, + "271": 3.339999999999938, + "272": 14.159999999999908, + "273": 44.669999999999916, + "274": 36.76999999999992, + "275": -15.070000000000025, + "276": 47.57999999999996, + "277": 14.60999999999992, + "278": 29.189999999999934, + "279": 32.689999999999955, + "280": -9.950000000000014, + "281": 34.77999999999991, + "282": 3.6899999999999635, + "283": 31.219999999999935, + "284": 4.25999999999997, + "285": -15.660000000000007, + "286": 13.629999999999976, + "287": 33.17999999999989, + "288": -0.5700000000000784, + "289": 33.59999999999989, + "290": 47.70999999999989, + "291": 49.729999999999905, + "292": 14.299999999999926, + "293": 55.01999999999994, + "294": 7.129999999999946, + "295": 16.76999999999986, + "296": 42.690000000000026, + "297": 31.47999999999991, + "298": 53.39999999999998, + "299": 21.41999999999994, + "300": 23.90999999999995, + "301": 42.98999999999989, + "302": 40.08999999999988, + "303": -5.340000000000016, + "304": 41.81999999999988, + "305": 4.379999999999972, + "306": 16.579999999999938, + "307": 44.09000000000002, + "308": 29.949999999999932, + "309": -6.280000000000088, + "310": 35.53999999999984, + "311": 28.089999999999947, + "312": 24.869999999999955, + "313": 32.00999999999999, + "314": 40.7799999999999, + "315": 31.34999999999993, + "316": 35.95999999999991, + "317": 60.019999999999825, + "318": 20.52999999999991, + "319": 13.309999999999894, + "320": 5.959999999999939, + "321": 17.189999999999948, + "322": 54.0699999999999, + "323": 39.47999999999989, + "324": 18.23999999999995, + "325": 25.979999999999887, + "326": 46.820000000000036, + "327": 32.4199999999999, + "328": 55.68999999999986, + "329": 42.029999999999966, + "330": 42.81999999999991, + "331": 9.429999999999959, + "332": 40.06999999999995, + "333": 20.679999999999925, + "334": 12.259999999999945, + "335": 53.54999999999991, + "336": 26.949999999999967, + "337": 57.819999999999915, + "338": 40.59999999999989, + "339": 14.219999999999976, + "340": 41.11999999999993, + "341": 26.529999999999887, + "342": 82.85999999999987, + "343": 54.69999999999993, + "344": 36.91999999999997, + "345": 63.75999999999992, + "346": 47.86999999999993, + "347": 15.77000000000001, + "348": 58.689999999999905, + "349": 17.36999999999992, + "350": 75.84999999999992, + "351": 38.12999999999988, + "352": 48.089999999999904, + "353": 30.059999999999967, + "354": 47.14999999999994, + "355": 15.969999999999947, + "356": 45.139999999999944, + "357": 35.809999999999974, + "358": 49.07999999999996, + "359": 30.229999999999972, + "360": 57.629999999999924, + "361": 48.519999999999996, + "362": 55.279999999999916, + "363": 69.58999999999995, + "364": 8.40999999999992, + "365": 32.8199999999999, + "366": 55.37999999999994, + "367": 24.869999999999965, + "368": 51.60999999999994, + "369": 45.65999999999997, + "370": 4.459999999999994, + "371": 81.42999999999995, + "372": 27.13999999999994, + "373": 61.599999999999945, + "374": 61.43999999999987, + "375": 20.80000000000003, + "376": 55.02999999999996, + "377": 61.54999999999984, + "378": 19.74999999999989, + "379": 46.219999999999985, + "380": 28.869999999999948, + "381": 37.1499999999999, + "382": 60.099999999999866, + "383": 67.3199999999999, + "384": 70.66000000000001, + "385": 57.57999999999991, + "386": 75.86999999999996, + "387": 51.979999999999905, + "388": -20.53000000000003, + "389": 61.079999999999814, + "390": 60.70999999999994, + "391": 38.579999999999956, + "392": 35.54999999999991, + "393": 46.25999999999992, + "394": 63.359999999999914, + "395": 66.2699999999999, + "396": 57.399999999999906, + "397": 41.03999999999989, + "398": 50.29999999999992, + "399": 50.559999999999874, + "400": 27.499999999999904, + "401": 90.24999999999999, + "402": 62.03999999999995, + "403": 65.60999999999987, + "404": 47.489999999999945, + "405": 46.15999999999988, + "406": 24.029999999999962, + "407": 53.949999999999896, + "408": 62.509999999999934, + "409": 73.69999999999996, + "410": 59.159999999999926, + "411": 35.57999999999987, + "412": 76.12999999999982, + "413": 23.87999999999991, + "414": 71.43999999999994, + "415": 47.32999999999995, + "416": 71.10999999999989, + "417": 39.41999999999994, + "418": 61.71999999999989, + "419": 58.929999999999964, + "420": 52.46999999999986, + "421": 54.10999999999988, + "422": 45.10999999999985, + "423": 65.15999999999983, + "424": 49.32999999999995, + "425": 73.25999999999983, + "426": 36.73999999999988, + "427": 43.499999999999915, + "428": 16.95999999999999, + "429": 55.30999999999987, + "430": 66.1899999999999, + "431": 74.72999999999993, + "432": 58.269999999999825, + "433": 62.199999999999974, + "434": 63.50999999999984, + "435": 41.00999999999989, + "436": 43.839999999999854, + "437": 47.52999999999988, + "438": 35.90999999999993, + "439": -5.400000000000036, + "440": 47.94999999999989, + "441": 60.04999999999994, + "442": 68.68999999999994, + "443": 65.46999999999994, + "444": 58.889999999999986, + "445": 66.55999999999992, + "446": 48.54999999999988, + "447": 57.32999999999997, + "448": 71.9599999999999, + "449": 58.319999999999936, + "450": 45.3899999999999, + "451": 51.959999999999965, + "452": 63.91999999999983, + "453": 44.42999999999995, + "454": 66.66999999999987, + "455": 63.289999999999864, + "456": 56.299999999999876, + "457": 53.309999999999924, + "458": 58.79999999999992, + "459": 49.09999999999991, + "460": 39.46999999999994, + "461": 62.69999999999989, + "462": 67.79999999999987, + "463": 69.79999999999981, + "464": 56.479999999999926, + "465": 75.68999999999994, + "466": 77.67999999999982, + "467": 63.219999999999914, + "468": 58.65999999999991, + "469": 82.18999999999983, + "470": 77.21999999999989, + "471": 61.6899999999999, + "472": 66.9599999999999, + "473": 72.02999999999989, + "474": 17.75999999999995, + "475": 75.15999999999988, + "476": 39.530000000000015, + "477": 51.84999999999989, + "478": 20.799999999999905, + "479": 60.65999999999991, + "480": 78.91999999999989, + "481": 63.79999999999984, + "482": 48.69999999999991, + "483": 29.709999999999887, + "484": 87.03999999999985, + "485": 49.729999999999905, + "486": 79.38999999999992, + "487": 44.49999999999987, + "488": 64.10999999999987, + "489": 78.28999999999992, + "490": 55.85999999999986, + "491": 73.13999999999987, + "492": 57.13999999999986, + "493": 57.32999999999987, + "494": 42.10999999999992, + "495": 78.93999999999987, + "496": 60.34999999999987, + "497": 62.12999999999992, + "498": 55.62999999999986, + "499": 88.21999999999991, + "500": 80.0799999999999, + "501": 97.4499999999999, + "502": 40.44999999999989, + "503": 54.74999999999985, + "504": 65.29999999999987, + "505": 92.9599999999999, + "506": 60.41999999999986, + "507": 68.62999999999991, + "508": 55.53999999999993, + "509": 62.129999999999896, + "510": 29.11999999999988, + "511": 62.89999999999985, + "512": 89.11999999999986, + "513": 58.33999999999986, + "514": 65.95999999999982, + "515": 44.56999999999986, + "516": 67.79999999999991, + "517": 64.34999999999984, + "518": 70.67999999999986, + "519": 69.77999999999989, + "520": 34.339999999999904, + "521": -19.680000000000057, + "522": 41.51999999999989, + "523": 31.16999999999991, + "524": 55.42999999999986, + "525": 46.69999999999986, + "526": 41.74999999999991, + "527": 85.8899999999998, + "528": 53.35999999999988, + "529": 57.659999999999854, + "530": 53.68999999999987, + "531": 62.419999999999845, + "532": 69.86999999999993, + "533": 71.6499999999999, + "534": 71.10999999999993, + "535": 71.1699999999999, + "536": 69.13999999999987, + "537": 42.4699999999999, + "538": 62.81999999999986, + "539": 67.7799999999999, + "540": 64.84999999999987, + "541": 46.23999999999984, + "542": 60.33999999999992, + "543": 57.64999999999983, + "544": 59.76999999999987, + "545": 47.08999999999992, + "546": 83.2099999999999, + "547": 87.65999999999988, + "548": 59.2199999999999, + "549": 35.779999999999916, + "550": 75.05999999999987, + "551": 16.699999999999932, + "552": 80.55999999999989, + "553": 67.99999999999993, + "554": 4.33999999999993, + "555": 63.51999999999988, + "556": 76.58999999999983, + "557": 62.71999999999987, + "558": 66.86999999999989, + "559": 2.429999999999919, + "560": 22.04999999999995, + "561": 52.72999999999986, + "562": 79.3599999999999, + "563": 36.519999999999996, + "564": 40.33999999999993, + "565": 51.28999999999994, + "566": 83.02999999999984, + "567": 74.00999999999985, + "568": 59.53999999999989, + "569": 50.489999999999874, + "570": 89.86999999999988, + "571": 43.7999999999999, + "572": 40.98999999999989, + "573": 73.21999999999989, + "574": 38.84999999999992, + "575": 54.66999999999989, + "576": 75.30999999999986, + "577": 56.409999999999854, + "578": 15.349999999999891, + "579": 67.74999999999991, + "580": 44.49999999999987, + "581": 41.91999999999992, + "582": 81.98999999999992, + "583": 54.11999999999987, + "584": 45.53999999999987, + "585": 73.87999999999985, + "586": 65.20999999999991, + "587": 66.29999999999984, + "588": 27.9099999999999, + "589": 88.97999999999993, + "590": 41.349999999999866, + "591": 71.80999999999985, + "592": 30.209999999999944, + "593": 70.7599999999999, + "594": 57.809999999999874, + "595": 62.61999999999985, + "596": 78.34999999999994, + "597": 57.789999999999864, + "598": 88.19999999999986, + "599": 69.78999999999982, + "600": 84.57999999999988, + "601": 62.049999999999876, + "602": 61.70999999999988, + "603": 88.40999999999994, + "604": 44.849999999999845, + "605": 54.809999999999874, + "606": 40.05999999999993, + "607": 45.78999999999994, + "608": 71.36999999999995, + "609": 77.25999999999988, + "610": 55.13999999999985, + "611": 68.14999999999989, + "612": 40.43999999999994, + "613": 36.559999999999945, + "614": 27.049999999999994, + "615": 24.469999999999875, + "616": 48.049999999999855, + "617": 78.8499999999999, + "618": 59.3599999999999, + "619": 96.93999999999987, + "620": 70.88999999999989, + "621": 90.40999999999988, + "622": 16.789999999999907, + "623": 91.88999999999982, + "624": 63.09999999999991, + "625": 94.99999999999989, + "626": 84.2899999999999, + "627": 36.77999999999988, + "628": 55.23999999999994, + "629": 25.559999999999892, + "630": 85.34999999999991, + "631": 90.93999999999986, + "632": 56.59999999999993, + "633": 82.9999999999999, + "634": 82.40999999999984, + "635": 72.03999999999985, + "636": 73.71999999999986, + "637": 36.0599999999999, + "638": 29.63999999999993, + "639": 67.5299999999999, + "640": 70.25999999999988, + "641": 86.66999999999992, + "642": 26.199999999999903, + "643": 89.00999999999989, + "644": 77.11999999999993, + "645": 88.12999999999991, + "646": 74.42999999999984, + "647": 85.10999999999983, + "648": 95.67999999999988, + "649": 89.8199999999999, + "650": 94.53999999999988, + "651": 80.91999999999987, + "652": 77.1599999999999, + "653": 83.19999999999995, + "654": 80.42999999999978, + "655": 93.12999999999991, + "656": 79.25999999999989, + "657": 80.28999999999994, + "658": 90.05999999999989, + "659": 87.8999999999999, + "660": 82.50999999999985, + "661": 80.87999999999997, + "662": 85.82999999999984, + "663": 71.14999999999984, + "664": 67.14999999999986, + "665": 64.47999999999995, + "666": 81.85999999999987, + "667": 71.77999999999993, + "668": 74.49999999999987, + "669": 88.01999999999984, + "670": 49.619999999999905, + "671": 87.53999999999992, + "672": 75.94999999999989, + "673": 96.44999999999987, + "674": 61.069999999999865, + "675": 79.25999999999988, + "676": 96.96999999999994, + "677": 66.85999999999981, + "678": 78.09999999999985, + "679": 83.48999999999987, + "680": 84.3199999999999, + "681": 92.7499999999999, + "682": 102.71999999999987, + "683": 75.51999999999987, + "684": 59.20999999999992, + "685": 102.98999999999987, + "686": 89.12999999999985, + "687": 99.39999999999989, + "688": 90.64999999999986, + "689": 87.73999999999987, + "690": 74.44999999999982, + "691": 46.939999999999884, + "692": 99.60999999999989, + "693": 77.02999999999984, + "694": 74.4699999999998, + "695": 89.31999999999991, + "696": 79.11999999999983, + "697": 64.69999999999987, + "698": 99.41999999999993, + "699": 79.41999999999992, + "700": 91.34999999999988, + "701": 78.63999999999986, + "702": 84.61999999999986, + "703": 89.1999999999999, + "704": 86.58999999999986, + "705": 57.85999999999992, + "706": 63.02999999999995, + "707": 79.87999999999981, + "708": 85.51999999999987, + "709": 81.30999999999983, + "710": 58.74999999999985, + "711": 97.47999999999989, + "712": 56.329999999999885, + "713": 60.639999999999915, + "714": 81.75999999999988, + "715": 100.21999999999991, + "716": 53.55999999999998, + "717": 84.63999999999986, + "718": 77.35999999999993, + "719": 93.23999999999991, + "720": 80.8599999999999, + "721": 99.93999999999987, + "722": 93.12999999999985, + "723": 64.61999999999983, + "724": 83.14999999999984, + "725": 60.479999999999926, + "726": 56.24999999999991, + "727": 65.77999999999994, + "728": 100.52999999999989, + "729": 100.33999999999992, + "730": 52.29999999999993, + "731": 94.60999999999989, + "732": 68.58999999999988, + "733": 98.22999999999993, + "734": 86.17999999999988, + "735": 99.38999999999989, + "736": 87.50999999999985, + "737": 99.46999999999989, + "738": 16.159999999999954, + "739": 71.85999999999987, + "740": 82.37999999999988, + "741": 66.0499999999999, + "742": 92.3299999999999, + "743": 89.84999999999984, + "744": 95.31999999999987, + "745": 84.71999999999986, + "746": 78.92999999999984, + "747": 93.5699999999999, + "748": 93.52999999999983, + "749": 89.48999999999991, + "750": 85.6499999999999, + "751": 71.53999999999985, + "752": 67.47999999999982, + "753": 84.32999999999986, + "754": 50.4699999999999, + "755": 83.53999999999992, + "756": 77.1399999999999, + "757": 95.58999999999985, + "758": 53.07999999999993, + "759": 92.86999999999985, + "760": 98.02999999999989, + "761": 44.7499999999999, + "762": 95.08999999999992, + "763": 58.31999999999992, + "764": 62.00999999999991, + "765": 66.59999999999994, + "766": 34.289999999999885, + "767": 76.93999999999987, + "768": 80.9399999999999, + "769": 87.79999999999987, + "770": 91.29999999999981, + "771": 73.79999999999987, + "772": 80.84999999999984, + "773": 96.99999999999991, + "774": 84.76999999999991, + "775": 90.72999999999985, + "776": 86.16999999999992, + "777": 86.59999999999987, + "778": 79.21999999999986, + "779": 95.07999999999984, + "780": 52.3599999999999, + "781": 86.30999999999986, + "782": 93.44999999999996, + "783": 68.1199999999999, + "784": 85.38999999999987, + "785": 90.94999999999989, + "786": 89.04999999999984, + "787": 85.90999999999985, + "788": 52.78999999999987, + "789": 62.48999999999994, + "790": 55.99999999999992, + "791": 55.00999999999992, + "792": 89.63999999999984, + "793": 97.70999999999988, + "794": 75.07999999999986, + "795": 55.79999999999991, + "796": 56.49999999999991, + "797": 101.19999999999992, + "798": 93.62999999999985, + "799": 85.59999999999982, + "800": 95.69999999999985, + "801": 86.62999999999984, + "802": 54.78999999999992, + "803": 94.71999999999986, + "804": 91.83999999999983, + "805": 90.16999999999986, + "806": 99.02999999999989, + "807": 92.68999999999983, + "808": 68.22999999999986, + "809": 57.33999999999992, + "810": 87.91999999999989, + "811": 90.87999999999992, + "812": 100.60999999999987, + "813": 3.6900000000000235, + "814": 97.26999999999985, + "815": 62.34999999999993, + "816": 87.77999999999983, + "817": 46.459999999999866, + "818": 47.78999999999992, + "819": 76.31999999999996, + "820": 77.87999999999991, + "821": 90.05999999999986, + "822": 97.64999999999986, + "823": 100.41999999999987, + "824": 74.38999999999993, + "825": 96.60999999999987, + "826": 76.24999999999984, + "827": 94.95999999999984, + "828": 48.219999999999914, + "829": 66.92999999999995, + "830": 99.10999999999989, + "831": 81.76999999999984, + "832": 8.239999999999947, + "833": 101.3899999999999, + "834": 43.83999999999988, + "835": 99.39999999999996, + "836": 99.57999999999988, + "837": 92.05999999999985, + "838": 87.89999999999984, + "839": 89.14999999999989, + "840": 79.41999999999987, + "841": 51.6599999999999, + "842": 88.8999999999998, + "843": 91.83999999999982, + "844": 74.77999999999989, + "845": 94.56999999999996, + "846": 93.10999999999986, + "847": 100.83999999999989, + "848": 52.9199999999999, + "849": 60.219999999999914, + "850": 53.67999999999989, + "851": 105.73999999999987, + "852": 99.39999999999984, + "853": 96.62999999999991, + "854": 90.24999999999991, + "855": 88.84999999999987, + "856": 54.88999999999989, + "857": 90.78999999999992, + "858": 85.85999999999983, + "859": 66.09999999999994, + "860": 101.8299999999998, + "861": 101.04999999999987, + "862": 83.80999999999986, + "863": 51.71999999999988, + "864": 77.50999999999988, + "865": 95.10999999999987, + "866": 53.18999999999985, + "867": 92.32999999999984, + "868": 95.95999999999985, + "869": 88.75999999999985, + "870": 96.64999999999988, + "871": 96.99999999999989, + "872": 57.99999999999991, + "873": 46.03999999999991, + "874": 82.18999999999988, + "875": 96.30999999999989, + "876": 64.29999999999986, + "877": 84.95999999999984, + "878": 92.92999999999981, + "879": 27.729999999999983, + "880": 88.03999999999984, + "881": 58.23999999999988, + "882": 25.149999999999956, + "883": 101.3299999999999, + "884": 48.2599999999999, + "885": 78.05999999999986, + "886": 89.76999999999984, + "887": 60.55999999999991, + "888": 87.27999999999984, + "889": 102.47999999999993, + "890": 60.76999999999987, + "891": 90.43999999999984, + "892": 60.86999999999987, + "893": 54.6899999999999, + "894": 103.0199999999999, + "895": 89.71999999999987, + "896": 102.36999999999989, + "897": 95.31999999999987, + "898": 75.11999999999986, + "899": 97.11999999999985, + "900": 47.9699999999999, + "901": 89.46999999999984, + "902": 53.61999999999989, + "903": 74.33999999999989, + "904": 68.34999999999988, + "905": 71.1499999999999, + "906": 77.70999999999982, + "907": 87.9899999999999, + "908": 56.069999999999936, + "909": 75.80999999999989, + "910": 74.84999999999988, + "911": 38.589999999999876, + "912": 49.5699999999999, + "913": 84.3299999999999, + "914": 70.18999999999981, + "915": 87.53999999999986, + "916": 82.86999999999985, + "917": 75.60999999999991, + "918": 98.35999999999999, + "919": 62.9399999999999, + "920": 68.05999999999985, + "921": 79.29999999999987, + "922": 76.33999999999985, + "923": 52.62999999999988, + "924": 84.28999999999989, + "925": 80.52999999999994, + "926": 91.28999999999985, + "927": 78.93999999999996, + "928": 88.6499999999999, + "929": 81.71999999999987, + "930": 56.21999999999999, + "931": 82.62999999999985, + "932": 74.1899999999998, + "933": 57.57999999999993, + "934": 88.31999999999987, + "935": 86.20999999999985, + "936": 100.8999999999999, + "937": 91.45999999999984, + "938": 76.67999999999986, + "939": 86.47999999999988, + "940": 89.4699999999999, + "941": 75.60999999999986, + "942": 52.4599999999999, + "943": 95.0799999999998, + "944": 91.96999999999989, + "945": 93.71999999999983, + "946": 54.33999999999992, + "947": 84.18999999999987, + "948": 87.27999999999989, + "949": 77.61999999999985, + "950": 88.46999999999984, + "951": 96.07999999999983, + "952": 100.27999999999983, + "953": 98.31999999999981, + "954": 77.91999999999986, + "955": 83.55999999999982, + "956": 61.11999999999987, + "957": 97.04999999999988, + "958": 54.61999999999988, + "959": 83.34999999999982, + "960": 96.85999999999984, + "961": 93.34999999999984, + "962": 55.779999999999845, + "963": 80.25999999999985, + "964": 92.15999999999983, + "965": 95.46999999999989, + "966": 76.94999999999985, + "967": 30.23999999999989, + "968": 85.83999999999989, + "969": 90.8899999999998, + "970": 57.699999999999875, + "971": 45.76999999999988, + "972": 80.5199999999998, + "973": 62.19999999999989, + "974": 63.879999999999846, + "975": 92.03999999999982, + "976": 53.329999999999885, + "977": 60.069999999999915, + "978": 61.579999999999885, + "979": 61.72999999999988, + "980": 47.74999999999987, + "981": 59.079999999999906, + "982": 61.999999999999844, + "983": 90.0099999999998, + "984": 87.45999999999984, + "985": 63.13999999999986, + "986": 62.959999999999866, + "987": 92.44999999999983, + "988": 72.71999999999989, + "989": 62.64000000000001, + "990": 91.84999999999985, + "991": 54.72999999999989, + "992": 65.33999999999988, + "993": 66.32999999999988, + "994": 98.44999999999986, + "995": 97.79999999999981, + "996": 93.82999999999983, + "997": 84.76999999999984, + "998": 48.80999999999989, + "999": 64.89999999999985, + "1000": 63.309999999999846 + }, + "session_total_reward_per_episode": { + "1": { + "1": -22.899999999999963, + "2": -11.84999999999998, + "3": -45.15000000000006, + "4": -11.449999999999983, + "5": -22.449999999999953, + "6": -14.549999999999981, + "7": -69.80000000000005, + "8": -23.149999999999963, + "9": -92.95, + "10": -1.6499999999999995, + "11": -41.85000000000005, + "12": -20.199999999999953, + "13": -2.049999999999983, + "14": -23.34999999999995, + "15": -66.40000000000009, + "16": -60.350000000000094, + "17": -12.69999999999998, + "18": -19.599999999999987, + "19": -13.349999999999982, + "20": -23.24999999999995, + "21": -14.399999999999986, + "22": -45.600000000000065, + "23": -49.10000000000007, + "24": -21.649999999999956, + "25": -95.7000000000001, + "26": -45.55000000000019, + "27": -21.749999999999957, + "28": -39.05, + "29": -42.900000000000105, + "30": -19.849999999999966, + "31": 7.00000000000002, + "32": -52.75000000000008, + "33": -28.799999999999976, + "34": -4.099999999999985, + "35": -34.749999999999986, + "36": -21.09999999999996, + "37": -37.00000000000011, + "38": -16.24999999999998, + "39": -15.299999999999986, + "40": -12.499999999999995, + "41": -83.54999999999981, + "42": -22.2, + "43": -84.75000000000009, + "44": -16.89999999999997, + "45": -25.49999999999999, + "46": -18.89999999999997, + "47": -11.349999999999987, + "48": -21.049999999999958, + "49": -22.99999999999995, + "50": -22.499999999999954, + "51": -70.44999999999999, + "52": -62.300000000000104, + "53": 3.049999999999968, + "54": -7.399999999999997, + "55": -16.799999999999972, + "56": -73.75, + "57": -33.30000000000002, + "58": -3.0000000000000067, + "59": -16.74999999999997, + "60": -21.699999999999957, + "61": -69.05000000000005, + "62": -98.54999999999998, + "63": -7.099999999999993, + "64": -3.749999999999984, + "65": -98.19999999999999, + "66": -60.90000000000017, + "67": -97.2, + "68": -22.199999999999953, + "69": -14.549999999999965, + "70": -20.999999999999957, + "71": -20.399999999999963, + "72": -5.599999999999977, + "73": -13.300000000000004, + "74": -14.649999999999979, + "75": -11.399999999999993, + "76": -6.699999999999988, + "77": -43.300000000000125, + "78": -30.449999999999992, + "79": -23.29999999999995, + "80": -75.85, + "81": 11.55, + "82": -37.24999999999999, + "83": -94.24999999999997, + "84": -18.74999999999999, + "85": -89.3, + "86": -27.350000000000026, + "87": -103.15000000000006, + "88": -73.15000000000002, + "89": -16.999999999999975, + "90": -31.54999999999993, + "91": -16.699999999999974, + "92": -22.699999999999953, + "93": -91.19999999999999, + "94": -18.949999999999967, + "95": -87.8, + "96": -17.89999999999997, + "97": -65.3, + "98": -16.24999999999998, + "99": -12.749999999999995, + "100": -2.199999999999976, + "101": -30.199999999999978, + "102": -69.74999999999997, + "103": -75.4, + "104": -63.35000000000011, + "105": -21.749999999999957, + "106": -15.04999999999998, + "107": -11.149999999999993, + "108": -95.4, + "109": -9.299999999999994, + "110": -7.399999999999994, + "111": -67.90000000000006, + "112": -66.00000000000001, + "113": -88.4, + "114": -14.949999999999976, + "115": 0.20000000000001994, + "116": -7.80000000000002, + "117": -10.3, + "118": 4.550000000000024, + "119": -42.250000000000114, + "120": -23.89999999999997, + "121": 8.499999999999986, + "122": -73.14999999999999, + "123": -18.749999999999968, + "124": -18.14999999999997, + "125": -2.8999999999999737, + "126": -6.199999999999982, + "127": -13.09999999999999, + "128": -11.849999999999987, + "129": 14.849999999999994, + "130": -14.749999999999977, + "131": -50.60000000000007, + "132": -39.65000000000005, + "133": 14.300000000000018, + "134": -9.399999999999993, + "135": -21.949999999999953, + "136": -16.69999999999997, + "137": 29.599999999999888, + "138": -19.99999999999996, + "139": 0.7500000000000469, + "140": 25.200000000000024, + "141": -18.399999999999967, + "142": -97.19999999999999, + "143": -90.15, + "144": 20.800000000000008, + "145": -7.900000000000008, + "146": -56.750000000000014, + "147": -81.70000000000005, + "148": -91.45, + "149": -31.10000000000001, + "150": -64.35, + "151": -59.49999999999999, + "152": -15.89999999999998, + "153": 8.65000000000002, + "154": -80.35000000000001, + "155": -84.64999999999996, + "156": -20.79999999999996, + "157": 1.900000000000028, + "158": -53.599999999999994, + "159": -86.80000000000001, + "160": -93.6, + "161": -92.14999999999995, + "162": -66.75000000000001, + "163": -78.65000000000002, + "164": -8.049999999999995, + "165": -87.99999999999997, + "166": 27.249999999999893, + "167": -35.30000000000001, + "168": -0.7999999999999727, + "169": -96.44999999999999, + "170": -53.09999999999996, + "171": -7.750000000000002, + "172": -1.2499999999999776, + "173": -63.39999999999997, + "174": -36.79999999999995, + "175": -10.09999999999999, + "176": -9.699999999999998, + "177": -48.2, + "178": -76.7, + "179": -73.59999999999995, + "180": -76.2, + "181": -88.39999999999999, + "182": -15.649999999999977, + "183": -91.14999999999998, + "184": -11.499999999999995, + "185": -21.949999999999953, + "186": -30.85000000000002, + "187": 40.64999999999981, + "188": 8.850000000000062, + "189": -77.00000000000004, + "190": -75.45, + "191": -0.8999999999999571, + "192": -47.25, + "193": -61.69999999999993, + "194": 7.100000000000066, + "195": -7.099999999999988, + "196": -4.050000000000007, + "197": -6.499999999999984, + "198": -82.9, + "199": 1.300000000000014, + "200": 9.849999999999971, + "201": -3.7499999999999805, + "202": 84.9000000000002, + "203": 8.45000000000005, + "204": -32.749999999999964, + "205": -36.44999999999997, + "206": -90.1, + "207": -84.05, + "208": -12.199999999999989, + "209": 13.94999999999997, + "210": -18.849999999999994, + "211": 16.80000000000005, + "212": 26.599999999999895, + "213": -22.84999999999995, + "214": -74.05, + "215": -8.149999999999993, + "216": -28.949999999999967, + "217": -61.29999999999995, + "218": -3.8000000000000043, + "219": -56.799999999999976, + "220": 25.85000000000001, + "221": -87.0, + "222": -64.14999999999999, + "223": -40.10000000000003, + "224": 5.250000000000007, + "225": -11.449999999999992, + "226": -0.39999999999999414, + "227": -65.19999999999999, + "228": -34.400000000000006, + "229": -5.9499999999999895, + "230": -19.349999999999966, + "231": 32.99999999999977, + "232": 6.500000000000082, + "233": -1.399999999999994, + "234": -46.099999999999966, + "235": 51.249999999999815, + "236": -68.25000000000001, + "237": -74.30000000000001, + "238": -4.049999999999976, + "239": -82.25, + "240": -28.799999999999937, + "241": 5.90000000000005, + "242": -1.949999999999961, + "243": -80.85, + "244": -12.649999999999988, + "245": -1.5999999999999868, + "246": -53.999999999999986, + "247": -65.85, + "248": -25.799999999999994, + "249": 0.8000000000000482, + "250": 8.250000000000032, + "251": 8.55000000000004, + "252": 7.000000000000038, + "253": -30.549999999999972, + "254": -49.400000000000034, + "255": 2.2000000000000446, + "256": 2.550000000000025, + "257": -17.399999999999984, + "258": -71.35, + "259": 13.550000000000004, + "260": -80.0, + "261": -10.74999999999999, + "262": 27.84999999999992, + "263": -10.95, + "264": -57.65000000000002, + "265": 25.99999999999989, + "266": 31.899999999999963, + "267": 2.4000000000000163, + "268": -71.5, + "269": -63.45000000000001, + "270": 78.64999999999993, + "271": -78.9, + "272": -13.149999999999956, + "273": -17.599999999999973, + "274": -14.24999999999999, + "275": -0.19999999999996576, + "276": -34.44999999999999, + "277": -1.999999999999969, + "278": -16.700000000000017, + "279": -55.699999999999996, + "280": -63.64999999999999, + "281": -0.04999999999998295, + "282": -35.45, + "283": -31.89999999999997, + "284": -69.44999999999997, + "285": -78.5, + "286": -1.1000000000000014, + "287": -74.20000000000002, + "288": -78.35000000000002, + "289": -81.80000000000001, + "290": -32.50000000000001, + "291": 8.750000000000028, + "292": -22.49999999999997, + "293": 6.8500000000000005, + "294": -91.6, + "295": 36.099999999999795, + "296": -81.25, + "297": 5.149999999999975, + "298": 7.249999999999992, + "299": -10.149999999999983, + "300": -68.54999999999993, + "301": -61.24999999999994, + "302": -13.749999999999988, + "303": -66.64999999999995, + "304": -72.1, + "305": -53.400000000000034, + "306": -41.95000000000002, + "307": 22.650000000000034, + "308": -78.69999999999999, + "309": -62.0, + "310": -72.04999999999997, + "311": -60.74999999999993, + "312": -77.45, + "313": -51.69999999999997, + "314": -78.50000000000001, + "315": -44.65000000000001, + "316": 15.80000000000004, + "317": 39.44999999999979, + "318": -43.999999999999964, + "319": -48.29999999999999, + "320": 42.99999999999991, + "321": -23.049999999999983, + "322": -4.899999999999984, + "323": 34.099999999999795, + "324": -62.24999999999992, + "325": -76.95, + "326": 7.3000000000000504, + "327": -101.30000000000013, + "328": -16.95000000000004, + "329": -50.199999999999996, + "330": -41.8, + "331": -60.84999999999993, + "332": 13.500000000000007, + "333": -53.04999999999998, + "334": 0.7500000000000511, + "335": 60.79999999999987, + "336": 6.50000000000005, + "337": 8.10000000000003, + "338": -63.7, + "339": -22.79999999999995, + "340": -82.69999999999999, + "341": -39.10000000000001, + "342": 39.599999999999795, + "343": -32.35000000000003, + "344": -65.24999999999994, + "345": 85.15000000000003, + "346": 18.34999999999998, + "347": -86.14999999999999, + "348": 30.99999999999976, + "349": -79.75, + "350": 43.44999999999984, + "351": -78.65000000000003, + "352": 34.799999999999834, + "353": -4.249999999999974, + "354": -39.35, + "355": -75.14999999999999, + "356": -67.94999999999999, + "357": -64.94999999999996, + "358": -54.19999999999996, + "359": -68.19999999999996, + "360": -38.10000000000001, + "361": 10.249999999999986, + "362": -2.0999999999999925, + "363": -10.299999999999955, + "364": -70.75, + "365": -59.25000000000002, + "366": -46.25000000000003, + "367": -61.64999999999998, + "368": 5.250000000000063, + "369": -24.54999999999994, + "370": -32.00000000000002, + "371": 25.10000000000001, + "372": -92.89999999999998, + "373": 26.450000000000102, + "374": -49.60000000000004, + "375": 13.300000000000011, + "376": -17.49999999999998, + "377": 7.600000000000042, + "378": -66.69999999999993, + "379": -25.049999999999994, + "380": -64.74999999999997, + "381": -64.34999999999998, + "382": -38.20000000000001, + "383": 59.04999999999991, + "384": 0.6000000000000636, + "385": 21.85000000000011, + "386": 14.049999999999986, + "387": -28.49999999999998, + "388": -65.89999999999996, + "389": 31.79999999999977, + "390": -54.74999999999997, + "391": -58.699999999999946, + "392": -73.99999999999999, + "393": 7.249999999999879, + "394": -62.55000000000001, + "395": -64.75000000000003, + "396": -64.69999999999992, + "397": -72.95, + "398": -57.300000000000026, + "399": 17.350000000000023, + "400": -77.60000000000005, + "401": 49.599999999999916, + "402": -78.75000000000009, + "403": -32.750000000000036, + "404": -13.849999999999985, + "405": -57.54999999999998, + "406": -67.64999999999996, + "407": -14.549999999999986, + "408": -38.69999999999999, + "409": -42.34999999999999, + "410": -75.05000000000001, + "411": -73.25000000000001, + "412": 36.849999999999795, + "413": -43.14999999999998, + "414": 50.84999999999989, + "415": -64.3999999999999, + "416": -17.599999999999984, + "417": -3.6999999999999673, + "418": -65.64999999999998, + "419": -11.450000000000015, + "420": -57.24999999999999, + "421": -65.54999999999995, + "422": -59.34999999999998, + "423": -64.79999999999997, + "424": -8.000000000000071, + "425": -12.900000000000041, + "426": -18.499999999999975, + "427": -24.499999999999975, + "428": -55.39999999999993, + "429": -30.89999999999997, + "430": -28.44999999999996, + "431": -12.949999999999976, + "432": -65.84999999999995, + "433": -50.99999999999996, + "434": -19.099999999999973, + "435": -68.4, + "436": -60.800000000000004, + "437": -3.9499999999999735, + "438": -10.999999999999922, + "439": -62.49999999999996, + "440": -57.299999999999976, + "441": -61.749999999999936, + "442": -46.04999999999999, + "443": -67.99999999999994, + "444": -62.64999999999992, + "445": 25.599999999999856, + "446": -55.09999999999995, + "447": -68.19999999999993, + "448": 3.499999999999991, + "449": -34.300000000000004, + "450": -29.700000000000006, + "451": 34.25000000000014, + "452": -12.100000000000064, + "453": -42.04999999999998, + "454": -29.10000000000001, + "455": -23.09999999999998, + "456": -26.79999999999995, + "457": -20.24999999999999, + "458": -52.2, + "459": -72.34999999999995, + "460": -65.34999999999991, + "461": -55.79999999999996, + "462": -41.65000000000001, + "463": -33.04999999999998, + "464": 22.450000000000095, + "465": 95.45000000000006, + "466": 41.64999999999985, + "467": -50.09999999999998, + "468": 6.800000000000042, + "469": 37.55, + "470": -13.75000000000001, + "471": 10.600000000000062, + "472": -62.24999999999993, + "473": -16.699999999999964, + "474": -56.79999999999995, + "475": -27.550000000000004, + "476": 84.70000000000005, + "477": -59.54999999999993, + "478": -74.04999999999998, + "479": -74.99999999999987, + "480": 28.04999999999995, + "481": -25.19999999999999, + "482": -96.60000000000001, + "483": -20.700000000000014, + "484": 77.85000000000011, + "485": -50.849999999999945, + "486": 28.700000000000042, + "487": -58.649999999999935, + "488": -35.949999999999974, + "489": -24.349999999999934, + "490": -54.0, + "491": -14.149999999999975, + "492": -44.8499999999999, + "493": -66.24999999999991, + "494": -10.149999999999965, + "495": 1.499999999999983, + "496": 26.350000000000072, + "497": -57.949999999999974, + "498": -54.799999999999955, + "499": 33.00000000000012, + "500": 7.250000000000083, + "501": 74.15000000000002, + "502": 21.699999999999974, + "503": -73.0999999999999, + "504": -15.34999999999998, + "505": 70.99999999999991, + "506": -66.84999999999991, + "507": -61.649999999999935, + "508": -58.94999999999998, + "509": -41.09999999999997, + "510": -26.349999999999994, + "511": -65.6999999999999, + "512": 44.44999999999985, + "513": -70.19999999999989, + "514": 65.74999999999986, + "515": -50.59999999999994, + "516": -66.84999999999997, + "517": -64.04999999999998, + "518": -25.099999999999998, + "519": -59.99999999999991, + "520": -67.19999999999997, + "521": -21.99999999999999, + "522": -49.89999999999998, + "523": -27.19999999999997, + "524": -51.04999999999998, + "525": -30.54999999999997, + "526": -38.25, + "527": 67.24999999999982, + "528": -63.899999999999935, + "529": -62.44999999999999, + "530": -36.05000000000003, + "531": -67.59999999999995, + "532": -67.89999999999992, + "533": -64.14999999999999, + "534": -22.099999999999984, + "535": -62.64999999999992, + "536": -52.49999999999997, + "537": -76.15000000000006, + "538": -15.750000000000046, + "539": -59.89999999999995, + "540": -1.1999999999999718, + "541": -63.65000000000005, + "542": -65.94999999999992, + "543": -5.55000000000003, + "544": -59.99999999999997, + "545": -66.7499999999999, + "546": 15.80000000000001, + "547": 87.35000000000007, + "548": -72.19999999999989, + "549": -64.09999999999992, + "550": -52.69999999999996, + "551": -9.40000000000001, + "552": -19.750000000000057, + "553": -62.94999999999993, + "554": -60.09999999999995, + "555": -39.7, + "556": 27.00000000000004, + "557": -52.04999999999994, + "558": -30.59999999999998, + "559": -86.75, + "560": -51.39999999999996, + "561": -61.20000000000003, + "562": 0.5499999999999772, + "563": -37.59999999999999, + "564": -18.650000000000027, + "565": -58.349999999999945, + "566": 55.750000000000014, + "567": -15.649999999999968, + "568": -27.250000000000007, + "569": -47.499999999999986, + "570": 100.40000000000032, + "571": -43.05, + "572": -62.24999999999993, + "573": 28.100000000000087, + "574": -65.99999999999996, + "575": 28.39999999999995, + "576": -2.0499999999999177, + "577": -58.399999999999935, + "578": -57.19999999999993, + "579": -24.6, + "580": -63.69999999999992, + "581": -4.249999999999938, + "582": 13.300000000000011, + "583": -51.749999999999964, + "584": -49.64999999999997, + "585": 50.100000000000136, + "586": 82.85000000000016, + "587": -34.00000000000001, + "588": -26.950000000000024, + "589": 102.25000000000016, + "590": -33.900000000000034, + "591": -1.549999999999984, + "592": -61.99999999999995, + "593": -56.95, + "594": 14.499999999999964, + "595": -66.7499999999999, + "596": 52.29999999999995, + "597": -50.99999999999997, + "598": 88.75, + "599": -23.750000000000014, + "600": 68.1499999999999, + "601": -47.39999999999999, + "602": -68.29999999999997, + "603": 62.750000000000156, + "604": -65.84999999999991, + "605": -3.4000000000000314, + "606": -23.75000000000003, + "607": 3.1499999999999764, + "608": -52.29999999999997, + "609": -13.599999999999982, + "610": -51.59999999999997, + "611": -37.8, + "612": -19.049999999999997, + "613": -55.84999999999996, + "614": -7.299999999999946, + "615": -79.05000000000001, + "616": 29.05000000000002, + "617": 6.500000000000016, + "618": -26.70000000000005, + "619": 79.24999999999993, + "620": -34.80000000000003, + "621": 47.85000000000002, + "622": 32.150000000000006, + "623": 88.59999999999998, + "624": -19.449999999999946, + "625": 49.79999999999995, + "626": 15.09999999999998, + "627": 38.949999999999996, + "628": 19.950000000000063, + "629": -12.799999999999974, + "630": 10.050000000000054, + "631": 67.65000000000006, + "632": -1.949999999999986, + "633": 21.60000000000012, + "634": 92.8000000000001, + "635": 25.64999999999996, + "636": 73.35000000000002, + "637": 3.9999999999999902, + "638": 42.89999999999986, + "639": -49.499999999999964, + "640": -18.200000000000045, + "641": 63.14999999999993, + "642": -16.550000000000015, + "643": 64.95000000000006, + "644": -41.099999999999966, + "645": 24.10000000000002, + "646": 34.84999999999997, + "647": 42.84999999999999, + "648": 62.49999999999998, + "649": 45.649999999999906, + "650": 72.89999999999999, + "651": 32.30000000000004, + "652": -26.04999999999997, + "653": 68.10000000000008, + "654": 70.24999999999977, + "655": 90.7000000000001, + "656": 88.55000000000003, + "657": 24.04999999999996, + "658": 57.899999999999956, + "659": 33.700000000000045, + "660": 59.29999999999987, + "661": 101.15000000000005, + "662": 41.899999999999935, + "663": 36.14999999999995, + "664": -51.19999999999997, + "665": 81.8500000000001, + "666": 88.4000000000001, + "667": 61.69999999999994, + "668": 19.299999999999983, + "669": 70.64999999999995, + "670": 46.49999999999997, + "671": 40.69999999999995, + "672": -27.34999999999998, + "673": 107.95, + "674": 44.99999999999982, + "675": 11.40000000000002, + "676": 90.50000000000016, + "677": 38.84999999999977, + "678": 56.59999999999999, + "679": 93.05000000000005, + "680": 57.399999999999956, + "681": 41.05000000000001, + "682": 90.94999999999996, + "683": 64.69999999999997, + "684": -54.09999999999994, + "685": 101.75000000000003, + "686": 53.74999999999992, + "687": 100.40000000000002, + "688": 35.8999999999998, + "689": 47.50000000000003, + "690": 32.59999999999986, + "691": 42.99999999999987, + "692": 76.00000000000003, + "693": -5.800000000000042, + "694": 3.199999999999882, + "695": 24.200000000000006, + "696": 43.40000000000005, + "697": 91.05000000000003, + "698": 84.25000000000014, + "699": 37.04999999999994, + "700": 30.149999999999984, + "701": 94.55000000000007, + "702": 94.60000000000008, + "703": 24.45, + "704": 30.49999999999995, + "705": -24.300000000000033, + "706": 82.0, + "707": 55.3499999999999, + "708": 76.55000000000014, + "709": 40.09999999999989, + "710": -10.999999999999964, + "711": 75.35000000000007, + "712": 62.09999999999993, + "713": 82.65000000000018, + "714": 8.700000000000028, + "715": 87.75000000000017, + "716": 84.55000000000001, + "717": 12.949999999999957, + "718": 73.14999999999998, + "719": 50.79999999999997, + "720": 60.599999999999994, + "721": 91.55000000000008, + "722": 93.15000000000006, + "723": 42.74999999999994, + "724": 77.49999999999997, + "725": 86.10000000000015, + "726": 69.45000000000003, + "727": 63.299999999999926, + "728": 86.40000000000002, + "729": 78.8000000000001, + "730": 92.50000000000004, + "731": 75.10000000000008, + "732": 50.99999999999997, + "733": 91.25000000000016, + "734": 85.25000000000003, + "735": 93.4500000000001, + "736": 65.05, + "737": 76.20000000000003, + "738": 57.95000000000003, + "739": 48.85, + "740": 66.79999999999995, + "741": 66.65000000000003, + "742": 76.25000000000011, + "743": 73.75000000000004, + "744": 76.15000000000006, + "745": 5.349999999999966, + "746": 45.9500000000001, + "747": 72.94999999999999, + "748": 104.60000000000005, + "749": 78.95000000000002, + "750": 67.34999999999998, + "751": 32.549999999999926, + "752": 48.449999999999825, + "753": 84.25000000000004, + "754": 53.54999999999998, + "755": 79.40000000000008, + "756": 103.10000000000002, + "757": 83.95, + "758": 92.45000000000007, + "759": 100.00000000000006, + "760": 85.30000000000001, + "761": -24.09999999999999, + "762": 53.000000000000014, + "763": 42.849999999999916, + "764": 85.10000000000004, + "765": 72.24999999999999, + "766": -34.849999999999994, + "767": 61.199999999999974, + "768": 90.8000000000001, + "769": 61.14999999999984, + "770": 81.09999999999995, + "771": 53.55000000000009, + "772": 60.849999999999895, + "773": 63.05000000000005, + "774": 53.400000000000055, + "775": 77.84999999999997, + "776": 94.4, + "777": 66.94999999999996, + "778": 67.34999999999995, + "779": 52.44999999999994, + "780": 101.69999999999999, + "781": 78.05000000000007, + "782": 46.29999999999982, + "783": 100.85000000000001, + "784": 73.85000000000004, + "785": 53.25, + "786": 84.30000000000004, + "787": 76.89999999999998, + "788": 77.30000000000008, + "789": 68.04999999999997, + "790": 80.60000000000011, + "791": 86.50000000000018, + "792": 52.39999999999994, + "793": 95.65000000000006, + "794": 88.14999999999999, + "795": 87.40000000000009, + "796": 56.29999999999998, + "797": 93.30000000000011, + "798": 85.0500000000001, + "799": 85.30000000000011, + "800": 72.05000000000001, + "801": 69.79999999999998, + "802": 76.30000000000007, + "803": 56.150000000000006, + "804": 65.74999999999997, + "805": 73.30000000000004, + "806": 76.89999999999998, + "807": 86.79999999999986, + "808": 84.99999999999997, + "809": 76.80000000000005, + "810": 86.0, + "811": 62.39999999999998, + "812": 88.30000000000003, + "813": 91.55000000000001, + "814": 75.59999999999994, + "815": 76.5, + "816": 65.29999999999993, + "817": 29.899999999999984, + "818": 77.55000000000007, + "819": 90.95000000000009, + "820": 73.30000000000004, + "821": 58.69999999999999, + "822": 87.59999999999997, + "823": 89.94999999999996, + "824": 68.29999999999997, + "825": 91.89999999999998, + "826": 74.89999999999995, + "827": 71.24999999999996, + "828": 70.69999999999999, + "829": 93.04999999999998, + "830": 88.30000000000003, + "831": 102.65000000000009, + "832": 23.799999999999955, + "833": 96.55000000000001, + "834": 89.35000000000002, + "835": 74.05000000000005, + "836": 90.3000000000001, + "837": 75.65, + "838": 81.5, + "839": 29.04999999999999, + "840": 78.9, + "841": 61.69999999999996, + "842": 46.19999999999999, + "843": 65.54999999999998, + "844": 60.95000000000003, + "845": 100.65000000000019, + "846": 73.50000000000003, + "847": 96.75000000000001, + "848": 57.24999999999997, + "849": 64.3, + "850": 59.04999999999996, + "851": 103.14999999999996, + "852": 86.50000000000007, + "853": 63.150000000000034, + "854": 67.30000000000008, + "855": 69.74999999999997, + "856": 89.69999999999999, + "857": 73.50000000000014, + "858": 58.80000000000003, + "859": 93.35000000000008, + "860": 98.75000000000001, + "861": 80.49999999999999, + "862": 78.50000000000006, + "863": 68.25000000000003, + "864": 102.9000000000001, + "865": 94.05000000000001, + "866": 46.65000000000003, + "867": 96.39999999999999, + "868": 100.6000000000001, + "869": 48.44999999999997, + "870": 88.05, + "871": 68.70000000000006, + "872": 75.75000000000001, + "873": 100.00000000000003, + "874": 102.50000000000006, + "875": 85.50000000000001, + "876": 21.64999999999999, + "877": 59.69999999999999, + "878": 70.19999999999999, + "879": 85.15000000000003, + "880": 88.3, + "881": 70.00000000000009, + "882": 92.64999999999999, + "883": 96.00000000000004, + "884": 86.60000000000002, + "885": 70.70000000000005, + "886": 53.69999999999994, + "887": 104.5, + "888": 63.85000000000002, + "889": 86.85000000000004, + "890": 81.45000000000003, + "891": 73.30000000000003, + "892": 94.95000000000003, + "893": 42.05, + "894": 93.99999999999999, + "895": 94.80000000000018, + "896": 91.7, + "897": 62.349999999999945, + "898": 66.35000000000001, + "899": 86.85000000000002, + "900": 37.30000000000004, + "901": 74.94999999999997, + "902": 92.05000000000008, + "903": 92.34999999999998, + "904": 61.80000000000008, + "905": 85.14999999999999, + "906": 84.49999999999993, + "907": 66.30000000000004, + "908": 88.05000000000001, + "909": 81.55000000000001, + "910": 99.35000000000001, + "911": 71.79999999999994, + "912": 87.15, + "913": 82.39999999999998, + "914": 38.949999999999974, + "915": 91.35000000000002, + "916": 69.45000000000007, + "917": 73.04999999999998, + "918": 72.00000000000003, + "919": 62.45000000000006, + "920": 46.89999999999998, + "921": 66.95000000000003, + "922": 77.94999999999997, + "923": 84.45, + "924": 75.39999999999998, + "925": 91.70000000000013, + "926": 80.74999999999997, + "927": 77.20000000000005, + "928": 79.20000000000005, + "929": 59.05, + "930": 66.14999999999992, + "931": 47.899999999999935, + "932": 89.64999999999996, + "933": 78.3499999999999, + "934": 91.60000000000005, + "935": 70.89999999999996, + "936": 85.45, + "937": 84.65000000000003, + "938": 82.84999999999997, + "939": 102.19999999999999, + "940": 53.80000000000001, + "941": 50.199999999999974, + "942": 72.7, + "943": 63.90000000000002, + "944": 80.15000000000003, + "945": 92.15, + "946": 13.999999999999988, + "947": 62.400000000000034, + "948": 73.60000000000005, + "949": 56.29999999999998, + "950": 84.25000000000003, + "951": 80.85000000000001, + "952": 84.45000000000005, + "953": 86.70000000000002, + "954": 87.04999999999995, + "955": 30.700000000000024, + "956": 82.05, + "957": 78.55000000000007, + "958": 83.95000000000006, + "959": 57.44999999999996, + "960": 83.45000000000006, + "961": 72.25000000000001, + "962": 73.05000000000001, + "963": 79.30000000000001, + "964": 81.55, + "965": 69.99999999999997, + "966": 67.20000000000003, + "967": 92.80000000000003, + "968": 72.10000000000002, + "969": 48.64999999999985, + "970": 71.94999999999997, + "971": 15.949999999999934, + "972": 61.44999999999984, + "973": 90.85000000000004, + "974": 96.55000000000003, + "975": 78.15000000000003, + "976": 84.40000000000009, + "977": 84.75000000000003, + "978": 52.95000000000003, + "979": 84.84999999999995, + "980": 52.20000000000008, + "981": 67.1, + "982": 84.00000000000001, + "983": 87.8500000000001, + "984": 76.8000000000001, + "985": 91.4499999999999, + "986": 80.74999999999999, + "987": 83.09999999999998, + "988": 92.9000000000001, + "989": 63.34999999999997, + "990": 66.49999999999997, + "991": 96.65000000000002, + "992": 101.85000000000002, + "993": 84.79999999999993, + "994": 91.65000000000003, + "995": 77.25000000000009, + "996": 64.0, + "997": 59.04999999999998, + "998": 72.10000000000002, + "999": 85.40000000000005, + "1000": 38.94999999999991 + }, + "2": { + "1": -11.099999999999989, + "2": -32.05000000000004, + "3": -58.200000000000095, + "4": -8.599999999999987, + "5": -81.89999999999999, + "6": -63.89999999999999, + "7": 2.050000000000006, + "8": -25.199999999999996, + "9": -21.249999999999957, + "10": -31.65000000000001, + "11": -65.34999999999998, + "12": -19.39999999999996, + "13": -81.10000000000001, + "14": -12.099999999999989, + "15": -27.799999999999933, + "16": -97.5, + "17": -13.399999999999984, + "18": -66.80000000000008, + "19": -18.29999999999997, + "20": -11.59999999999998, + "21": -71.05000000000005, + "22": -15.149999999999983, + "23": -18.54999999999997, + "24": -51.90000000000001, + "25": -19.54999999999996, + "26": -64.8500000000001, + "27": -78.94999999999996, + "28": -24.649999999999935, + "29": -63.800000000000104, + "30": -15.949999999999978, + "31": -0.24999999999996536, + "32": -7.449999999999997, + "33": -13.29999999999999, + "34": -18.64999999999997, + "35": -22.499999999999954, + "36": -36.20000000000002, + "37": -15.999999999999979, + "38": -23.04999999999995, + "39": -20.099999999999966, + "40": -14.149999999999977, + "41": -13.849999999999989, + "42": -20.69999999999996, + "43": -15.349999999999977, + "44": -11.650000000000004, + "45": -19.74999999999996, + "46": -11.849999999999993, + "47": -10.049999999999992, + "48": -78.7999999999999, + "49": -23.29999999999995, + "50": -16.199999999999967, + "51": -22.399999999999952, + "52": -12.14999999999999, + "53": -20.849999999999962, + "54": -46.05000000000007, + "55": -19.199999999999964, + "56": -17.249999999999975, + "57": -19.649999999999963, + "58": -10.7, + "59": -41.35000000000013, + "60": -35.35000000000003, + "61": -19.899999999999963, + "62": -18.949999999999964, + "63": -1.7999999999999938, + "64": -18.19999999999997, + "65": -7.05000000000001, + "66": -5.949999999999997, + "67": -2.649999999999964, + "68": -14.55, + "69": -33.54999999999995, + "70": -104.4, + "71": -62.25000000000008, + "72": -14.099999999999989, + "73": -8.1, + "74": -15.299999999999986, + "75": -7.699999999999998, + "76": -14.649999999999984, + "77": 0.20000000000002705, + "78": -4.3999999999999995, + "79": -8.850000000000009, + "80": -1.399999999999962, + "81": -94.55, + "82": -26.200000000000006, + "83": -5.899999999999989, + "84": -10.299999999999997, + "85": 14.200000000000006, + "86": -67.09999999999997, + "87": -23.849999999999948, + "88": -19.74999999999996, + "89": -19.899999999999963, + "90": 2.4999999999999614, + "91": -15.899999999999983, + "92": -21.899999999999956, + "93": 8.400000000000063, + "94": -47.25000000000005, + "95": -11.949999999999987, + "96": -3.649999999999981, + "97": 3.550000000000037, + "98": -10.849999999999996, + "99": -17.74999999999997, + "100": -17.89999999999997, + "101": -6.999999999999993, + "102": -14.49999999999999, + "103": -31.800000000000008, + "104": -21.199999999999957, + "105": -14.39999999999998, + "106": -5.749999999999986, + "107": -2.4499999999999744, + "108": -25.14999999999999, + "109": 2.0000000000000373, + "110": -28.29999999999995, + "111": -14.14999999999999, + "112": -83.15, + "113": -1.4000000000000008, + "114": -2.14999999999997, + "115": -49.30000000000006, + "116": -9.449999999999987, + "117": 9.500000000000043, + "118": 13.65000000000003, + "119": -5.350000000000008, + "120": -10.849999999999982, + "121": -6.64999999999998, + "122": -18.74999999999999, + "123": 0.9500000000000433, + "124": -7.499999999999983, + "125": -18.09999999999997, + "126": -22.499999999999975, + "127": 7.350000000000016, + "128": 6.75000000000004, + "129": 14.700000000000049, + "130": 16.899999999999995, + "131": -52.54999999999996, + "132": -89.35, + "133": -17.34999999999997, + "134": 33.99999999999979, + "135": 8.900000000000048, + "136": 0.5499999999999989, + "137": -11.349999999999998, + "138": 14.650000000000023, + "139": -5.5000000000000036, + "140": -9.500000000000004, + "141": -40.30000000000011, + "142": 20.849999999999973, + "143": -2.049999999999981, + "144": 39.899999999999736, + "145": -87.6, + "146": 37.74999999999996, + "147": 14.400000000000034, + "148": 9.50000000000004, + "149": 10.95000000000001, + "150": -2.549999999999983, + "151": -0.8499999999999777, + "152": -8.100000000000007, + "153": 23.649999999999913, + "154": -4.599999999999987, + "155": -70.4, + "156": 39.649999999999956, + "157": -80.55000000000001, + "158": 8.550000000000013, + "159": 41.49999999999975, + "160": -28.099999999999977, + "161": -17.599999999999973, + "162": 42.6499999999998, + "163": -36.35, + "164": -41.80000000000015, + "165": -4.450000000000005, + "166": 6.150000000000029, + "167": 23.65000000000003, + "168": 36.250000000000014, + "169": 16.650000000000045, + "170": 15.200000000000077, + "171": 33.14999999999989, + "172": 63.14999999999998, + "173": -10.550000000000002, + "174": -23.29999999999995, + "175": 16.749999999999893, + "176": -74.75000000000003, + "177": 31.75000000000006, + "178": 14.049999999999985, + "179": 61.94999999999975, + "180": 20.499999999999943, + "181": 39.69999999999988, + "182": 6.100000000000064, + "183": -7.250000000000073, + "184": -6.950000000000007, + "185": -69.10000000000001, + "186": 29.150000000000073, + "187": -76.60000000000002, + "188": 45.299999999999905, + "189": -50.74999999999996, + "190": -48.10000000000014, + "191": 43.74999999999974, + "192": 10.300000000000058, + "193": 54.9499999999999, + "194": 38.9, + "195": 8.150000000000055, + "196": 7.00000000000001, + "197": 44.59999999999982, + "198": 72.34999999999984, + "199": -47.9, + "200": -49.45000000000003, + "201": 46.29999999999976, + "202": -46.099999999999994, + "203": -2.8499999999999783, + "204": -9.450000000000045, + "205": 2.600000000000059, + "206": 31.550000000000026, + "207": -65.50000000000004, + "208": 55.29999999999975, + "209": -39.000000000000014, + "210": 72.29999999999986, + "211": 64.34999999999982, + "212": 42.44999999999975, + "213": 13.750000000000027, + "214": -21.300000000000033, + "215": 21.99999999999996, + "216": -12.299999999999985, + "217": 12.149999999999999, + "218": -67.85000000000007, + "219": 9.400000000000013, + "220": 50.24999999999981, + "221": -7.300000000000061, + "222": 99.84999999999995, + "223": 53.84999999999977, + "224": 27.100000000000044, + "225": -50.69999999999999, + "226": 17.0, + "227": 62.94999999999987, + "228": 61.14999999999988, + "229": -33.30000000000001, + "230": -56.850000000000016, + "231": 7.650000000000029, + "232": 75.10000000000004, + "233": 10.900000000000059, + "234": 41.2, + "235": 20.80000000000005, + "236": 65.34999999999977, + "237": 92.25000000000003, + "238": -34.149999999999984, + "239": 55.24999999999974, + "240": 24.999999999999925, + "241": 50.74999999999988, + "242": 59.09999999999977, + "243": -67.4, + "244": 66.49999999999979, + "245": 10.300000000000068, + "246": 69.09999999999977, + "247": 98.94999999999973, + "248": -65.04999999999994, + "249": -79.70000000000003, + "250": 104.89999999999986, + "251": 71.6999999999999, + "252": 87.69999999999986, + "253": -50.74999999999998, + "254": -56.29999999999996, + "255": 91.54999999999977, + "256": 48.89999999999978, + "257": 34.55000000000007, + "258": 20.249999999999936, + "259": 68.99999999999976, + "260": 46.94999999999985, + "261": 68.79999999999974, + "262": 38.44999999999996, + "263": 84.59999999999978, + "264": -74.45000000000006, + "265": 44.349999999999866, + "266": 70.54999999999973, + "267": 29.450000000000028, + "268": 83.2499999999998, + "269": 3.2499999999999343, + "270": -70.75, + "271": -6.750000000000014, + "272": 66.94999999999982, + "273": 59.899999999999835, + "274": 83.99999999999979, + "275": -65.85000000000002, + "276": 40.94999999999991, + "277": 81.09999999999972, + "278": 7.450000000000036, + "279": 28.399999999999917, + "280": -59.750000000000064, + "281": 58.849999999999795, + "282": 68.55, + "283": 22.59999999999995, + "284": 0.4000000000000341, + "285": -93.34999999999998, + "286": -1.4999999999999711, + "287": 22.900000000000023, + "288": 11.349999999999937, + "289": 79.09999999999978, + "290": 91.54999999999995, + "291": 58.19999999999978, + "292": 36.849999999999994, + "293": 78.14999999999985, + "294": 15.799999999999962, + "295": 58.599999999999774, + "296": 7.149999999999958, + "297": 39.99999999999988, + "298": 34.80000000000001, + "299": 86.79999999999978, + "300": 54.09999999999981, + "301": 91.29999999999973, + "302": 61.09999999999973, + "303": -11.449999999999982, + "304": 75.79999999999986, + "305": 33.04999999999986, + "306": -17.800000000000043, + "307": 89.59999999999977, + "308": 68.39999999999988, + "309": -55.85000000000005, + "310": 69.39999999999975, + "311": 88.19999999999987, + "312": 57.09999999999975, + "313": 23.20000000000005, + "314": 94.19999999999975, + "315": 91.14999999999979, + "316": 33.54999999999974, + "317": 94.79999999999976, + "318": 98.44999999999976, + "319": 53.449999999999726, + "320": 81.09999999999974, + "321": -12.450000000000038, + "322": 95.29999999999973, + "323": -6.65000000000002, + "324": 88.59999999999977, + "325": 101.29999999999987, + "326": 107.70000000000005, + "327": 101.29999999999995, + "328": 102.39999999999979, + "329": 15.799999999999955, + "330": 38.05000000000004, + "331": 67.99999999999989, + "332": 74.34999999999978, + "333": -19.399999999999967, + "334": 96.14999999999979, + "335": -5.7500000000000036, + "336": -9.849999999999985, + "337": 87.19999999999975, + "338": 97.5499999999998, + "339": 27.20000000000004, + "340": 43.799999999999976, + "341": 92.39999999999976, + "342": 92.79999999999976, + "343": 90.64999999999978, + "344": 90.10000000000004, + "345": 20.650000000000023, + "346": 96.4999999999999, + "347": -85.49999999999997, + "348": 38.299999999999955, + "349": 99.84999999999987, + "350": 93.09999999999981, + "351": 59.24999999999997, + "352": 66.74999999999984, + "353": 89.54999999999976, + "354": 60.39999999999989, + "355": 13.699999999999973, + "356": 99.69999999999985, + "357": 25.949999999999886, + "358": 79.24999999999976, + "359": -9.149999999999986, + "360": 94.29999999999974, + "361": 103.09999999999992, + "362": 99.3499999999999, + "363": 95.34999999999974, + "364": -43.89999999999998, + "365": 103.39999999999976, + "366": 102.34999999999978, + "367": 106.49999999999972, + "368": 101.34999999999975, + "369": 103.99999999999991, + "370": -72.85000000000001, + "371": 86.74999999999973, + "372": -9.499999999999982, + "373": 97.89999999999976, + "374": 100.44999999999976, + "375": -84.24999999999997, + "376": 101.34999999999975, + "377": 78.59999999999984, + "378": 100.59999999999978, + "379": -28.049999999999972, + "380": 9.80000000000001, + "381": 104.94999999999978, + "382": 102.79999999999977, + "383": 93.59999999999994, + "384": 64.25000000000009, + "385": 77.09999999999992, + "386": 92.24999999999979, + "387": 98.04999999999973, + "388": -76.55, + "389": 94.49999999999979, + "390": 89.34999999999977, + "391": 26.050000000000026, + "392": 27.999999999999975, + "393": -24.05000000000006, + "394": 106.04999999999976, + "395": 105.99999999999974, + "396": 102.39999999999975, + "397": 59.6499999999999, + "398": 97.99999999999977, + "399": 101.49999999999973, + "400": -18.7, + "401": 105.6000000000001, + "402": 95.99999999999977, + "403": 103.69999999999982, + "404": 90.14999999999976, + "405": 96.34999999999987, + "406": 87.99999999999982, + "407": 93.29999999999971, + "408": 98.89999999999986, + "409": 104.39999999999972, + "410": 97.2499999999999, + "411": 100.04999999999974, + "412": 86.5499999999998, + "413": -60.74999999999996, + "414": 99.79999999999977, + "415": 82.84999999999978, + "416": 101.19999999999975, + "417": 1.5000000000000357, + "418": 102.49999999999976, + "419": 65.89999999999989, + "420": 103.89999999999974, + "421": 96.84999999999974, + "422": 101.39999999999976, + "423": 102.14999999999976, + "424": 101.99999999999972, + "425": 101.74999999999976, + "426": 102.39999999999976, + "427": 106.99999999999977, + "428": -73.45, + "429": 104.14999999999974, + "430": 100.09999999999984, + "431": 102.49999999999979, + "432": 101.19999999999975, + "433": 101.24999999999973, + "434": 102.44999999999976, + "435": 102.59999999999977, + "436": 98.84999999999977, + "437": 85.04999999999976, + "438": -77.7, + "439": -89.75, + "440": 33.79999999999973, + "441": 94.74999999999979, + "442": 99.84999999999975, + "443": 99.64999999999978, + "444": 103.6, + "445": 101.64999999999976, + "446": 52.39999999999978, + "447": 100.19999999999976, + "448": 80.09999999999977, + "449": 103.1999999999998, + "450": 97.54999999999977, + "451": 87.94999999999976, + "452": 103.49999999999974, + "453": 75.04999999999977, + "454": 94.59999999999977, + "455": 84.64999999999982, + "456": 99.49999999999977, + "457": -0.9500000000000004, + "458": 82.89999999999972, + "459": 103.79999999999977, + "460": 102.39999999999974, + "461": 106.64999999999976, + "462": 95.24999999999979, + "463": 97.79999999999977, + "464": 84.49999999999982, + "465": -11.800000000000033, + "466": 101.04999999999978, + "467": 106.29999999999974, + "468": 18.449999999999903, + "469": 105.19999999999975, + "470": 105.59999999999972, + "471": 82.29999999999977, + "472": 103.44999999999976, + "473": 104.19999999999978, + "474": 104.94999999999975, + "475": 106.19999999999972, + "476": 101.19999999999976, + "477": 106.44999999999973, + "478": -66.74999999999999, + "479": 98.14999999999978, + "480": 102.29999999999976, + "481": 102.44999999999976, + "482": 85.79999999999973, + "483": -77.75, + "484": 95.94999999999976, + "485": 101.19999999999976, + "486": 97.69999999999975, + "487": 104.59999999999975, + "488": 102.24999999999977, + "489": 103.8499999999998, + "490": 103.74999999999977, + "491": 104.39999999999971, + "492": 100.64999999999974, + "493": 105.04999999999976, + "494": -41.75000000000008, + "495": 105.84999999999972, + "496": 106.59999999999972, + "497": 99.04999999999977, + "498": 86.34999999999974, + "499": 104.54999999999976, + "500": 102.44999999999978, + "501": 104.69999999999979, + "502": 104.39999999999976, + "503": 107.34999999999974, + "504": 94.84999999999981, + "505": 104.34999999999977, + "506": 100.34999999999975, + "507": 104.14999999999976, + "508": 81.09999999999985, + "509": 97.69999999999976, + "510": -71.14999999999999, + "511": 101.94999999999973, + "512": 98.79999999999978, + "513": 104.79999999999976, + "514": 103.84999999999977, + "515": 103.94999999999973, + "516": 100.04999999999976, + "517": 104.74999999999974, + "518": 101.99999999999977, + "519": 105.14999999999975, + "520": 106.69999999999973, + "521": -84.45, + "522": 101.99999999999974, + "523": 60.84999999999993, + "524": 68.99999999999977, + "525": 84.59999999999974, + "526": 97.84999999999977, + "527": 104.34999999999977, + "528": 104.64999999999975, + "529": 104.89999999999975, + "530": 104.09999999999977, + "531": 103.44999999999976, + "532": 105.94999999999986, + "533": 104.24999999999977, + "534": 73.69999999999986, + "535": 106.49999999999973, + "536": 107.44999999999973, + "537": 85.34999999999978, + "538": 97.04999999999977, + "539": 103.79999999999977, + "540": 97.24999999999976, + "541": 42.84999999999979, + "542": 54.34999999999977, + "543": 67.29999999999974, + "544": 80.19999999999973, + "545": 19.350000000000026, + "546": 90.84999999999975, + "547": 49.69999999999974, + "548": 68.64999999999976, + "549": 82.89999999999972, + "550": 105.49999999999973, + "551": 4.50000000000003, + "552": 103.19999999999978, + "553": 98.44999999999976, + "554": 48.04999999999981, + "555": 58.39999999999973, + "556": 105.39999999999974, + "557": 84.39999999999976, + "558": 55.44999999999979, + "559": -74.25, + "560": 7.249999999999989, + "561": 103.24999999999976, + "562": 101.64999999999986, + "563": 105.40000000000003, + "564": -83.85000000000001, + "565": 20.699999999999964, + "566": 61.54999999999973, + "567": 108.19999999999976, + "568": 70.19999999999975, + "569": 40.29999999999995, + "570": 69.89999999999972, + "571": 35.2, + "572": 72.19999999999983, + "573": 112.84999999999992, + "574": 100.04999999999973, + "575": 30.000000000000053, + "576": 102.49999999999977, + "577": 103.89999999999976, + "578": 51.749999999999716, + "579": 74.24999999999977, + "580": 49.34999999999983, + "581": 1.2000000000000497, + "582": 103.49999999999976, + "583": 79.14999999999988, + "584": 36.14999999999989, + "585": 104.04999999999973, + "586": -4.849999999999975, + "587": 106.94999999999973, + "588": 48.39999999999986, + "589": 73.79999999999973, + "590": 71.19999999999976, + "591": 106.14999999999974, + "592": 90.19999999999982, + "593": 102.44999999999975, + "594": -26.200000000000017, + "595": 104.79999999999973, + "596": 54.59999999999989, + "597": 75.1999999999998, + "598": 88.89999999999976, + "599": 100.14999999999976, + "600": 99.24999999999977, + "601": 55.299999999999756, + "602": 68.29999999999973, + "603": 102.04999999999977, + "604": 101.69999999999978, + "605": 76.04999999999977, + "606": 105.39999999999972, + "607": 102.49999999999977, + "608": 102.59999999999977, + "609": 102.14999999999976, + "610": 105.94999999999972, + "611": 65.59999999999975, + "612": 104.44999999999975, + "613": 107.2499999999998, + "614": -82.45, + "615": -5.00000000000005, + "616": 80.24999999999972, + "617": 98.04999999999977, + "618": 67.89999999999984, + "619": 99.74999999999976, + "620": 103.09999999999977, + "621": 103.19999999999973, + "622": -49.15000000000008, + "623": 98.69999999999976, + "624": 56.09999999999976, + "625": 107.44999999999975, + "626": 103.59999999999977, + "627": 38.049999999999756, + "628": -41.949999999999996, + "629": -88.2, + "630": 104.7999999999998, + "631": 107.59999999999972, + "632": -9.949999999999976, + "633": 86.69999999999972, + "634": 104.49999999999977, + "635": 86.89999999999984, + "636": 61.24999999999979, + "637": 73.19999999999973, + "638": -31.000000000000007, + "639": 76.99999999999979, + "640": 80.19999999999976, + "641": 74.8999999999998, + "642": 103.79999999999976, + "643": 97.39999999999974, + "644": 107.44999999999976, + "645": 97.94999999999979, + "646": 104.74999999999976, + "647": 102.64999999999972, + "648": 104.89999999999972, + "649": 104.59999999999974, + "650": 102.74999999999976, + "651": 102.89999999999976, + "652": 105.34999999999972, + "653": 105.59999999999975, + "654": 101.99999999999977, + "655": 101.19999999999978, + "656": 106.59999999999972, + "657": 105.04999999999974, + "658": 76.14999999999978, + "659": 104.69999999999976, + "660": 103.04999999999977, + "661": -3.800000000000068, + "662": 103.94999999999976, + "663": 103.99999999999976, + "664": 101.04999999999976, + "665": 103.44999999999976, + "666": 98.09999999999977, + "667": 90.65, + "668": 69.89999999999976, + "669": 103.59999999999977, + "670": 105.09999999999975, + "671": 104.19999999999976, + "672": 104.79999999999974, + "673": 66.19999999999978, + "674": -6.449999999999992, + "675": 104.34999999999977, + "676": 79.89999999999976, + "677": 97.49999999999977, + "678": 81.64999999999975, + "679": 48.19999999999988, + "680": 89.09999999999975, + "681": 108.29999999999974, + "682": 105.74999999999973, + "683": 102.54999999999977, + "684": 38.79999999999977, + "685": 103.69999999999976, + "686": 80.2999999999998, + "687": 103.49999999999977, + "688": 107.7999999999998, + "689": 104.84999999999975, + "690": 100.39999999999976, + "691": -71.64999999999999, + "692": 104.24999999999976, + "693": 102.64999999999976, + "694": 104.09999999999977, + "695": 105.09999999999978, + "696": 93.24999999999976, + "697": 70.94999999999972, + "698": 104.84999999999975, + "699": 65.79999999999981, + "700": 108.39999999999974, + "701": 100.54999999999971, + "702": 104.79999999999976, + "703": 102.84999999999977, + "704": 103.49999999999977, + "705": 104.89999999999974, + "706": 101.14999999999975, + "707": 104.89999999999972, + "708": 103.94999999999975, + "709": 102.49999999999977, + "710": 62.44999999999977, + "711": 102.39999999999976, + "712": 105.99999999999973, + "713": 104.49999999999974, + "714": 105.34999999999974, + "715": 106.69999999999972, + "716": 38.44999999999985, + "717": 103.64999999999976, + "718": 103.09999999999977, + "719": 102.89999999999978, + "720": 35.2499999999998, + "721": 103.99999999999976, + "722": 105.04999999999974, + "723": 103.39999999999976, + "724": 104.54999999999977, + "725": -81.7, + "726": 104.54999999999976, + "727": 100.89999999999975, + "728": 105.44999999999975, + "729": 111.64999999999979, + "730": 104.69999999999976, + "731": 99.89999999999978, + "732": 2.8999999999999346, + "733": 104.39999999999976, + "734": 103.14999999999976, + "735": 102.99999999999977, + "736": 103.39999999999976, + "737": 105.69999999999975, + "738": -85.25, + "739": 41.99999999999972, + "740": 103.99999999999976, + "741": 17.600000000000016, + "742": 65.44999999999976, + "743": 102.24999999999977, + "744": 102.49999999999977, + "745": 105.89999999999972, + "746": 102.64999999999978, + "747": 104.69999999999976, + "748": 102.79999999999977, + "749": 102.19999999999978, + "750": 104.49999999999973, + "751": 102.64999999999978, + "752": 104.74999999999977, + "753": 104.54999999999976, + "754": 99.79999999999978, + "755": 103.94999999999976, + "756": 66.09999999999991, + "757": 103.99999999999976, + "758": -85.1, + "759": 103.29999999999977, + "760": 106.04999999999974, + "761": 99.29999999999973, + "762": 104.89999999999975, + "763": 104.34999999999977, + "764": 103.69999999999976, + "765": 102.14999999999978, + "766": 104.84999999999974, + "767": 103.09999999999977, + "768": 104.04999999999974, + "769": 104.69999999999975, + "770": 104.49999999999976, + "771": 108.84999999999974, + "772": 101.49999999999976, + "773": 103.69999999999976, + "774": 60.79999999999977, + "775": 103.29999999999977, + "776": 104.84999999999975, + "777": 104.29999999999976, + "778": 102.84999999999977, + "779": 103.89999999999976, + "780": 104.54999999999977, + "781": 103.79999999999976, + "782": 105.59999999999974, + "783": 102.84999999999977, + "784": 104.69999999999976, + "785": 101.59999999999977, + "786": 96.09999999999974, + "787": 105.99999999999972, + "788": 104.34999999999977, + "789": 103.79999999999977, + "790": 103.24999999999977, + "791": 102.89999999999976, + "792": 96.19999999999976, + "793": 105.09999999999978, + "794": 52.8499999999999, + "795": 105.24999999999974, + "796": 107.49999999999972, + "797": 111.64999999999986, + "798": 104.59999999999975, + "799": 73.74999999999973, + "800": 104.69999999999975, + "801": 105.49999999999974, + "802": -69.5, + "803": 105.69999999999975, + "804": 103.89999999999976, + "805": 105.19999999999973, + "806": 103.89999999999976, + "807": 107.24999999999974, + "808": 105.39999999999974, + "809": 106.69999999999972, + "810": 104.59999999999975, + "811": 81.64999999999984, + "812": 103.84999999999977, + "813": -63.90000000000002, + "814": 106.59999999999972, + "815": -68.5, + "816": 103.59999999999977, + "817": 104.99999999999974, + "818": 104.29999999999977, + "819": -9.799999999999994, + "820": 104.19999999999976, + "821": 109.09999999999977, + "822": 103.04999999999977, + "823": 108.49999999999974, + "824": 105.24999999999972, + "825": 103.94999999999976, + "826": 107.69999999999972, + "827": 103.49999999999977, + "828": 59.39999999999974, + "829": -74.35000000000001, + "830": 103.84999999999977, + "831": 91.04999999999978, + "832": 103.54999999999977, + "833": 105.19999999999979, + "834": 102.84999999999974, + "835": 106.09999999999972, + "836": 104.04999999999976, + "837": 104.59999999999977, + "838": 109.09999999999977, + "839": 103.29999999999977, + "840": 104.14999999999976, + "841": 103.34999999999977, + "842": 106.19999999999972, + "843": 103.59999999999977, + "844": 100.54999999999977, + "845": 103.84999999999977, + "846": 104.44999999999976, + "847": 103.89999999999978, + "848": 105.84999999999974, + "849": -61.300000000000004, + "850": 103.79999999999976, + "851": 105.59999999999974, + "852": 103.64999999999976, + "853": 105.74999999999974, + "854": 106.0999999999998, + "855": 109.14999999999975, + "856": 106.79999999999971, + "857": 105.39999999999975, + "858": 101.14999999999976, + "859": 104.24999999999973, + "860": 104.19999999999976, + "861": 106.94999999999973, + "862": 102.94999999999978, + "863": 104.84999999999975, + "864": 103.49999999999976, + "865": 103.04999999999977, + "866": 105.94999999999973, + "867": 102.34999999999978, + "868": 107.29999999999974, + "869": 104.14999999999976, + "870": 103.34999999999977, + "871": 103.89999999999974, + "872": -77.69999999999999, + "873": -86.14999999999999, + "874": 103.19999999999976, + "875": 108.6499999999998, + "876": 105.44999999999975, + "877": 105.89999999999974, + "878": 105.14999999999972, + "879": 103.64999999999976, + "880": 70.64999999999974, + "881": 103.09999999999977, + "882": 105.24999999999984, + "883": 103.69999999999976, + "884": 107.29999999999973, + "885": 103.59999999999975, + "886": 105.59999999999974, + "887": 104.69999999999978, + "888": 102.34999999999978, + "889": 102.99999999999977, + "890": 107.69999999999972, + "891": 104.24999999999976, + "892": 100.89999999999978, + "893": 103.69999999999978, + "894": 106.34999999999972, + "895": 107.39999999999974, + "896": 103.44999999999978, + "897": 103.54999999999977, + "898": 101.14999999999975, + "899": 104.14999999999976, + "900": 105.84999999999972, + "901": 69.94999999999983, + "902": -37.80000000000014, + "903": 9.05000000000001, + "904": -22.399999999999988, + "905": 49.749999999999915, + "906": 22.449999999999854, + "907": 66.74999999999989, + "908": 69.29999999999987, + "909": 30.299999999999937, + "910": 83.39999999999984, + "911": 23.899999999999878, + "912": -70.00000000000003, + "913": 43.499999999999865, + "914": 51.34999999999983, + "915": 40.50000000000001, + "916": 55.599999999999795, + "917": -5.299999999999992, + "918": 99.69999999999979, + "919": 24.89999999999985, + "920": 32.19999999999985, + "921": 94.69999999999978, + "922": 18.75000000000001, + "923": -1.2500000000000309, + "924": 49.799999999999876, + "925": 6.649999999999977, + "926": 92.04999999999981, + "927": 38.100000000000016, + "928": 52.099999999999895, + "929": 61.399999999999984, + "930": 53.399999999999935, + "931": 79.69999999999986, + "932": 66.04999999999977, + "933": -14.500000000000021, + "934": 73.94999999999979, + "935": 71.44999999999979, + "936": 104.69999999999982, + "937": 93.74999999999982, + "938": 21.799999999999986, + "939": 107.49999999999973, + "940": 83.3499999999999, + "941": 77.09999999999995, + "942": 92.24999999999986, + "943": 103.34999999999977, + "944": 104.64999999999979, + "945": 103.59999999999975, + "946": -20.79999999999999, + "947": 64.84999999999978, + "948": 104.14999999999976, + "949": 55.599999999999916, + "950": 97.54999999999976, + "951": 103.24999999999976, + "952": 107.04999999999974, + "953": 104.79999999999976, + "954": 103.04999999999977, + "955": 89.69999999999976, + "956": 86.39999999999988, + "957": 104.04999999999977, + "958": 103.89999999999976, + "959": 87.29999999999978, + "960": 95.09999999999981, + "961": 104.09999999999975, + "962": 103.49999999999977, + "963": 87.14999999999976, + "964": 101.9999999999998, + "965": 103.49999999999977, + "966": 82.94999999999978, + "967": 108.14999999999974, + "968": 77.59999999999977, + "969": 103.89999999999976, + "970": 109.74999999999976, + "971": 108.89999999999976, + "972": 103.59999999999977, + "973": 108.09999999999974, + "974": 103.24999999999977, + "975": 105.39999999999975, + "976": 105.04999999999976, + "977": 107.74999999999977, + "978": 103.74999999999977, + "979": 103.49999999999976, + "980": -77.8, + "981": 108.69999999999973, + "982": 105.54999999999973, + "983": 103.49999999999976, + "984": 106.99999999999973, + "985": 103.89999999999976, + "986": -63.9, + "987": 102.89999999999978, + "988": 109.24999999999977, + "989": 111.94999999999995, + "990": 106.79999999999974, + "991": -64.75000000000001, + "992": 107.59999999999972, + "993": 98.29999999999976, + "994": 103.39999999999976, + "995": 104.49999999999976, + "996": 88.94999999999982, + "997": 103.24999999999977, + "998": -62.95, + "999": -70.9, + "1000": 103.34999999999977 + }, + "3": { + "1": -64.2500000000001, + "2": -10.899999999999991, + "3": -30.800000000000004, + "4": -14.649999999999977, + "5": -75.69999999999999, + "6": -60.350000000000094, + "7": -21.8, + "8": -93.69999999999996, + "9": -19.499999999999964, + "10": -34.64999999999998, + "11": -17.999999999999968, + "12": -38.15000000000004, + "13": -15.749999999999979, + "14": -15.34999999999998, + "15": -21.599999999999955, + "16": -55.05000000000011, + "17": -10.049999999999995, + "18": -20.949999999999957, + "19": -53.30000000000008, + "20": -13.199999999999989, + "21": -19.29999999999997, + "22": -10.65000000000001, + "23": -9.000000000000002, + "24": -103.89999999999996, + "25": -12.64999999999999, + "26": -1.8999999999999888, + "27": -26.54999999999997, + "28": -34.600000000000044, + "29": -29.650000000000013, + "30": -64.30000000000015, + "31": -43.50000000000005, + "32": -29.600000000000023, + "33": -18.999999999999993, + "34": -101.0, + "35": -21.499999999999957, + "36": -21.499999999999957, + "37": -8.699999999999983, + "38": -6.550000000000001, + "39": -20.74999999999996, + "40": -17.999999999999968, + "41": -64.0500000000001, + "42": -17.349999999999977, + "43": -17.099999999999973, + "44": -14.899999999999965, + "45": -10.499999999999995, + "46": -13.849999999999985, + "47": -54.05000000000008, + "48": -16.79999999999998, + "49": -16.849999999999973, + "50": -61.650000000000155, + "51": -15.699999999999987, + "52": -47.80000000000007, + "53": -75.5, + "54": -25.049999999999944, + "55": -95.6, + "56": -7.65, + "57": -3.150000000000033, + "58": -16.649999999999977, + "59": -15.199999999999985, + "60": -17.099999999999977, + "61": -1.149999999999972, + "62": -93.65, + "63": -20.349999999999962, + "64": -7.749999999999991, + "65": -21.049999999999958, + "66": -23.19999999999995, + "67": -40.60000000000015, + "68": -18.699999999999967, + "69": -76.6999999999999, + "70": 5.15000000000003, + "71": -14.299999999999981, + "72": -8.399999999999997, + "73": -23.29999999999995, + "74": -21.550000000000004, + "75": -11.699999999999982, + "76": -66.05000000000005, + "77": -93.85, + "78": -15.749999999999982, + "79": -101.05000000000001, + "80": -11.600000000000007, + "81": -85.0000000000001, + "82": -6.999999999999995, + "83": 22.04999999999996, + "84": -47.15000000000007, + "85": -3.4999999999999805, + "86": -18.049999999999972, + "87": -97.4, + "88": -77.79999999999995, + "89": 9.00000000000001, + "90": -15.049999999999983, + "91": -4.350000000000004, + "92": -21.499999999999954, + "93": -3.6999999999999797, + "94": -39.69999999999998, + "95": -57.45000000000009, + "96": -17.349999999999973, + "97": -7.249999999999995, + "98": -14.199999999999978, + "99": -11.699999999999987, + "100": 2.5000000000000444, + "101": -12.649999999999984, + "102": -3.750000000000001, + "103": -20.349999999999962, + "104": -90.05, + "105": -18.299999999999972, + "106": -0.9000000000000015, + "107": -57.05, + "108": -2.399999999999965, + "109": -49.15000000000007, + "110": -20.49999999999996, + "111": -8.749999999999996, + "112": -79.10000000000001, + "113": -17.14999999999997, + "114": -3.0499999999999785, + "115": -64.35, + "116": -47.39999999999996, + "117": -10.999999999999996, + "118": -12.199999999999989, + "119": -16.89999999999998, + "120": -64.85000000000001, + "121": -6.749999999999996, + "122": 7.750000000000069, + "123": 13.75, + "124": -3.0999999999999863, + "125": -27.09999999999994, + "126": -16.649999999999977, + "127": 19.349999999999955, + "128": -49.350000000000044, + "129": -21.2, + "130": -39.49999999999999, + "131": -74.44999999999999, + "132": -5.449999999999989, + "133": -0.3499999999999986, + "134": -14.499999999999979, + "135": -21.699999999999953, + "136": -1.7499999999999736, + "137": -12.149999999999993, + "138": 23.949999999999946, + "139": -48.3, + "140": -11.49999999999999, + "141": -43.150000000000006, + "142": -11.04999999999999, + "143": -18.09999999999997, + "144": -11.1, + "145": 19.249999999999986, + "146": -90.4, + "147": 35.69999999999979, + "148": -78.85, + "149": -39.95000000000009, + "150": -6.799999999999987, + "151": -10.35, + "152": 16.45000000000007, + "153": 8.500000000000078, + "154": -18.199999999999967, + "155": -1.0999999999999819, + "156": 12.350000000000023, + "157": -36.69999999999998, + "158": 14.750000000000039, + "159": -13.999999999999991, + "160": -2.399999999999996, + "161": 14.250000000000043, + "162": -80.15000000000002, + "163": -19.499999999999964, + "164": 18.00000000000006, + "165": -41.499999999999964, + "166": 6.550000000000042, + "167": 5.700000000000033, + "168": -15.95000000000001, + "169": -10.549999999999978, + "170": -89.14999999999996, + "171": -0.2999999999999714, + "172": -17.70000000000004, + "173": -9.450000000000001, + "174": 14.849999999999959, + "175": -90.44999999999999, + "176": -11.799999999999978, + "177": -56.5, + "178": -13.249999999999984, + "179": -55.35, + "180": -17.699999999999974, + "181": -17.85000000000002, + "182": -7.799999999999989, + "183": -49.900000000000006, + "184": 27.400000000000055, + "185": 31.449999999999942, + "186": -49.59999999999996, + "187": 16.20000000000001, + "188": 2.5500000000000336, + "189": 27.44999999999993, + "190": -3.049999999999991, + "191": -60.84999999999995, + "192": 5.850000000000024, + "193": -7.199999999999984, + "194": -48.800000000000004, + "195": -69.60000000000004, + "196": 25.200000000000067, + "197": -37.649999999999956, + "198": -64.1500000000001, + "199": -48.59999999999997, + "200": -71.94999999999993, + "201": -18.249999999999968, + "202": -14.450000000000003, + "203": 4.750000000000047, + "204": 30.049999999999923, + "205": -5.549999999999984, + "206": -32.64999999999997, + "207": 16.450000000000045, + "208": -46.25, + "209": 11.549999999999937, + "210": 15.100000000000076, + "211": 23.450000000000063, + "212": -6.50000000000001, + "213": -35.00000000000002, + "214": 16.199999999999992, + "215": 46.099999999999845, + "216": -11.49999999999999, + "217": 5.550000000000029, + "218": 23.749999999999908, + "219": -56.75000000000003, + "220": 2.400000000000005, + "221": -9.299999999999951, + "222": 83.19999999999989, + "223": 46.249999999999915, + "224": 16.449999999999932, + "225": 34.49999999999993, + "226": -86.15, + "227": -8.049999999999992, + "228": -39.1, + "229": 15.749999999999899, + "230": -53.80000000000006, + "231": -24.649999999999956, + "232": 6.149999999999947, + "233": -27.50000000000003, + "234": 10.249999999999982, + "235": -9.850000000000056, + "236": -49.05, + "237": -25.099999999999987, + "238": 1.4500000000000328, + "239": 44.749999999999794, + "240": -23.800000000000022, + "241": 49.34999999999976, + "242": 26.250000000000018, + "243": 12.250000000000032, + "244": -5.773159728050814e-15, + "245": -15.94999999999996, + "246": 5.600000000000033, + "247": -12.049999999999978, + "248": 36.699999999999775, + "249": 27.94999999999998, + "250": -0.34999999999997033, + "251": -46.449999999999996, + "252": -21.749999999999957, + "253": 35.649999999999984, + "254": 47.79999999999981, + "255": 2.3000000000000114, + "256": 49.75000000000003, + "257": 48.54999999999982, + "258": 18.55000000000003, + "259": 25.85000000000007, + "260": -0.9500000000000135, + "261": 35.8999999999999, + "262": 62.64999999999988, + "263": -6.200000000000021, + "264": 41.94999999999994, + "265": 49.94999999999991, + "266": 49.49999999999995, + "267": -13.349999999999987, + "268": 67.94999999999983, + "269": 41.39999999999988, + "270": 15.000000000000068, + "271": -47.39999999999999, + "272": -82.35, + "273": 13.600000000000065, + "274": 43.84999999999982, + "275": 36.19999999999991, + "276": 39.64999999999994, + "277": 40.99999999999974, + "278": 11.800000000000047, + "279": 32.94999999999998, + "280": 81.80000000000007, + "281": 58.499999999999936, + "282": -15.399999999999983, + "283": 8.40000000000001, + "284": 30.95, + "285": 14.400000000000006, + "286": -10.149999999999995, + "287": 44.84999999999989, + "288": 49.5999999999999, + "289": 69.59999999999988, + "290": 63.049999999999784, + "291": 86.50000000000001, + "292": 47.64999999999981, + "293": 71.1499999999998, + "294": -7.049999999999991, + "295": 47.34999999999976, + "296": 102.65000000000008, + "297": 66.04999999999983, + "298": 63.899999999999935, + "299": 1.5000000000000955, + "300": 24.95000000000004, + "301": 54.74999999999998, + "302": -13.150000000000004, + "303": 52.64999999999996, + "304": 40.2999999999998, + "305": 83.10000000000001, + "306": -0.29999999999999305, + "307": -7.599999999999988, + "308": 58.74999999999989, + "309": 47.24999999999983, + "310": 70.59999999999985, + "311": 26.299999999999976, + "312": 38.4999999999999, + "313": 0.2999999999999834, + "314": 41.84999999999984, + "315": 91.40000000000015, + "316": -50.15000000000002, + "317": 56.24999999999983, + "318": 38.24999999999996, + "319": 5.849999999999965, + "320": 32.30000000000001, + "321": 47.35000000000001, + "322": 58.45000000000001, + "323": 11.04999999999999, + "324": -0.04999999999999771, + "325": 53.2999999999998, + "326": 84.10000000000014, + "327": 18.20000000000004, + "328": 68.14999999999982, + "329": 96.35000000000022, + "330": 64.09999999999994, + "331": 56.850000000000044, + "332": 95.80000000000014, + "333": 64.24999999999976, + "334": 13.299999999999962, + "335": 78.4499999999999, + "336": 55.099999999999945, + "337": 93.25, + "338": -9.449999999999983, + "339": 46.64999999999997, + "340": 82.04999999999998, + "341": 41.94999999999985, + "342": 94.45000000000003, + "343": 28.599999999999973, + "344": -11.29999999999999, + "345": 83.59999999999975, + "346": 12.250000000000037, + "347": 43.54999999999998, + "348": 85.2000000000001, + "349": 46.2999999999999, + "350": 48.49999999999998, + "351": 75.39999999999988, + "352": -18.69999999999997, + "353": 55.399999999999935, + "354": 97.35000000000015, + "355": 29.99999999999998, + "356": 87.15000000000005, + "357": 103.3500000000002, + "358": 16.800000000000047, + "359": 88.55000000000007, + "360": 36.15, + "361": 11.800000000000036, + "362": 58.44999999999989, + "363": 69.04999999999995, + "364": 20.099999999999994, + "365": 44.09999999999997, + "366": 82.40000000000019, + "367": 99.25000000000024, + "368": 74.4, + "369": 71.5000000000001, + "370": 105.35000000000024, + "371": 99.60000000000014, + "372": 66.64999999999998, + "373": 84.60000000000004, + "374": 81.05000000000011, + "375": 80.60000000000004, + "376": 59.999999999999915, + "377": 48.59999999999975, + "378": 63.6999999999998, + "379": 83.6, + "380": 82.09999999999994, + "381": -15.900000000000022, + "382": 59.14999999999999, + "383": 38.09999999999995, + "384": 98.75000000000016, + "385": 15.499999999999963, + "386": 102.85000000000022, + "387": 55.10000000000002, + "388": 68.54999999999988, + "389": 38.649999999999864, + "390": 105.55000000000022, + "391": 58.999999999999915, + "392": 67.6999999999999, + "393": 57.949999999999946, + "394": 94.20000000000017, + "395": 104.05000000000017, + "396": 96.35000000000018, + "397": 51.54999999999997, + "398": 105.35000000000015, + "399": 1.1499999999999722, + "400": 79.45000000000003, + "401": 101.05000000000004, + "402": 100.25000000000023, + "403": 76.55000000000001, + "404": 109.10000000000022, + "405": 65.04999999999995, + "406": 23.99999999999995, + "407": 52.29999999999998, + "408": 63.89999999999994, + "409": 97.10000000000015, + "410": 96.30000000000014, + "411": 54.949999999999825, + "412": 102.79999999999993, + "413": 60.199999999999996, + "414": 112.80000000000018, + "415": 41.849999999999945, + "416": 101.45, + "417": 61.75000000000006, + "418": 95.55000000000017, + "419": 98.70000000000019, + "420": 27.899999999999984, + "421": 86.70000000000005, + "422": 54.19999999999976, + "423": 95.19999999999978, + "424": 104.40000000000023, + "425": 102.59999999999984, + "426": 31.499999999999872, + "427": 12.299999999999995, + "428": 86.5000000000001, + "429": 22.599999999999987, + "430": 80.25000000000006, + "431": 101.30000000000011, + "432": 90.4999999999998, + "433": 98.25000000000006, + "434": 36.59999999999995, + "435": 56.649999999999906, + "436": 3.4499999999999584, + "437": -12.400000000000013, + "438": 80.20000000000003, + "439": 78.24999999999987, + "440": 85.50000000000007, + "441": 69.74999999999997, + "442": 108.8000000000003, + "443": 97.45000000000016, + "444": 90.2000000000002, + "445": 92.35000000000018, + "446": 50.95000000000005, + "447": 91.25000000000017, + "448": 82.90000000000016, + "449": 102.20000000000023, + "450": 59.39999999999996, + "451": 95.95000000000019, + "452": 29.04999999999998, + "453": 90.85000000000004, + "454": 78.85000000000001, + "455": 65.79999999999993, + "456": 52.50000000000005, + "457": 105.95000000000009, + "458": 91.10000000000024, + "459": 72.14999999999995, + "460": 97.20000000000013, + "461": 95.50000000000003, + "462": 102.59999999999997, + "463": 84.1499999999998, + "464": 35.199999999999925, + "465": 92.90000000000018, + "466": 60.79999999999991, + "467": 55.84999999999985, + "468": 81.05000000000011, + "469": 70.69999999999986, + "470": 100.45000000000017, + "471": 74.04999999999991, + "472": 104.45000000000017, + "473": 62.149999999999984, + "474": 54.949999999999996, + "475": 93.70000000000016, + "476": 100.90000000000025, + "477": 17.750000000000092, + "478": 59.64999999999989, + "479": 73.59999999999997, + "480": 65.99999999999987, + "481": 38.74999999999993, + "482": 102.6500000000002, + "483": 46.249999999999886, + "484": 63.749999999999936, + "485": 55.399999999999956, + "486": 109.40000000000022, + "487": 29.250000000000014, + "488": 58.700000000000045, + "489": 104.35000000000022, + "490": 59.49999999999989, + "491": 101.25000000000007, + "492": 53.249999999999915, + "493": 53.24999999999991, + "494": 79.4000000000001, + "495": 88.95000000000007, + "496": 20.14999999999995, + "497": 88.15000000000012, + "498": 66.89999999999999, + "499": 97.7000000000002, + "500": 94.10000000000011, + "501": 105.90000000000026, + "502": 41.849999999999945, + "503": 51.449999999999925, + "504": 50.84999999999991, + "505": 105.50000000000021, + "506": 67.54999999999993, + "507": 103.3500000000002, + "508": 97.4500000000002, + "509": 61.2000000000001, + "510": 69.74999999999993, + "511": 70.64999999999992, + "512": 96.05000000000014, + "513": 53.84999999999992, + "514": 63.29999999999977, + "515": -5.150000000000013, + "516": 102.50000000000018, + "517": 73.14999999999993, + "518": 74.95000000000002, + "519": 101.80000000000008, + "520": 102.05000000000004, + "521": 32.34999999999997, + "522": 47.699999999999946, + "523": 47.24999999999991, + "524": 59.1999999999999, + "525": 51.69999999999993, + "526": 38.35000000000001, + "527": 58.29999999999991, + "528": 28.94999999999998, + "529": 42.69999999999996, + "530": 34.19999999999997, + "531": 66.7999999999999, + "532": 105.80000000000021, + "533": 112.15000000000025, + "534": 103.3000000000002, + "535": 108.55000000000024, + "536": 80.05000000000005, + "537": 72.74999999999999, + "538": 54.64999999999994, + "539": 96.65000000000012, + "540": 63.64999999999992, + "541": 44.54999999999993, + "542": 104.45000000000023, + "543": 28.499999999999957, + "544": 82.9, + "545": 75.54999999999997, + "546": 96.45000000000013, + "547": 93.70000000000014, + "548": 97.45000000000013, + "549": 34.199999999999996, + "550": 112.20000000000017, + "551": 50.54999999999987, + "552": 108.80000000000024, + "553": 91.20000000000006, + "554": 4.450000000000015, + "555": 101.75000000000018, + "556": 53.84999999999991, + "557": 75.0, + "558": 100.8000000000002, + "559": 23.44999999999999, + "560": 12.899999999999986, + "561": 40.34999999999995, + "562": 95.30000000000011, + "563": 84.00000000000014, + "564": 96.35000000000014, + "565": 97.75000000000014, + "566": 97.79999999999995, + "567": 81.44999999999987, + "568": 94.65, + "569": 59.7499999999999, + "570": 71.39999999999993, + "571": 22.75, + "572": -5.800000000000004, + "573": 36.89999999999998, + "574": 63.29999999999989, + "575": 22.34999999999997, + "576": 70.19999999999992, + "577": 81.7499999999999, + "578": 61.94999999999994, + "579": 106.95000000000016, + "580": 29.20000000000001, + "581": 55.499999999999936, + "582": 79.25000000000004, + "583": 36.849999999999916, + "584": 32.69999999999993, + "585": 4.449999999999999, + "586": 38.7499999999999, + "587": 51.69999999999991, + "588": -0.6000000000000152, + "589": 60.84999999999991, + "590": 51.749999999999886, + "591": 66.49999999999993, + "592": 94.25000000000007, + "593": 91.25000000000014, + "594": 96.19999999999993, + "595": 64.29999999999988, + "596": 104.9000000000002, + "597": 59.299999999999955, + "598": 80.25, + "599": 68.84999999999981, + "600": 101.30000000000014, + "601": 98.95000000000012, + "602": 101.20000000000014, + "603": 93.45000000000005, + "604": 43.89999999999987, + "605": 52.349999999999945, + "606": 101.3500000000002, + "607": 98.20000000000019, + "608": 99.45000000000022, + "609": 91.50000000000013, + "610": 22.7, + "611": 103.40000000000013, + "612": 101.95000000000016, + "613": 84.0500000000001, + "614": 92.95000000000012, + "615": 38.39999999999995, + "616": -16.799999999999983, + "617": 80.15000000000002, + "618": 93.05000000000014, + "619": 98.45000000000016, + "620": 100.30000000000017, + "621": 95.14999999999998, + "622": 69.69999999999993, + "623": 60.04999999999989, + "624": 98.75000000000018, + "625": 108.40000000000026, + "626": 103.45000000000017, + "627": 89.34999999999988, + "628": 92.7000000000001, + "629": 19.199999999999925, + "630": 100.40000000000019, + "631": 89.89999999999999, + "632": 87.00000000000013, + "633": 102.20000000000017, + "634": 54.24999999999986, + "635": 41.19999999999994, + "636": 25.700000000000006, + "637": 79.35000000000005, + "638": 109.90000000000006, + "639": 103.00000000000016, + "640": 83.40000000000008, + "641": 110.35000000000025, + "642": 8.400000000000011, + "643": 88.65000000000009, + "644": 108.50000000000023, + "645": 109.2500000000002, + "646": 28.749999999999982, + "647": 71.59999999999991, + "648": 103.80000000000018, + "649": 115.35000000000026, + "650": 91.30000000000011, + "651": 62.799999999999926, + "652": 102.5000000000002, + "653": 100.7500000000001, + "654": 24.099999999999888, + "655": 107.95000000000019, + "656": 61.89999999999989, + "657": 105.65000000000018, + "658": 106.8000000000002, + "659": 104.90000000000023, + "660": 92.70000000000013, + "661": 101.45000000000024, + "662": 70.69999999999997, + "663": 24.700000000000003, + "664": 88.95000000000003, + "665": 91.95000000000012, + "666": 15.100000000000007, + "667": 52.64999999999994, + "668": 103.85000000000014, + "669": 56.999999999999936, + "670": 37.09999999999996, + "671": 101.85000000000021, + "672": 100.30000000000014, + "673": 100.15000000000013, + "674": 71.89999999999995, + "675": 77.95000000000006, + "676": 103.20000000000019, + "677": -4.400000000000001, + "678": 73.95, + "679": 68.2999999999999, + "680": 102.7000000000002, + "681": 106.95000000000019, + "682": 109.4500000000001, + "683": 74.64999999999996, + "684": 113.95000000000024, + "685": 102.95000000000017, + "686": 100.69999999999996, + "687": 90.15000000000008, + "688": 102.20000000000019, + "689": 78.3, + "690": 51.59999999999995, + "691": 76.60000000000001, + "692": 105.4000000000001, + "693": 77.90000000000003, + "694": 54.34999999999986, + "695": 108.60000000000022, + "696": 65.49999999999993, + "697": 45.899999999999956, + "698": 101.00000000000016, + "699": 105.85000000000022, + "700": 108.15000000000025, + "701": 35.54999999999997, + "702": 13.60000000000002, + "703": 112.25000000000024, + "704": 88.1, + "705": 102.3500000000002, + "706": 107.4500000000002, + "707": 36.5999999999999, + "708": 35.34999999999992, + "709": 63.49999999999995, + "710": 50.199999999999946, + "711": 108.15000000000015, + "712": 49.24999999999996, + "713": 47.39999999999993, + "714": 87.30000000000008, + "715": 101.80000000000017, + "716": 104.0000000000002, + "717": 97.80000000000007, + "718": 105.10000000000024, + "719": 109.20000000000024, + "720": 99.25000000000023, + "721": 98.30000000000001, + "722": 57.199999999999946, + "723": -32.70000000000001, + "724": 23.69999999999999, + "725": 101.44999999999997, + "726": 70.14999999999998, + "727": 102.90000000000022, + "728": 102.10000000000011, + "729": 104.35000000000022, + "730": 36.10000000000001, + "731": 91.05000000000008, + "732": 79.79999999999995, + "733": 92.29999999999995, + "734": 30.250000000000036, + "735": 89.5499999999999, + "736": 65.69999999999995, + "737": 102.5000000000002, + "738": 76.39999999999999, + "739": 63.44999999999994, + "740": 46.549999999999955, + "741": 37.09999999999995, + "742": 106.40000000000013, + "743": 64.94999999999993, + "744": 83.25000000000004, + "745": 99.8000000000001, + "746": 56.99999999999983, + "747": 94.70000000000017, + "748": 50.199999999999946, + "749": 100.70000000000024, + "750": 105.55000000000021, + "751": 50.64999999999995, + "752": 18.3, + "753": 36.55, + "754": 78.45000000000003, + "755": 18.149999999999995, + "756": 60.099999999999966, + "757": 82.99999999999997, + "758": 98.75000000000007, + "759": 51.099999999999945, + "760": 98.75000000000017, + "761": 42.049999999999955, + "762": 110.5500000000003, + "763": 105.80000000000021, + "764": 92.55000000000001, + "765": 101.35000000000015, + "766": 60.899999999999935, + "767": 10.500000000000057, + "768": 8.450000000000008, + "769": 110.70000000000024, + "770": 60.09999999999993, + "771": 51.69999999999993, + "772": 28.699999999999974, + "773": 110.35000000000022, + "774": 107.55000000000024, + "775": 62.44999999999991, + "776": 94.85000000000012, + "777": 73.54999999999998, + "778": 17.700000000000067, + "779": 109.95000000000007, + "780": 19.799999999999972, + "781": 35.35000000000003, + "782": 105.30000000000024, + "783": 75.94999999999999, + "784": 34.59999999999997, + "785": 88.55000000000004, + "786": 57.64999999999992, + "787": 34.850000000000115, + "788": 59.749999999999794, + "789": 67.45, + "790": 66.25, + "791": 55.449999999999925, + "792": 99.69999999999993, + "793": 80.75000000000009, + "794": 36.849999999999945, + "795": 62.74999999999996, + "796": 82.40000000000008, + "797": 88.10000000000005, + "798": 67.79999999999998, + "799": 58.04999999999991, + "800": 96.19999999999999, + "801": 49.09999999999993, + "802": 58.85000000000005, + "803": 101.30000000000005, + "804": 83.24999999999996, + "805": 58.04999999999985, + "806": 97.40000000000013, + "807": 60.15000000000003, + "808": -56.99999999999997, + "809": 99.75000000000014, + "810": 94.50000000000017, + "811": 95.45000000000016, + "812": 99.60000000000007, + "813": 100.25000000000016, + "814": 93.60000000000012, + "815": 93.95000000000016, + "816": 62.64999999999991, + "817": 58.64999999999985, + "818": 28.59999999999999, + "819": 82.50000000000004, + "820": 84.30000000000005, + "821": 75.60000000000001, + "822": 90.90000000000005, + "823": 95.15000000000015, + "824": 92.60000000000016, + "825": 78.80000000000008, + "826": 30.89999999999997, + "827": 93.09999999999997, + "828": 87.70000000000007, + "829": 105.30000000000027, + "830": 107.85000000000022, + "831": 94.24999999999984, + "832": 76.39999999999998, + "833": 96.20000000000017, + "834": 10.149999999999993, + "835": 94.25000000000001, + "836": 94.1500000000001, + "837": 69.84999999999997, + "838": 37.799999999999955, + "839": 101.1000000000002, + "840": 17.549999999999983, + "841": 78.10000000000001, + "842": 83.4999999999998, + "843": 79.54999999999986, + "844": 32.15, + "845": 51.249999999999915, + "846": 78.75000000000003, + "847": 91.60000000000011, + "848": 80.65000000000008, + "849": 88.9000000000001, + "850": 73.89999999999996, + "851": 109.00000000000018, + "852": 91.94999999999992, + "853": 107.10000000000028, + "854": 90.10000000000014, + "855": 88.9500000000001, + "856": 62.399999999999935, + "857": 61.299999999999905, + "858": 58.099999999999824, + "859": 99.55000000000021, + "860": 98.74999999999979, + "861": 106.45000000000005, + "862": 28.99999999999993, + "863": 59.599999999999866, + "864": -28.099999999999998, + "865": 73.35000000000002, + "866": 94.64999999999974, + "867": 52.94999999999982, + "868": 73.04999999999986, + "869": 82.5, + "870": 88.90000000000008, + "871": 104.40000000000013, + "872": 84.2, + "873": 7.800000000000001, + "874": 55.79999999999995, + "875": 88.45000000000012, + "876": -12.30000000000002, + "877": 48.399999999999885, + "878": 81.39999999999979, + "879": 102.65000000000012, + "880": 72.39999999999986, + "881": 92.04999999999976, + "882": 29.199999999999967, + "883": 98.70000000000014, + "884": 23.949999999999985, + "885": 10.499999999999943, + "886": 73.0, + "887": 67.35000000000001, + "888": 63.29999999999993, + "889": 106.95000000000012, + "890": 47.49999999999989, + "891": 66.39999999999988, + "892": 78.29999999999978, + "893": 96.19999999999997, + "894": 98.85000000000016, + "895": 44.44999999999992, + "896": 103.10000000000024, + "897": 97.55000000000008, + "898": 30.54999999999996, + "899": 88.09999999999991, + "900": 77.29999999999995, + "901": 94.39999999999988, + "902": 6.749999999999973, + "903": 64.29999999999995, + "904": 91.94999999999978, + "905": 10.450000000000053, + "906": 72.39999999999985, + "907": 96.35000000000005, + "908": 97.50000000000006, + "909": 60.69999999999993, + "910": -19.55, + "911": 80.24999999999983, + "912": 43.44999999999997, + "913": 82.79999999999981, + "914": 46.99999999999979, + "915": 92.09999999999978, + "916": 77.7999999999999, + "917": 98.40000000000003, + "918": 98.70000000000009, + "919": 62.74999999999975, + "920": 49.949999999999754, + "921": 21.599999999999998, + "922": 80.09999999999981, + "923": 70.69999999999979, + "924": 86.4000000000001, + "925": 95.00000000000009, + "926": 68.14999999999975, + "927": 63.04999999999991, + "928": 102.95000000000005, + "929": 78.94999999999983, + "930": 36.20000000000001, + "931": 73.89999999999999, + "932": 49.39999999999978, + "933": 77.75000000000007, + "934": 80.99999999999997, + "935": 77.85000000000004, + "936": 101.80000000000021, + "937": 69.29999999999987, + "938": 67.29999999999994, + "939": 90.7999999999999, + "940": 99.30000000000014, + "941": 40.79999999999979, + "942": 63.2499999999999, + "943": 96.79999999999977, + "944": 99.85000000000015, + "945": 62.499999999999886, + "946": 98.1000000000001, + "947": 87.44999999999999, + "948": 101.54999999999977, + "949": 81.39999999999984, + "950": 53.09999999999992, + "951": 80.09999999999975, + "952": 94.94999999999978, + "953": 92.14999999999978, + "954": 97.79999999999974, + "955": 87.79999999999983, + "956": 96.94999999999972, + "957": 94.20000000000003, + "958": 70.79999999999978, + "959": 65.5499999999998, + "960": 100.24999999999979, + "961": 102.64999999999993, + "962": 89.14999999999975, + "963": 21.099999999999984, + "964": 69.24999999999987, + "965": 93.15000000000018, + "966": 22.149999999999956, + "967": 87.44999999999975, + "968": 69.29999999999981, + "969": 91.24999999999977, + "970": 51.74999999999979, + "971": 78.74999999999993, + "972": 30.599999999999845, + "973": 94.5999999999999, + "974": 46.84999999999975, + "975": 64.99999999999977, + "976": 53.299999999999756, + "977": 87.60000000000004, + "978": 92.59999999999972, + "979": 93.34999999999977, + "980": 48.94999999999978, + "981": 97.70000000000009, + "982": 97.14999999999975, + "983": 48.74999999999975, + "984": 91.74999999999982, + "985": 101.14999999999986, + "986": 81.0499999999998, + "987": 63.1999999999999, + "988": 106.24999999999982, + "989": 66.89999999999992, + "990": 74.19999999999976, + "991": 92.29999999999974, + "992": 91.79999999999977, + "993": 81.50000000000001, + "994": 88.85000000000001, + "995": 99.09999999999972, + "996": 108.34999999999977, + "997": 54.049999999999926, + "998": 44.44999999999992, + "999": 101.74999999999973, + "1000": -39.899999999999984 + }, + "4": { + "1": -53.10000000000009, + "2": -17.299999999999972, + "3": -51.25000000000008, + "4": -48.30000000000006, + "5": -29.899999999999956, + "6": -23.449999999999964, + "7": -16.149999999999984, + "8": -38.750000000000036, + "9": -22.449999999999953, + "10": -48.40000000000015, + "11": -27.99999999999999, + "12": -7.199999999999988, + "13": -31.100000000000016, + "14": -25.95000000000002, + "15": -12.349999999999994, + "16": -17.799999999999976, + "17": -98.6, + "18": -43.65000000000011, + "19": -21.449999999999957, + "20": -52.95000000000008, + "21": -66.30000000000008, + "22": -39.55000000000012, + "23": -42.600000000000044, + "24": -81.64999999999998, + "25": -21.999999999999954, + "26": -15.499999999999979, + "27": -63.50000000000011, + "28": -20.249999999999982, + "29": -20.799999999999958, + "30": -13.249999999999982, + "31": -18.34999999999997, + "32": -53.20000000000015, + "33": -7.799999999999997, + "34": 5.850000000000034, + "35": -0.6999999999999571, + "36": -6.050000000000013, + "37": -20.19999999999996, + "38": -20.54999999999996, + "39": -13.349999999999985, + "40": -7.3499999999999925, + "41": -66.85000000000004, + "42": 8.750000000000043, + "43": -27.30000000000002, + "44": -12.34999999999999, + "45": -18.499999999999964, + "46": -33.24999999999999, + "47": -86.95, + "48": -16.8, + "49": -64.25000000000006, + "50": 3.5000000000000275, + "51": -7.499999999999999, + "52": -15.299999999999978, + "53": -23.94999999999995, + "54": -34.59999999999999, + "55": -11.35000000000001, + "56": -10.599999999999987, + "57": -31.75000000000003, + "58": -107.1, + "59": -30.550000000000022, + "60": -50.90000000000005, + "61": -103.75, + "62": -27.749999999999936, + "63": -13.699999999999983, + "64": 4.0500000000000576, + "65": -80.45000000000002, + "66": -18.549999999999965, + "67": -55.40000000000009, + "68": 21.0, + "69": 0.10000000000001108, + "70": -85.60000000000002, + "71": -67.35000000000008, + "72": -48.90000000000001, + "73": -13.649999999999986, + "74": -47.75000000000005, + "75": -15.749999999999979, + "76": -45.75000000000005, + "77": -63.7000000000001, + "78": -12.949999999999987, + "79": 21.599999999999916, + "80": -100.49999999999999, + "81": -56.700000000000095, + "82": -4.249999999999967, + "83": -19.099999999999966, + "84": -46.19999999999993, + "85": -21.299999999999965, + "86": 10.850000000000058, + "87": -17.94999999999997, + "88": -93.25, + "89": 11.400000000000013, + "90": -90.45, + "91": -19.299999999999965, + "92": -23.29999999999995, + "93": -60.950000000000095, + "94": -19.999999999999964, + "95": -16.699999999999974, + "96": -16.49999999999998, + "97": -18.24999999999997, + "98": -61.20000000000009, + "99": -86.45000000000002, + "100": -14.000000000000007, + "101": -7.049999999999984, + "102": -18.199999999999967, + "103": -28.550000000000004, + "104": -10.149999999999991, + "105": 5.500000000000013, + "106": -36.200000000000045, + "107": 31.4499999999998, + "108": -14.249999999999984, + "109": -69.05000000000005, + "110": 13.29999999999998, + "111": -81.05, + "112": 5.85000000000002, + "113": 2.7500000000000577, + "114": 2.05000000000003, + "115": 4.0500000000000504, + "116": -10.849999999999996, + "117": -90.05000000000001, + "118": -17.00000000000003, + "119": 4.550000000000038, + "120": -9.449999999999985, + "121": -11.75000000000004, + "122": -10.299999999999981, + "123": -18.949999999999967, + "124": 13.150000000000013, + "125": -63.050000000000104, + "126": -14.649999999999984, + "127": -22.59999999999995, + "128": 5.7499999999999005, + "129": 27.45000000000002, + "130": 5.699999999999998, + "131": -14.199999999999964, + "132": 25.299999999999972, + "133": -45.70000000000005, + "134": -5.649999999999976, + "135": -18.100000000000044, + "136": -13.150000000000006, + "137": 4.9499999999999655, + "138": 35.549999999999876, + "139": -3.3000000000001, + "140": 14.70000000000002, + "141": -9.150000000000004, + "142": -44.44999999999999, + "143": -14.649999999999977, + "144": -67.49999999999997, + "145": -67.69999999999997, + "146": -81.55000000000001, + "147": -34.65000000000004, + "148": 12.049999999999867, + "149": -7.5999999999999845, + "150": -8.249999999999984, + "151": 24.850000000000065, + "152": -15.649999999999979, + "153": 3.350000000000044, + "154": 40.74999999999999, + "155": -46.250000000000014, + "156": -6.149999999999986, + "157": 37.29999999999989, + "158": -15.699999999999973, + "159": -10.100000000000007, + "160": 7.750000000000041, + "161": 7.600000000000026, + "162": -13.49999999999997, + "163": -26.54999999999995, + "164": 30.449999999999932, + "165": -87.79999999999998, + "166": -68.70000000000002, + "167": -13.14999999999999, + "168": -7.299999999999983, + "169": 38.149999999999814, + "170": -79.34999999999998, + "171": -17.149999999999956, + "172": -16.649999999999974, + "173": 19.750000000000025, + "174": -63.00000000000011, + "175": 62.44999999999998, + "176": -0.44999999999996, + "177": -22.899999999999984, + "178": -7.2999999999999865, + "179": -94.5, + "180": -33.549999999999955, + "181": -94.2, + "182": -2.7499999999999885, + "183": -8.149999999999988, + "184": 5.450000000000016, + "185": 5.150000000000009, + "186": -4.550000000000033, + "187": 26.00000000000004, + "188": -38.09999999999999, + "189": -71.90000000000003, + "190": -73.1, + "191": 21.55000000000002, + "192": -31.149999999999963, + "193": -81.7, + "194": 50.449999999999854, + "195": -13.750000000000012, + "196": -41.70000000000003, + "197": -56.850000000000094, + "198": -5.399999999999981, + "199": 57.49999999999982, + "200": -52.40000000000001, + "201": -37.000000000000064, + "202": -73.34999999999994, + "203": 16.79999999999994, + "204": 44.499999999999744, + "205": 14.450000000000077, + "206": 48.34999999999985, + "207": 71.29999999999991, + "208": 23.250000000000014, + "209": -21.499999999999957, + "210": -21.34999999999996, + "211": 22.599999999999966, + "212": 34.799999999999805, + "213": 67.64999999999989, + "214": 73.19999999999978, + "215": -70.24999999999999, + "216": -48.74999999999998, + "217": -37.49999999999997, + "218": 82.90000000000016, + "219": -75.79999999999998, + "220": -89.75, + "221": 61.34999999999975, + "222": 30.749999999999762, + "223": -65.69999999999993, + "224": 20.500000000000025, + "225": 19.40000000000006, + "226": 14.349999999999914, + "227": 25.150000000000055, + "228": -50.04999999999994, + "229": 28.75000000000007, + "230": 51.749999999999815, + "231": 71.89999999999988, + "232": 73.29999999999981, + "233": -10.599999999999962, + "234": 69.34999999999987, + "235": -10.749999999999964, + "236": 44.24999999999973, + "237": 31.649999999999945, + "238": -16.09999999999998, + "239": -20.59999999999999, + "240": 21.25000000000007, + "241": 0.600000000000027, + "242": -2.7999999999999785, + "243": 38.84999999999985, + "244": 6.450000000000021, + "245": -63.199999999999974, + "246": 58.8999999999998, + "247": 38.30000000000006, + "248": 92.59999999999974, + "249": -9.850000000000032, + "250": -57.20000000000011, + "251": 57.74999999999977, + "252": 83.04999999999974, + "253": 64.39999999999992, + "254": -10.049999999999992, + "255": -57.05000000000007, + "256": 30.000000000000007, + "257": -0.8999999999999915, + "258": 21.20000000000008, + "259": 55.5999999999998, + "260": 83.64999999999976, + "261": 21.499999999999982, + "262": 32.65000000000008, + "263": -84.10000000000001, + "264": 98.24999999999974, + "265": 48.29999999999984, + "266": 60.7999999999999, + "267": 87.09999999999975, + "268": 33.10000000000005, + "269": -48.59999999999994, + "270": 78.24999999999987, + "271": 60.6499999999999, + "272": 34.199999999999896, + "273": 77.79999999999973, + "274": 26.800000000000033, + "275": -12.200000000000014, + "276": 88.24999999999977, + "277": 9.700000000000067, + "278": 101.29999999999971, + "279": 64.9999999999998, + "280": 17.199999999999896, + "281": 95.39999999999976, + "282": 84.74999999999977, + "283": 71.14999999999989, + "284": 34.29999999999986, + "285": 36.90000000000001, + "286": 61.74999999999984, + "287": 99.34999999999972, + "288": 86.09999999999981, + "289": 74.09999999999977, + "290": 74.9499999999998, + "291": 63.29999999999985, + "292": 80.34999999999977, + "293": 24.899999999999956, + "294": 82.44999999999978, + "295": -32.3, + "296": 77.89999999999985, + "297": -12.899999999999988, + "298": 54.99999999999979, + "299": 50.74999999999978, + "300": 77.74999999999984, + "301": 82.19999999999979, + "302": 98.29999999999978, + "303": 38.049999999999976, + "304": 78.04999999999977, + "305": -23.89999999999995, + "306": 85.59999999999984, + "307": 9.450000000000026, + "308": 39.24999999999997, + "309": 61.19999999999976, + "310": 49.149999999999764, + "311": 89.84999999999982, + "312": 11.500000000000043, + "313": 85.79999999999976, + "314": 54.1499999999998, + "315": 93.39999999999976, + "316": 91.59999999999977, + "317": 99.94999999999978, + "318": 80.19999999999978, + "319": 99.44999999999976, + "320": -71.65000000000002, + "321": 87.69999999999978, + "322": 94.69999999999979, + "323": 92.19999999999979, + "324": 77.49999999999987, + "325": 68.59999999999977, + "326": 99.74999999999976, + "327": 82.59999999999982, + "328": 94.94999999999973, + "329": 88.49999999999979, + "330": 85.94999999999976, + "331": 28.299999999999844, + "332": 85.24999999999977, + "333": 89.9499999999998, + "334": -50.50000000000008, + "335": 85.99999999999984, + "336": -1.0999999999999823, + "337": 92.64999999999978, + "338": 99.99999999999976, + "339": 29.99999999999981, + "340": 87.29999999999976, + "341": 81.7499999999998, + "342": 95.59999999999975, + "343": 91.99999999999974, + "344": 97.39999999999978, + "345": 96.09999999999978, + "346": 98.94999999999975, + "347": 99.6999999999998, + "348": 98.89999999999975, + "349": 84.34999999999975, + "350": 91.24999999999977, + "351": 83.09999999999978, + "352": 80.39999999999984, + "353": -86.1, + "354": 69.79999999999976, + "355": 82.64999999999972, + "356": 100.24999999999979, + "357": 91.74999999999977, + "358": 100.14999999999978, + "359": 88.5999999999998, + "360": 102.74999999999976, + "361": 21.94999999999991, + "362": 89.49999999999979, + "363": 95.79999999999976, + "364": 83.89999999999979, + "365": 83.2999999999998, + "366": 100.69999999999978, + "367": -84.44999999999999, + "368": 10.150000000000038, + "369": -11.899999999999991, + "370": 100.19999999999976, + "371": 102.44999999999973, + "372": 76.44999999999979, + "373": 101.89999999999976, + "374": 99.94999999999979, + "375": 8.650000000000025, + "376": 32.750000000000014, + "377": 82.79999999999984, + "378": 73.24999999999972, + "379": 103.49999999999976, + "380": 92.99999999999977, + "381": 103.09999999999977, + "382": 103.84999999999975, + "383": 96.04999999999974, + "384": 103.09999999999977, + "385": 95.09999999999977, + "386": 83.4999999999998, + "387": 103.39999999999976, + "388": -80.0, + "389": 90.1499999999998, + "390": 97.59999999999972, + "391": 83.09999999999978, + "392": 94.44999999999976, + "393": 100.39999999999976, + "394": 97.99999999999979, + "395": 90.99999999999982, + "396": 82.79999999999977, + "397": 99.09999999999977, + "398": 105.24999999999974, + "399": 65.94999999999976, + "400": 98.39999999999976, + "401": 103.09999999999977, + "402": 100.69999999999976, + "403": 98.74999999999976, + "404": -44.05000000000007, + "405": 91.44999999999976, + "406": 27.000000000000046, + "407": 98.64999999999976, + "408": 96.44999999999978, + "409": 104.99999999999983, + "410": 90.99999999999979, + "411": 101.9999999999998, + "412": 103.34999999999988, + "413": 98.29999999999974, + "414": 78.59999999999987, + "415": 99.34999999999987, + "416": 101.69999999999978, + "417": 77.8999999999998, + "418": 101.14999999999978, + "419": 103.09999999999977, + "420": 103.49999999999976, + "421": 100.69999999999978, + "422": 91.44999999999978, + "423": 100.54999999999977, + "424": -23.149999999999952, + "425": 99.79999999999976, + "426": -9.349999999999985, + "427": 106.89999999999979, + "428": 101.99999999999977, + "429": 84.2499999999998, + "430": 93.34999999999981, + "431": 104.69999999999982, + "432": 83.09999999999977, + "433": 59.49999999999984, + "434": 101.59999999999975, + "435": 98.99999999999977, + "436": 96.89999999999976, + "437": 105.84999999999984, + "438": 97.19999999999976, + "439": -11.799999999999985, + "440": 102.49999999999976, + "441": 98.44999999999979, + "442": 108.44999999999986, + "443": 103.54999999999977, + "444": 101.14999999999976, + "445": 104.74999999999976, + "446": 100.69999999999976, + "447": 87.79999999999976, + "448": 96.5499999999998, + "449": 75.94999999999978, + "450": 103.49999999999977, + "451": 84.84999999999974, + "452": 101.14999999999976, + "453": 23.049999999999898, + "454": 98.69999999999978, + "455": 102.39999999999978, + "456": 62.19999999999973, + "457": 102.74999999999976, + "458": 105.44999999999975, + "459": 65.6499999999999, + "460": -38.89999999999997, + "461": 62.44999999999979, + "462": 97.44999999999978, + "463": 102.69999999999976, + "464": 107.89999999999979, + "465": 103.59999999999977, + "466": 104.99999999999974, + "467": 103.04999999999977, + "468": 102.84999999999977, + "469": 104.79999999999974, + "470": 100.19999999999978, + "471": 104.59999999999977, + "472": 102.79999999999977, + "473": 104.94999999999975, + "474": -80.69999999999999, + "475": 102.34999999999977, + "476": -84.00000000000003, + "477": 96.09999999999975, + "478": 80.79999999999973, + "479": 102.24999999999977, + "480": 98.30000000000008, + "481": 103.39999999999975, + "482": 56.09999999999979, + "483": 103.54999999999977, + "484": 103.74999999999972, + "485": 67.74999999999976, + "486": 62.94999999999975, + "487": 101.99999999999977, + "488": 103.24999999999974, + "489": 104.49999999999974, + "490": 75.29999999999983, + "491": 72.84999999999977, + "492": 77.44999999999978, + "493": 102.69999999999978, + "494": 96.14999999999976, + "495": 94.8499999999998, + "496": 106.64999999999972, + "497": 80.89999999999989, + "498": 84.44999999999976, + "499": 101.94999999999976, + "500": 99.89999999999978, + "501": 105.89999999999974, + "502": -35.199999999999996, + "503": 106.69999999999973, + "504": 94.59999999999981, + "505": 101.49999999999977, + "506": 103.19999999999976, + "507": 103.99999999999972, + "508": 96.74999999999982, + "509": 97.8499999999998, + "510": 104.59999999999974, + "511": 102.74999999999977, + "512": 103.64999999999976, + "513": 100.39999999999975, + "514": 99.19999999999978, + "515": 71.64999999999976, + "516": 104.09999999999974, + "517": 104.34999999999975, + "518": 102.94999999999978, + "519": 97.54999999999977, + "520": 106.24999999999973, + "521": -75.3, + "522": 4.75, + "523": 100.34999999999975, + "524": 106.69999999999975, + "525": 56.699999999999854, + "526": 16.30000000000001, + "527": 101.29999999999977, + "528": 93.09999999999977, + "529": 103.54999999999977, + "530": 66.2999999999999, + "531": 102.59999999999977, + "532": 102.74999999999977, + "533": 102.84999999999977, + "534": 102.84999999999977, + "535": 104.59999999999977, + "536": 107.89999999999974, + "537": 104.04999999999974, + "538": 75.74999999999977, + "539": 102.29999999999977, + "540": 81.34999999999978, + "541": 102.09999999999977, + "542": 105.89999999999974, + "543": 99.54999999999974, + "544": 102.84999999999975, + "545": 105.84999999999972, + "546": 105.59999999999972, + "547": 103.54999999999977, + "548": 98.64999999999978, + "549": 103.39999999999976, + "550": 106.34999999999975, + "551": 84.14999999999976, + "552": 108.59999999999974, + "553": 104.34999999999975, + "554": -78.9, + "555": 91.19999999999978, + "556": 101.54999999999977, + "557": 103.49999999999977, + "558": 104.79999999999974, + "559": 48.14999999999982, + "560": 40.99999999999998, + "561": 93.69999999999982, + "562": 104.44999999999976, + "563": 105.49999999999974, + "564": 102.24999999999977, + "565": 93.59999999999978, + "566": 105.74999999999974, + "567": 99.74999999999976, + "568": 62.39999999999994, + "569": 100.64999999999974, + "570": 104.39999999999972, + "571": 103.89999999999976, + "572": 103.34999999999977, + "573": 85.79999999999977, + "574": 3.1499999999999915, + "575": 102.04999999999977, + "576": 104.64999999999978, + "577": 59.09999999999975, + "578": -81.00000000000001, + "579": 103.59999999999977, + "580": 105.69999999999972, + "581": 53.79999999999983, + "582": 104.79999999999974, + "583": 102.84999999999977, + "584": 104.44999999999978, + "585": 104.39999999999975, + "586": 104.54999999999976, + "587": 103.79999999999977, + "588": 105.59999999999972, + "589": 102.54999999999976, + "590": 103.54999999999977, + "591": 83.54999999999977, + "592": -75.8, + "593": 105.89999999999972, + "594": 102.09999999999977, + "595": 105.74999999999973, + "596": 103.19999999999976, + "597": 102.94999999999978, + "598": 107.04999999999974, + "599": 103.89999999999976, + "600": 104.39999999999976, + "601": 100.99999999999976, + "602": 106.09999999999974, + "603": 105.34999999999975, + "604": 105.09999999999974, + "605": 103.74999999999977, + "606": 102.89999999999976, + "607": -78.65, + "608": 102.89999999999978, + "609": 107.24999999999973, + "610": 102.64999999999976, + "611": 106.94999999999973, + "612": -82.80000000000001, + "613": 104.09999999999977, + "614": 104.39999999999976, + "615": 104.14999999999976, + "616": 43.74999999999976, + "617": 104.49999999999976, + "618": 60.74999999999977, + "619": 105.39999999999975, + "620": 103.29999999999977, + "621": 106.49999999999993, + "622": 107.54999999999974, + "623": 107.99999999999974, + "624": 76.69999999999978, + "625": 108.29999999999974, + "626": 102.99999999999977, + "627": 104.54999999999976, + "628": 103.74999999999977, + "629": 105.54999999999973, + "630": 104.64999999999975, + "631": 102.89999999999976, + "632": 105.39999999999974, + "633": 104.14999999999976, + "634": 104.59999999999975, + "635": 104.29999999999977, + "636": 103.94999999999976, + "637": 97.84999999999977, + "638": -79.04999999999998, + "639": 103.04999999999977, + "640": 100.74999999999979, + "641": 102.74999999999977, + "642": 104.09999999999977, + "643": 106.04999999999971, + "644": 106.64999999999989, + "645": 104.09999999999977, + "646": 103.24999999999976, + "647": 103.04999999999977, + "648": 103.99999999999976, + "649": 81.19999999999976, + "650": 102.79999999999977, + "651": 102.99999999999977, + "652": 101.94999999999978, + "653": 39.1, + "654": 105.84999999999972, + "655": 60.34999999999975, + "656": 96.39999999999979, + "657": 62.69999999999998, + "658": 105.24999999999974, + "659": 92.44999999999975, + "660": 103.69999999999976, + "661": 101.39999999999978, + "662": 103.09999999999977, + "663": 103.24999999999976, + "664": 94.4499999999998, + "665": 89.79999999999987, + "666": 103.54999999999976, + "667": 103.99999999999976, + "668": 73.64999999999976, + "669": 103.69999999999976, + "670": -14.500000000000007, + "671": 105.04999999999976, + "672": 104.94999999999975, + "673": 103.34999999999977, + "674": 90.89999999999982, + "675": 100.99999999999977, + "676": 105.09999999999981, + "677": 103.64999999999976, + "678": 71.59999999999977, + "679": 107.94999999999973, + "680": 73.14999999999976, + "681": 103.24999999999977, + "682": 103.29999999999977, + "683": 54.29999999999975, + "684": 98.69999999999975, + "685": 104.54999999999973, + "686": 105.6499999999998, + "687": 103.09999999999977, + "688": 103.84999999999977, + "689": 104.34999999999974, + "690": 83.54999999999977, + "691": 84.8499999999998, + "692": 105.44999999999982, + "693": 106.54999999999973, + "694": 106.24999999999983, + "695": 103.94999999999976, + "696": 105.94999999999973, + "697": 12.799999999999969, + "698": 103.29999999999984, + "699": 109.09999999999975, + "700": 101.99999999999974, + "701": 104.79999999999977, + "702": 103.79999999999976, + "703": 102.64999999999976, + "704": 103.29999999999977, + "705": 106.94999999999973, + "706": 104.69999999999976, + "707": 103.09999999999977, + "708": 103.74999999999976, + "709": 103.14999999999978, + "710": 102.79999999999974, + "711": 99.24999999999977, + "712": 103.04999999999977, + "713": 102.69999999999978, + "714": 103.74999999999976, + "715": 102.74999999999976, + "716": 83.99999999999983, + "717": 104.39999999999975, + "718": 104.84999999999975, + "719": 103.59999999999977, + "720": 103.39999999999976, + "721": 102.74999999999977, + "722": 104.84999999999974, + "723": 104.49999999999976, + "724": 105.64999999999974, + "725": 92.49999999999977, + "726": 102.49999999999976, + "727": 104.34999999999988, + "728": 104.39999999999975, + "729": 103.44999999999976, + "730": 106.94999999999979, + "731": 103.14999999999978, + "732": 103.69999999999976, + "733": 111.44999999999993, + "734": 102.94999999999978, + "735": 100.39999999999976, + "736": 99.54999999999978, + "737": 104.89999999999975, + "738": 104.19999999999976, + "739": 95.89999999999995, + "740": 105.29999999999994, + "741": 105.59999999999972, + "742": 104.19999999999976, + "743": 105.44999999999972, + "744": 105.84999999999974, + "745": 106.94999999999973, + "746": 107.84999999999972, + "747": 94.49999999999974, + "748": 104.84999999999975, + "749": 107.29999999999973, + "750": 104.04999999999976, + "751": 103.99999999999976, + "752": 62.34999999999977, + "753": 107.54999999999973, + "754": -84.6, + "755": 106.64999999999972, + "756": 85.69999999999983, + "757": 103.04999999999977, + "758": 57.54999999999978, + "759": 104.79999999999976, + "760": 96.04999999999976, + "761": 3.80000000000006, + "762": 102.79999999999977, + "763": -65.80000000000001, + "764": 106.64999999999974, + "765": 31.64999999999985, + "766": -68.35000000000001, + "767": 103.54999999999976, + "768": 104.84999999999977, + "769": 58.199999999999754, + "770": 103.89999999999976, + "771": 49.699999999999775, + "772": 109.94999999999985, + "773": 104.74999999999976, + "774": 104.59999999999975, + "775": 105.79999999999981, + "776": 31.39999999999987, + "777": 103.64999999999976, + "778": 103.34999999999977, + "779": 105.04999999999974, + "780": -68.1, + "781": 107.39999999999975, + "782": 109.50000000000018, + "783": -19.25, + "784": 108.39999999999989, + "785": 107.59999999999982, + "786": 102.39999999999978, + "787": 104.29999999999977, + "788": -84.00000000000003, + "789": 112.49999999999994, + "790": -77.4, + "791": 104.19999999999975, + "792": 92.74999999999983, + "793": 104.19999999999976, + "794": 104.29999999999976, + "795": -84.54999999999998, + "796": 103.89999999999979, + "797": 109.0499999999998, + "798": 105.29999999999977, + "799": 105.89999999999972, + "800": 103.14999999999976, + "801": 101.99999999999977, + "802": 105.19999999999973, + "803": 105.04999999999974, + "804": 105.74999999999972, + "805": 104.89999999999993, + "806": 105.34999999999974, + "807": 104.39999999999976, + "808": 103.64999999999976, + "809": 105.74999999999973, + "810": 49.74999999999981, + "811": 108.69999999999995, + "812": 106.39999999999971, + "813": -36.95000000000001, + "814": 105.59999999999975, + "815": 105.94999999999973, + "816": 103.34999999999977, + "817": 103.39999999999976, + "818": -76.6, + "819": 111.94999999999992, + "820": 103.34999999999977, + "821": 104.14999999999976, + "822": 106.59999999999972, + "823": 104.29999999999976, + "824": -3.8000000000000043, + "825": 103.29999999999977, + "826": 65.14999999999979, + "827": 103.99999999999976, + "828": 100.14999999999978, + "829": 104.44999999999976, + "830": 104.99999999999973, + "831": 78.94999999999978, + "832": -75.35, + "833": 102.49999999999977, + "834": -86.45000000000013, + "835": 116.35000000000032, + "836": 103.14999999999976, + "837": 105.34999999999972, + "838": 105.79999999999974, + "839": 108.69999999999975, + "840": 105.44999999999973, + "841": -88.44999999999999, + "842": 104.59999999999975, + "843": 104.24999999999976, + "844": 105.24999999999973, + "845": 113.80000000000021, + "846": 104.79999999999974, + "847": 104.94999999999982, + "848": 104.59999999999975, + "849": 103.39999999999976, + "850": 107.94999999999979, + "851": 105.69999999999972, + "852": 109.09999999999977, + "853": 106.29999999999971, + "854": 82.74999999999974, + "855": 71.29999999999978, + "856": -68.34999999999998, + "857": 106.49999999999996, + "858": 107.69999999999975, + "859": 105.39999999999972, + "860": 103.34999999999977, + "861": 107.74999999999974, + "862": 103.74999999999976, + "863": 100.79999999999973, + "864": 106.19999999999973, + "865": 100.79999999999976, + "866": -81.0, + "867": 105.69999999999986, + "868": 103.09999999999977, + "869": 104.09999999999977, + "870": 102.69999999999978, + "871": 103.94999999999976, + "872": 105.09999999999975, + "873": 103.94999999999973, + "874": 46.699999999999896, + "875": 94.54999999999978, + "876": 103.79999999999977, + "877": 106.24999999999973, + "878": 104.14999999999975, + "879": -73.69999999999997, + "880": 104.59999999999982, + "881": -77.44999999999999, + "882": -15.000000000000014, + "883": 104.84999999999975, + "884": -81.95, + "885": 105.14999999999975, + "886": 109.24999999999979, + "887": -77.85, + "888": 104.19999999999976, + "889": 113.75, + "890": -38.10000000000001, + "891": 104.69999999999976, + "892": -72.05000000000001, + "893": -73.80000000000001, + "894": 113.64999999999988, + "895": 104.19999999999976, + "896": 107.64999999999974, + "897": 109.29999999999978, + "898": 109.04999999999981, + "899": 109.24999999999976, + "900": 104.84999999999977, + "901": 104.24999999999976, + "902": 105.79999999999974, + "903": 104.04999999999977, + "904": 104.54999999999974, + "905": 104.94999999999975, + "906": 105.09999999999975, + "907": 101.49999999999972, + "908": -79.94999999999999, + "909": 103.29999999999977, + "910": 105.89999999999972, + "911": 102.64999999999976, + "912": 85.34999999999981, + "913": 104.69999999999976, + "914": 106.59999999999972, + "915": 106.44999999999972, + "916": 106.59999999999974, + "917": 107.64999999999974, + "918": 116.70000000000027, + "919": 59.150000000000034, + "920": 102.74999999999977, + "921": 104.89999999999972, + "922": 104.89999999999974, + "923": 107.19999999999972, + "924": 106.19999999999975, + "925": 104.79999999999976, + "926": 111.64999999999999, + "927": 109.04999999999976, + "928": 104.39999999999975, + "929": 105.34999999999975, + "930": 115.10000000000018, + "931": 108.39999999999975, + "932": 60.249999999999766, + "933": 40.69999999999997, + "934": 97.94999999999975, + "935": 105.39999999999974, + "936": 108.44999999999976, + "937": 105.89999999999974, + "938": 106.14999999999972, + "939": 106.09999999999972, + "940": 105.29999999999973, + "941": 104.44999999999978, + "942": 108.59999999999977, + "943": 105.79999999999973, + "944": 71.04999999999976, + "945": 106.94999999999973, + "946": 75.59999999999977, + "947": 103.14999999999978, + "948": 102.74999999999977, + "949": 106.99999999999973, + "950": 103.24999999999976, + "951": 110.54999999999983, + "952": 110.44999999999989, + "953": 104.49999999999974, + "954": 39.849999999999824, + "955": 104.94999999999975, + "956": -63.350000000000016, + "957": 104.04999999999977, + "958": -88.25, + "959": 103.24999999999977, + "960": 102.44999999999976, + "961": 83.34999999999975, + "962": -69.80000000000001, + "963": 108.59999999999974, + "964": 103.94999999999976, + "965": 105.39999999999974, + "966": 107.39999999999974, + "967": -45.80000000000007, + "968": 105.10000000000008, + "969": 103.69999999999973, + "970": 105.59999999999985, + "971": -79.0, + "972": 102.84999999999977, + "973": 103.44999999999976, + "974": 104.74999999999973, + "975": 103.29999999999977, + "976": -82.45000000000002, + "977": 105.59999999999974, + "978": 104.49999999999983, + "979": -83.35, + "980": 106.89999999999974, + "981": -83.85, + "982": -81.3, + "983": 103.49999999999972, + "984": 56.149999999999764, + "985": 106.19999999999978, + "986": 110.19999999999976, + "987": 108.69999999999976, + "988": 108.39999999999975, + "989": -41.05000000000001, + "990": 107.40000000000003, + "991": 46.79999999999997, + "992": 110.34999999999987, + "993": 106.39999999999972, + "994": 104.39999999999975, + "995": 106.09999999999972, + "996": 104.19999999999976, + "997": 107.14999999999974, + "998": 105.64999999999972, + "999": 103.14999999999976, + "1000": 107.24999999999979 + }, + "5": { + "1": -4.399999999999995, + "2": -48.50000000000004, + "3": -109.5, + "4": -54.500000000000085, + "5": -15.949999999999978, + "6": -80.89999999999992, + "7": -15.349999999999982, + "8": -23.29999999999995, + "9": -34.350000000000016, + "10": -49.800000000000054, + "11": -46.95000000000006, + "12": -22.699999999999953, + "13": -32.35000000000003, + "14": -24.199999999999942, + "15": -51.150000000000176, + "16": -52.20000000000008, + "17": -68.60000000000007, + "18": -30.400000000000006, + "19": -19.99999999999996, + "20": -73.15000000000002, + "21": -17.949999999999974, + "22": -12.949999999999987, + "23": -61.25, + "24": -35.19999999999998, + "25": -70.70000000000005, + "26": -96.6, + "27": -48.550000000000146, + "28": -4.599999999999975, + "29": -7.4, + "30": -44.050000000000175, + "31": -4.29999999999998, + "32": -21.999999999999957, + "33": -78.30000000000004, + "34": -15.099999999999985, + "35": -55.60000000000003, + "36": -51.800000000000075, + "37": -20.39999999999996, + "38": -22.499999999999954, + "39": -104.3, + "40": -45.75000000000005, + "41": 2.100000000000044, + "42": -21.099999999999987, + "43": -14.99999999999998, + "44": -94.15, + "45": -45.70000000000012, + "46": -17.399999999999974, + "47": -29.099999999999948, + "48": -13.749999999999986, + "49": -87.25, + "50": -47.04999999999999, + "51": -23.89999999999995, + "52": -47.75000000000007, + "53": -13.699999999999976, + "54": -17.74999999999997, + "55": -23.799999999999972, + "56": -16.49999999999998, + "57": -21.299999999999958, + "58": -13.099999999999985, + "59": -96.69999999999997, + "60": -23.44999999999995, + "61": -8.399999999999995, + "62": -37.65000000000005, + "63": -20.349999999999962, + "64": -19.049999999999958, + "65": -17.24999999999998, + "66": -8.550000000000006, + "67": -18.14999999999997, + "68": -69.45000000000005, + "69": -16.999999999999975, + "70": -72.19999999999999, + "71": -29.599999999999994, + "72": -19.049999999999965, + "73": -7.249999999999993, + "74": -16.049999999999983, + "75": -17.49999999999997, + "76": -18.29999999999997, + "77": -15.799999999999976, + "78": -6.299999999999986, + "79": -17.24999999999997, + "80": -20.999999999999957, + "81": -12.84999999999996, + "82": -77.1, + "83": -18.34999999999997, + "84": -16.24999999999998, + "85": -51.75000000000008, + "86": -19.649999999999963, + "87": -0.5999999999999621, + "88": 4.700000000000018, + "89": -39.45000000000005, + "90": -19.79999999999996, + "91": -15.999999999999979, + "92": -45.39999999999999, + "93": 9.800000000000018, + "94": -81.9, + "95": 0.5499999999999933, + "96": -3.149999999999971, + "97": -16.899999999999974, + "98": -3.899999999999987, + "99": -36.55000000000004, + "100": -63.0000000000001, + "101": 13.300000000000004, + "102": -51.25000000000008, + "103": -1.849999999999997, + "104": -42.54999999999998, + "105": -84.10000000000001, + "106": -97.94999999999999, + "107": -17.65, + "108": -18.44999999999997, + "109": -15.399999999999975, + "110": 24.44999999999998, + "111": -40.20000000000011, + "112": -6.250000000000002, + "113": -22.74999999999996, + "114": -5.699999999999991, + "115": -18.64999999999996, + "116": -1.3500000000000008, + "117": 14.250000000000053, + "118": -53.49999999999998, + "119": -71.89999999999999, + "120": -29.299999999999994, + "121": 24.949999999999925, + "122": 17.000000000000025, + "123": -26.649999999999945, + "124": -50.449999999999974, + "125": 27.949999999999967, + "126": -71.39999999999999, + "127": -21.999999999999954, + "128": -15.499999999999979, + "129": -19.799999999999965, + "130": 10.100000000000012, + "131": -56.80000000000011, + "132": 19.60000000000007, + "133": -1.2499999999999811, + "134": -16.149999999999945, + "135": 1.1000000000000354, + "136": -8.749999999999972, + "137": -9.65, + "138": -16.649999999999977, + "139": -14.499999999999984, + "140": -9.949999999999998, + "141": 3.2500000000000187, + "142": 32.10000000000004, + "143": -3.199999999999998, + "144": 9.300000000000034, + "145": -26.29999999999997, + "146": 11.149999999999995, + "147": -3.199999999999984, + "148": -26.599999999999973, + "149": -12.699999999999967, + "150": -0.19999999999997642, + "151": -18.649999999999967, + "152": -42.80000000000001, + "153": 14.649999999999956, + "154": 5.300000000000017, + "155": -9.89999999999999, + "156": -0.4499999999999653, + "157": -40.65000000000006, + "158": 0.2000000000000146, + "159": -2.250000000000001, + "160": -85.30000000000001, + "161": 2.050000000000021, + "162": 3.450000000000025, + "163": -85.69999999999999, + "164": 2.5000000000000036, + "165": -0.5999999999999849, + "166": -10.249999999999996, + "167": -24.849999999999977, + "168": -8.5, + "169": -25.899999999999984, + "170": 18.2, + "171": -94.3, + "172": 5.500000000000007, + "173": 17.050000000000065, + "174": -19.39999999999999, + "175": -8.04999999999999, + "176": -9.949999999999987, + "177": -42.550000000000054, + "178": 27.35000000000007, + "179": -0.19999999999995866, + "180": 23.549999999999894, + "181": 44.899999999999885, + "182": 32.14999999999996, + "183": -15.999999999999975, + "184": 57.5999999999998, + "185": 16.999999999999996, + "186": -10.549999999999995, + "187": 10.550000000000061, + "188": -90.0, + "189": -9.900000000000002, + "190": 20.500000000000007, + "191": 19.15000000000004, + "192": -2.2999999999999963, + "193": -1.799999999999986, + "194": 22.149999999999963, + "195": -14.949999999999976, + "196": 17.100000000000044, + "197": -13.999999999999964, + "198": -0.6499999999999884, + "199": 46.599999999999746, + "200": 43.94999999999977, + "201": 27.95000000000003, + "202": 20.10000000000007, + "203": 51.59999999999976, + "204": 5.600000000000055, + "205": 40.349999999999866, + "206": -56.4, + "207": -7.200000000000007, + "208": 10.650000000000082, + "209": 53.599999999999795, + "210": -49.849999999999994, + "211": 46.29999999999992, + "212": -30.24999999999998, + "213": 52.39999999999978, + "214": 87.45000000000012, + "215": -84.95, + "216": 67.34999999999988, + "217": 46.09999999999985, + "218": 77.1499999999999, + "219": 1.9499999999999933, + "220": 46.24999999999981, + "221": 21.699999999999953, + "222": 34.3499999999999, + "223": -7.899999999999994, + "224": 41.84999999999974, + "225": 7.3500000000000085, + "226": 66.79999999999977, + "227": -2.6999999999999664, + "228": 11.500000000000039, + "229": 0.800000000000008, + "230": -7.500000000000008, + "231": -87.85, + "232": 62.39999999999993, + "233": -1.3500000000000272, + "234": 36.59999999999988, + "235": 91.64999999999998, + "236": 8.9, + "237": -81.39999999999996, + "238": 47.749999999999886, + "239": -9.55000000000004, + "240": 28.299999999999844, + "241": 83.0500000000001, + "242": 40.69999999999978, + "243": 28.649999999999892, + "244": -62.500000000000014, + "245": 72.35000000000011, + "246": -23.900000000000006, + "247": 81.30000000000008, + "248": 63.649999999999764, + "249": 25.249999999999947, + "250": 12.100000000000067, + "251": 58.84999999999993, + "252": 14.199999999999969, + "253": 86.15000000000022, + "254": -10.150000000000007, + "255": 69.09999999999984, + "256": 42.04999999999995, + "257": 36.2, + "258": 56.79999999999995, + "259": 64.35000000000014, + "260": 68.44999999999978, + "261": 81.19999999999982, + "262": 35.15000000000003, + "263": -4.699999999999989, + "264": 106.55000000000028, + "265": 48.44999999999992, + "266": 24.25000000000001, + "267": -54.64999999999994, + "268": 59.049999999999926, + "269": 46.24999999999976, + "270": 32.99999999999999, + "271": 89.09999999999981, + "272": 65.14999999999978, + "273": 89.64999999999995, + "274": 43.44999999999994, + "275": -33.30000000000004, + "276": 103.50000000000017, + "277": -56.74999999999996, + "278": 42.0999999999999, + "279": 92.80000000000008, + "280": -25.349999999999973, + "281": -38.799999999999976, + "282": -83.99999999999997, + "283": 85.84999999999978, + "284": 25.099999999999923, + "285": 42.24999999999992, + "286": 19.150000000000002, + "287": 72.99999999999984, + "288": -71.55000000000001, + "289": 26.99999999999999, + "290": 41.49999999999989, + "291": 31.899999999999864, + "292": -70.84999999999997, + "293": 94.0500000000001, + "294": 36.04999999999999, + "295": -25.900000000000027, + "296": 107.00000000000024, + "297": 59.099999999999866, + "298": 106.05000000000018, + "299": -21.799999999999986, + "300": 31.29999999999999, + "301": 47.9499999999999, + "302": 67.9499999999999, + "303": -39.30000000000008, + "304": 87.04999999999998, + "305": -16.950000000000028, + "306": 57.3499999999999, + "307": 106.35000000000026, + "308": 62.04999999999991, + "309": -21.999999999999982, + "310": 60.59999999999983, + "311": -3.1500000000000057, + "312": 94.70000000000009, + "313": 102.45000000000014, + "314": 92.20000000000016, + "315": -74.55, + "316": 89.00000000000003, + "317": 9.649999999999999, + "318": -70.24999999999997, + "319": -43.899999999999984, + "320": -54.94999999999995, + "321": -13.600000000000005, + "322": 26.799999999999976, + "323": 66.69999999999987, + "324": -12.59999999999998, + "325": -16.349999999999984, + "326": -64.74999999999986, + "327": 61.29999999999982, + "328": 29.900000000000013, + "329": 59.699999999999875, + "330": 67.79999999999981, + "331": -45.15000000000005, + "332": -68.54999999999997, + "333": 21.650000000000002, + "334": 1.5999999999999868, + "335": 48.249999999999915, + "336": 84.09999999999981, + "337": 7.899999999999989, + "338": 78.59999999999984, + "339": -9.949999999999996, + "340": 75.14999999999996, + "341": -44.34999999999996, + "342": 91.85000000000001, + "343": 94.60000000000016, + "344": 73.64999999999999, + "345": 33.3, + "346": 13.299999999999997, + "347": 107.25000000000024, + "348": 40.049999999999976, + "349": -63.89999999999992, + "350": 102.9500000000002, + "351": 51.54999999999981, + "352": 77.19999999999999, + "353": 95.70000000000012, + "354": 47.54999999999994, + "355": 28.650000000000055, + "356": 6.55, + "357": 22.949999999999967, + "358": 103.40000000000018, + "359": 51.34999999999995, + "360": 93.05000000000014, + "361": 95.5000000000001, + "362": 31.199999999999985, + "363": 98.0500000000002, + "364": 52.69999999999979, + "365": -7.450000000000001, + "366": 37.69999999999999, + "367": 64.69999999999985, + "368": 66.89999999999988, + "369": 89.24999999999979, + "370": -78.4, + "371": 93.25000000000013, + "372": 94.99999999999991, + "373": -2.849999999999964, + "374": 75.34999999999977, + "375": 85.70000000000003, + "376": 98.55000000000013, + "377": 90.14999999999976, + "378": -72.09999999999992, + "379": 97.10000000000015, + "380": 24.199999999999978, + "381": 57.94999999999993, + "382": 72.89999999999978, + "383": 49.799999999999926, + "384": 86.60000000000001, + "385": 78.34999999999981, + "386": 86.70000000000007, + "387": 31.84999999999996, + "388": 51.24999999999995, + "389": 50.29999999999987, + "390": 65.79999999999991, + "391": 83.45, + "392": 61.59999999999988, + "393": 89.7500000000001, + "394": 81.09999999999984, + "395": 95.04999999999974, + "396": 70.14999999999976, + "397": 67.84999999999984, + "398": 0.1999999999999731, + "399": 66.84999999999991, + "400": 55.949999999999775, + "401": 91.90000000000013, + "402": 92.0000000000001, + "403": 81.7999999999998, + "404": 96.0999999999998, + "405": 35.499999999999794, + "406": 48.799999999999955, + "407": 40.050000000000004, + "408": 92.00000000000006, + "409": 104.35, + "410": 86.29999999999978, + "411": -5.849999999999988, + "412": 51.099999999999746, + "413": 64.79999999999974, + "414": 15.150000000000034, + "415": 77.00000000000006, + "416": 68.79999999999991, + "417": 59.64999999999974, + "418": 75.04999999999976, + "419": 38.39999999999998, + "420": 84.29999999999978, + "421": 51.8499999999998, + "422": 37.84999999999993, + "423": 92.69999999999979, + "424": 71.39999999999985, + "425": 75.04999999999986, + "426": 77.64999999999975, + "427": 15.799999999999992, + "428": 25.150000000000013, + "429": 96.44999999999975, + "430": 85.69999999999978, + "431": 78.09999999999994, + "432": 82.39999999999974, + "433": 103.0000000000002, + "434": 95.99999999999973, + "435": 15.200000000000014, + "436": 80.79999999999978, + "437": 63.09999999999979, + "438": 90.84999999999981, + "439": 58.799999999999876, + "440": 75.24999999999987, + "441": 99.05000000000011, + "442": 72.39999999999982, + "443": 94.69999999999992, + "444": 62.14999999999993, + "445": 8.450000000000015, + "446": 93.79999999999976, + "447": 75.60000000000014, + "448": 96.74999999999976, + "449": 44.54999999999987, + "450": -3.7999999999999967, + "451": -43.19999999999999, + "452": 97.99999999999974, + "453": 75.25000000000003, + "454": 90.29999999999978, + "455": 86.69999999999976, + "456": 94.09999999999978, + "457": 79.04999999999977, + "458": 66.74999999999987, + "459": 76.2499999999999, + "460": 101.99999999999974, + "461": 104.69999999999983, + "462": 85.34999999999978, + "463": 97.39999999999975, + "464": 32.35, + "465": 98.29999999999976, + "466": 79.89999999999982, + "467": 101.00000000000016, + "468": 84.14999999999975, + "469": 92.69999999999979, + "470": 93.59999999999977, + "471": 36.899999999999956, + "472": 86.34999999999977, + "473": 105.54999999999991, + "474": 66.39999999999993, + "475": 101.09999999999975, + "476": -5.149999999999967, + "477": 98.49999999999977, + "478": 104.34999999999987, + "479": 104.29999999999991, + "480": 99.94999999999979, + "481": 99.59999999999974, + "482": 95.54999999999986, + "483": 97.19999999999982, + "484": 93.89999999999976, + "485": 75.14999999999996, + "486": 98.19999999999982, + "487": 45.299999999999784, + "488": 92.29999999999977, + "489": 103.09999999999977, + "490": 94.74999999999977, + "491": 101.34999999999977, + "492": 99.19999999999976, + "493": 91.89999999999979, + "494": 86.89999999999976, + "495": 103.54999999999976, + "496": 41.999999999999964, + "497": 100.49999999999977, + "498": 95.24999999999977, + "499": 103.89999999999974, + "500": 96.69999999999976, + "501": 96.59999999999977, + "502": 69.49999999999974, + "503": 81.34999999999978, + "504": 101.54999999999977, + "505": 82.44999999999978, + "506": 97.84999999999975, + "507": 93.29999999999977, + "508": 61.34999999999976, + "509": 94.99999999999979, + "510": 68.74999999999973, + "511": 104.84999999999975, + "512": 102.64999999999974, + "513": 102.84999999999975, + "514": -2.3000000000000753, + "515": 102.99999999999974, + "516": 99.1999999999998, + "517": 103.54999999999978, + "518": 98.59999999999978, + "519": 104.39999999999974, + "520": -76.1, + "521": 50.99999999999976, + "522": 103.04999999999976, + "523": -25.400000000000066, + "524": 93.29999999999983, + "525": 71.04999999999976, + "526": 94.49999999999976, + "527": 98.24999999999976, + "528": 103.99999999999986, + "529": 99.59999999999977, + "530": 99.89999999999976, + "531": 106.84999999999974, + "532": 102.74999999999976, + "533": 103.14999999999976, + "534": 97.79999999999974, + "535": 98.84999999999975, + "536": 102.79999999999977, + "537": 26.35000000000006, + "538": 102.39999999999985, + "539": 96.04999999999976, + "540": 83.19999999999983, + "541": 105.34999999999974, + "542": 102.94999999999975, + "543": 98.44999999999975, + "544": 92.8999999999998, + "545": 101.44999999999975, + "546": 107.34999999999982, + "547": 103.99999999999973, + "548": 103.54999999999974, + "549": 22.50000000000004, + "550": 103.94999999999972, + "551": -46.300000000000004, + "552": 101.94999999999975, + "553": 108.94999999999999, + "554": 108.19999999999978, + "555": 105.94999999999972, + "556": 95.14999999999975, + "557": 102.74999999999973, + "558": 103.89999999999972, + "559": 101.54999999999978, + "560": 100.49999999999977, + "561": 87.5499999999998, + "562": 94.84999999999977, + "563": -74.69999999999999, + "564": 105.59999999999975, + "565": 102.74999999999976, + "566": 94.29999999999977, + "567": 96.29999999999978, + "568": 97.69999999999978, + "569": 99.24999999999977, + "570": 103.24999999999973, + "571": 100.19999999999973, + "572": 97.44999999999979, + "573": 102.44999999999972, + "574": 93.74999999999999, + "575": 90.54999999999976, + "576": 101.24999999999977, + "577": 95.69999999999978, + "578": 101.24999999999974, + "579": 78.54999999999987, + "580": 101.94999999999972, + "581": 103.34999999999975, + "582": 109.10000000000002, + "583": 103.49999999999976, + "584": 104.04999999999971, + "585": 106.39999999999972, + "586": 104.74999999999973, + "587": 103.04999999999977, + "588": 13.099999999999978, + "589": 105.45000000000007, + "590": 14.149999999999906, + "591": 104.39999999999975, + "592": 104.3999999999998, + "593": 111.14999999999989, + "594": 102.44999999999976, + "595": 104.99999999999976, + "596": 76.74999999999986, + "597": 102.49999999999976, + "598": 76.04999999999984, + "599": 99.79999999999977, + "600": 49.79999999999981, + "601": 102.39999999999976, + "602": 101.24999999999977, + "603": 78.44999999999993, + "604": 39.39999999999976, + "605": 45.299999999999955, + "606": -85.6, + "607": 103.74999999999976, + "608": 104.19999999999995, + "609": 98.99999999999976, + "610": 95.99999999999974, + "611": 102.59999999999985, + "612": 97.64999999999979, + "613": -56.74999999999997, + "614": 27.650000000000027, + "615": 63.849999999999724, + "616": 103.99999999999976, + "617": 105.04999999999991, + "618": 101.79999999999977, + "619": 101.84999999999977, + "620": 82.54999999999974, + "621": 99.34999999999972, + "622": -76.30000000000007, + "623": 104.09999999999975, + "624": 103.39999999999975, + "625": 101.04999999999976, + "626": 96.29999999999986, + "627": -87.0, + "628": 101.74999999999977, + "629": 104.04999999999977, + "630": 106.84999999999974, + "631": 86.64999999999978, + "632": 102.49999999999974, + "633": 100.34999999999974, + "634": 55.89999999999977, + "635": 102.14999999999975, + "636": 104.34999999999974, + "637": -74.10000000000002, + "638": 105.44999999999972, + "639": 104.09999999999977, + "640": 105.1499999999998, + "641": 82.19999999999985, + "642": -68.75, + "643": 87.99999999999983, + "644": 104.09999999999977, + "645": 105.24999999999974, + "646": 100.54999999999974, + "647": 105.39999999999974, + "648": 103.19999999999976, + "649": 102.29999999999977, + "650": 102.94999999999976, + "651": 103.59999999999977, + "652": 102.04999999999976, + "653": 102.44999999999976, + "654": 99.94999999999976, + "655": 105.44999999999973, + "656": 42.85000000000001, + "657": 103.99999999999976, + "658": 104.19999999999976, + "659": 103.74999999999974, + "660": 53.79999999999975, + "661": 104.19999999999976, + "662": 109.49999999999976, + "663": 87.64999999999975, + "664": 102.49999999999973, + "665": -44.65000000000006, + "666": 104.14999999999976, + "667": 49.89999999999995, + "668": 105.79999999999973, + "669": 105.14999999999974, + "670": 73.8999999999998, + "671": 85.89999999999984, + "672": 97.04999999999978, + "673": 104.59999999999975, + "674": 103.99999999999976, + "675": 101.59999999999977, + "676": 106.14999999999982, + "677": 98.69999999999978, + "678": 106.69999999999975, + "679": 99.94999999999978, + "680": 99.24999999999976, + "681": 104.19999999999976, + "682": 104.14999999999976, + "683": 81.3999999999999, + "684": 98.69999999999976, + "685": 101.99999999999972, + "686": 105.24999999999974, + "687": 99.84999999999977, + "688": 103.49999999999976, + "689": 103.69999999999975, + "690": 104.09999999999972, + "691": 101.89999999999976, + "692": 106.94999999999976, + "693": 103.84999999999974, + "694": 104.44999999999973, + "695": 104.74999999999973, + "696": 87.49999999999972, + "697": 102.79999999999974, + "698": 103.69999999999976, + "699": 79.29999999999987, + "700": 108.04999999999973, + "701": 57.749999999999766, + "702": 106.29999999999973, + "703": 103.79999999999977, + "704": 107.54999999999983, + "705": -0.600000000000033, + "706": -80.14999999999999, + "707": 99.44999999999975, + "708": 107.99999999999976, + "709": 97.29999999999976, + "710": 89.29999999999977, + "711": 102.24999999999973, + "712": -38.75000000000003, + "713": -34.04999999999999, + "714": 103.69999999999973, + "715": 102.09999999999974, + "716": -43.19999999999999, + "717": 104.39999999999975, + "718": 0.5999999999999659, + "719": 99.69999999999976, + "720": 105.79999999999973, + "721": 103.09999999999977, + "722": 105.39999999999976, + "723": 105.14999999999976, + "724": 104.34999999999977, + "725": 104.04999999999976, + "726": -65.4, + "727": -42.550000000000054, + "728": 104.29999999999984, + "729": 103.44999999999976, + "730": -78.75, + "731": 103.84999999999975, + "732": 105.54999999999973, + "733": 91.7499999999998, + "734": 109.29999999999976, + "735": 110.54999999999991, + "736": 103.84999999999975, + "737": 108.04999999999973, + "738": -72.50000000000001, + "739": 109.09999999999977, + "740": 89.24999999999977, + "741": 103.29999999999976, + "742": 109.34999999999975, + "743": 102.84999999999977, + "744": 108.84999999999975, + "745": 105.59999999999974, + "746": 81.19999999999978, + "747": 100.99999999999977, + "748": 105.19999999999972, + "749": 58.29999999999976, + "750": 46.79999999999987, + "751": 67.84999999999987, + "752": 103.54999999999974, + "753": 88.74999999999976, + "754": 105.14999999999974, + "755": 109.55, + "756": 70.69999999999975, + "757": 103.94999999999975, + "758": 101.74999999999972, + "759": 105.14999999999972, + "760": 103.99999999999974, + "761": 102.69999999999978, + "762": 104.19999999999975, + "763": 104.39999999999975, + "764": -77.95, + "765": 25.599999999999895, + "766": 108.89999999999975, + "767": 106.34999999999977, + "768": 96.54999999999981, + "769": 104.24999999999976, + "770": 106.89999999999972, + "771": 105.19999999999973, + "772": 103.24999999999977, + "773": 103.14999999999976, + "774": 97.49999999999976, + "775": 104.24999999999977, + "776": 105.34999999999978, + "777": 84.54999999999984, + "778": 104.84999999999975, + "779": 104.04999999999973, + "780": 103.84999999999977, + "781": 106.94999999999973, + "782": 100.54999999999976, + "783": 80.19999999999978, + "784": 105.39999999999972, + "785": 103.74999999999977, + "786": 104.79999999999976, + "787": 107.49999999999973, + "788": 106.54999999999973, + "789": -39.34999999999994, + "790": 107.29999999999974, + "791": -74.0, + "792": 107.14999999999974, + "793": 102.84999999999977, + "794": 93.24999999999972, + "795": 108.14999999999974, + "796": -67.60000000000002, + "797": 103.89999999999976, + "798": 105.39999999999972, + "799": 104.99999999999973, + "800": 102.39999999999975, + "801": 106.74999999999977, + "802": 103.09999999999972, + "803": 105.39999999999974, + "804": 100.54999999999977, + "805": 109.39999999999976, + "806": 111.59999999999977, + "807": 104.84999999999974, + "808": 104.09999999999977, + "809": -102.30000000000001, + "810": 104.74999999999974, + "811": 106.19999999999973, + "812": 104.89999999999974, + "813": -72.50000000000001, + "814": 104.94999999999975, + "815": 103.84999999999977, + "816": 103.99999999999974, + "817": -64.64999999999999, + "818": 105.09999999999974, + "819": 105.99999999999972, + "820": 24.24999999999989, + "821": 102.74999999999977, + "822": 100.09999999999977, + "823": 104.19999999999976, + "824": 109.59999999999981, + "825": 105.09999999999972, + "826": 102.59999999999977, + "827": 102.94999999999976, + "828": -76.85000000000002, + "829": 106.19999999999975, + "830": 90.54999999999974, + "831": 41.94999999999977, + "832": -87.19999999999999, + "833": 106.49999999999974, + "834": 103.29999999999977, + "835": 106.24999999999972, + "836": 106.24999999999973, + "837": 104.84999999999975, + "838": 105.29999999999974, + "839": 103.59999999999977, + "840": 91.04999999999981, + "841": 103.59999999999975, + "842": 103.99999999999976, + "843": 106.24999999999973, + "844": 74.99999999999987, + "845": 103.29999999999977, + "846": 104.04999999999976, + "847": 106.99999999999973, + "848": -83.75000000000001, + "849": 105.79999999999973, + "850": -76.30000000000001, + "851": 105.24999999999972, + "852": 105.79999999999973, + "853": 100.84999999999977, + "854": 104.99999999999976, + "855": 105.09999999999972, + "856": 83.89999999999976, + "857": 107.24999999999983, + "858": 103.54999999999977, + "859": -72.05, + "860": 104.09999999999972, + "861": 103.59999999999977, + "862": 104.84999999999975, + "863": -74.89999999999999, + "864": 103.04999999999977, + "865": 104.29999999999977, + "866": 99.69999999999975, + "867": 104.24999999999974, + "868": 95.74999999999976, + "869": 104.59999999999975, + "870": 100.24999999999977, + "871": 104.04999999999976, + "872": 102.64999999999976, + "873": 104.59999999999975, + "874": 102.74999999999977, + "875": 104.39999999999975, + "876": 102.89999999999978, + "877": 104.54999999999977, + "878": 103.74999999999977, + "879": -79.10000000000001, + "880": 104.24999999999976, + "881": 103.49999999999977, + "882": -86.35000000000002, + "883": 103.39999999999976, + "884": 105.39999999999975, + "885": 100.3499999999998, + "886": 107.29999999999973, + "887": 104.09999999999975, + "888": 102.69999999999978, + "889": 101.84999999999977, + "890": 105.29999999999974, + "891": 103.54999999999977, + "892": 102.24999999999976, + "893": 105.29999999999973, + "894": 102.24999999999977, + "895": 97.74999999999973, + "896": 105.94999999999972, + "897": 103.84999999999977, + "898": 68.49999999999977, + "899": 97.24999999999977, + "900": -85.44999999999999, + "901": 103.79999999999977, + "902": 101.29999999999978, + "903": 101.94999999999976, + "904": 105.84999999999981, + "905": 105.44999999999982, + "906": 104.09999999999975, + "907": 109.04999999999977, + "908": 105.44999999999975, + "909": 103.19999999999978, + "910": 105.14999999999979, + "911": -85.65, + "912": 101.89999999999976, + "913": 108.25000000000006, + "914": 107.04999999999971, + "915": 107.29999999999977, + "916": 104.89999999999978, + "917": 104.24999999999976, + "918": 104.69999999999975, + "919": 105.4499999999998, + "920": 108.49999999999986, + "921": 108.34999999999987, + "922": 99.99999999999974, + "923": 2.0499999999999616, + "924": 103.64999999999975, + "925": 104.49999999999974, + "926": 103.84999999999977, + "927": 107.3, + "928": 104.59999999999977, + "929": 103.84999999999977, + "930": 10.249999999999922, + "931": 103.24999999999976, + "932": 105.59999999999974, + "933": 105.59999999999977, + "934": 97.09999999999977, + "935": 105.44999999999973, + "936": 104.09999999999972, + "937": 103.69999999999976, + "938": 105.29999999999974, + "939": 25.800000000000054, + "940": 105.59999999999975, + "941": 105.49999999999974, + "942": -74.49999999999999, + "943": 105.54999999999974, + "944": 104.14999999999976, + "945": 103.39999999999976, + "946": 104.79999999999976, + "947": 103.09999999999977, + "948": 54.35, + "949": 87.79999999999978, + "950": 104.19999999999976, + "951": 105.64999999999974, + "952": 104.49999999999974, + "953": 103.44999999999976, + "954": 61.849999999999945, + "955": 104.64999999999974, + "956": 103.54999999999977, + "957": 104.39999999999976, + "958": 102.69999999999978, + "959": 103.19999999999976, + "960": 103.04999999999977, + "961": 104.39999999999976, + "962": 82.99999999999973, + "963": 105.14999999999974, + "964": 104.04999999999974, + "965": 105.29999999999981, + "966": 105.04999999999974, + "967": -91.4, + "968": 105.09999999999974, + "969": 106.94999999999995, + "970": -50.55, + "971": 104.24999999999976, + "972": 104.09999999999981, + "973": -86.0, + "974": -32.00000000000002, + "975": 108.34999999999977, + "976": 106.34999999999984, + "977": -85.35, + "978": -45.9, + "979": 110.29999999999991, + "980": 108.49999999999976, + "981": 105.74999999999972, + "982": 104.59999999999977, + "983": 106.44999999999972, + "984": 105.59999999999974, + "985": -87.0, + "986": 106.69999999999978, + "987": 104.34999999999975, + "988": -53.199999999999974, + "989": 112.05000000000018, + "990": 104.34999999999972, + "991": 102.64999999999976, + "992": -84.9, + "993": -39.350000000000044, + "994": 103.94999999999976, + "995": 102.04999999999977, + "996": 103.64999999999976, + "997": 100.3499999999998, + "998": 84.7999999999998, + "999": 105.09999999999974, + "1000": 106.89999999999974 + } + }, + "config": { + "io_settings": { + "save_agent_actions": true, + "save_step_metadata": false, + "save_pcap_logs": false, + "save_sys_logs": false, + "sys_log_level": "WARNING" + }, + "game": { + "max_episode_length": 128, + "ports": [ + "HTTP", + "POSTGRES_SERVER" + ], + "protocols": [ + "ICMP", + "TCP", + "UDP" + ], + "thresholds": { + "nmne": { + "high": 10, + "medium": 5, + "low": 0 + } + } + }, + "agents": [ + { + "ref": "client_2_green_user", + "team": "GREEN", + "type": "ProbabilisticAgent", + "agent_settings": { + "action_probabilities": { + "0": 0.3, + "1": 0.6, + "2": 0.1 + } + }, + "observation_space": null, + "action_space": { + "action_list": [ + { + "type": "DONOTHING" + }, + { + "type": "NODE_APPLICATION_EXECUTE" + } + ], + "options": { + "nodes": [ + { + "node_name": "client_2", + "applications": [ + { + "application_name": "WebBrowser" + }, + { + "application_name": "DatabaseClient" + } + ] + } + ], + "max_folders_per_node": 1, + "max_files_per_folder": 1, + "max_services_per_node": 1, + "max_applications_per_node": 2 + }, + "action_map": { + "0": { + "action": "DONOTHING", + "options": {} + }, + "1": { + "action": "NODE_APPLICATION_EXECUTE", + "options": { + "node_id": 0, + "application_id": 0 + } + }, + "2": { + "action": "NODE_APPLICATION_EXECUTE", + "options": { + "node_id": 0, + "application_id": 1 + } + } + } + }, + "reward_function": { + "reward_components": [ + { + "type": "WEBPAGE_UNAVAILABLE_PENALTY", + "weight": 0.25, + "options": { + "node_hostname": "client_2" + } + }, + { + "type": "GREEN_ADMIN_DATABASE_UNREACHABLE_PENALTY", + "weight": 0.05, + "options": { + "node_hostname": "client_2" + } + } + ] + } + }, + { + "ref": "client_1_green_user", + "team": "GREEN", + "type": "ProbabilisticAgent", + "agent_settings": { + "action_probabilities": { + "0": 0.3, + "1": 0.6, + "2": 0.1 + } + }, + "observation_space": null, + "action_space": { + "action_list": [ + { + "type": "DONOTHING" + }, + { + "type": "NODE_APPLICATION_EXECUTE" + } + ], + "options": { + "nodes": [ + { + "node_name": "client_1", + "applications": [ + { + "application_name": "WebBrowser" + }, + { + "application_name": "DatabaseClient" + } + ] + } + ], + "max_folders_per_node": 1, + "max_files_per_folder": 1, + "max_services_per_node": 1, + "max_applications_per_node": 2 + }, + "action_map": { + "0": { + "action": "DONOTHING", + "options": {} + }, + "1": { + "action": "NODE_APPLICATION_EXECUTE", + "options": { + "node_id": 0, + "application_id": 0 + } + }, + "2": { + "action": "NODE_APPLICATION_EXECUTE", + "options": { + "node_id": 0, + "application_id": 1 + } + } + } + }, + "reward_function": { + "reward_components": [ + { + "type": "WEBPAGE_UNAVAILABLE_PENALTY", + "weight": 0.25, + "options": { + "node_hostname": "client_1" + } + }, + { + "type": "GREEN_ADMIN_DATABASE_UNREACHABLE_PENALTY", + "weight": 0.05, + "options": { + "node_hostname": "client_1" + } + } + ] + } + }, + { + "ref": "data_manipulation_attacker", + "team": "RED", + "type": "RedDatabaseCorruptingAgent", + "observation_space": null, + "action_space": { + "action_list": [ + { + "type": "DONOTHING" + }, + { + "type": "NODE_APPLICATION_EXECUTE" + } + ], + "options": { + "nodes": [ + { + "node_name": "client_1", + "applications": [ + { + "application_name": "DataManipulationBot" + } + ] + }, + { + "node_name": "client_2", + "applications": [ + { + "application_name": "DataManipulationBot" + } + ] + } + ], + "max_folders_per_node": 1, + "max_files_per_folder": 1, + "max_services_per_node": 1 + } + }, + "reward_function": { + "reward_components": [ + { + "type": "DUMMY" + } + ] + }, + "agent_settings": { + "start_settings": { + "start_step": 25, + "frequency": 20, + "variance": 5 + } + } + }, + { + "ref": "defender", + "team": "BLUE", + "type": "ProxyAgent", + "observation_space": { + "type": "CUSTOM", + "options": { + "components": [ + { + "type": "NODES", + "label": "NODES", + "options": { + "hosts": [ + { + "hostname": "domain_controller" + }, + { + "hostname": "web_server", + "services": [ + { + "service_name": "WebServer" + } + ] + }, + { + "hostname": "database_server", + "folders": [ + { + "folder_name": "database", + "files": [ + { + "file_name": "database.db" + } + ] + } + ] + }, + { + "hostname": "backup_server" + }, + { + "hostname": "security_suite" + }, + { + "hostname": "client_1" + }, + { + "hostname": "client_2" + } + ], + "num_services": 1, + "num_applications": 0, + "num_folders": 1, + "num_files": 1, + "num_nics": 2, + "include_num_access": false, + "include_nmne": true, + "monitored_traffic": { + "icmp": [ + "NONE" + ], + "tcp": [ + "DNS" + ] + }, + "routers": [ + { + "hostname": "router_1" + } + ], + "num_ports": 0, + "ip_list": [ + "192.168.1.10", + "192.168.1.12", + "192.168.1.14", + "192.168.1.16", + "192.168.1.110", + "192.168.10.21", + "192.168.10.22", + "192.168.10.110" + ], + "wildcard_list": [ + "0.0.0.1" + ], + "port_list": [ + 80, + 5432 + ], + "protocol_list": [ + "ICMP", + "TCP", + "UDP" + ], + "num_rules": 10 + } + }, + { + "type": "LINKS", + "label": "LINKS", + "options": { + "link_references": [ + "router_1:eth-1<->switch_1:eth-8", + "router_1:eth-2<->switch_2:eth-8", + "switch_1:eth-1<->domain_controller:eth-1", + "switch_1:eth-2<->web_server:eth-1", + "switch_1:eth-3<->database_server:eth-1", + "switch_1:eth-4<->backup_server:eth-1", + "switch_1:eth-7<->security_suite:eth-1", + "switch_2:eth-1<->client_1:eth-1", + "switch_2:eth-2<->client_2:eth-1", + "switch_2:eth-7<->security_suite:eth-2" + ] + } + }, + { + "type": "NONE", + "label": "ICS", + "options": {} + } + ] + } + }, + "action_space": { + "action_list": [ + { + "type": "DONOTHING" + }, + { + "type": "NODE_SERVICE_SCAN" + }, + { + "type": "NODE_SERVICE_STOP" + }, + { + "type": "NODE_SERVICE_START" + }, + { + "type": "NODE_SERVICE_PAUSE" + }, + { + "type": "NODE_SERVICE_RESUME" + }, + { + "type": "NODE_SERVICE_RESTART" + }, + { + "type": "NODE_SERVICE_DISABLE" + }, + { + "type": "NODE_SERVICE_ENABLE" + }, + { + "type": "NODE_SERVICE_FIX" + }, + { + "type": "NODE_FILE_SCAN" + }, + { + "type": "NODE_FILE_CHECKHASH" + }, + { + "type": "NODE_FILE_DELETE" + }, + { + "type": "NODE_FILE_REPAIR" + }, + { + "type": "NODE_FILE_RESTORE" + }, + { + "type": "NODE_FOLDER_SCAN" + }, + { + "type": "NODE_FOLDER_CHECKHASH" + }, + { + "type": "NODE_FOLDER_REPAIR" + }, + { + "type": "NODE_FOLDER_RESTORE" + }, + { + "type": "NODE_OS_SCAN" + }, + { + "type": "NODE_SHUTDOWN" + }, + { + "type": "NODE_STARTUP" + }, + { + "type": "NODE_RESET" + }, + { + "type": "ROUTER_ACL_ADDRULE" + }, + { + "type": "ROUTER_ACL_REMOVERULE" + }, + { + "type": "HOST_NIC_ENABLE" + }, + { + "type": "HOST_NIC_DISABLE" + } + ], + "action_map": { + "0": { + "action": "DONOTHING", + "options": {} + }, + "1": { + "action": "NODE_SERVICE_SCAN", + "options": { + "node_id": 1, + "service_id": 0 + } + }, + "2": { + "action": "NODE_SERVICE_STOP", + "options": { + "node_id": 1, + "service_id": 0 + } + }, + "3": { + "action": "NODE_SERVICE_START", + "options": { + "node_id": 1, + "service_id": 0 + } + }, + "4": { + "action": "NODE_SERVICE_PAUSE", + "options": { + "node_id": 1, + "service_id": 0 + } + }, + "5": { + "action": "NODE_SERVICE_RESUME", + "options": { + "node_id": 1, + "service_id": 0 + } + }, + "6": { + "action": "NODE_SERVICE_RESTART", + "options": { + "node_id": 1, + "service_id": 0 + } + }, + "7": { + "action": "NODE_SERVICE_DISABLE", + "options": { + "node_id": 1, + "service_id": 0 + } + }, + "8": { + "action": "NODE_SERVICE_ENABLE", + "options": { + "node_id": 1, + "service_id": 0 + } + }, + "9": { + "action": "NODE_FILE_SCAN", + "options": { + "node_id": 2, + "folder_id": 0, + "file_id": 0 + } + }, + "10": { + "action": "NODE_FILE_CHECKHASH", + "options": { + "node_id": 2, + "folder_id": 0, + "file_id": 0 + } + }, + "11": { + "action": "NODE_FILE_DELETE", + "options": { + "node_id": 2, + "folder_id": 0, + "file_id": 0 + } + }, + "12": { + "action": "NODE_FILE_REPAIR", + "options": { + "node_id": 2, + "folder_id": 0, + "file_id": 0 + } + }, + "13": { + "action": "NODE_SERVICE_FIX", + "options": { + "node_id": 2, + "service_id": 0 + } + }, + "14": { + "action": "NODE_FOLDER_SCAN", + "options": { + "node_id": 2, + "folder_id": 0 + } + }, + "15": { + "action": "NODE_FOLDER_CHECKHASH", + "options": { + "node_id": 2, + "folder_id": 0 + } + }, + "16": { + "action": "NODE_FOLDER_REPAIR", + "options": { + "node_id": 2, + "folder_id": 0 + } + }, + "17": { + "action": "NODE_FOLDER_RESTORE", + "options": { + "node_id": 2, + "folder_id": 0 + } + }, + "18": { + "action": "NODE_OS_SCAN", + "options": { + "node_id": 0 + } + }, + "19": { + "action": "NODE_SHUTDOWN", + "options": { + "node_id": 0 + } + }, + "20": { + "action": "NODE_STARTUP", + "options": { + "node_id": 0 + } + }, + "21": { + "action": "NODE_RESET", + "options": { + "node_id": 0 + } + }, + "22": { + "action": "NODE_OS_SCAN", + "options": { + "node_id": 1 + } + }, + "23": { + "action": "NODE_SHUTDOWN", + "options": { + "node_id": 1 + } + }, + "24": { + "action": "NODE_STARTUP", + "options": { + "node_id": 1 + } + }, + "25": { + "action": "NODE_RESET", + "options": { + "node_id": 1 + } + }, + "26": { + "action": "NODE_OS_SCAN", + "options": { + "node_id": 2 + } + }, + "27": { + "action": "NODE_SHUTDOWN", + "options": { + "node_id": 2 + } + }, + "28": { + "action": "NODE_STARTUP", + "options": { + "node_id": 2 + } + }, + "29": { + "action": "NODE_RESET", + "options": { + "node_id": 2 + } + }, + "30": { + "action": "NODE_OS_SCAN", + "options": { + "node_id": 3 + } + }, + "31": { + "action": "NODE_SHUTDOWN", + "options": { + "node_id": 3 + } + }, + "32": { + "action": "NODE_STARTUP", + "options": { + "node_id": 3 + } + }, + "33": { + "action": "NODE_RESET", + "options": { + "node_id": 3 + } + }, + "34": { + "action": "NODE_OS_SCAN", + "options": { + "node_id": 4 + } + }, + "35": { + "action": "NODE_SHUTDOWN", + "options": { + "node_id": 4 + } + }, + "36": { + "action": "NODE_STARTUP", + "options": { + "node_id": 4 + } + }, + "37": { + "action": "NODE_RESET", + "options": { + "node_id": 4 + } + }, + "38": { + "action": "NODE_OS_SCAN", + "options": { + "node_id": 5 + } + }, + "39": { + "action": "NODE_SHUTDOWN", + "options": { + "node_id": 5 + } + }, + "40": { + "action": "NODE_STARTUP", + "options": { + "node_id": 5 + } + }, + "41": { + "action": "NODE_RESET", + "options": { + "node_id": 5 + } + }, + "42": { + "action": "NODE_OS_SCAN", + "options": { + "node_id": 6 + } + }, + "43": { + "action": "NODE_SHUTDOWN", + "options": { + "node_id": 6 + } + }, + "44": { + "action": "NODE_STARTUP", + "options": { + "node_id": 6 + } + }, + "45": { + "action": "NODE_RESET", + "options": { + "node_id": 6 + } + }, + "46": { + "action": "ROUTER_ACL_ADDRULE", + "options": { + "target_router": "router_1", + "position": 1, + "permission": 2, + "source_ip_id": 7, + "dest_ip_id": 1, + "source_port_id": 1, + "dest_port_id": 1, + "protocol_id": 1, + "source_wildcard_id": 0, + "dest_wildcard_id": 0 + } + }, + "47": { + "action": "ROUTER_ACL_ADDRULE", + "options": { + "target_router": "router_1", + "position": 2, + "permission": 2, + "source_ip_id": 8, + "dest_ip_id": 1, + "source_port_id": 1, + "dest_port_id": 1, + "protocol_id": 1, + "source_wildcard_id": 0, + "dest_wildcard_id": 0 + } + }, + "48": { + "action": "ROUTER_ACL_ADDRULE", + "options": { + "target_router": "router_1", + "position": 3, + "permission": 2, + "source_ip_id": 7, + "dest_ip_id": 3, + "source_port_id": 1, + "dest_port_id": 1, + "protocol_id": 3, + "source_wildcard_id": 0, + "dest_wildcard_id": 0 + } + }, + "49": { + "action": "ROUTER_ACL_ADDRULE", + "options": { + "target_router": "router_1", + "position": 4, + "permission": 2, + "source_ip_id": 8, + "dest_ip_id": 3, + "source_port_id": 1, + "dest_port_id": 1, + "protocol_id": 3, + "source_wildcard_id": 0, + "dest_wildcard_id": 0 + } + }, + "50": { + "action": "ROUTER_ACL_ADDRULE", + "options": { + "target_router": "router_1", + "position": 5, + "permission": 2, + "source_ip_id": 7, + "dest_ip_id": 4, + "source_port_id": 1, + "dest_port_id": 1, + "protocol_id": 3, + "source_wildcard_id": 0, + "dest_wildcard_id": 0 + } + }, + "51": { + "action": "ROUTER_ACL_ADDRULE", + "options": { + "target_router": "router_1", + "position": 6, + "permission": 2, + "source_ip_id": 8, + "dest_ip_id": 4, + "source_port_id": 1, + "dest_port_id": 1, + "protocol_id": 3, + "source_wildcard_id": 0, + "dest_wildcard_id": 0 + } + }, + "52": { + "action": "ROUTER_ACL_REMOVERULE", + "options": { + "target_router": "router_1", + "position": 0 + } + }, + "53": { + "action": "ROUTER_ACL_REMOVERULE", + "options": { + "target_router": "router_1", + "position": 1 + } + }, + "54": { + "action": "ROUTER_ACL_REMOVERULE", + "options": { + "target_router": "router_1", + "position": 2 + } + }, + "55": { + "action": "ROUTER_ACL_REMOVERULE", + "options": { + "target_router": "router_1", + "position": 3 + } + }, + "56": { + "action": "ROUTER_ACL_REMOVERULE", + "options": { + "target_router": "router_1", + "position": 4 + } + }, + "57": { + "action": "ROUTER_ACL_REMOVERULE", + "options": { + "target_router": "router_1", + "position": 5 + } + }, + "58": { + "action": "ROUTER_ACL_REMOVERULE", + "options": { + "target_router": "router_1", + "position": 6 + } + }, + "59": { + "action": "ROUTER_ACL_REMOVERULE", + "options": { + "target_router": "router_1", + "position": 7 + } + }, + "60": { + "action": "ROUTER_ACL_REMOVERULE", + "options": { + "target_router": "router_1", + "position": 8 + } + }, + "61": { + "action": "ROUTER_ACL_REMOVERULE", + "options": { + "target_router": "router_1", + "position": 9 + } + }, + "62": { + "action": "HOST_NIC_DISABLE", + "options": { + "node_id": 0, + "nic_id": 0 + } + }, + "63": { + "action": "HOST_NIC_ENABLE", + "options": { + "node_id": 0, + "nic_id": 0 + } + }, + "64": { + "action": "HOST_NIC_DISABLE", + "options": { + "node_id": 1, + "nic_id": 0 + } + }, + "65": { + "action": "HOST_NIC_ENABLE", + "options": { + "node_id": 1, + "nic_id": 0 + } + }, + "66": { + "action": "HOST_NIC_DISABLE", + "options": { + "node_id": 2, + "nic_id": 0 + } + }, + "67": { + "action": "HOST_NIC_ENABLE", + "options": { + "node_id": 2, + "nic_id": 0 + } + }, + "68": { + "action": "HOST_NIC_DISABLE", + "options": { + "node_id": 3, + "nic_id": 0 + } + }, + "69": { + "action": "HOST_NIC_ENABLE", + "options": { + "node_id": 3, + "nic_id": 0 + } + }, + "70": { + "action": "HOST_NIC_DISABLE", + "options": { + "node_id": 4, + "nic_id": 0 + } + }, + "71": { + "action": "HOST_NIC_ENABLE", + "options": { + "node_id": 4, + "nic_id": 0 + } + }, + "72": { + "action": "HOST_NIC_DISABLE", + "options": { + "node_id": 4, + "nic_id": 1 + } + }, + "73": { + "action": "HOST_NIC_ENABLE", + "options": { + "node_id": 4, + "nic_id": 1 + } + }, + "74": { + "action": "HOST_NIC_DISABLE", + "options": { + "node_id": 5, + "nic_id": 0 + } + }, + "75": { + "action": "HOST_NIC_ENABLE", + "options": { + "node_id": 5, + "nic_id": 0 + } + }, + "76": { + "action": "HOST_NIC_DISABLE", + "options": { + "node_id": 6, + "nic_id": 0 + } + }, + "77": { + "action": "HOST_NIC_ENABLE", + "options": { + "node_id": 6, + "nic_id": 0 + } + } + }, + "options": { + "nodes": [ + { + "node_name": "domain_controller" + }, + { + "node_name": "web_server", + "applications": [ + { + "application_name": "DatabaseClient" + } + ], + "services": [ + { + "service_name": "WebServer" + } + ] + }, + { + "node_name": "database_server", + "folders": [ + { + "folder_name": "database", + "files": [ + { + "file_name": "database.db" + } + ] + } + ], + "services": [ + { + "service_name": "DatabaseService" + } + ] + }, + { + "node_name": "backup_server" + }, + { + "node_name": "security_suite" + }, + { + "node_name": "client_1" + }, + { + "node_name": "client_2" + } + ], + "max_folders_per_node": 2, + "max_files_per_folder": 2, + "max_services_per_node": 2, + "max_nics_per_node": 8, + "max_acl_rules": 10, + "ip_list": [ + "192.168.1.10", + "192.168.1.12", + "192.168.1.14", + "192.168.1.16", + "192.168.1.110", + "192.168.10.21", + "192.168.10.22", + "192.168.10.110" + ] + } + }, + "reward_function": { + "reward_components": [ + { + "type": "DATABASE_FILE_INTEGRITY", + "weight": 0.4, + "options": { + "node_hostname": "database_server", + "folder_name": "database", + "file_name": "database.db" + } + }, + { + "type": "SHARED_REWARD", + "weight": 1.0, + "options": { + "agent_name": "client_1_green_user" + } + }, + { + "type": "SHARED_REWARD", + "weight": 1.0, + "options": { + "agent_name": "client_2_green_user" + } + } + ] + }, + "agent_settings": { + "flatten_obs": true, + "action_masking": true + } + } + ], + "simulation": { + "network": { + "nmne_config": { + "capture_nmne": true, + "nmne_capture_keywords": [ + "DELETE" + ] + }, + "nodes": [ + { + "hostname": "router_1", + "type": "router", + "num_ports": 5, + "ports": { + "1": { + "ip_address": "192.168.1.1", + "subnet_mask": "255.255.255.0" + }, + "2": { + "ip_address": "192.168.10.1", + "subnet_mask": "255.255.255.0" + } + }, + "acl": { + "18": { + "action": "PERMIT", + "src_port": "POSTGRES_SERVER", + "dst_port": "POSTGRES_SERVER" + }, + "19": { + "action": "PERMIT", + "src_port": "DNS", + "dst_port": "DNS" + }, + "20": { + "action": "PERMIT", + "src_port": "FTP", + "dst_port": "FTP" + }, + "21": { + "action": "PERMIT", + "src_port": "HTTP", + "dst_port": "HTTP" + }, + "22": { + "action": "PERMIT", + "src_port": "ARP", + "dst_port": "ARP" + }, + "23": { + "action": "PERMIT", + "protocol": "ICMP" + } + } + }, + { + "hostname": "switch_1", + "type": "switch", + "num_ports": 8 + }, + { + "hostname": "switch_2", + "type": "switch", + "num_ports": 8 + }, + { + "hostname": "domain_controller", + "type": "server", + "ip_address": "192.168.1.10", + "subnet_mask": "255.255.255.0", + "default_gateway": "192.168.1.1", + "services": [ + { + "type": "DNSServer", + "options": { + "domain_mapping": { + "arcd.com": "192.168.1.12" + } + } + } + ] + }, + { + "hostname": "web_server", + "type": "server", + "ip_address": "192.168.1.12", + "subnet_mask": "255.255.255.0", + "default_gateway": "192.168.1.1", + "dns_server": "192.168.1.10", + "services": [ + { + "type": "WebServer" + } + ], + "applications": [ + { + "type": "DatabaseClient", + "options": { + "db_server_ip": "192.168.1.14" + } + } + ] + }, + { + "hostname": "database_server", + "type": "server", + "ip_address": "192.168.1.14", + "subnet_mask": "255.255.255.0", + "default_gateway": "192.168.1.1", + "dns_server": "192.168.1.10", + "services": [ + { + "type": "DatabaseService", + "options": { + "backup_server_ip": "192.168.1.16" + } + }, + { + "type": "FTPClient" + } + ] + }, + { + "hostname": "backup_server", + "type": "server", + "ip_address": "192.168.1.16", + "subnet_mask": "255.255.255.0", + "default_gateway": "192.168.1.1", + "dns_server": "192.168.1.10", + "services": [ + { + "type": "FTPServer" + } + ] + }, + { + "hostname": "security_suite", + "type": "server", + "ip_address": "192.168.1.110", + "subnet_mask": "255.255.255.0", + "default_gateway": "192.168.1.1", + "dns_server": "192.168.1.10", + "network_interfaces": { + "2": { + "ip_address": "192.168.10.110", + "subnet_mask": "255.255.255.0" + } + } + }, + { + "hostname": "client_1", + "type": "computer", + "ip_address": "192.168.10.21", + "subnet_mask": "255.255.255.0", + "default_gateway": "192.168.10.1", + "dns_server": "192.168.1.10", + "applications": [ + { + "type": "DataManipulationBot", + "options": { + "port_scan_p_of_success": 0.8, + "data_manipulation_p_of_success": 0.8, + "payload": "DELETE", + "server_ip": "192.168.1.14" + } + }, + { + "type": "WebBrowser", + "options": { + "target_url": "http://arcd.com/users/" + } + }, + { + "type": "DatabaseClient", + "options": { + "db_server_ip": "192.168.1.14" + } + } + ], + "services": [ + { + "type": "DNSClient" + } + ] + }, + { + "hostname": "client_2", + "type": "computer", + "ip_address": "192.168.10.22", + "subnet_mask": "255.255.255.0", + "default_gateway": "192.168.10.1", + "dns_server": "192.168.1.10", + "applications": [ + { + "type": "WebBrowser", + "options": { + "target_url": "http://arcd.com/users/" + } + }, + { + "type": "DataManipulationBot", + "options": { + "port_scan_p_of_success": 0.8, + "data_manipulation_p_of_success": 0.8, + "payload": "DELETE", + "server_ip": "192.168.1.14" + } + }, + { + "type": "DatabaseClient", + "options": { + "db_server_ip": "192.168.1.14" + } + } + ], + "services": [ + { + "type": "DNSClient" + } + ] + } + ], + "links": [ + { + "endpoint_a_hostname": "router_1", + "endpoint_a_port": 1, + "endpoint_b_hostname": "switch_1", + "endpoint_b_port": 8 + }, + { + "endpoint_a_hostname": "router_1", + "endpoint_a_port": 2, + "endpoint_b_hostname": "switch_2", + "endpoint_b_port": 8 + }, + { + "endpoint_a_hostname": "switch_1", + "endpoint_a_port": 1, + "endpoint_b_hostname": "domain_controller", + "endpoint_b_port": 1 + }, + { + "endpoint_a_hostname": "switch_1", + "endpoint_a_port": 2, + "endpoint_b_hostname": "web_server", + "endpoint_b_port": 1 + }, + { + "endpoint_a_hostname": "switch_1", + "endpoint_a_port": 3, + "endpoint_b_hostname": "database_server", + "endpoint_b_port": 1 + }, + { + "endpoint_a_hostname": "switch_1", + "endpoint_a_port": 4, + "endpoint_b_hostname": "backup_server", + "endpoint_b_port": 1 + }, + { + "endpoint_a_hostname": "switch_1", + "endpoint_a_port": 7, + "endpoint_b_hostname": "security_suite", + "endpoint_b_port": 1 + }, + { + "endpoint_a_hostname": "switch_2", + "endpoint_a_port": 1, + "endpoint_b_hostname": "client_1", + "endpoint_b_port": 1 + }, + { + "endpoint_a_hostname": "switch_2", + "endpoint_a_port": 2, + "endpoint_b_hostname": "client_2", + "endpoint_b_port": 1 + }, + { + "endpoint_a_hostname": "switch_2", + "endpoint_a_port": 7, + "endpoint_b_hostname": "security_suite", + "endpoint_b_port": 2 + } + ] + } + } + } +} diff --git a/benchmark/static/styles.css b/benchmark/static/styles.css new file mode 100644 index 00000000..4fbb9bd5 --- /dev/null +++ b/benchmark/static/styles.css @@ -0,0 +1,34 @@ +body { + font-family: 'Arial', sans-serif; + line-height: 1.6; + /* margin: 1cm; */ +} +h1, h2, h3, h4, h5, h6 { + font-weight: bold; + /* margin: 1em 0; */ +} +p { + /* margin: 0.5em 0; */ +} +ul, ol { + margin: 1em 0; + padding-left: 1.5em; +} +pre { + background: #f4f4f4; + padding: 0.5em; + overflow-x: auto; +} +img { + max-width: 100%; + height: auto; +} +table { + width: 100%; + border-collapse: collapse; + margin: 1em 0; +} +th, td { + padding: 0.5em; + border: 1px solid #ddd; +} diff --git a/docs/_static/c2_sequence.png b/docs/_static/c2_sequence.png new file mode 100644 index 00000000..9c7ba397 Binary files /dev/null and b/docs/_static/c2_sequence.png differ diff --git a/docs/index.rst b/docs/index.rst index 93da9b88..118f7ebf 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -25,6 +25,7 @@ What is PrimAITE? source/game_layer source/simulation source/config + source/rewards source/customising_scenarios source/varying_config_files source/environment @@ -59,6 +60,8 @@ The ARCD Primary-level AI Training Environment (**PrimAITE**) provides an effect - Modelling background (green) pattern-of-life; - Operates at machine-speed to enable fast training cycles via Reinforcement Learning (RL). +PrimAITE has been designed as an extensible environment and toolkit to support the development, test, training and evaluation of AI-based cyber defensive agents. Whilst PrimAITE ships with a number of example modelled scenarios (a.k.a. Use Cases), it has not been developed to mandate the solving of a single cyber challenge, and instead provides a highly flexible environment application that can be extended and reconfigured by the user to suit their specific cyber defence training and evaluation needs. PrimAITE provides default networks, red agent and green agent behaviour, reward functions, and action / observation space configuration, all of which can be utilised out of the box, but which ultimately can (and in some instances should) be built upon and / or reconfigured to meet the needs of different defensive agent developers. The PrimAITE user guide provides comprehensive instruction on all PrimAITE features, functionality and components, and can be consulted in order to help guide users in any reconfiguration or enhancements they wish to undertake; a library of example Jupyter notebooks are also provided to support such work. + Features ^^^^^^^^ diff --git a/docs/source/action_masking.rst b/docs/source/action_masking.rst index 30b1376d..264ab254 100644 --- a/docs/source/action_masking.rst +++ b/docs/source/action_masking.rst @@ -9,6 +9,8 @@ about which actions are invalid based on the current environment state. For inst software on a node that is turned off. Therefore, if an agent has a NODE_SOFTWARE_INSTALL in it's action map for that node, the action mask will show `0` in the corresponding entry. +*Note: just because an action is available in the action mask does not mean it will be successful when executed. It just means it's possible to try to execute the action at this time.* + Configuration ============= Action masking is supported for agents that use the `ProxyAgent` class (the class used for connecting to RL algorithms). @@ -23,95 +25,121 @@ The following logic is applied: +==========================================+=====================================================================+ | **DONOTHING** | Always Possible. | +------------------------------------------+---------------------------------------------------------------------+ -| **NODE_HOST_SERVICE_SCAN** | Node is on. Service is running. | +| **NODE_SERVICE_SCAN** | Node is on. Service is running. | +------------------------------------------+---------------------------------------------------------------------+ -| **NODE_HOST_SERVICE_STOP** | Node is on. Service is running. | +| **NODE_SERVICE_STOP** | Node is on. Service is running. | +------------------------------------------+---------------------------------------------------------------------+ -| **NODE_HOST_SERVICE_START** | Node is on. Service is stopped. | +| **NODE_SERVICE_START** | Node is on. Service is stopped. | +------------------------------------------+---------------------------------------------------------------------+ -| **NODE_HOST_SERVICE_PAUSE** | Node is on. Service is running. | +| **NODE_SERVICE_PAUSE** | Node is on. Service is running. | +------------------------------------------+---------------------------------------------------------------------+ -| **NODE_HOST_SERVICE_RESUME** | Node is on. Service is paused. | +| **NODE_SERVICE_RESUME** | Node is on. Service is paused. | +------------------------------------------+---------------------------------------------------------------------+ -| **NODE_HOST_SERVICE_RESTART** | Node is on. Service is running. | +| **NODE_SERVICE_RESTART** | Node is on. Service is running. | +------------------------------------------+---------------------------------------------------------------------+ -| **NODE_HOST_SERVICE_DISABLE** | Node is on. | +| **NODE_SERVICE_DISABLE** | Node is on. | +------------------------------------------+---------------------------------------------------------------------+ -| **NODE_HOST_SERVICE_ENABLE** | Node is on. Service is disabled. | +| **NODE_SERVICE_ENABLE** | Node is on. Service is disabled. | +------------------------------------------+---------------------------------------------------------------------+ -| **NODE_HOST_SERVICE_FIX** | Node is on. Service is running. | +| **NODE_SERVICE_FIX** | Node is on. Service is running. | +------------------------------------------+---------------------------------------------------------------------+ -| **NODE_HOST_APPLICATION_EXECUTE** | Node is on. | +| **NODE_APPLICATION_EXECUTE** | Node is on. | +------------------------------------------+---------------------------------------------------------------------+ -| **NODE_HOST_APPLICATION_SCAN** | Node is on. Application is running. | +| **NODE_APPLICATION_SCAN** | Node is on. Application is running. | +------------------------------------------+---------------------------------------------------------------------+ -| **NODE_HOST_APPLICATION_CLOSE** | Node is on. Application is running. | +| **NODE_APPLICATION_CLOSE** | Node is on. Application is running. | +------------------------------------------+---------------------------------------------------------------------+ -| **NODE_HOST_APPLICATION_FIX** | Node is on. Application is running. | +| **NODE_APPLICATION_FIX** | Node is on. Application is running. | +------------------------------------------+---------------------------------------------------------------------+ -| **NODE_HOST_APPLICATION_INSTALL** | Node is on. | +| **NODE_APPLICATION_INSTALL** | Node is on. | +------------------------------------------+---------------------------------------------------------------------+ -| **NODE_HOST_APPLICATION_REMOVE** | Node is on. | +| **NODE_APPLICATION_REMOVE** | Node is on. | +------------------------------------------+---------------------------------------------------------------------+ -| **NODE_HOST_FILE_SCAN** | Node is on. File exists. File not deleted. | +| **NODE_FILE_SCAN** | Node is on. File exists. File not deleted. | +------------------------------------------+---------------------------------------------------------------------+ -| **NODE_HOST_FILE_CREATE** | Node is on. | +| **NODE_FILE_CREATE** | Node is on. | +------------------------------------------+---------------------------------------------------------------------+ -| **NODE_HOST_FILE_CHECKHASH** | Node is on. File exists. File not deleted. | +| **NODE_FILE_CHECKHASH** | Node is on. File exists. File not deleted. | +------------------------------------------+---------------------------------------------------------------------+ -| **NODE_HOST_FILE_DELETE** | Node is on. File exists. | +| **NODE_FILE_DELETE** | Node is on. File exists. | +------------------------------------------+---------------------------------------------------------------------+ -| **NODE_HOST_FILE_REPAIR** | Node is on. File exists. File not deleted. | +| **NODE_FILE_REPAIR** | Node is on. File exists. File not deleted. | +------------------------------------------+---------------------------------------------------------------------+ -| **NODE_HOST_FILE_RESTORE** | Node is on. File exists. File is deleted. | +| **NODE_FILE_RESTORE** | Node is on. File exists. File is deleted. | +------------------------------------------+---------------------------------------------------------------------+ -| **NODE_HOST_FILE_CORRUPT** | Node is on. File exists. File not deleted. | +| **NODE_FILE_CORRUPT** | Node is on. File exists. File not deleted. | +------------------------------------------+---------------------------------------------------------------------+ -| **NODE_HOST_FILE_ACCESS** | Node is on. File exists. File not deleted. | +| **NODE_FILE_ACCESS** | Node is on. File exists. File not deleted. | +------------------------------------------+---------------------------------------------------------------------+ -| **NODE_HOST_FOLDER_CREATE** | Node is on. | +| **NODE_FOLDER_CREATE** | Node is on. | +------------------------------------------+---------------------------------------------------------------------+ -| **NODE_HOST_FOLDER_SCAN** | Node is on. Folder exists. Folder not deleted. | +| **NODE_FOLDER_SCAN** | Node is on. Folder exists. Folder not deleted. | +------------------------------------------+---------------------------------------------------------------------+ -| **NODE_HOST_FOLDER_CHECKHASH** | Node is on. Folder exists. Folder not deleted. | +| **NODE_FOLDER_CHECKHASH** | Node is on. Folder exists. Folder not deleted. | +------------------------------------------+---------------------------------------------------------------------+ -| **NODE_HOST_FOLDER_REPAIR** | Node is on. Folder exists. Folder not deleted. | +| **NODE_FOLDER_REPAIR** | Node is on. Folder exists. Folder not deleted. | +------------------------------------------+---------------------------------------------------------------------+ -| **NODE_HOST_FOLDER_RESTORE** | Node is on. Folder exists. Folder is deleted. | +| **NODE_FOLDER_RESTORE** | Node is on. Folder exists. Folder is deleted. | +------------------------------------------+---------------------------------------------------------------------+ -| **NODE_HOST_OS_SCAN** | Node is on. | +| **NODE_OS_SCAN** | Node is on. | +------------------------------------------+---------------------------------------------------------------------+ -| **NODE_HOST_NIC_ENABLE** | NIC is disabled. Node is on. | +| **HOST_NIC_ENABLE** | NIC is disabled. Node is on. | +------------------------------------------+---------------------------------------------------------------------+ -| **NODE_HOST_NIC_DISABLE** | NIC is enabled. Node is on. | +| **HOST_NIC_DISABLE** | NIC is enabled. Node is on. | +------------------------------------------+---------------------------------------------------------------------+ -| **NODE_HOST_SHUTDOWN** | Node is on. | +| **NODE_SHUTDOWN** | Node is on. | +------------------------------------------+---------------------------------------------------------------------+ -| **NODE_HOST_STARTUP** | Node is off. | +| **NODE_STARTUP** | Node is off. | +------------------------------------------+---------------------------------------------------------------------+ -| **NODE_HOST_RESET** | Node is on. | +| **NODE_RESET** | Node is on. | +------------------------------------------+---------------------------------------------------------------------+ -| **NODE_HOST_NMAP_PING_SCAN** | Node is on. | +| **NODE_NMAP_PING_SCAN** | Node is on. | +------------------------------------------+---------------------------------------------------------------------+ -| **NODE_HOST_NMAP_PORT_SCAN** | Node is on. | +| **NODE_NMAP_PORT_SCAN** | Node is on. | +------------------------------------------+---------------------------------------------------------------------+ -| **NODE_HOST_NMAP_NETWORK_SERVICE_RECON** | Node is on. | +| **NODE_NMAP_NETWORK_SERVICE_RECON** | Node is on. | +------------------------------------------+---------------------------------------------------------------------+ -| **NODE_ROUTER_PORT_ENABLE** | Router is on. | +| **NETWORK_PORT_ENABLE** | Node is on. Router is on. | +------------------------------------------+---------------------------------------------------------------------+ -| **NODE_ROUTER_PORT_DISABLE** | Router is on. | +| **NETWORK_PORT_DISABLE** | Router is on. | +------------------------------------------+---------------------------------------------------------------------+ -| **NODE_ROUTER_ACL_ADDRULE** | Router is on. | +| **ROUTER_ACL_ADDRULE** | Router is on. | +------------------------------------------+---------------------------------------------------------------------+ -| **NODE_ROUTER_ACL_REMOVERULE** | Router is on. | +| **ROUTER_ACL_REMOVERULE** | Router is on. | +------------------------------------------+---------------------------------------------------------------------+ -| **NODE_FIREWALL_PORT_ENABLE** | Firewall is on. | +| **FIREWALL_ACL_ADDRULE** | Firewall is on. | +------------------------------------------+---------------------------------------------------------------------+ -| **NODE_FIREWALL_PORT_DISABLE** | Firewall is on. | +| **FIREWALL_ACL_REMOVERULE** | Firewall is on. | +------------------------------------------+---------------------------------------------------------------------+ -| **NODE_FIREWALL_ACL_ADDRULE** | Firewall is on. | +| **NODE_NMAP_PING_SCAN** | Node is on. | +------------------------------------------+---------------------------------------------------------------------+ -| **NODE_FIREWALL_ACL_REMOVERULE** | Firewall is on. | +| **NODE_NMAP_PORT_SCAN** | Node is on. | ++------------------------------------------+---------------------------------------------------------------------+ +| **NODE_NMAP_NETWORK_SERVICE_RECON** | Node is on. | ++------------------------------------------+---------------------------------------------------------------------+ +| **CONFIGURE_DATABASE_CLIENT** | Node is on. | ++------------------------------------------+---------------------------------------------------------------------+ +| **CONFIGURE_RANSOMWARE_SCRIPT** | Node is on. | ++------------------------------------------+---------------------------------------------------------------------+ +| **CONFIGURE_DOSBOT** | Node is on. | ++------------------------------------------+---------------------------------------------------------------------+ +| **CONFIGURE_C2_BEACON** | Node is on. | ++------------------------------------------+---------------------------------------------------------------------+ +| **C2_SERVER_RANSOMWARE_LAUNCH** | Node is on. | ++------------------------------------------+---------------------------------------------------------------------+ +| **C2_SERVER_RANSOMWARE_CONFIGURE** | Node is on. | ++------------------------------------------+---------------------------------------------------------------------+ +| **C2_SERVER_TERMINAL_COMMAND** | Node is on. | ++------------------------------------------+---------------------------------------------------------------------+ +| **C2_SERVER_DATA_EXFILTRATE** | Node is on. | ++------------------------------------------+---------------------------------------------------------------------+ +| **NODE_ACCOUNTS_CHANGE_PASSWORD** | Node is on. | ++------------------------------------------+---------------------------------------------------------------------+ +| **SSH_TO_REMOTE** | Node is on. | ++------------------------------------------+---------------------------------------------------------------------+ +| **SESSIONS_REMOTE_LOGOFF** | Node is on. | ++------------------------------------------+---------------------------------------------------------------------+ +| **NODE_SEND_REMOTE_COMMAND** | Node is on. | +------------------------------------------+---------------------------------------------------------------------+ diff --git a/docs/source/configuration/agents.rst b/docs/source/configuration/agents.rst index 2fe35ac7..dece94c5 100644 --- a/docs/source/configuration/agents.rst +++ b/docs/source/configuration/agents.rst @@ -172,3 +172,8 @@ The amount of timesteps that the frequency can randomly change. --------------- If ``True``, gymnasium flattening will be performed on the observation space before sending to the agent. Set this to ``True`` if your agent does not support nested observation spaces. + +``Agent History`` +----------------- + +Agents will record their action log for each step. This is a summary of what the agent did, along with response information from requests within the simulation. diff --git a/docs/source/configuration/game.rst b/docs/source/configuration/game.rst index 02ee8110..2048708c 100644 --- a/docs/source/configuration/game.rst +++ b/docs/source/configuration/game.rst @@ -28,6 +28,7 @@ This section defines high-level settings that apply across the game, currently i high: 10 medium: 5 low: 0 + seed: 1 ``max_episode_length`` ---------------------- @@ -54,3 +55,8 @@ See :ref:`List of IPProtocols ` for a list of protocols. -------------- These are used to determine the thresholds of high, medium and low categories for counted observation occurrences. + +``seed`` +-------- + +Used to configure the random seeds used within PrimAITE, ensuring determinism within episode/session runs. If empty or set to -1, no seed is set. diff --git a/docs/source/configuration/simulation/nodes/common/common_node_attributes.rst b/docs/source/configuration/simulation/nodes/common/common_node_attributes.rst index e648e4a1..6a95911f 100644 --- a/docs/source/configuration/simulation/nodes/common/common_node_attributes.rst +++ b/docs/source/configuration/simulation/nodes/common/common_node_attributes.rst @@ -53,3 +53,27 @@ The number of time steps required to occur in order for the node to cycle from ` Optional. Default value is ``3``. The number of time steps required to occur in order for the node to cycle from ``ON`` to ``SHUTTING_DOWN`` and then finally ``OFF``. + +``users`` +--------- + +The list of pre-existing users that are additional to the default admin user (``username=admin``, ``password=admin``). +Additional users are configured as an array and must contain a ``username``, ``password``, and can contain an optional +boolean ``is_admin``. + +Example of adding two additional users to a node: + +.. code-block:: yaml + + simulation: + network: + nodes: + - hostname: [hostname] + type: [Node Type] + users: + - username: jane.doe + password: '1234' + is_admin: true + - username: john.doe + password: password_1 + is_admin: false diff --git a/docs/source/primaite-dependencies.rst b/docs/source/primaite-dependencies.rst index 04987054..8367ee61 100644 --- a/docs/source/primaite-dependencies.rst +++ b/docs/source/primaite-dependencies.rst @@ -7,7 +7,7 @@ +===================+=========+====================================+=======================================================================================================+====================================================================+ | gymnasium | 0.28.1 | MIT License | A standard API for reinforcement learning and a diverse set of reference environments (formerly Gym). | https://farama.org | +-------------------+---------+------------------------------------+-------------------------------------------------------------------------------------------------------+--------------------------------------------------------------------+ -| ipywidgets | 8.1.3 | BSD License | Jupyter interactive widgets | http://jupyter.org | +| ipywidgets | 8.1.5 | BSD License | Jupyter interactive widgets | http://jupyter.org | +-------------------+---------+------------------------------------+-------------------------------------------------------------------------------------------------------+--------------------------------------------------------------------+ | jupyterlab | 3.6.1 | BSD License | JupyterLab computational environment | https://jupyter.org | +-------------------+---------+------------------------------------+-------------------------------------------------------------------------------------------------------+--------------------------------------------------------------------+ @@ -23,7 +23,7 @@ +-------------------+---------+------------------------------------+-------------------------------------------------------------------------------------------------------+--------------------------------------------------------------------+ | plotly | 5.15.0 | MIT License | An open-source, interactive data visualization library for Python | https://plotly.com/python/ | +-------------------+---------+------------------------------------+-------------------------------------------------------------------------------------------------------+--------------------------------------------------------------------+ -| polars | 0.18.4 | MIT License | Blazingly fast DataFrame library | https://www.pola.rs/ | +| polars | 0.20.30 | MIT License | Blazingly fast DataFrame library | https://www.pola.rs/ | +-------------------+---------+------------------------------------+-------------------------------------------------------------------------------------------------------+--------------------------------------------------------------------+ | prettytable | 3.8.0 | BSD License (BSD (3 clause)) | A simple Python library for easily displaying tabular data in a visually appealing ASCII table format | https://github.com/jazzband/prettytable | +-------------------+---------+------------------------------------+-------------------------------------------------------------------------------------------------------+--------------------------------------------------------------------+ @@ -31,7 +31,7 @@ +-------------------+---------+------------------------------------+-------------------------------------------------------------------------------------------------------+--------------------------------------------------------------------+ | PyYAML | 6.0 | MIT License | YAML parser and emitter for Python | https://pyyaml.org/ | +-------------------+---------+------------------------------------+-------------------------------------------------------------------------------------------------------+--------------------------------------------------------------------+ -| ray | 2.23.0 | Apache 2.0 | Ray provides a simple, universal API for building distributed applications. | https://github.com/ray-project/ray | +| ray | 2.32.0 | Apache 2.0 | Ray provides a simple, universal API for building distributed applications. | https://github.com/ray-project/ray | +-------------------+---------+------------------------------------+-------------------------------------------------------------------------------------------------------+--------------------------------------------------------------------+ | stable-baselines3 | 2.1.0 | MIT | Pytorch version of Stable Baselines, implementations of reinforcement learning algorithms. | https://github.com/DLR-RM/stable-baselines3 | +-------------------+---------+------------------------------------+-------------------------------------------------------------------------------------------------------+--------------------------------------------------------------------+ @@ -39,7 +39,7 @@ +-------------------+---------+------------------------------------+-------------------------------------------------------------------------------------------------------+--------------------------------------------------------------------+ | typer | 0.9.0 | MIT License | Typer, build great CLIs. Easy to code. Based on Python type hints. | https://github.com/tiangolo/typer | +-------------------+---------+------------------------------------+-------------------------------------------------------------------------------------------------------+--------------------------------------------------------------------+ -| Deepdiff | 7.0.1 | MIT License | Deep difference of dictionaries, iterables, strings, and any other object objects. | https://github.com/seperman/deepdiff | +| Deepdiff | 8.0.1 | MIT License | Deep difference of dictionaries, iterables, strings, and any other object objects. | https://github.com/seperman/deepdiff | +-------------------+---------+------------------------------------+-------------------------------------------------------------------------------------------------------+--------------------------------------------------------------------+ -| sb3_contrib | 2.3.0 | MIT License | Contrib package for Stable-Baselines3 - Experimental reinforcement learning (RL) code (Action Masking)| https://github.com/Stable-Baselines-Team/stable-baselines3-contrib | +| sb3_contrib | 2.1.0 | MIT License | Contrib package for Stable-Baselines3 - Experimental reinforcement learning (RL) code (Action Masking)| https://github.com/Stable-Baselines-Team/stable-baselines3-contrib | +-------------------+---------+------------------------------------+-------------------------------------------------------------------------------------------------------+--------------------------------------------------------------------+ diff --git a/docs/source/rewards.rst b/docs/source/rewards.rst new file mode 100644 index 00000000..0163284c --- /dev/null +++ b/docs/source/rewards.rst @@ -0,0 +1,126 @@ +.. only:: comment + + © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK + +Rewards +####### + +Rewards in PrimAITE are based on a system of individual components that react to events in the simulation. An agent's reward function is calculated as the weighted sum of several reward components. + +Some rewards, such as the ``GreenAdminDatabaseUnreachablePenalty``, can be marked as 'sticky' in their configuration. Setting this to ``True`` will mean that they continue to output the same value after an event until another event of that type. +In the instance of the ``GreenAdminDatabaseUnreachablePenalty``, the database admin reward will stay negative until the next successful database request is made, even if the database admin agents do nothing and the database returns a good state. + +Components +********** +The following API pages describe the use of each reward component and the possible configuration options. An example of configuring each via yaml is also provided. + +:py:class:`primaite.game.agent.rewards.DummyReward` + +.. code-block:: yaml + + agents: + - ref: agent_name + # ... + reward_function: + reward_components: + - type: DUMMY + weight: 1.0 + + +:py:class:`primaite.game.agent.rewards.DatabaseFileIntegrity` + +.. code-block:: yaml + + agents: + - ref: agent_name + # ... + reward_function: + reward_components: + - type: DATABASE_FILE_INTEGRITY + weight: 1.0 + options: + node_hostname: server_1 + folder_name: database + file_name: database.db + + +:py:class:`primaite.game.agent.rewards.WebServer404Penalty` + +.. code-block:: yaml + + agents: + - ref: agent_name + # ... + reward_function: + reward_components: + - type: WEB_SERVER_404_PENALTY + node_hostname: web_server + weight: 1.0 + options: + service_name: WebService + sticky: false + + +:py:class:`primaite.game.agent.rewards.WebpageUnavailablePenalty` + +.. code-block:: yaml + + agents: + - ref: agent_name + # ... + reward_function: + reward_components: + - type: WEBPAGE_UNAVAILABLE_PENALTY + node_hostname: computer_1 + weight: 1.0 + options: + sticky: false + + +:py:class:`primaite.game.agent.rewards.GreenAdminDatabaseUnreachablePenalty` + +.. code-block:: yaml + + agents: + - ref: agent_name + # ... + reward_function: + reward_components: + - type: GREEN_ADMIN_DATABASE_UNREACHABLE_PENALTY + weight: 1.0 + options: + node_hostname: admin_pc_1 + sticky: false + + +:py:class:`primaite.game.agent.rewards.SharedReward` + +.. code-block:: yaml + + agents: + - ref: scripted_agent + # ... + - ref: agent_name + # ... + reward_function: + reward_components: + - type: SHARED_REWARD + weight: 1.0 + options: + agent_name: scripted_agent + + +:py:class:`primaite.game.agent.rewards.ActionPenalty` + +.. code-block:: yaml + + agents: + - ref: agent_name + # ... + reward_function: + reward_components: + - type: ACTION_PENALTY + weight: 1.0 + options: + action_penalty: -0.3 + do_nothing_penalty: 0.0 diff --git a/docs/source/simulation_components/network/base_hardware.rst b/docs/source/simulation_components/network/base_hardware.rst index 9e42b1de..ce1e5c74 100644 --- a/docs/source/simulation_components/network/base_hardware.rst +++ b/docs/source/simulation_components/network/base_hardware.rst @@ -97,8 +97,8 @@ Node Behaviours/Functions - **receive_frame()**: Handles the processing of incoming network frames. - **apply_timestep()**: Advances the state of the node according to the simulation timestep. - **power_on()**: Initiates the node, enabling all connected Network Interfaces and starting all Services and - Applications, taking into account the `start_up_duration`. -- **power_off()**: Stops the node's operations, adhering to the `shut_down_duration`. + Applications, taking into account the ``start_up_duration``. +- **power_off()**: Stops the node's operations, adhering to the ``shut_down_duration``. - **ping()**: Sends ICMP echo requests to a specified IP address to test connectivity. - **has_enabled_network_interface()**: Checks if the node has any network interfaces enabled, facilitating network communication. @@ -109,3 +109,205 @@ Node Behaviours/Functions The Node class handles installation of system software, network connectivity, frame processing, system logging, and power states. It establishes baseline functionality while allowing subclassing to model specific node types like hosts, routers, firewalls etc. The flexible architecture enables composing complex network topologies. + +User, UserManager, and UserSessionManager +========================================= + +The ``base.py`` module also includes essential classes for managing users and their sessions within the PrimAITE +simulation. These are the ``User``, ``UserManager``, and ``UserSessionManager`` classes. The base ``Node`` class comes +with ``UserManager``, and ``UserSessionManager`` classes pre-installed. + +User Class +---------- + +The ``User`` class represents a user in the system. It includes attributes such as ``username``, ``password``, +``disabled``, and ``is_admin`` to define the user's credentials and status. + +Example Usage +^^^^^^^^^^^^^ + +Creating a user: + .. code-block:: python + + user = User(username="john_doe", password="12345") + +UserManager Class +----------------- + +The ``UserManager`` class handles user management tasks such as creating users, authenticating them, changing passwords, +and enabling or disabling user accounts. It maintains a dictionary of users and provides methods to manage them +effectively. + +Example Usage +^^^^^^^^^^^^^ + +Creating a ``UserManager`` instance and adding a user: + .. code-block:: python + + user_manager = UserManager() + user_manager.add_user(username="john_doe", password="12345") + +Authenticating a user: + .. code-block:: python + + user = user_manager.authenticate_user(username="john_doe", password="12345") + +UserSessionManager Class +------------------------ + +The ``UserSessionManager`` class manages user sessions, including local and remote sessions. It handles session creation, +timeouts, and provides methods for logging users in and out. + +Example Usage +^^^^^^^^^^^^^ + +Creating a ``UserSessionManager`` instance and logging a user in locally: + .. code-block:: python + + session_manager = UserSessionManager() + session_id = session_manager.local_login(username="john_doe", password="12345") + +Logging a user out: + .. code-block:: python + + session_manager.local_logout() + +Practical Examples +------------------ + +Below are unit tests which act as practical examples illustrating how to use the ``User``, ``UserManager``, and +``UserSessionManager`` classes within the context of a client-server network simulation. + +Setting up a Client-Server Network +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: python + + from typing import Tuple + from uuid import uuid4 + + import pytest + + from primaite.simulator.network.container import Network + from primaite.simulator.network.hardware.nodes.host.computer import Computer + from primaite.simulator.network.hardware.nodes.host.server import Server + + @pytest.fixture(scope="function") + def client_server_network() -> Tuple[Computer, Server, Network]: + network = Network() + + client = Computer( + hostname="client", + ip_address="192.168.1.2", + subnet_mask="255.255.255.0", + default_gateway="192.168.1.1", + start_up_duration=0, + ) + client.power_on() + + server = Server( + hostname="server", + ip_address="192.168.1.3", + subnet_mask="255.255.255.0", + default_gateway="192.168.1.1", + start_up_duration=0, + ) + server.power_on() + + network.connect(client.network_interface[1], server.network_interface[1]) + + return client, server, network + +Local Login Success +^^^^^^^^^^^^^^^^^^^ + +.. code-block:: python + + def test_local_login_success(client_server_network): + client, server, network = client_server_network + + assert not client.user_session_manager.local_user_logged_in + + client.user_session_manager.local_login(username="admin", password="admin") + + assert client.user_session_manager.local_user_logged_in + +Local Login Failure +^^^^^^^^^^^^^^^^^^^ + +.. code-block:: python + + def test_local_login_failure(client_server_network): + client, server, network = client_server_network + + assert not client.user_session_manager.local_user_logged_in + + client.user_session_manager.local_login(username="jane.doe", password="12345") + + assert not client.user_session_manager.local_user_logged_in + +Adding a New User and Successful Local Login +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: python + + def test_new_user_local_login_success(client_server_network): + client, server, network = client_server_network + + assert not client.user_session_manager.local_user_logged_in + + client.user_manager.add_user(username="jane.doe", password="12345") + + client.user_session_manager.local_login(username="jane.doe", password="12345") + + assert client.user_session_manager.local_user_logged_in + +Clearing Previous Login on New Local Login +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: python + + def test_new_local_login_clears_previous_login(client_server_network): + client, server, network = client_server_network + + assert not client.user_session_manager.local_user_logged_in + + current_session_id = client.user_session_manager.local_login(username="admin", password="admin") + + assert client.user_session_manager.local_user_logged_in + + assert client.user_session_manager.local_session.user.username == "admin" + + client.user_manager.add_user(username="jane.doe", password="12345") + + new_session_id = client.user_session_manager.local_login(username="jane.doe", password="12345") + + assert client.user_session_manager.local_user_logged_in + + assert client.user_session_manager.local_session.user.username == "jane.doe" + + assert new_session_id != current_session_id + +Persistent Login for the Same User +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: python + + def test_new_local_login_attempt_same_uses_persists(client_server_network): + client, server, network = client_server_network + + assert not client.user_session_manager.local_user_logged_in + + current_session_id = client.user_session_manager.local_login(username="admin", password="admin") + + assert client.user_session_manager.local_user_logged_in + + assert client.user_session_manager.local_session.user.username == "admin" + + new_session_id = client.user_session_manager.local_login(username="admin", password="admin") + + assert client.user_session_manager.local_user_logged_in + + assert client.user_session_manager.local_session.user.username == "admin" + + assert new_session_id == current_session_id diff --git a/docs/source/simulation_components/network/nodes/host_node.rst b/docs/source/simulation_components/network/nodes/host_node.rst index 301cd783..b8aae098 100644 --- a/docs/source/simulation_components/network/nodes/host_node.rst +++ b/docs/source/simulation_components/network/nodes/host_node.rst @@ -49,3 +49,5 @@ fundamental network operations: 5. **NTP (Network Time Protocol) Client:** Synchronises the host's clock with network time servers. 6. **Web Browser:** A simulated application that allows the host to request and display web content. + +7. **Terminal:** A simulated service that allows the host to connect to remote hosts and execute commands. diff --git a/docs/source/simulation_components/network/nodes/wireless_router.rst b/docs/source/simulation_components/network/nodes/wireless_router.rst index c78c8419..80f0e124 100644 --- a/docs/source/simulation_components/network/nodes/wireless_router.rst +++ b/docs/source/simulation_components/network/nodes/wireless_router.rst @@ -3,7 +3,7 @@ © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK ###### -Router +Wireless Router ###### The ``WirelessRouter`` class extends the functionality of the standard ``Router`` class within PrimAITE, diff --git a/docs/source/simulation_components/system/applications/c2_suite.rst b/docs/source/simulation_components/system/applications/c2_suite.rst new file mode 100644 index 00000000..d045949a --- /dev/null +++ b/docs/source/simulation_components/system/applications/c2_suite.rst @@ -0,0 +1,319 @@ +.. only:: comment + + © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK + +.. _C2_Suite: + +Command and Control Application Suite +##################################### + +Comprising of two applications, the Command and Control (C2) suite intends to introduce +malicious network architecture and further the realism of red agents within PrimAITE. + +Overview: +========= + +These two new classes give red agents a cyber realistic way of leveraging the capabilities of the ``Terminal`` application whilst introducing more opportunities for the blue agent(s) to notice and subvert a red agent during an episode. + +For a more in-depth look at the command and control applications then please refer to the ``C2-E2E-Notebook``. + +``C2 Server`` +""""""""""""" + +The C2 Server application is intended to represent the malicious infrastructure already under the control of an adversary. + +The C2 Server is configured to listen and await ``keep alive`` traffic from a C2 beacon. Once received the C2 Server is able to send and receive C2 commands. + +Currently, the C2 Server offers four commands: + ++---------------------+---------------------------------------------------------------------------+ +|C2 Command | Meaning | ++=====================+===========================================================================+ +|RANSOMWARE_CONFIGURE | Configures an installed ransomware script based on the passed parameters. | ++---------------------+---------------------------------------------------------------------------+ +|RANSOMWARE_LAUNCH | Launches the installed ransomware script. | ++---------------------+---------------------------------------------------------------------------+ +|DATA_EXFILTRATION | Copies a target file from a remote node to the C2 Beacon & Server via FTP | ++---------------------+---------------------------------------------------------------------------+ +|TERMINAL | Executes a command via the terminal installed on the C2 Beacons Host. | ++---------------------+---------------------------------------------------------------------------+ + + +It's important to note that in order to keep PrimAITE realistic from a cyber perspective, +the C2 Server application should never be visible or actionable upon directly by the blue agent. + +This is because in the real world, C2 servers are hosted on ephemeral public domains that would not be accessible by private network blue agent. +Therefore granting blue agent(s) the ability to perform counter measures directly against the application would be unrealistic. + +It is more accurate to see the host that the C2 Beacon is installed on as being able to route to the C2 Server (Internet Access). + +``C2 Beacon`` +""""""""""""" + +The C2 Beacon application is intended to represent malware that is used to establish and maintain contact to a C2 Server within a compromised network. + +A C2 Beacon will need to be first configured with the C2 Server IP Address which can be done via the ``configure`` method. + +Once installed and configured; the C2 beacon can establish connection with the C2 Server via executing the application. + +This will send an initial ``keep alive`` to the given C2 Server (The C2 Server IPv4Address must be given upon C2 Beacon configuration). +Which is then resolved and responded by another ``Keep Alive`` by the C2 server back to the C2 beacon to confirm connection. + +The C2 Beacon will send out periodic keep alive based on its configuration parameters to configure it's active connection with the C2 server. + +It's recommended that a C2 Beacon is installed and configured mid episode by a Red Agent for a more cyber realistic simulation. + +Usage +===== + +As mentioned, the C2 Suite is intended to grant Red Agents further flexibility whilst also expanding a blue agent's observation space. + +Adding to this, the following behaviour of the C2 beacon can be configured by users for increased domain randomisation: + ++---------------------+---------------------------------------------------------------------------+ +|Configuration Option | Option Meaning | ++=====================+===========================================================================+ +|c2_server_ip_address | The IP Address of the C2 Server. (The C2 Server must be running) | ++---------------------+---------------------------------------------------------------------------+ +|keep_alive_frequency | How often should the C2 Beacon confirm it's connection in timesteps. | ++---------------------+---------------------------------------------------------------------------+ +|masquerade_protocol | What protocol should the C2 traffic masquerade as? (HTTP, FTP or DNS) | ++---------------------+---------------------------------------------------------------------------+ +|masquerade_port | What port should the C2 traffic use? (TCP or UDP) | ++---------------------+---------------------------------------------------------------------------+ + + +Implementation +============== + +Both applications inherit from an abstract C2 which handles the keep alive functionality and main logic. +However, each host implements it's own receive methods. + +- The ``C2 Beacon`` is responsible for the following logic: + - Establishes and confirms connection to the C2 Server via sending ``C2Payload.KEEP_ALIVE``. + - Receives and executes C2 Commands given by the C2 Server via ``C2Payload.INPUT``. + - Returns the RequestResponse of the C2 Commands executed back the C2 Server via ``C2Payload.OUTPUT``. + +- The ``C2 Server`` is responsible for the following logic: + - Listens and resolves connection to a C2 Beacon via responding to ``C2Payload.KEEP_ALIVE``. + - Sends C2 Commands to the C2 Beacon via ``C2Payload.INPUT``. + - Receives the RequestResponse of the C2 Commands executed by C2 Beacon via ``C2Payload.OUTPUT``. + +The sequence diagram below clarifies the functionality of both applications: + +.. image:: ../../../../_static/c2_sequence.png + :width: 1000 + :align: center + + +For further details and more in-depth examples please refer to the ``Command-&-Control notebook`` + +Examples +======== + +Python +"""""" +.. code-block:: python + + from primaite.simulator.network.container import Network + from primaite.simulator.network.hardware.nodes.host.computer import Computer + from primaite.simulator.network.hardware.nodes.network.switch import Switch + from primaite.simulator.system.applications.database_client import DatabaseClient + from primaite.simulator.system.applications.red_applications.ransomware_script import RansomwareScript + from primaite.simulator.system.services.database.database_service import DatabaseService + from primaite.simulator.system.applications.red_applications.c2.c2_server import C2Command, C2Server + from primaite.simulator.system.applications.red_applications.c2.c2_beacon import C2Beacon + + # Network Setup + network = Network() + + + switch = Switch(hostname="switch", start_up_duration=0, num_ports=4) + switch.power_on() + + node_a = Computer(hostname="node_a", ip_address="192.168.0.10", subnet_mask="255.255.255.0", start_up_duration=0) + node_a.power_on() + network.connect(node_a.network_interface[1], switch.network_interface[1]) + + node_b = Computer(hostname="node_b", ip_address="192.168.0.11", subnet_mask="255.255.255.0", start_up_duration=0) + node_b.power_on() + + network.connect(node_b.network_interface[1], switch.network_interface[2]) + + node_c = Computer(hostname="node_c", ip_address="192.168.0.12", subnet_mask="255.255.255.0", start_up_duration=0) + node_c.power_on() + network.connect(node_c.network_interface[1], switch.network_interface[3]) + + node_c.software_manager.install(software_class=DatabaseService) + node_b.software_manager.install(software_class=DatabaseClient) + node_b.software_manager.install(software_class=RansomwareScript) + node_a.software_manager.install(software_class=C2Server) + + # C2 Application objects + + c2_server_host: Computer = network.get_node_by_hostname("node_a") + c2_beacon_host: Computer = network.get_node_by_hostname("node_b") + + c2_server: C2Server = c2_server_host.software_manager.software["C2Server"] + c2_beacon: C2Beacon = c2_beacon_host.software_manager.software["C2Beacon"] + + # Configuring the C2 Beacon + c2_beacon.configure(c2_server_ip_address="192.168.0.10", keep_alive_frequency=5) + + # Launching the C2 Server (Needs to be running in order to listen for connections) + c2_server.run() + + # Establishing connection + c2_beacon.establish() + + # Example command: Creating a file + + file_create_command = { + "commands": [ + ["file_system", "create", "folder", "test_folder"], + ["file_system", "create", "file", "test_folder", "example_file", "True"], + ], + "username": "admin", + "password": "admin", + "ip_address": None, + } + + c2_server.send_command(C2Command.TERMINAL, command_options=file_create_command) + + # Example command: Installing and configuring Ransomware: + + ransomware_installation_command = { "commands": [ + ["software_manager","application","install","RansomwareScript"], + ], + "username": "admin", + "password": "admin", + "ip_address": None, + } + c2_server.send_command(given_command=C2Command.TERMINAL, command_options=ransomware_installation_command) + + ransomware_config = {"server_ip_address": "192.168.0.12"} + + c2_server.send_command(given_command=C2Command.RANSOMWARE_CONFIGURE, command_options=ransomware_config) + + c2_beacon_host.software_manager.show() + + # Example command: File Exfiltration + + data_exfil_options = { + "username": "admin", + "password": "admin", + "ip_address": None, + "target_ip_address": "192.168.0.12", + "target_file_name": "database.db", + "target_folder_name": "database", + } + + c2_server.send_command(given_command=C2Command.DATA_EXFILTRATION, command_options=data_exfil_options) + + # Example command: Launching Ransomware + + c2_server.send_command(given_command=C2Command.RANSOMWARE_LAUNCH, command_options={}) + + + +Via Configuration +""""""""""""""""" + +.. code-block:: yaml + + simulation: + network: + nodes: + - ref: example_computer_1 + hostname: computer_a + type: computer + ... + applications: + type: C2Server + ... + hostname: computer_b + type: computer + ... + # A C2 Beacon will not automatically connection to a C2 Server. + # Either an agent must use application_execute. + # Or a if using the simulation layer - .establish(). + applications: + type: C2Beacon + options: + c2_server_ip_address: ... + keep_alive_frequency: 5 + masquerade_protocol: tcp + masquerade_port: http + listen_on_ports: + - 80 + - 53 + - 21 + + + +C2 Beacon Configuration +======================= + +``c2_server_ip_address`` +"""""""""""""""""""""""" + +IP address of the ``C2Server`` that the C2 Beacon will use to establish connection. + +This must be a valid octet i.e. in the range of ``0.0.0.0`` and ``255.255.255.255``. + + +``Keep Alive Frequency`` +"""""""""""""""""""""""" + +How often should the C2 Beacon confirm it's connection in timesteps. + +For example, if the keep alive Frequency is set to one then every single timestep +the C2 connection will be confirmed. + +It's worth noting that this may be a useful option when investigating +network blue agent observation space. + +This must be a valid integer i.e ``10``. Defaults to ``5``. + + +``Masquerade Protocol`` +""""""""""""""""""""""" + +The protocol that the C2 Beacon will use to communicate to the C2 Server with. + +Currently only ``TCP`` and ``UDP`` are valid masquerade protocol options. + +It's worth noting that this may be a useful option to bypass ACL rules. + +This must be a string i.e *UDP*. Defaults to ``TCP``. + +*Please refer to the ``IPProtocol`` class for further reference.* + +``Masquerade Port`` +""""""""""""""""""" + +What port that the C2 Beacon will use to communicate to the C2 Server with. + +Currently only ``FTP``, ``HTTP`` and ``DNS`` are valid masquerade port options. + +It's worth noting that this may be a useful option to bypass ACL rules. + +This must be a string i.e ``DNS``. Defaults to ``HTTP``. + +*Please refer to the ``IPProtocol`` class for further reference.* + +``Common Attributes`` +^^^^^^^^^^^^^^^^^^^^^ + +See :ref:`Common Configuration` + + +C2 Server Configuration +======================= + +*The C2 Server does not currently offer any unique configuration options and will configure itself to match the C2 beacon's network behaviour.* + +``Common Attributes`` +^^^^^^^^^^^^^^^^^^^^^ + +See :ref:`Common Configuration` diff --git a/docs/source/simulation_components/system/applications/data_manipulation_bot.rst b/docs/source/simulation_components/system/applications/data_manipulation_bot.rst index 8bcbb265..1a387514 100644 --- a/docs/source/simulation_components/system/applications/data_manipulation_bot.rst +++ b/docs/source/simulation_components/system/applications/data_manipulation_bot.rst @@ -158,10 +158,6 @@ If not using the data manipulation bot manually, it needs to be used with a data Configuration ============= -.. include:: ../common/common_configuration.rst - -.. |SOFTWARE_NAME| replace:: DataManipulationBot -.. |SOFTWARE_NAME_BACKTICK| replace:: ``DataManipulationBot`` ``server_ip`` """"""""""""" @@ -203,3 +199,8 @@ Optional. Default value is ``0.1``. The chance of the ``DataManipulationBot`` to succeed with a data manipulation attack. This must be a float value between ``0`` and ``1``. + +``Common Attributes`` +^^^^^^^^^^^^^^^^^^^^^ + +See :ref:`Common Configuration` diff --git a/docs/source/simulation_components/system/applications/database_client.rst b/docs/source/simulation_components/system/applications/database_client.rst index d51465b2..1fea78ab 100644 --- a/docs/source/simulation_components/system/applications/database_client.rst +++ b/docs/source/simulation_components/system/applications/database_client.rst @@ -90,11 +90,6 @@ Via Configuration Configuration ============= -.. include:: ../common/common_configuration.rst - -.. |SOFTWARE_NAME| replace:: DatabaseClient -.. |SOFTWARE_NAME_BACKTICK| replace:: ``DatabaseClient`` - ``db_server_ip`` """""""""""""""" @@ -109,3 +104,8 @@ This must be a valid octet i.e. in the range of ``0.0.0.0`` and ``255.255.255.25 Optional. Default value is ``None``. The password that the ``DatabaseClient`` will use to access the :ref:`DatabaseService`. + +``Common Attributes`` +^^^^^^^^^^^^^^^^^^^^^ + +See :ref:`Common Configuration` diff --git a/docs/source/simulation_components/system/applications/dos_bot.rst b/docs/source/simulation_components/system/applications/dos_bot.rst index 9925dc93..6ad45424 100644 --- a/docs/source/simulation_components/system/applications/dos_bot.rst +++ b/docs/source/simulation_components/system/applications/dos_bot.rst @@ -98,11 +98,6 @@ Via Configuration Configuration ============= -.. include:: ../common/common_configuration.rst - -.. |SOFTWARE_NAME| replace:: DoSBot -.. |SOFTWARE_NAME_BACKTICK| replace:: ``DoSBot`` - ``target_ip_address`` """"""""""""""""""""" @@ -161,3 +156,8 @@ Optional. Default value is ``1000``. The maximum number of sessions the ``DoSBot`` is able to make. This must be an integer value equal to or greater than ``0``. + +``Common Attributes`` +^^^^^^^^^^^^^^^^^^^^^ + +See :ref:`Common Configuration` diff --git a/docs/source/simulation_components/system/applications/nmap.rst b/docs/source/simulation_components/system/applications/nmap.rst index 1e7f5ea4..dbb8a022 100644 --- a/docs/source/simulation_components/system/applications/nmap.rst +++ b/docs/source/simulation_components/system/applications/nmap.rst @@ -346,10 +346,8 @@ Perform a full box scan on all ports, over both TCP and UDP, on a whole subnet: | 192.168.1.13 | 219 | ARP | UDP | +--------------+------+-----------------+----------+ -Configuration -============= -.. include:: ../common/common_configuration.rst +``Common Attributes`` +""""""""""""""""""""" -.. |SOFTWARE_NAME| replace:: NMAP -.. |SOFTWARE_NAME_BACKTICK| replace:: ``NMAP`` +See :ref:`Common Configuration` diff --git a/docs/source/simulation_components/system/applications/ransomware_script.rst b/docs/source/simulation_components/system/applications/ransomware_script.rst index a2a853e9..5bff6991 100644 --- a/docs/source/simulation_components/system/applications/ransomware_script.rst +++ b/docs/source/simulation_components/system/applications/ransomware_script.rst @@ -72,10 +72,6 @@ Configuration The RansomwareScript inherits configuration options such as ``fix_duration`` from its parent class. However, for the ``RansomwareScript`` the most relevant option is ``server_ip``. -.. include:: ../common/common_configuration.rst - -.. |SOFTWARE_NAME| replace:: RansomwareScript -.. |SOFTWARE_NAME_BACKTICK| replace:: ``RansomwareScript`` ``server_ip`` """"""""""""" @@ -83,3 +79,8 @@ The RansomwareScript inherits configuration options such as ``fix_duration`` fro IP address of the :ref:`DatabaseService` which the ``RansomwareScript`` will encrypt. This must be a valid octet i.e. in the range of ``0.0.0.0`` and ``255.255.255.255``. + +``Common Attributes`` +^^^^^^^^^^^^^^^^^^^^^ + +See :ref:`Common Configuration` diff --git a/docs/source/simulation_components/system/applications/web_browser.rst b/docs/source/simulation_components/system/applications/web_browser.rst index dbe2da28..c56c450d 100644 --- a/docs/source/simulation_components/system/applications/web_browser.rst +++ b/docs/source/simulation_components/system/applications/web_browser.rst @@ -92,10 +92,6 @@ Via Configuration Configuration ============= -.. include:: ../common/common_configuration.rst - -.. |SOFTWARE_NAME| replace:: WebBrowser -.. |SOFTWARE_NAME_BACKTICK| replace:: ``WebBrowser`` ``target_url`` """""""""""""" @@ -109,3 +105,9 @@ The domain ``arcd.com`` can be matched by - http://arcd.com/ - http://arcd.com/users/ - arcd.com + + +``Common Attributes`` +^^^^^^^^^^^^^^^^^^^^^ + +See :ref:`Common Configuration` diff --git a/docs/source/simulation_components/system/common/common_configuration.rst b/docs/source/simulation_components/system/common/common_configuration.rst index e35ee378..c53ac8b8 100644 --- a/docs/source/simulation_components/system/common/common_configuration.rst +++ b/docs/source/simulation_components/system/common/common_configuration.rst @@ -2,26 +2,56 @@ © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK -``ref`` -======= +.. _Common Configuration: -Human readable name used as reference for the |SOFTWARE_NAME_BACKTICK|. Not used in code. +Common Configuration +"""""""""""""""""""" -``type`` -======== +ref +""" -The type of software that should be added. To add |SOFTWARE_NAME| this must be |SOFTWARE_NAME_BACKTICK|. +Human readable name used as reference for the software class. Not used in code. -``options`` -=========== +type +"""" -The configuration options are the attributes that fall under the options for an application. +The type of software that should be added. To add the required software, this must be it's name. +options +""""""" +The configuration options are the attributes that fall under the options for an application or service. -``fix_duration`` -"""""""""""""""" +fix_duration +"""""""""""" Optional. Default value is ``2``. -The number of timesteps the |SOFTWARE_NAME| will remain in a ``FIXING`` state before going into a ``GOOD`` state. +The number of timesteps the software will remain in a ``FIXING`` state before going into a ``GOOD`` state. + + +listen_on_ports +^^^^^^^^^^^^^^^ + +Optional. The set of ports to listen on. This is in addition to the main port the software is designated. This can either be +the string name of ports or the port integers + +Example: + +.. code-block:: yaml + + simulation: + network: + nodes: + - hostname: [hostname] + type: [Node Type] + services: + - type: [Service Type] + options: + listen_on_ports: + - 631 + applications: + - type: [Application Type] + options: + listen_on_ports: + - SMB diff --git a/docs/source/simulation_components/system/services/database_service.rst b/docs/source/simulation_components/system/services/database_service.rst index 2f0452f0..f3e800cd 100644 --- a/docs/source/simulation_components/system/services/database_service.rst +++ b/docs/source/simulation_components/system/services/database_service.rst @@ -94,11 +94,6 @@ Via Configuration Configuration ============= -.. include:: ../common/common_configuration.rst - -.. |SOFTWARE_NAME| replace:: DatabaseService -.. |SOFTWARE_NAME_BACKTICK| replace:: ``DatabaseService`` - ``backup_server_ip`` """""""""""""""""""" @@ -114,3 +109,8 @@ This must be a valid octet i.e. in the range of ``0.0.0.0`` and ``255.255.255.25 Optional. Default value is ``None``. The password that needs to be provided by connecting clients in order to create a successful connection. + +``Common Attributes`` +^^^^^^^^^^^^^^^^^^^^^ + +See :ref:`Common Configuration` diff --git a/docs/source/simulation_components/system/services/dns_client.rst b/docs/source/simulation_components/system/services/dns_client.rst index c0025114..eca152f0 100644 --- a/docs/source/simulation_components/system/services/dns_client.rst +++ b/docs/source/simulation_components/system/services/dns_client.rst @@ -84,10 +84,6 @@ Via Configuration Configuration ============= -.. include:: ../common/common_configuration.rst - -.. |SOFTWARE_NAME| replace:: DNSClient -.. |SOFTWARE_NAME_BACKTICK| replace:: ``DNSClient`` ``dns_server`` """""""""""""" @@ -97,3 +93,8 @@ Optional. Default value is ``None``. The IP Address of the :ref:`DNSServer`. This must be a valid octet i.e. in the range of ``0.0.0.0`` and ``255.255.255.255``. + +``Common Attributes`` +^^^^^^^^^^^^^^^^^^^^^ + +See :ref:`Common Configuration` diff --git a/docs/source/simulation_components/system/services/dns_server.rst b/docs/source/simulation_components/system/services/dns_server.rst index b681f32f..1e30b9bd 100644 --- a/docs/source/simulation_components/system/services/dns_server.rst +++ b/docs/source/simulation_components/system/services/dns_server.rst @@ -83,16 +83,17 @@ Via Configuration Configuration ============= -.. include:: ../common/common_configuration.rst -.. |SOFTWARE_NAME| replace:: DNSServer -.. |SOFTWARE_NAME_BACKTICK| replace:: ``DNSServer`` - -domain_mapping -"""""""""""""" +``domain_mapping`` +"""""""""""""""""" Domain mapping takes the domain and IP Addresses as a key-value pairs i.e. If the domain is "arcd.com" and the IP Address attributed to the domain is 192.168.0.10, then the value should be ``arcd.com: 192.168.0.10`` The key must be a string and the IP Address must be a valid octet i.e. in the range of ``0.0.0.0`` and ``255.255.255.255``. + +``Common Attributes`` +^^^^^^^^^^^^^^^^^^^^^ + +See :ref:`Common Configuration` diff --git a/docs/source/simulation_components/system/services/ftp_client.rst b/docs/source/simulation_components/system/services/ftp_client.rst index fdf9cfcf..c8a21743 100644 --- a/docs/source/simulation_components/system/services/ftp_client.rst +++ b/docs/source/simulation_components/system/services/ftp_client.rst @@ -83,7 +83,7 @@ Via Configuration Configuration ============= -.. include:: ../common/common_configuration.rst +``Common Attributes`` +^^^^^^^^^^^^^^^^^^^^^ -.. |SOFTWARE_NAME| replace:: FTPClient -.. |SOFTWARE_NAME_BACKTICK| replace:: ``FTPClient`` +See :ref:`Common Configuration` diff --git a/docs/source/simulation_components/system/services/ftp_server.rst b/docs/source/simulation_components/system/services/ftp_server.rst index 9b26157d..f52fa043 100644 --- a/docs/source/simulation_components/system/services/ftp_server.rst +++ b/docs/source/simulation_components/system/services/ftp_server.rst @@ -81,14 +81,14 @@ Via Configuration Configuration ============= -.. include:: ../common/common_configuration.rst - -.. |SOFTWARE_NAME| replace:: FTPServer -.. |SOFTWARE_NAME_BACKTICK| replace:: ``FTPServer`` - ``server_password`` """"""""""""""""""" Optional. Default value is ``None``. The password that needs to be provided by a connecting :ref:`FTPClient` in order to create a successful connection. + +``Common Attributes`` +^^^^^^^^^^^^^^^^^^^^^ + +See :ref:`Common Configuration` diff --git a/docs/source/simulation_components/system/services/ntp_client.rst b/docs/source/simulation_components/system/services/ntp_client.rst index 6faad108..7af831bf 100644 --- a/docs/source/simulation_components/system/services/ntp_client.rst +++ b/docs/source/simulation_components/system/services/ntp_client.rst @@ -80,11 +80,6 @@ Via Configuration Configuration ============= -.. include:: ../common/common_configuration.rst - -.. |SOFTWARE_NAME| replace:: NTPClient -.. |SOFTWARE_NAME_BACKTICK| replace:: ``NTPClient`` - ``ntp_server_ip`` """"""""""""""""" @@ -93,3 +88,8 @@ Optional. Default value is ``None``. The IP address of an NTP Server which provides a time that the ``NTPClient`` can synchronise to. This must be a valid octet i.e. in the range of ``0.0.0.0`` and ``255.255.255.255``. + +``Common Attributes`` +^^^^^^^^^^^^^^^^^^^^^ + +See :ref:`Common Configuration` diff --git a/docs/source/simulation_components/system/services/ntp_server.rst b/docs/source/simulation_components/system/services/ntp_server.rst index 3ddb51ea..a09c8bdd 100644 --- a/docs/source/simulation_components/system/services/ntp_server.rst +++ b/docs/source/simulation_components/system/services/ntp_server.rst @@ -75,10 +75,8 @@ Via Configuration - ref: ntp_server type: NTPServer -Configuration -============= -.. include:: ../common/common_configuration.rst +``Common Attributes`` +^^^^^^^^^^^^^^^^^^^^^ -.. |SOFTWARE_NAME| replace:: NTPServer -.. |SOFTWARE_NAME_BACKTICK| replace:: ``NTPServer`` +See :ref:`Common Configuration` diff --git a/docs/source/simulation_components/system/services/terminal.rst b/docs/source/simulation_components/system/services/terminal.rst new file mode 100644 index 00000000..6909786e --- /dev/null +++ b/docs/source/simulation_components/system/services/terminal.rst @@ -0,0 +1,181 @@ +.. only:: comment + + © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK + +.. _Terminal: + +Terminal +######## + +The ``Terminal.py`` class provides a generic terminal simulation, by extending the base Service class within PrimAITE. The aim of this is to act as the primary entrypoint for Nodes within the environment. + + +Overview +======== + +The Terminal service uses Secure Socket (SSH) as the communication method between terminals. They operate on port 22, and are part of the services automatically +installed on Nodes when they are instantiated. + +Key capabilities +"""""""""""""""" + + - Ensures packets are matched to an existing session + - Simulates common Terminal processes/commands. + - Leverages the Service base class for install/uninstall, status tracking etc. + + +Implementation +"""""""""""""" + + - Manages remote connections in a dictionary by session ID. + - Processes commands, forwarding to the ``RequestManager`` or ``SessionManager`` where appropriate. + - Extends Service class. + - A detailed guide on the implementation and functionality of the Terminal class can be found in the "Terminal-Processing" jupyter notebook. + + +Usage +""""" + + - Pre-Installs on all ``Nodes`` (with the exception of ``Switches``). + - Terminal Clients connect, execute commands and disconnect from remote nodes. + - Ensures that users are logged in to the component before executing any commands. + - Service runs on SSH port 22 by default. + +Usage +===== + +The below code examples demonstrate how to create a terminal, a remote terminal, and how to send a basic application install command to a remote node. + +Python +"""""" + +.. code-block:: python + + from ipaddress import IPv4Address + + from primaite.simulator.network.hardware.nodes.host.computer import Computer + from primaite.simulator.system.services.terminal.terminal import Terminal + from primaite.simulator.network.hardware.node_operating_state import NodeOperatingState + + client = Computer( + hostname="client", + ip_address="192.168.10.21", + subnet_mask="255.255.255.0", + default_gateway="192.168.10.1", + operating_state=NodeOperatingState.ON, + ) + + terminal: Terminal = client.software_manager.software.get("Terminal") + +Creating Remote Terminal Connection +""""""""""""""""""""""""""""""""""" + + +.. code-block:: python + + from primaite.simulator.system.services.terminal.terminal import Terminal + from primaite.simulator.network.container import Network + from primaite.simulator.network.hardware.nodes.host.computer import Computer + from primaite.simulator.system.services.terminal.terminal import RemoteTerminalConnection + + + network = Network() + node_a = Computer(hostname="node_a", ip_address="192.168.0.10", subnet_mask="255.255.255.0", start_up_duration=0) + node_a.power_on() + node_b = Computer(hostname="node_b", ip_address="192.168.0.11", subnet_mask="255.255.255.0", start_up_duration=0) + node_b.power_on() + network.connect(node_a.network_interface[1], node_b.network_interface[1]) + + terminal_a: Terminal = node_a.software_manager.software.get("Terminal") + + + term_a_term_b_remote_connection: RemoteTerminalConnection = terminal_a.login(username="admin", password="Admin123!", ip_address="192.168.0.11") + + + +Executing a basic application install command +""""""""""""""""""""""""""""""""""""""""""""" + +.. code-block:: python + + from primaite.simulator.system.services.terminal.terminal import Terminal + from primaite.simulator.network.container import Network + from primaite.simulator.network.hardware.nodes.host.computer import Computer + from primaite.simulator.system.services.terminal.terminal import RemoteTerminalConnection + from primaite.simulator.system.applications.red_applications.ransomware_script import RansomwareScript + + + network = Network() + node_a = Computer(hostname="node_a", ip_address="192.168.0.10", subnet_mask="255.255.255.0", start_up_duration=0) + node_a.power_on() + node_b = Computer(hostname="node_b", ip_address="192.168.0.11", subnet_mask="255.255.255.0", start_up_duration=0) + node_b.power_on() + network.connect(node_a.network_interface[1], node_b.network_interface[1]) + + terminal_a: Terminal = node_a.software_manager.software.get("Terminal") + + + term_a_term_b_remote_connection: RemoteTerminalConnection = terminal_a.login(username="admin", password="Admin123!", ip_address="192.168.0.11") + + term_a_term_b_remote_connection.execute(["software_manager", "application", "install", "RansomwareScript"]) + + + +Creating a folder on a remote node +"""""""""""""""""""""""""""""""""" + +.. code-block:: python + + from primaite.simulator.system.services.terminal.terminal import Terminal + from primaite.simulator.network.container import Network + from primaite.simulator.network.hardware.nodes.host.computer import Computer + from primaite.simulator.system.services.terminal.terminal import RemoteTerminalConnection + from primaite.simulator.system.applications.red_applications.ransomware_script import RansomwareScript + + + network = Network() + node_a = Computer(hostname="node_a", ip_address="192.168.0.10", subnet_mask="255.255.255.0", start_up_duration=0) + node_a.power_on() + node_b = Computer(hostname="node_b", ip_address="192.168.0.11", subnet_mask="255.255.255.0", start_up_duration=0) + node_b.power_on() + network.connect(node_a.network_interface[1], node_b.network_interface[1]) + + terminal_a: Terminal = node_a.software_manager.software.get("Terminal") + + + term_a_term_b_remote_connection: RemoteTerminalConnection = terminal_a.login(username="admin", password="Admin123!", ip_address="192.168.0.11") + + term_a_term_b_remote_connection.execute(["file_system", "create", "folder", "downloads"]) + + +Disconnect from Remote Node +""""""""""""""""""""""""""" + +.. code-block:: python + + from primaite.simulator.system.services.terminal.terminal import Terminal + from primaite.simulator.network.container import Network + from primaite.simulator.network.hardware.nodes.host.computer import Computer + from primaite.simulator.system.services.terminal.terminal import RemoteTerminalConnection + from primaite.simulator.system.applications.red_applications.ransomware_script import RansomwareScript + + + network = Network() + node_a = Computer(hostname="node_a", ip_address="192.168.0.10", subnet_mask="255.255.255.0", start_up_duration=0) + node_a.power_on() + node_b = Computer(hostname="node_b", ip_address="192.168.0.11", subnet_mask="255.255.255.0", start_up_duration=0) + node_b.power_on() + network.connect(node_a.network_interface[1], node_b.network_interface[1]) + + terminal_a: Terminal = node_a.software_manager.software.get("Terminal") + + + term_a_term_b_remote_connection: RemoteTerminalConnection = terminal_a.login(username="admin", password="Admin123!", ip_address="192.168.0.11") + + term_a_term_b_remote_connection.disconnect() + + +``Common Attributes`` +^^^^^^^^^^^^^^^^^^^^^ + +See :ref:`Common Configuration` diff --git a/docs/source/simulation_components/system/services/web_server.rst b/docs/source/simulation_components/system/services/web_server.rst index f0294223..cec20a60 100644 --- a/docs/source/simulation_components/system/services/web_server.rst +++ b/docs/source/simulation_components/system/services/web_server.rst @@ -75,10 +75,8 @@ Via Configuration - ref: web_server type: WebServer -Configuration -============= -.. include:: ../common/common_configuration.rst +``Common Attributes`` +^^^^^^^^^^^^^^^^^^^^^ -.. |SOFTWARE_NAME| replace:: WebServer -.. |SOFTWARE_NAME_BACKTICK| replace:: ``WebServer`` +See :ref:`Common Configuration` diff --git a/docs/source/simulation_components/system/software.rst b/docs/source/simulation_components/system/software.rst index 3acfb9b4..c8f0e2d3 100644 --- a/docs/source/simulation_components/system/software.rst +++ b/docs/source/simulation_components/system/software.rst @@ -2,6 +2,8 @@ © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +.. _software: + Software ======== @@ -63,3 +65,10 @@ Processes ######### `To be implemented` + +Common Software Configuration +############################# + +Below is a list of the common configuration items within Software components of PrimAITE: + +.. include:: common/common_configuration.rst diff --git a/pyproject.toml b/pyproject.toml index e29fd504..354df8b2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -52,7 +52,7 @@ license-files = ["LICENSE"] [project.optional-dependencies] rl = [ - "ray[rllib] >= 2.20.0, < 2.33", + "ray[rllib] >= 2.20.0, <2.33", "tensorflow==2.12.0", "stable-baselines3[extra]==2.1.0", "sb3-contrib==2.1.0", @@ -75,7 +75,8 @@ dev = [ "wheel==0.38.4", "nbsphinx==0.9.4", "nbmake==1.5.4", - "pytest-xdist==3.3.1" + "pytest-xdist==3.3.1", + "md2pdf", ] [project.scripts] diff --git a/run_test_and_coverage.py b/run_test_and_coverage.py new file mode 100644 index 00000000..3bd9072d --- /dev/null +++ b/run_test_and_coverage.py @@ -0,0 +1,22 @@ +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +import subprocess +import sys +from typing import Any + + +def run_command(command: Any): + """Runs a command and returns the exit code.""" + result = subprocess.run(command, shell=True) + if result.returncode != 0: + sys.exit(result.returncode) + + +# Run pytest with coverage +run_command( + "coverage run -m --source=primaite pytest -v -o junit_family=xunit2 " + "--junitxml=junit/test-results.xml --cov-fail-under=80" +) + +# Generate coverage reports if tests passed +run_command("coverage xml -o coverage.xml -i") +run_command("coverage html -d htmlcov -i") diff --git a/src/primaite/VERSION b/src/primaite/VERSION index 944880fa..15a27998 100644 --- a/src/primaite/VERSION +++ b/src/primaite/VERSION @@ -1 +1 @@ -3.2.0 +3.3.0 diff --git a/src/primaite/config/_package_data/multi_lan_internet_network_example.yaml b/src/primaite/config/_package_data/multi_lan_internet_network_example.yaml index 09e85d03..61562418 100644 --- a/src/primaite/config/_package_data/multi_lan_internet_network_example.yaml +++ b/src/primaite/config/_package_data/multi_lan_internet_network_example.yaml @@ -25,7 +25,7 @@ simulation: db_server_ip: 10.10.1.11 - type: WebBrowser options: - target_url: http://sometech.ai + target_url: http://sometech.ai/users/ - hostname: pc_2 type: computer @@ -39,7 +39,7 @@ simulation: db_server_ip: 10.10.1.11 - type: WebBrowser options: - target_url: http://sometech.ai + target_url: http://sometech.ai/users/ - hostname: server_1 type: server @@ -221,7 +221,7 @@ simulation: subnet_mask: 255.255.255.0 acl: - 2: # Allow the some_tech_web_srv to connect to the Database Service on some_tech_db_srv + 11: # Allow the some_tech_web_srv to connect to the Database Service on some_tech_db_srv action: PERMIT src_ip: 94.10.180.6 src_wildcard_mask: 0.0.0.0 @@ -229,7 +229,7 @@ simulation: dst_ip: 10.10.1.11 dst_wildcard_mask: 0.0.0.0 dst_port: POSTGRES_SERVER - 3: # Allow the Database Service on some_tech_db_srv to respond to some_tech_web_srv + 12: # Allow the Database Service on some_tech_db_srv to respond to some_tech_web_srv action: PERMIT src_ip: 10.10.1.11 src_wildcard_mask: 0.0.0.0 @@ -237,7 +237,7 @@ simulation: dst_ip: 94.10.180.6 dst_wildcard_mask: 0.0.0.0 dst_port: POSTGRES_SERVER - 4: # Prevent the Junior engineer from downloading files from the some_tech_storage_srv over FTP + 13: # Prevent the Junior engineer from downloading files from the some_tech_storage_srv over FTP action: DENY src_ip: 10.10.2.12 src_wildcard_mask: 0.0.0.0 @@ -245,33 +245,41 @@ simulation: dst_ip: 10.10.1.12 dst_wildcard_mask: 0.0.0.0 dst_port: FTP - 5: # Allow communication between Engineering and the DB & Storage subnet + 14: # Prevent the Junior engineer from connecting to some_tech_storage_srv over SSH + action: DENY + src_ip: 10.10.2.12 + src_wildcard_mask: 0.0.0.0 + src_port: SSH + dst_ip: 10.10.1.12 + dst_wildcard_mask: 0.0.0.0 + dst_port: SSH + 15: # Allow communication between Engineering and the DB & Storage subnet action: PERMIT src_ip: 10.10.2.0 src_wildcard_mask: 0.0.0.255 dst_ip: 10.10.1.0 dst_wildcard_mask: 0.0.0.255 - 6: # Allow communication between the DB & Storage subnet and Engineering + 16: # Allow communication between the DB & Storage subnet and Engineering action: PERMIT src_ip: 10.10.1.0 src_wildcard_mask: 0.0.0.255 dst_ip: 10.10.2.0 dst_wildcard_mask: 0.0.0.255 - 7: # Allow the SomeTech network to use HTTP + 17: # Allow the SomeTech network to use HTTP action: PERMIT src_port: HTTP dst_port: HTTP - 8: # Allow the SomeTech internal network to use ARP + 18: # Allow the SomeTech internal network to use ARP action: PERMIT src_ip: 10.10.0.0 src_wildcard_mask: 0.0.255.255 src_port: ARP - 9: # Allow the SomeTech internal network to use ICMP + 19: # Allow the SomeTech internal network to use ICMP action: PERMIT src_ip: 10.10.0.0 src_wildcard_mask: 0.0.255.255 protocol: ICMP - 10: + 21: action: PERMIT src_ip: 94.10.180.6 src_wildcard_mask: 0.0.0.0 @@ -279,10 +287,14 @@ simulation: dst_ip: 10.10.0.0 dst_wildcard_mask: 0.0.255.255 dst_port: HTTP - 11: # Permit SomeTech to use DNS + 22: # Permit SomeTech to use DNS action: PERMIT src_port: DNS dst_port: DNS + 23: # Permit SomeTech to use SSH + action: PERMIT + src_port: SSH + dst_port: SSH default_route: # Default route to all external networks next_hop_ip_address: 10.10.4.2 # NI int on some_tech_fw @@ -332,7 +344,7 @@ simulation: db_server_ip: 10.10.1.11 - type: WebBrowser options: - target_url: http://sometech.ai + target_url: http://sometech.ai/users/ - hostname: some_tech_snr_dev_pc type: computer @@ -346,7 +358,7 @@ simulation: db_server_ip: 10.10.1.11 - type: WebBrowser options: - target_url: http://sometech.ai + target_url: http://sometech.ai/users/ - hostname: some_tech_jnr_dev_pc type: computer @@ -360,7 +372,7 @@ simulation: db_server_ip: 10.10.1.11 - type: WebBrowser options: - target_url: http://sometech.ai + target_url: http://sometech.ai/users/ links: # Home/Office Lan Links diff --git a/src/primaite/config/_package_data/scenario_with_placeholders/scenario.yaml b/src/primaite/config/_package_data/scenario_with_placeholders/scenario.yaml index 81848b2d..dfd200f3 100644 --- a/src/primaite/config/_package_data/scenario_with_placeholders/scenario.yaml +++ b/src/primaite/config/_package_data/scenario_with_placeholders/scenario.yaml @@ -129,6 +129,10 @@ agents: simulation: network: + nmne_config: + capture_nmne: true + nmne_capture_keywords: + - DELETE nodes: - hostname: client type: computer diff --git a/src/primaite/game/agent/actions.py b/src/primaite/game/agent/actions.py index 7263cfc1..2e6189c0 100644 --- a/src/primaite/game/agent/actions.py +++ b/src/primaite/game/agent/actions.py @@ -1071,6 +1071,247 @@ class NodeNetworkServiceReconAction(AbstractAction): ] +class ConfigureC2BeaconAction(AbstractAction): + """Action which configures a C2 Beacon based on the parameters given.""" + + class _Opts(BaseModel): + """Schema for options that can be passed to this action.""" + + c2_server_ip_address: str + keep_alive_frequency: int = Field(default=5, ge=1) + masquerade_protocol: str = Field(default="TCP") + masquerade_port: str = Field(default="HTTP") + + @field_validator( + "c2_server_ip_address", + "keep_alive_frequency", + "masquerade_protocol", + "masquerade_port", + mode="before", + ) + @classmethod + def not_none(cls, v: str, info: ValidationInfo) -> int: + """If None is passed, use the default value instead.""" + if v is None: + return cls.model_fields[info.field_name].default + return v + + def __init__(self, manager: "ActionManager", **kwargs) -> None: + super().__init__(manager=manager) + + def form_request(self, node_id: int, config: Dict) -> RequestFormat: + """Return the action formatted as a request that can be ingested by the simulation.""" + node_name = self.manager.get_node_name_by_idx(node_id) + if node_name is None: + return ["do_nothing"] + config = ConfigureC2BeaconAction._Opts( + c2_server_ip_address=config["c2_server_ip_address"], + keep_alive_frequency=config["keep_alive_frequency"], + masquerade_port=config["masquerade_port"], + masquerade_protocol=config["masquerade_protocol"], + ) + + ConfigureC2BeaconAction._Opts.model_validate(config) # check that options adhere to schema + + return ["network", "node", node_name, "application", "C2Beacon", "configure", config.__dict__] + + +class NodeAccountsChangePasswordAction(AbstractAction): + """Action which changes the password for a user.""" + + def __init__(self, manager: "ActionManager", **kwargs) -> None: + super().__init__(manager=manager) + + def form_request(self, node_id: str, username: str, current_password: str, new_password: str) -> RequestFormat: + """Return the action formatted as a request which can be ingested by the PrimAITE simulation.""" + node_name = self.manager.get_node_name_by_idx(node_id) + return [ + "network", + "node", + node_name, + "service", + "UserManager", + "change_password", + username, + current_password, + new_password, + ] + + +class NodeSessionsRemoteLoginAction(AbstractAction): + """Action which performs a remote session login.""" + + def __init__(self, manager: "ActionManager", **kwargs) -> None: + super().__init__(manager=manager) + + def form_request(self, node_id: str, username: str, password: str, remote_ip: str) -> RequestFormat: + """Return the action formatted as a request which can be ingested by the PrimAITE simulation.""" + node_name = self.manager.get_node_name_by_idx(node_id) + return [ + "network", + "node", + node_name, + "service", + "Terminal", + "ssh_to_remote", + username, + password, + remote_ip, + ] + + +class NodeSessionsRemoteLogoutAction(AbstractAction): + """Action which performs a remote session logout.""" + + def __init__(self, manager: "ActionManager", **kwargs) -> None: + super().__init__(manager=manager) + + def form_request(self, node_id: str, remote_ip: str) -> RequestFormat: + """Return the action formatted as a request which can be ingested by the PrimAITE simulation.""" + node_name = self.manager.get_node_name_by_idx(node_id) + return ["network", "node", node_name, "service", "Terminal", "remote_logoff", remote_ip] + + +class RansomwareConfigureC2ServerAction(AbstractAction): + """Action which sends a command from the C2 Server to the C2 Beacon which configures a local RansomwareScript.""" + + def __init__(self, manager: "ActionManager", **kwargs) -> None: + super().__init__(manager=manager) + + def form_request(self, node_id: int, config: Dict) -> RequestFormat: + """Return the action formatted as a request that can be ingested by the simulation.""" + node_name = self.manager.get_node_name_by_idx(node_id) + if node_name is None: + return ["do_nothing"] + # Using the ransomware scripts model to validate. + ConfigureRansomwareScriptAction._Opts.model_validate(config) # check that options adhere to schema + return ["network", "node", node_name, "application", "C2Server", "ransomware_configure", config] + + +class RansomwareLaunchC2ServerAction(AbstractAction): + """Action which causes the C2 Server to send a command to the C2 Beacon to launch the RansomwareScript.""" + + def __init__(self, manager: "ActionManager", **kwargs) -> None: + super().__init__(manager=manager) + + def form_request(self, node_id: int) -> RequestFormat: + """Return the action formatted as a request that can be ingested by the simulation.""" + node_name = self.manager.get_node_name_by_idx(node_id) + if node_name is None: + return ["do_nothing"] + # This action currently doesn't require any further configuration options. + return ["network", "node", node_name, "application", "C2Server", "ransomware_launch"] + + +class ExfiltrationC2ServerAction(AbstractAction): + """Action which exfiltrates a target file from a certain node onto the C2 beacon and then the C2 Server.""" + + class _Opts(BaseModel): + """Schema for options that can be passed to this action.""" + + username: Optional[str] + password: Optional[str] + target_ip_address: str + target_file_name: str + target_folder_name: str + exfiltration_folder_name: Optional[str] + + def __init__(self, manager: "ActionManager", **kwargs) -> None: + super().__init__(manager=manager) + + def form_request( + self, + node_id: int, + account: dict, + target_ip_address: str, + target_file_name: str, + target_folder_name: str, + exfiltration_folder_name: Optional[str], + ) -> RequestFormat: + """Return the action formatted as a request that can be ingested by the simulation.""" + node_name = self.manager.get_node_name_by_idx(node_id) + if node_name is None: + return ["do_nothing"] + + command_model = { + "target_file_name": target_file_name, + "target_folder_name": target_folder_name, + "exfiltration_folder_name": exfiltration_folder_name, + "target_ip_address": target_ip_address, + "username": account["username"], + "password": account["password"], + } + ExfiltrationC2ServerAction._Opts.model_validate(command_model) + return ["network", "node", node_name, "application", "C2Server", "exfiltrate", command_model] + + +class NodeSendRemoteCommandAction(AbstractAction): + """Action which sends a terminal command to a remote node via SSH.""" + + def __init__(self, manager: "ActionManager", **kwargs) -> None: + super().__init__(manager=manager) + + def form_request(self, node_id: int, remote_ip: str, command: RequestFormat) -> RequestFormat: + """Return the action formatted as a request which can be ingested by the PrimAITE simulation.""" + node_name = self.manager.get_node_name_by_idx(node_id) + return [ + "network", + "node", + node_name, + "service", + "Terminal", + "send_remote_command", + remote_ip, + {"command": command}, + ] + + +class TerminalC2ServerAction(AbstractAction): + """Action which causes the C2 Server to send a command to the C2 Beacon to execute the terminal command passed.""" + + class _Opts(BaseModel): + """Schema for options that can be passed to this action.""" + + commands: Union[List[RequestFormat], RequestFormat] + ip_address: Optional[str] + username: Optional[str] + password: Optional[str] + + def __init__(self, manager: "ActionManager", **kwargs) -> None: + super().__init__(manager=manager) + + def form_request(self, node_id: int, commands: List, ip_address: Optional[str], account: dict) -> RequestFormat: + """Return the action formatted as a request that can be ingested by the simulation.""" + node_name = self.manager.get_node_name_by_idx(node_id) + if node_name is None: + return ["do_nothing"] + + command_model = { + "commands": commands, + "ip_address": ip_address, + "username": account["username"], + "password": account["password"], + } + + TerminalC2ServerAction._Opts.model_validate(command_model) + return ["network", "node", node_name, "application", "C2Server", "terminal_command", command_model] + + +class RansomwareLaunchC2ServerAction(AbstractAction): + """Action which causes the C2 Server to send a command to the C2 Beacon to launch the RansomwareScript.""" + + def __init__(self, manager: "ActionManager", **kwargs) -> None: + super().__init__(manager=manager) + + def form_request(self, node_id: int) -> RequestFormat: + """Return the action formatted as a request that can be ingested by the simulation.""" + node_name = self.manager.get_node_name_by_idx(node_id) + if node_name is None: + return ["do_nothing"] + # This action currently doesn't require any further configuration options. + return ["network", "node", node_name, "application", "C2Server", "ransomware_launch"] + + class ActionManager: """Class which manages the action space for an agent.""" @@ -1122,6 +1363,15 @@ class ActionManager: "CONFIGURE_DATABASE_CLIENT": ConfigureDatabaseClientAction, "CONFIGURE_RANSOMWARE_SCRIPT": ConfigureRansomwareScriptAction, "CONFIGURE_DOSBOT": ConfigureDoSBotAction, + "CONFIGURE_C2_BEACON": ConfigureC2BeaconAction, + "C2_SERVER_RANSOMWARE_LAUNCH": RansomwareLaunchC2ServerAction, + "C2_SERVER_RANSOMWARE_CONFIGURE": RansomwareConfigureC2ServerAction, + "C2_SERVER_TERMINAL_COMMAND": TerminalC2ServerAction, + "C2_SERVER_DATA_EXFILTRATE": ExfiltrationC2ServerAction, + "NODE_ACCOUNTS_CHANGE_PASSWORD": NodeAccountsChangePasswordAction, + "SSH_TO_REMOTE": NodeSessionsRemoteLoginAction, + "SESSIONS_REMOTE_LOGOFF": NodeSessionsRemoteLogoutAction, + "NODE_SEND_REMOTE_COMMAND": NodeSendRemoteCommandAction, } """Dictionary which maps action type strings to the corresponding action class.""" diff --git a/src/primaite/game/agent/interface.py b/src/primaite/game/agent/interface.py index f57dc191..14b97821 100644 --- a/src/primaite/game/agent/interface.py +++ b/src/primaite/game/agent/interface.py @@ -36,6 +36,8 @@ class AgentHistoryItem(BaseModel): reward: Optional[float] = None + reward_info: Dict[str, Any] = {} + class AgentStartSettings(BaseModel): """Configuration values for when an agent starts performing actions.""" diff --git a/src/primaite/game/agent/observations/file_system_observations.py b/src/primaite/game/agent/observations/file_system_observations.py index cb48fe7d..1c73d026 100644 --- a/src/primaite/game/agent/observations/file_system_observations.py +++ b/src/primaite/game/agent/observations/file_system_observations.py @@ -23,8 +23,10 @@ class FileObservation(AbstractObservation, identifier="FILE"): """Name of the file, used for querying simulation state dictionary.""" include_num_access: Optional[bool] = None """Whether to include the number of accesses to the file in the observation.""" + file_system_requires_scan: Optional[bool] = None + """If True, the file must be scanned to update the health state. Tf False, the true state is always shown.""" - def __init__(self, where: WhereType, include_num_access: bool) -> None: + def __init__(self, where: WhereType, include_num_access: bool, file_system_requires_scan: bool) -> None: """ Initialise a file observation instance. @@ -34,9 +36,13 @@ class FileObservation(AbstractObservation, identifier="FILE"): :type where: WhereType :param include_num_access: Whether to include the number of accesses to the file in the observation. :type include_num_access: bool + :param file_system_requires_scan: If True, the file must be scanned to update the health state. Tf False, + the true state is always shown. + :type file_system_requires_scan: bool """ self.where: WhereType = where self.include_num_access: bool = include_num_access + self.file_system_requires_scan: bool = file_system_requires_scan self.default_observation: ObsType = {"health_status": 0} if self.include_num_access: @@ -74,7 +80,11 @@ class FileObservation(AbstractObservation, identifier="FILE"): file_state = access_from_nested_dict(state, self.where) if file_state is NOT_PRESENT_IN_STATE: return self.default_observation - obs = {"health_status": file_state["visible_status"]} + if self.file_system_requires_scan: + health_status = file_state["visible_status"] + else: + health_status = file_state["health_status"] + obs = {"health_status": health_status} if self.include_num_access: obs["num_access"] = self._categorise_num_access(file_state["num_access"]) return obs @@ -104,8 +114,15 @@ class FileObservation(AbstractObservation, identifier="FILE"): :type parent_where: WhereType, optional :return: Constructed file observation instance. :rtype: FileObservation + :param file_system_requires_scan: If True, the folder must be scanned to update the health state. Tf False, + the true state is always shown. + :type file_system_requires_scan: bool """ - return cls(where=parent_where + ["files", config.file_name], include_num_access=config.include_num_access) + return cls( + where=parent_where + ["files", config.file_name], + include_num_access=config.include_num_access, + file_system_requires_scan=config.file_system_requires_scan, + ) class FolderObservation(AbstractObservation, identifier="FOLDER"): @@ -122,9 +139,16 @@ class FolderObservation(AbstractObservation, identifier="FOLDER"): """Number of spaces for file observations in this folder.""" include_num_access: Optional[bool] = None """Whether files in this folder should include the number of accesses in their observation.""" + file_system_requires_scan: Optional[bool] = None + """If True, the folder must be scanned to update the health state. Tf False, the true state is always shown.""" def __init__( - self, where: WhereType, files: Iterable[FileObservation], num_files: int, include_num_access: bool + self, + where: WhereType, + files: Iterable[FileObservation], + num_files: int, + include_num_access: bool, + file_system_requires_scan: bool, ) -> None: """ Initialise a folder observation instance. @@ -138,12 +162,23 @@ class FolderObservation(AbstractObservation, identifier="FOLDER"): :type num_files: int :param include_num_access: Whether to include the number of accesses to files in the observation. :type include_num_access: bool + :param file_system_requires_scan: If True, the folder must be scanned to update the health state. Tf False, + the true state is always shown. + :type file_system_requires_scan: bool """ self.where: WhereType = where + self.file_system_requires_scan: bool = file_system_requires_scan + self.files: List[FileObservation] = files while len(self.files) < num_files: - self.files.append(FileObservation(where=None, include_num_access=include_num_access)) + self.files.append( + FileObservation( + where=None, + include_num_access=include_num_access, + file_system_requires_scan=self.file_system_requires_scan, + ) + ) while len(self.files) > num_files: truncated_file = self.files.pop() msg = f"Too many files in folder observation. Truncating file {truncated_file}" @@ -168,7 +203,10 @@ class FolderObservation(AbstractObservation, identifier="FOLDER"): if folder_state is NOT_PRESENT_IN_STATE: return self.default_observation - health_status = folder_state["health_status"] + if self.file_system_requires_scan: + health_status = folder_state["visible_status"] + else: + health_status = folder_state["health_status"] obs = {} @@ -209,6 +247,13 @@ class FolderObservation(AbstractObservation, identifier="FOLDER"): # pass down shared/common config items for file_config in config.files: file_config.include_num_access = config.include_num_access + file_config.file_system_requires_scan = config.file_system_requires_scan files = [FileObservation.from_config(config=f, parent_where=where) for f in config.files] - return cls(where=where, files=files, num_files=config.num_files, include_num_access=config.include_num_access) + return cls( + where=where, + files=files, + num_files=config.num_files, + include_num_access=config.include_num_access, + file_system_requires_scan=config.file_system_requires_scan, + ) diff --git a/src/primaite/game/agent/observations/firewall_observation.py b/src/primaite/game/agent/observations/firewall_observation.py index 4f1a9d90..42ceaff0 100644 --- a/src/primaite/game/agent/observations/firewall_observation.py +++ b/src/primaite/game/agent/observations/firewall_observation.py @@ -10,6 +10,7 @@ from primaite import getLogger from primaite.game.agent.observations.acl_observation import ACLObservation from primaite.game.agent.observations.nic_observations import PortObservation from primaite.game.agent.observations.observations import AbstractObservation, WhereType +from primaite.game.agent.utils import access_from_nested_dict, NOT_PRESENT_IN_STATE _LOGGER = getLogger(__name__) @@ -32,6 +33,8 @@ class FirewallObservation(AbstractObservation, identifier="FIREWALL"): """List of protocols for encoding ACLs.""" num_rules: Optional[int] = None """Number of rules ACL rules to show.""" + include_users: Optional[bool] = True + """If True, report user session information.""" def __init__( self, @@ -41,6 +44,7 @@ class FirewallObservation(AbstractObservation, identifier="FIREWALL"): port_list: List[int], protocol_list: List[str], num_rules: int, + include_users: bool, ) -> None: """ Initialise a firewall observation instance. @@ -58,9 +62,13 @@ class FirewallObservation(AbstractObservation, identifier="FIREWALL"): :type protocol_list: List[str] :param num_rules: Number of rules configured in the firewall. :type num_rules: int + :param include_users: If True, report user session information. + :type include_users: bool """ self.where: WhereType = where - + self.include_users: bool = include_users + self.max_users: int = 3 + """Maximum number of remote sessions observable, excess sessions are truncated.""" self.ports: List[PortObservation] = [ PortObservation(where=self.where + ["NICs", port_num]) for port_num in (1, 2, 3) ] @@ -142,6 +150,9 @@ class FirewallObservation(AbstractObservation, identifier="FIREWALL"): :return: Observation containing the status of ports and ACLs for internal, DMZ, and external traffic. :rtype: ObsType """ + firewall_state = access_from_nested_dict(state, self.where) + if firewall_state is NOT_PRESENT_IN_STATE: + return self.default_observation obs = { "PORTS": {i + 1: p.observe(state) for i, p in enumerate(self.ports)}, "ACL": { @@ -159,6 +170,12 @@ class FirewallObservation(AbstractObservation, identifier="FIREWALL"): }, }, } + if self.include_users: + sess = firewall_state["services"]["UserSessionManager"] + obs["users"] = { + "local_login": 1 if sess["current_local_user"] else 0, + "remote_sessions": min(self.max_users, len(sess["active_remote_sessions"])), + } return obs @property @@ -218,4 +235,5 @@ class FirewallObservation(AbstractObservation, identifier="FIREWALL"): port_list=config.port_list, protocol_list=config.protocol_list, num_rules=config.num_rules, + include_users=config.include_users, ) diff --git a/src/primaite/game/agent/observations/host_observations.py b/src/primaite/game/agent/observations/host_observations.py index f9fd9b1a..4419ccc7 100644 --- a/src/primaite/game/agent/observations/host_observations.py +++ b/src/primaite/game/agent/observations/host_observations.py @@ -48,6 +48,12 @@ class HostObservation(AbstractObservation, identifier="HOST"): """A dict containing which traffic types are to be included in the observation.""" include_num_access: Optional[bool] = None """Whether to include the number of accesses to files observations on this host.""" + file_system_requires_scan: Optional[bool] = None + """ + If True, files and folders must be scanned to update the health state. If False, true state is always shown. + """ + include_users: Optional[bool] = True + """If True, report user session information.""" def __init__( self, @@ -64,6 +70,8 @@ class HostObservation(AbstractObservation, identifier="HOST"): include_nmne: bool, monitored_traffic: Optional[Dict], include_num_access: bool, + file_system_requires_scan: bool, + include_users: bool, ) -> None: """ Initialise a host observation instance. @@ -95,10 +103,18 @@ class HostObservation(AbstractObservation, identifier="HOST"): :type monitored_traffic: Dict :param include_num_access: Flag to include the number of accesses to files. :type include_num_access: bool + :param file_system_requires_scan: If True, the files and folders must be scanned to update the health state. + If False, the true state is always shown. + :type file_system_requires_scan: bool + :param include_users: If True, report user session information. + :type include_users: bool """ self.where: WhereType = where self.include_num_access = include_num_access + self.include_users = include_users + self.max_users: int = 3 + """Maximum number of remote sessions observable, excess sessions are truncated.""" # Ensure lists have lengths equal to specified counts by truncating or padding self.services: List[ServiceObservation] = services @@ -120,7 +136,13 @@ class HostObservation(AbstractObservation, identifier="HOST"): self.folders: List[FolderObservation] = folders while len(self.folders) < num_folders: self.folders.append( - FolderObservation(where=None, files=[], num_files=num_files, include_num_access=include_num_access) + FolderObservation( + where=None, + files=[], + num_files=num_files, + include_num_access=include_num_access, + file_system_requires_scan=file_system_requires_scan, + ) ) while len(self.folders) > num_folders: truncated_folder = self.folders.pop() @@ -151,6 +173,8 @@ class HostObservation(AbstractObservation, identifier="HOST"): if self.include_num_access: self.default_observation["num_file_creations"] = 0 self.default_observation["num_file_deletions"] = 0 + if self.include_users: + self.default_observation["users"] = {"local_login": 0, "remote_sessions": 0} def observe(self, state: Dict) -> ObsType: """ @@ -178,6 +202,12 @@ class HostObservation(AbstractObservation, identifier="HOST"): if self.include_num_access: obs["num_file_creations"] = node_state["file_system"]["num_file_creations"] obs["num_file_deletions"] = node_state["file_system"]["num_file_deletions"] + if self.include_users: + sess = node_state["services"]["UserSessionManager"] + obs["users"] = { + "local_login": 1 if sess["current_local_user"] else 0, + "remote_sessions": min(self.max_users, len(sess["active_remote_sessions"])), + } return obs @property @@ -202,6 +232,10 @@ class HostObservation(AbstractObservation, identifier="HOST"): if self.include_num_access: shape["num_file_creations"] = spaces.Discrete(4) shape["num_file_deletions"] = spaces.Discrete(4) + if self.include_users: + shape["users"] = spaces.Dict( + {"local_login": spaces.Discrete(2), "remote_sessions": spaces.Discrete(self.max_users + 1)} + ) return spaces.Dict(shape) @classmethod @@ -226,6 +260,7 @@ class HostObservation(AbstractObservation, identifier="HOST"): for folder_config in config.folders: folder_config.include_num_access = config.include_num_access folder_config.num_files = config.num_files + folder_config.file_system_requires_scan = config.file_system_requires_scan for nic_config in config.network_interfaces: nic_config.include_nmne = config.include_nmne @@ -257,4 +292,6 @@ class HostObservation(AbstractObservation, identifier="HOST"): include_nmne=config.include_nmne, monitored_traffic=config.monitored_traffic, include_num_access=config.include_num_access, + file_system_requires_scan=config.file_system_requires_scan, + include_users=config.include_users, ) diff --git a/src/primaite/game/agent/observations/node_observations.py b/src/primaite/game/agent/observations/node_observations.py index f7bfcc99..e263cadb 100644 --- a/src/primaite/game/agent/observations/node_observations.py +++ b/src/primaite/game/agent/observations/node_observations.py @@ -44,6 +44,10 @@ class NodesObservation(AbstractObservation, identifier="NODES"): """A dict containing which traffic types are to be included in the observation.""" include_num_access: Optional[bool] = None """Flag to include the number of accesses.""" + file_system_requires_scan: bool = True + """If True, the folder must be scanned to update the health state. Tf False, the true state is always shown.""" + include_users: Optional[bool] = True + """If True, report user session information.""" num_ports: Optional[int] = None """Number of ports.""" ip_list: Optional[List[str]] = None @@ -187,6 +191,10 @@ class NodesObservation(AbstractObservation, identifier="NODES"): host_config.monitored_traffic = config.monitored_traffic if host_config.include_num_access is None: host_config.include_num_access = config.include_num_access + if host_config.file_system_requires_scan is None: + host_config.file_system_requires_scan = config.file_system_requires_scan + if host_config.include_users is None: + host_config.include_users = config.include_users for router_config in config.routers: if router_config.num_ports is None: @@ -201,6 +209,8 @@ class NodesObservation(AbstractObservation, identifier="NODES"): router_config.protocol_list = config.protocol_list if router_config.num_rules is None: router_config.num_rules = config.num_rules + if router_config.include_users is None: + router_config.include_users = config.include_users for firewall_config in config.firewalls: if firewall_config.ip_list is None: @@ -213,6 +223,8 @@ class NodesObservation(AbstractObservation, identifier="NODES"): firewall_config.protocol_list = config.protocol_list if firewall_config.num_rules is None: firewall_config.num_rules = config.num_rules + if firewall_config.include_users is None: + firewall_config.include_users = config.include_users hosts = [HostObservation.from_config(config=c, parent_where=where) for c in config.hosts] routers = [RouterObservation.from_config(config=c, parent_where=where) for c in config.routers] diff --git a/src/primaite/game/agent/observations/router_observation.py b/src/primaite/game/agent/observations/router_observation.py index f1d4ec8e..d064936a 100644 --- a/src/primaite/game/agent/observations/router_observation.py +++ b/src/primaite/game/agent/observations/router_observation.py @@ -39,6 +39,8 @@ class RouterObservation(AbstractObservation, identifier="ROUTER"): """List of protocols for encoding ACLs.""" num_rules: Optional[int] = None """Number of rules ACL rules to show.""" + include_users: Optional[bool] = True + """If True, report user session information.""" def __init__( self, @@ -46,6 +48,7 @@ class RouterObservation(AbstractObservation, identifier="ROUTER"): ports: List[PortObservation], num_ports: int, acl: ACLObservation, + include_users: bool, ) -> None: """ Initialise a router observation instance. @@ -59,12 +62,16 @@ class RouterObservation(AbstractObservation, identifier="ROUTER"): :type num_ports: int :param acl: ACL observation representing the access control list of the router. :type acl: ACLObservation + :param include_users: If True, report user session information. + :type include_users: bool """ self.where: WhereType = where self.ports: List[PortObservation] = ports self.acl: ACLObservation = acl self.num_ports: int = num_ports - + self.include_users: bool = include_users + self.max_users: int = 3 + """Maximum number of remote sessions observable, excess sessions are truncated.""" while len(self.ports) < num_ports: self.ports.append(PortObservation(where=None)) while len(self.ports) > num_ports: @@ -95,6 +102,12 @@ class RouterObservation(AbstractObservation, identifier="ROUTER"): obs["ACL"] = self.acl.observe(state) if self.ports: obs["PORTS"] = {i + 1: p.observe(state) for i, p in enumerate(self.ports)} + if self.include_users: + sess = router_state["services"]["UserSessionManager"] + obs["users"] = { + "local_login": 1 if sess["current_local_user"] else 0, + "remote_sessions": min(self.max_users, len(sess["active_remote_sessions"])), + } return obs @property @@ -143,4 +156,4 @@ class RouterObservation(AbstractObservation, identifier="ROUTER"): ports = [PortObservation.from_config(config=c, parent_where=where) for c in config.ports] acl = ACLObservation.from_config(config=config.acl, parent_where=where) - return cls(where=where, ports=ports, num_ports=config.num_ports, acl=acl) + return cls(where=where, ports=ports, num_ports=config.num_ports, acl=acl, include_users=config.include_users) diff --git a/src/primaite/game/agent/rewards.py b/src/primaite/game/agent/rewards.py index c959ee5b..1de34b40 100644 --- a/src/primaite/game/agent/rewards.py +++ b/src/primaite/game/agent/rewards.py @@ -47,7 +47,15 @@ class AbstractReward: @abstractmethod def calculate(self, state: Dict, last_action_response: "AgentHistoryItem") -> float: - """Calculate the reward for the current state.""" + """Calculate the reward for the current state. + + :param state: Current simulation state + :type state: Dict + :param last_action_response: Current agent history state + :type last_action_response: AgentHistoryItem state + :return: Reward value + :rtype: float + """ return 0.0 @classmethod @@ -67,7 +75,15 @@ class DummyReward(AbstractReward): """Dummy reward function component which always returns 0.""" def calculate(self, state: Dict, last_action_response: "AgentHistoryItem") -> float: - """Calculate the reward for the current state.""" + """Calculate the reward for the current state. + + :param state: Current simulation state + :type state: Dict + :param last_action_response: Current agent history state + :type last_action_response: AgentHistoryItem state + :return: Reward value + :rtype: float + """ return 0.0 @classmethod @@ -109,8 +125,12 @@ class DatabaseFileIntegrity(AbstractReward): def calculate(self, state: Dict, last_action_response: "AgentHistoryItem") -> float: """Calculate the reward for the current state. - :param state: The current state of the simulation. + :param state: Current simulation state :type state: Dict + :param last_action_response: Current agent history state + :type last_action_response: AgentHistoryItem state + :return: Reward value + :rtype: float """ database_file_state = access_from_nested_dict(state, self.location_in_state) if database_file_state is NOT_PRESENT_IN_STATE: @@ -151,33 +171,52 @@ class DatabaseFileIntegrity(AbstractReward): class WebServer404Penalty(AbstractReward): """Reward function component which penalises the agent when the web server returns a 404 error.""" - def __init__(self, node_hostname: str, service_name: str) -> None: + def __init__(self, node_hostname: str, service_name: str, sticky: bool = True) -> None: """Initialise the reward component. :param node_hostname: Hostname of the node which contains the web server service. :type node_hostname: str :param service_name: Name of the web server service. :type service_name: str + :param sticky: If True, calculate the reward based on the most recent response status. If False, only calculate + the reward if there were any responses this timestep. + :type sticky: bool """ + self.sticky: bool = sticky + self.reward: float = 0.0 + """Reward value calculated last time any responses were seen. Used for persisting sticky rewards.""" self.location_in_state = ["network", "nodes", node_hostname, "services", service_name] def calculate(self, state: Dict, last_action_response: "AgentHistoryItem") -> float: """Calculate the reward for the current state. - :param state: The current state of the simulation. + :param state: Current simulation state :type state: Dict + :param last_action_response: Current agent history state + :type last_action_response: AgentHistoryItem state + :return: Reward value + :rtype: float """ web_service_state = access_from_nested_dict(state, self.location_in_state) + + # if webserver is no longer installed on the node, return 0 if web_service_state is NOT_PRESENT_IN_STATE: return 0.0 - most_recent_return_code = web_service_state["last_response_status_code"] - # TODO: reward needs to use the current web state. Observation should return web state at the time of last scan. - if most_recent_return_code == 200: - return 1.0 - elif most_recent_return_code == 404: - return -1.0 - else: - return 0.0 + + codes = web_service_state.get("response_codes_this_timestep") + if codes: + + def status2rew(status: int) -> int: + """Map status codes to reward values.""" + return 1.0 if status == 200 else -1.0 if status == 404 else 0.0 + + self.reward = sum(map(status2rew, codes)) / len(codes) # convert form HTTP codes to rewards and average + elif not self.sticky: # there are no codes, but reward is not sticky, set reward to 0 + self.reward = 0.0 + else: # skip calculating if sticky and no new codes. instead, reuse last step's value + pass + + return self.reward @classmethod def from_config(cls, config: Dict) -> "WebServer404Penalty": @@ -197,23 +236,29 @@ class WebServer404Penalty(AbstractReward): ) _LOGGER.warning(msg) raise ValueError(msg) + sticky = config.get("sticky", True) - return cls(node_hostname=node_hostname, service_name=service_name) + return cls(node_hostname=node_hostname, service_name=service_name, sticky=sticky) class WebpageUnavailablePenalty(AbstractReward): """Penalises the agent when the web browser fails to fetch a webpage.""" - def __init__(self, node_hostname: str) -> None: + def __init__(self, node_hostname: str, sticky: bool = True) -> None: """ Initialise the reward component. :param node_hostname: Hostname of the node which has the web browser. :type node_hostname: str + :param sticky: If True, calculate the reward based on the most recent response status. If False, only calculate + the reward if there were any responses this timestep. + :type sticky: bool """ self._node: str = node_hostname self.location_in_state: List[str] = ["network", "nodes", node_hostname, "applications", "WebBrowser"] - self._last_request_failed: bool = False + self.sticky: bool = sticky + self.reward: float = 0.0 + """Reward value calculated last time any responses were seen. Used for persisting sticky rewards.""" def calculate(self, state: Dict, last_action_response: "AgentHistoryItem") -> float: """ @@ -222,32 +267,50 @@ class WebpageUnavailablePenalty(AbstractReward): When the green agent requests to execute the browser application, and that request fails, this reward component will keep track of that information. In that case, it doesn't matter whether the last webpage had a 200 status code, because there has been an unsuccessful request since. + :param state: Current simulation state + :type state: Dict + :param last_action_response: Current agent history state + :type last_action_response: AgentHistoryItem state + :return: Reward value + :rtype: float """ - if last_action_response.request == ["network", "node", self._node, "application", "WebBrowser", "execute"]: - self._last_request_failed = last_action_response.response.status != "success" - - # if agent couldn't even get as far as sending the request (because for example the node was off), then - # apply a penalty - if self._last_request_failed: - return -1.0 - - # If the last request did actually go through, then check if the webpage also loaded web_browser_state = access_from_nested_dict(state, self.location_in_state) - if web_browser_state is NOT_PRESENT_IN_STATE or "history" not in web_browser_state: + + if web_browser_state is NOT_PRESENT_IN_STATE: + self.reward = 0.0 + + # check if the most recent action was to request the webpage + request_attempted = last_action_response.request == [ + "network", + "node", + self._node, + "application", + "WebBrowser", + "execute", + ] + + # skip calculating if sticky and no new codes, reusing last step value + if not request_attempted and self.sticky: + return self.reward + + if last_action_response.response.status != "success": + self.reward = -1.0 + elif web_browser_state is NOT_PRESENT_IN_STATE or not web_browser_state["history"]: _LOGGER.debug( "Web browser reward could not be calculated because the web browser history on node", f"{self._node} was not reported in the simulation state. Returning 0.0", ) - return 0.0 # 0 if the web browser cannot be found - if not web_browser_state["history"]: - return 0.0 # 0 if no requests have been attempted yet - outcome = web_browser_state["history"][-1]["outcome"] - if outcome == "PENDING": - return 0.0 # 0 if a request was attempted but not yet resolved - elif outcome == 200: - return 1.0 # 1 for successful request - else: # includes failure codes and SERVER_UNREACHABLE - return -1.0 # -1 for failure + self.reward = 0.0 + else: + outcome = web_browser_state["history"][-1]["outcome"] + if outcome == "PENDING": + self.reward = 0.0 # 0 if a request was attempted but not yet resolved + elif outcome == 200: + self.reward = 1.0 # 1 for successful request + else: # includes failure codes and SERVER_UNREACHABLE + self.reward = -1.0 # -1 for failure + + return self.reward @classmethod def from_config(cls, config: dict) -> AbstractReward: @@ -258,22 +321,28 @@ class WebpageUnavailablePenalty(AbstractReward): :type config: Dict """ node_hostname = config.get("node_hostname") - return cls(node_hostname=node_hostname) + sticky = config.get("sticky", True) + return cls(node_hostname=node_hostname, sticky=sticky) class GreenAdminDatabaseUnreachablePenalty(AbstractReward): """Penalises the agent when the green db clients fail to connect to the database.""" - def __init__(self, node_hostname: str) -> None: + def __init__(self, node_hostname: str, sticky: bool = True) -> None: """ Initialise the reward component. :param node_hostname: Hostname of the node where the database client sits. :type node_hostname: str + :param sticky: If True, calculate the reward based on the most recent response status. If False, only calculate + the reward if there were any responses this timestep. + :type sticky: bool """ self._node: str = node_hostname self.location_in_state: List[str] = ["network", "nodes", node_hostname, "applications", "DatabaseClient"] - self._last_request_failed: bool = False + self.sticky: bool = sticky + self.reward: float = 0.0 + """Reward value calculated last time any responses were seen. Used for persisting sticky rewards.""" def calculate(self, state: Dict, last_action_response: "AgentHistoryItem") -> float: """ @@ -283,26 +352,33 @@ class GreenAdminDatabaseUnreachablePenalty(AbstractReward): component will keep track of that information. In that case, it doesn't matter whether the last successful request returned was able to connect to the database server, because there has been an unsuccessful request since. + :param state: Current simulation state + :type state: Dict + :param last_action_response: Current agent history state + :type last_action_response: AgentHistoryItem state + :return: Reward value + :rtype: float """ - if last_action_response.request == ["network", "node", self._node, "application", "DatabaseClient", "execute"]: - self._last_request_failed = last_action_response.response.status != "success" + request_attempted = last_action_response.request == [ + "network", + "node", + self._node, + "application", + "DatabaseClient", + "execute", + ] - # if agent couldn't even get as far as sending the request (because for example the node was off), then - # apply a penalty - if self._last_request_failed: - return -1.0 + if request_attempted: # if agent makes request, always recalculate fresh value + last_action_response.reward_info = {"connection_attempt_status": last_action_response.response.status} + self.reward = 1.0 if last_action_response.response.status == "success" else -1.0 + elif not self.sticky: # if no new request and not sticky, set reward to 0 + last_action_response.reward_info = {"connection_attempt_status": "n/a"} + self.reward = 0.0 + else: # if no new request and sticky, reuse reward value from last step + last_action_response.reward_info = {"connection_attempt_status": "n/a"} + pass - # If the last request was actually sent, then check if the connection was established. - db_state = access_from_nested_dict(state, self.location_in_state) - if db_state is NOT_PRESENT_IN_STATE or "last_connection_successful" not in db_state: - _LOGGER.debug(f"Can't calculate reward for {self.__class__.__name__}") - return 0.0 - last_connection_successful = db_state["last_connection_successful"] - if last_connection_successful is False: - return -1.0 - elif last_connection_successful is True: - return 1.0 - return 0.0 + return self.reward @classmethod def from_config(cls, config: Dict) -> AbstractReward: @@ -313,7 +389,8 @@ class GreenAdminDatabaseUnreachablePenalty(AbstractReward): :type config: Dict """ node_hostname = config.get("node_hostname") - return cls(node_hostname=node_hostname) + sticky = config.get("sticky", True) + return cls(node_hostname=node_hostname, sticky=sticky) class SharedReward(AbstractReward): @@ -346,7 +423,15 @@ class SharedReward(AbstractReward): """Method that retrieves an agent's current reward given the agent's name.""" def calculate(self, state: Dict, last_action_response: "AgentHistoryItem") -> float: - """Simply access the other agent's reward and return it.""" + """Simply access the other agent's reward and return it. + + :param state: Current simulation state + :type state: Dict + :param last_action_response: Current agent history state + :type last_action_response: AgentHistoryItem state + :return: Reward value + :rtype: float + """ return self.callback(self.agent_name) @classmethod @@ -379,7 +464,15 @@ class ActionPenalty(AbstractReward): self.do_nothing_penalty = do_nothing_penalty def calculate(self, state: Dict, last_action_response: "AgentHistoryItem") -> float: - """Calculate the penalty to be applied.""" + """Calculate the penalty to be applied. + + :param state: Current simulation state + :type state: Dict + :param last_action_response: Current agent history state + :type last_action_response: AgentHistoryItem state + :return: Reward value + :rtype: float + """ if last_action_response.action == "DONOTHING": return self.do_nothing_penalty else: @@ -436,6 +529,7 @@ class RewardFunction: weight = comp_and_weight[1] total += weight * comp.calculate(state=state, last_action_response=last_action_response) self.current_reward = total + return self.current_reward @classmethod diff --git a/src/primaite/game/agent/scripted_agents/probabilistic_agent.py b/src/primaite/game/agent/scripted_agents/probabilistic_agent.py index f5905ad0..cd44644f 100644 --- a/src/primaite/game/agent/scripted_agents/probabilistic_agent.py +++ b/src/primaite/game/agent/scripted_agents/probabilistic_agent.py @@ -22,8 +22,6 @@ class ProbabilisticAgent(AbstractScriptedAgent): """Strict validation.""" action_probabilities: Dict[int, float] """Probability to perform each action in the action map. The sum of probabilities should sum to 1.""" - random_seed: Optional[int] = None - """Random seed. If set, each episode the agent will choose the same random sequence of actions.""" # TODO: give the option to still set a random seed, but have it vary each episode in a predictable way # for example if the user sets seed 123, have it be 123 + episode_num, so that each ep it's the next seed. @@ -59,17 +57,18 @@ class ProbabilisticAgent(AbstractScriptedAgent): num_actions = len(action_space.action_map) settings = {"action_probabilities": {i: 1 / num_actions for i in range(num_actions)}} - # If seed not specified, set it to None so that numpy chooses a random one. - settings.setdefault("random_seed") - + # The random number seed for np.random is dependent on whether a random number seed is set + # in the config file. If there is one it is processed by set_random_seed() in environment.py + # and as a consequence the the sequence of rng_seed's used here will be repeatable. self.settings = ProbabilisticAgent.Settings(**settings) - - self.rng = np.random.default_rng(self.settings.random_seed) + rng_seed = np.random.randint(0, 65535) + self.rng = np.random.default_rng(rng_seed) # convert probabilities from self.probabilities = np.asarray(list(self.settings.action_probabilities.values())) super().__init__(agent_name, action_space, observation_space, reward_function) + self.logger.debug(f"ProbabilisticAgent RNG seed: {rng_seed}") def get_action(self, obs: ObsType, timestep: int = 0) -> Tuple[str, Dict]: """ diff --git a/src/primaite/game/game.py b/src/primaite/game/game.py index 5ef8c14c..045b2467 100644 --- a/src/primaite/game/game.py +++ b/src/primaite/game/game.py @@ -1,7 +1,7 @@ # © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK """PrimAITE game - Encapsulates the simulation and agents.""" from ipaddress import IPv4Address -from typing import Dict, List, Optional +from typing import Dict, List, Optional, Union import numpy as np from pydantic import BaseModel, ConfigDict @@ -18,7 +18,7 @@ from primaite.game.agent.scripted_agents.tap001 import TAP001 from primaite.game.science import graph_has_cycle, topological_sort from primaite.simulator import SIM_OUTPUT from primaite.simulator.network.airspace import AirSpaceFrequency -from primaite.simulator.network.hardware.base import NodeOperatingState +from primaite.simulator.network.hardware.base import NetworkInterface, NodeOperatingState, UserManager from primaite.simulator.network.hardware.nodes.host.computer import Computer from primaite.simulator.network.hardware.nodes.host.host_node import NIC from primaite.simulator.network.hardware.nodes.host.server import Printer, Server @@ -26,11 +26,14 @@ from primaite.simulator.network.hardware.nodes.network.firewall import Firewall from primaite.simulator.network.hardware.nodes.network.router import Router from primaite.simulator.network.hardware.nodes.network.switch import Switch from primaite.simulator.network.hardware.nodes.network.wireless_router import WirelessRouter -from primaite.simulator.network.nmne import set_nmne_config +from primaite.simulator.network.nmne import NMNEConfig +from primaite.simulator.network.transmission.network_layer import IPProtocol from primaite.simulator.network.transmission.transport_layer import Port from primaite.simulator.sim_container import Simulation from primaite.simulator.system.applications.application import Application from primaite.simulator.system.applications.database_client import DatabaseClient # noqa: F401 +from primaite.simulator.system.applications.red_applications.c2.c2_beacon import C2Beacon # noqa: F401 +from primaite.simulator.system.applications.red_applications.c2.c2_server import C2Server # noqa: F401 from primaite.simulator.system.applications.red_applications.data_manipulation_bot import ( # noqa: F401 DataManipulationBot, ) @@ -44,7 +47,10 @@ from primaite.simulator.system.services.ftp.ftp_client import FTPClient from primaite.simulator.system.services.ftp.ftp_server import FTPServer from primaite.simulator.system.services.ntp.ntp_client import NTPClient from primaite.simulator.system.services.ntp.ntp_server import NTPServer +from primaite.simulator.system.services.service import Service +from primaite.simulator.system.services.terminal.terminal import Terminal from primaite.simulator.system.services.web_server.web_server import WebServer +from primaite.simulator.system.software import Software _LOGGER = getLogger(__name__) @@ -57,6 +63,7 @@ SERVICE_TYPES_MAPPING = { "FTPServer": FTPServer, "NTPClient": NTPClient, "NTPServer": NTPServer, + "Terminal": Terminal, } """List of available services that can be installed on nodes in the PrimAITE Simulation.""" @@ -70,6 +77,8 @@ class PrimaiteGameOptions(BaseModel): model_config = ConfigDict(extra="forbid") + seed: int = None + """Random number seed for RNGs.""" max_episode_length: int = 256 """Maximum number of episodes for the PrimAITE game.""" ports: List[str] @@ -264,9 +273,12 @@ class PrimaiteGame: nodes_cfg = network_config.get("nodes", []) links_cfg = network_config.get("links", []) + # Set the NMNE capture config + NetworkInterface.nmne_config = NMNEConfig(**network_config.get("nmne_config", {})) for node_cfg in nodes_cfg: n_type = node_cfg["type"] + new_node = None if n_type == "computer": new_node = Computer( hostname=node_cfg["hostname"], @@ -316,6 +328,25 @@ class PrimaiteGame: msg = f"invalid node type {n_type} in config" _LOGGER.error(msg) raise ValueError(msg) + + if "users" in node_cfg and new_node.software_manager.software.get("UserManager"): + user_manager: UserManager = new_node.software_manager.software["UserManager"] # noqa + for user_cfg in node_cfg["users"]: + user_manager.add_user(**user_cfg, bypass_can_perform_action=True) + + def _set_software_listen_on_ports(software: Union[Software, Service], software_cfg: Dict): + """Set listener ports on software.""" + listen_on_ports = [] + for port_id in set(software_cfg.get("options", {}).get("listen_on_ports", [])): + port = None + if isinstance(port_id, int): + port = Port(port_id) + elif isinstance(port_id, str): + port = Port[port_id] + if port: + listen_on_ports.append(port) + software.listen_on_ports = set(listen_on_ports) + if "services" in node_cfg: for service_cfg in node_cfg["services"]: new_service = None @@ -329,6 +360,7 @@ class PrimaiteGame: if "fix_duration" in service_cfg.get("options", {}): new_service.fixing_duration = service_cfg["options"]["fix_duration"] + _set_software_listen_on_ports(new_service, service_cfg) # start the service new_service.start() else: @@ -378,6 +410,8 @@ class PrimaiteGame: _LOGGER.error(msg) raise ValueError(msg) + _set_software_listen_on_ports(new_application, application_cfg) + # run the application new_application.run() @@ -422,6 +456,15 @@ class PrimaiteGame: dos_intensity=float(opt.get("dos_intensity", "1.0")), max_sessions=int(opt.get("max_sessions", "1000")), ) + elif application_type == "C2Beacon": + if "options" in application_cfg: + opt = application_cfg["options"] + new_application.configure( + c2_server_ip_address=IPv4Address(opt.get("c2_server_ip_address")), + keep_alive_frequency=(opt.get("keep_alive_frequency", 5)), + masquerade_protocol=IPProtocol[(opt.get("masquerade_protocol", IPProtocol.TCP))], + masquerade_port=Port[(opt.get("masquerade_port", Port.HTTP))], + ) if "network_interfaces" in node_cfg: for nic_num, nic_cfg in node_cfg["network_interfaces"].items(): new_node.connect_nic(NIC(ip_address=nic_cfg["ip_address"], subnet_mask=nic_cfg["subnet_mask"])) @@ -533,10 +576,7 @@ class PrimaiteGame: # Validate that if any agents are sharing rewards, they aren't forming an infinite loop. game.setup_reward_sharing() - # Set the NMNE capture config - set_nmne_config(network_config.get("nmne_config", {})) game.update_agents(game.get_sim_state()) - return game def setup_reward_sharing(self): diff --git a/src/primaite/notebooks/Command-&-Control-E2E-Demonstration.ipynb b/src/primaite/notebooks/Command-&-Control-E2E-Demonstration.ipynb new file mode 100644 index 00000000..45af6c12 --- /dev/null +++ b/src/primaite/notebooks/Command-&-Control-E2E-Demonstration.ipynb @@ -0,0 +1,1826 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Command and Control Application Suite E2E Demonstration\n", + "\n", + "© Crown-owned copyright 2024, Defence Science and Technology Laboratory UK\n", + "\n", + "This notebook demonstrates the current implementation of the command and control (C2) server and beacon applications in primAITE." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Imports\n", + "import yaml\n", + "from primaite.config.load import data_manipulation_config_path\n", + "from primaite.session.environment import PrimaiteGymEnv\n", + "from primaite.simulator.network.hardware.nodes.network.router import Router\n", + "from primaite.simulator.system.applications.red_applications.c2.c2_beacon import C2Beacon\n", + "from primaite.simulator.system.applications.red_applications.c2.c2_server import C2Server\n", + "from primaite.simulator.system.applications.red_applications.c2.abstract_c2 import C2Command\n", + "from primaite.simulator.system.applications.red_applications.ransomware_script import RansomwareScript\n", + "from primaite.simulator.network.hardware.nodes.host.computer import Computer\n", + "from primaite.simulator.network.hardware.nodes.host.server import Server" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## **Notebook Setup** | **Network Configuration:**\n", + "\n", + "This notebook uses the same network setup as UC2. Please refer to the main [UC2-E2E-Demo notebook for further reference](./Data-Manipulation-E2E-Demonstration.ipynb).\n", + "\n", + "However, this notebook replaces the red agent used in UC2 with a custom proxy red agent built for this notebook." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "custom_c2_agent = \"\"\"\n", + " - ref: CustomC2Agent\n", + " team: RED\n", + " type: ProxyAgent\n", + " observation_space: null\n", + " action_space:\n", + " action_list:\n", + " - type: DONOTHING\n", + " - type: NODE_APPLICATION_INSTALL\n", + " - type: NODE_APPLICATION_EXECUTE\n", + " - type: CONFIGURE_C2_BEACON\n", + " - type: C2_SERVER_RANSOMWARE_LAUNCH\n", + " - type: C2_SERVER_RANSOMWARE_CONFIGURE\n", + " - type: C2_SERVER_TERMINAL_COMMAND\n", + " - type: C2_SERVER_DATA_EXFILTRATE\n", + " options:\n", + " nodes:\n", + " - node_name: web_server\n", + " applications: \n", + " - application_name: C2Beacon\n", + " - node_name: client_1\n", + " applications: \n", + " - application_name: C2Server\n", + " max_folders_per_node: 1\n", + " max_files_per_folder: 1\n", + " max_services_per_node: 2\n", + " max_nics_per_node: 8\n", + " max_acl_rules: 10\n", + " ip_list:\n", + " - 192.168.1.21\n", + " - 192.168.1.14\n", + " wildcard_list:\n", + " - 0.0.0.1\n", + " action_map:\n", + " 0:\n", + " action: DONOTHING\n", + " options: {}\n", + " 1:\n", + " action: NODE_APPLICATION_INSTALL\n", + " options:\n", + " node_id: 0\n", + " application_name: C2Beacon\n", + " 2:\n", + " action: CONFIGURE_C2_BEACON\n", + " options:\n", + " node_id: 0\n", + " config:\n", + " c2_server_ip_address: 192.168.10.21\n", + " keep_alive_frequency:\n", + " masquerade_protocol:\n", + " masquerade_port:\n", + " 3:\n", + " action: NODE_APPLICATION_EXECUTE\n", + " options:\n", + " node_id: 0\n", + " application_id: 0 \n", + " 4:\n", + " action: C2_SERVER_TERMINAL_COMMAND\n", + " options:\n", + " node_id: 1\n", + " ip_address:\n", + " account:\n", + " username: admin\n", + " password: admin\n", + " commands:\n", + " - \n", + " - software_manager\n", + " - application\n", + " - install\n", + " - RansomwareScript\n", + " 5:\n", + " action: C2_SERVER_RANSOMWARE_CONFIGURE\n", + " options:\n", + " node_id: 1\n", + " config:\n", + " server_ip_address: 192.168.1.14\n", + " payload: ENCRYPT\n", + " 6:\n", + " action: C2_SERVER_DATA_EXFILTRATE\n", + " options:\n", + " node_id: 1\n", + " target_file_name: \"database.db\"\n", + " target_folder_name: \"database\"\n", + " exfiltration_folder_name: \"spoils\"\n", + " target_ip_address: 192.168.1.14\n", + " account:\n", + " username: admin\n", + " password: admin \n", + "\n", + " 7:\n", + " action: C2_SERVER_RANSOMWARE_LAUNCH\n", + " options:\n", + " node_id: 1\n", + " 8:\n", + " action: CONFIGURE_C2_BEACON\n", + " options:\n", + " node_id: 0\n", + " config:\n", + " c2_server_ip_address: 192.168.10.21\n", + " keep_alive_frequency: 10\n", + " masquerade_protocol: TCP\n", + " masquerade_port: DNS\n", + " 9:\n", + " action: CONFIGURE_C2_BEACON\n", + " options:\n", + " node_id: 0\n", + " config:\n", + " c2_server_ip_address: 192.168.10.22\n", + " keep_alive_frequency:\n", + " masquerade_protocol:\n", + " masquerade_port:\n", + "\n", + " reward_function:\n", + " reward_components:\n", + " - type: DUMMY\n", + "\"\"\"\n", + "c2_agent_yaml = yaml.safe_load(custom_c2_agent)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "with open(data_manipulation_config_path()) as f:\n", + " cfg = yaml.safe_load(f)\n", + " # removing all agents & adding the custom agent.\n", + " cfg['agents'] = {}\n", + " cfg['agents'] = c2_agent_yaml\n", + " \n", + "\n", + "env = PrimaiteGymEnv(env_config=cfg)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## **Notebook Setup** | Network Prerequisites\n", + "\n", + "Before the Red Agent is able to perform any C2 specific actions, the C2 Server needs to be installed and run.\n", + "This is because in higher fidelity environments (and the real-world) a C2 server would not be accessible by a private network blue agent and the C2 Server would already be in place before the an adversary (Red Agent) starts.\n", + "\n", + "The cells below install and run the C2 Server on client_1 directly via the simulation API." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "client_1: Computer = env.game.simulation.network.get_node_by_hostname(\"client_1\")\n", + "client_1.software_manager.install(C2Server)\n", + "c2_server: C2Server = client_1.software_manager.software[\"C2Server\"]\n", + "c2_server.run()\n", + "client_1.software_manager.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## **Command and Control** | C2 Beacon Actions\n", + "\n", + "Before a C2 Server can accept any commands it must first establish connection with a C2 Beacon.\n", + "\n", + "A red agent is able to install, configure and establish a C2 beacon at any point in an episode. The code cells below demonstrate the actions and option parameters that are needed to perform this." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### **Command and Control** | C2 Beacon Actions | NODE_APPLICATION_INSTALL\n", + "\n", + "The custom proxy red agent defined at the start of this notebook has been configured to install the C2 Beacon as action ``1`` in it's action map. \n", + "\n", + "The below yaml snippet shows all the relevant agent options for this action:\n", + "\n", + "```yaml\n", + " action_space:\n", + " action_list:\n", + " ...\n", + " - type: NODE_APPLICATION_INSTALL\n", + " ...\n", + " options:\n", + " nodes: # Node List\n", + " - node_name: web_server\n", + " applications: \n", + " - application_name: C2Beacon\n", + " ...\n", + " ...\n", + " action_map:\n", + " 1:\n", + " action: NODE_APPLICATION_INSTALL \n", + " options:\n", + " node_id: 0 # Index 0 at the node list.\n", + " application_name: C2Beacon\n", + "```" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "env.step(1)\n", + "web_server: Computer = env.game.simulation.network.get_node_by_hostname(\"web_server\")\n", + "web_server.software_manager.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### **Command and Control** | C2 Beacon Actions | CONFIGURE_C2_BEACON \n", + "\n", + "The custom proxy red agent defined at the start of this notebook can configure the C2 Beacon via action ``2`` in it's action map. \n", + "\n", + "The yaml snippet below shows all the relevant agent options for this action:\n", + "\n", + "```yaml\n", + " action_space:\n", + " action_list:\n", + " ...\n", + " - type: CONFIGURE_C2_BEACON\n", + " ...\n", + " options:\n", + " nodes: # Node List\n", + " - node_name: web_server\n", + " ...\n", + " ...\n", + " action_map:\n", + " ...\n", + " 2:\n", + " action: CONFIGURE_C2_BEACON\n", + " options:\n", + " node_id: 0 # Node Index\n", + " config: # Further information about these config options can be found at the bottom of this notebook.\n", + " c2_server_ip_address: 192.168.10.21\n", + " keep_alive_frequency:\n", + " masquerade_protocol:\n", + " masquerade_port:\n", + "```" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "env.step(2)\n", + "c2_beacon: C2Beacon = web_server.software_manager.software[\"C2Beacon\"]\n", + "web_server.software_manager.show()\n", + "c2_beacon.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### **Command and Control** | C2 Beacon Actions | NODE_APPLICATION_EXECUTE\n", + "\n", + "The final action is ``NODE_APPLICATION_EXECUTE`` which is used to establish a connection for the C2 application. This action can be called by the Red Agent via action ``3`` in it's action map. \n", + "\n", + "The yaml snippet below shows all the relevant agent options for this action:\n", + "\n", + "```yaml\n", + " action_space:\n", + " action_list:\n", + " ...\n", + " - type: NODE_APPLICATION_EXECUTE\n", + " ...\n", + " options:\n", + " nodes: # Node List\n", + " - node_name: web_server\n", + " applications: \n", + " - application_name: C2Beacon\n", + " ...\n", + " ...\n", + " action_map:\n", + " ...\n", + " 3:\n", + " action: NODE_APPLICATION_EXECUTE\n", + " options:\n", + " node_id: 0\n", + " application_id: 0\n", + "```" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "env.step(3) " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "c2_beacon.show()\n", + "c2_server.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## **Command and Control** | C2 Server Actions\n", + "\n", + "Once the C2 suite has been successfully established, the C2 Server based actions become available to the Red Agent. \n", + "\n", + "\n", + "This next section will demonstrate the different actions that become available to a red agent after establishing a C2 connection:" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### **Command and Control** | C2 Server Actions | C2_SERVER_TERMINAL_COMMAND\n", + "\n", + "The C2 Server's terminal action: ``C2_SERVER_TERMINAL_COMMAND`` is indexed at ``4`` in it's action map. \n", + "\n", + "This action leverages the terminal service that is installed by default on all nodes to grant red agents a lot more configurability. If you're unfamiliar with terminals then it's recommended that you refer to the ``Terminal Processing`` notebook.\n", + "\n", + "It's worth noting that an additional benefit a red agent has when using the terminal service via the C2 Server is that you can execute multiple commands in one action. \n", + "\n", + "In this notebook, the ``C2_SERVER_TERMINAL_COMMAND`` is used to install a RansomwareScript application on the ``web_server`` node.\n", + "\n", + "The yaml snippet below shows all the relevant agent options for this action:\n", + "\n", + "``` yaml\n", + " action_space:\n", + " action_list:\n", + " ...\n", + " - type: C2_SERVER_TERMINAL_COMMAND\n", + " ...\n", + " options:\n", + " nodes: # Node List\n", + " ...\n", + " - node_name: client_1\n", + " applications: \n", + " - application_name: C2Server\n", + " ...\n", + " action_map:\n", + " 4:\n", + " action: C2_SERVER_TERMINAL_COMMAND\n", + " options:\n", + " node_id: 1\n", + " ip_address:\n", + " account:\n", + " username: admin\n", + " password: admin\n", + " commands:\n", + " - \n", + " - software_manager\n", + " - application\n", + " - install\n", + " - RansomwareScript\n", + "```" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "env.step(4)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "client_1.software_manager.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### **Command and Control** | C2 Server Actions | C2_SERVER_RANSOMWARE_CONFIGURE\n", + "\n", + "Another action the C2 Server grants is the ability for a Red Agent to configure the RansomwareScript via the C2 Server rather than the note directly.\n", + "\n", + "This action is indexed as action ``5``.\n", + "\n", + "The yaml snippet below shows all the relevant agent options for this action:\n", + "\n", + "``` yaml\n", + " action_space:\n", + " action_list:\n", + " ...\n", + " - type: C2_SERVER_RANSOMWARE_CONFIGURE\n", + " ...\n", + " options:\n", + " nodes: # Node List\n", + " ...\n", + " - node_name: client_1\n", + " applications: \n", + " - application_name: C2Server\n", + " ...\n", + " action_map:\n", + " 5:\n", + " action: C2_SERVER_RANSOMWARE_CONFIG\n", + " options:\n", + " node_id: 1\n", + " config:\n", + " server_ip_address: 192.168.1.14\n", + " payload: ENCRYPT\n", + "```\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "env.step(5)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "ransomware_script: RansomwareScript = web_server.software_manager.software[\"RansomwareScript\"]\n", + "web_server.software_manager.show()\n", + "ransomware_script.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### **Command and Control** | C2 Server Actions | C2_SERVER_DATA_EXFILTRATE\n", + "\n", + "The second to last action available is the ``C2_SERVER_DATA_EXFILTRATE`` which is indexed as action ``6`` in the action map.\n", + "\n", + "This action can be used to exfiltrate a target file on a remote node to the C2 Beacon and the C2 Server's host file system via the ``FTP`` services.\n", + "\n", + "The below yaml snippet shows all the relevant agent options for this action:\n", + "\n", + "``` yaml\n", + " action_space:\n", + " action_list:\n", + " ...\n", + " - type: C2_SERVER_DATA_EXFILTRATE\n", + " ...\n", + " options:\n", + " nodes: # Node List\n", + " ...\n", + " - node_name: client_1\n", + " applications: \n", + " - application_name: C2Server\n", + " ...\n", + " action_map:\n", + " 6:\n", + " action: C2_SERVER_DATA_EXFILTRATE\n", + " options:\n", + " node_id: 1\n", + " target_file_name: \"database.db\"\n", + " target_folder_name: \"database\"\n", + " exfiltration_folder_name: \"spoils\"\n", + " target_ip_address: \"192.168.1.14\"\n", + " account:\n", + " username: \"admin\",\n", + " password: \"admin\"\n", + "\n", + "```" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "env.step(6)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "client_1: Computer = env.game.simulation.network.get_node_by_hostname(\"client_1\")\n", + "client_1.software_manager.file_system.show(full=True)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "web_server: Computer = env.game.simulation.network.get_node_by_hostname(\"web_server\")\n", + "web_server.software_manager.file_system.show(full=True)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### **Command and Control** | C2 Server Actions | C2_SERVER_RANSOMWARE_LAUNCH\n", + "\n", + "Finally, the last available action is for the C2_SERVER_RANSOMWARE_LAUNCH to start the ransomware script installed on the same node as the C2 beacon.\n", + "\n", + "This action is indexed as action ``7``.\n", + "\n", + "\"The yaml snippet below shows all the relevant agent options for this action:\n", + "\n", + "``` yaml\n", + " action_space:\n", + " action_list:\n", + " ...\n", + " - type: C2_SERVER_RANSOMWARE_LAUNCH\n", + " ...\n", + " options:\n", + " nodes: # Node List\n", + " ...\n", + " - node_name: client_1\n", + " applications: \n", + " - application_name: C2Server\n", + " ...\n", + " action_map:\n", + " 7:\n", + " action: C2_SERVER_RANSOMWARE_LAUNCH\n", + " options:\n", + " node_id: 1\n", + "```\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "env.step(7)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "database_server: Server = env.game.simulation.network.get_node_by_hostname(\"database_server\")\n", + "database_server.software_manager.file_system.show(full=True)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## **Command and Control** | Blue Agent Relevance\n", + "\n", + "The next section of the notebook will demonstrate the impact the command and control suite has on the Blue Agent's observation space as well as some potential actions that can be used to prevent the attack from being successful.\n", + "\n", + "The code cell below recreates the UC2 network and swaps out the previous custom red agent with a custom blue agent. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "custom_blue_agent_yaml = \"\"\" \n", + " - ref: defender\n", + " team: BLUE\n", + " type: ProxyAgent\n", + "\n", + " observation_space:\n", + " type: CUSTOM\n", + " options:\n", + " components:\n", + " - type: NODES\n", + " label: NODES\n", + " options:\n", + " hosts:\n", + " - hostname: web_server\n", + " applications:\n", + " - application_name: C2Beacon\n", + " - application_name: RansomwareScript\n", + " folders:\n", + " - folder_name: exfiltration_folder\n", + " files:\n", + " - file_name: database.db\n", + " - hostname: database_server\n", + " folders:\n", + " - folder_name: exfiltration_folder\n", + " files:\n", + " - file_name: database.db\n", + " - hostname: client_1\n", + " - hostname: client_2\n", + " num_services: 0\n", + " num_applications: 2\n", + " num_folders: 1\n", + " num_files: 1\n", + " num_nics: 1\n", + " include_num_access: true\n", + " include_nmne: false\n", + " monitored_traffic:\n", + " icmp:\n", + " - NONE\n", + " tcp:\n", + " - HTTP\n", + " - DNS\n", + " - FTP\n", + " routers:\n", + " - hostname: router_1\n", + " num_ports: 3\n", + " ip_list:\n", + " - 192.168.1.10\n", + " - 192.168.1.12\n", + " - 192.168.1.14\n", + " - 192.168.1.16\n", + " - 192.168.1.110\n", + " - 192.168.10.21\n", + " - 192.168.10.22\n", + " - 192.168.10.110\n", + " wildcard_list:\n", + " - 0.0.0.1\n", + " port_list:\n", + " - 80\n", + " - 53\n", + " - 21\n", + " protocol_list:\n", + " - ICMP\n", + " - TCP\n", + " - UDP\n", + " num_rules: 10\n", + "\n", + " - type: LINKS\n", + " label: LINKS\n", + " options:\n", + " link_references:\n", + " - router_1:eth-1<->switch_1:eth-8\n", + " - router_1:eth-2<->switch_2:eth-8\n", + " - switch_1:eth-1<->web_server:eth-1\n", + " - switch_1:eth-2<->web_server:eth-1\n", + " - switch_1:eth-3<->database_server:eth-1\n", + " - switch_1:eth-4<->backup_server:eth-1\n", + " - switch_1:eth-7<->security_suite:eth-1\n", + " - switch_2:eth-1<->client_1:eth-1\n", + " - switch_2:eth-2<->client_2:eth-1\n", + " - switch_2:eth-7<->security_suite:eth-2\n", + " - type: \"NONE\"\n", + " label: ICS\n", + " options: {}\n", + " \n", + " action_space:\n", + " action_list:\n", + " - type: NODE_APPLICATION_REMOVE\n", + " - type: NODE_SHUTDOWN\n", + " - type: ROUTER_ACL_ADDRULE\n", + " - type: DONOTHING\n", + " action_map:\n", + " 0:\n", + " action: DONOTHING\n", + " options: {}\n", + " 1:\n", + " action: NODE_APPLICATION_REMOVE\n", + " options:\n", + " node_id: 0\n", + " application_name: C2Beacon\n", + " 2:\n", + " action: NODE_SHUTDOWN\n", + " options:\n", + " node_id: 0\n", + " 3:\n", + " action: ROUTER_ACL_ADDRULE\n", + " options:\n", + " target_router: router_1\n", + " position: 1\n", + " permission: 2\n", + " source_ip_id: 2\n", + " dest_ip_id: 3\n", + " source_port_id: 2\n", + " dest_port_id: 2\n", + " protocol_id: 1\n", + " source_wildcard_id: 0\n", + " dest_wildcard_id: 0 \n", + "\n", + "\n", + " options:\n", + " nodes:\n", + " - node_name: web_server\n", + " applications:\n", + " - application_name: C2Beacon\n", + "\n", + " - node_name: database_server\n", + " folders:\n", + " - folder_name: database\n", + " files:\n", + " - file_name: database.db\n", + " services:\n", + " - service_name: DatabaseService\n", + " - node_name: router_1\n", + "\n", + " max_folders_per_node: 2\n", + " max_files_per_folder: 2\n", + " max_services_per_node: 2\n", + " max_nics_per_node: 8\n", + " max_acl_rules: 10\n", + " ip_list:\n", + " - 192.168.10.21\n", + " - 192.168.1.12\n", + " wildcard_list:\n", + " - 0.0.0.1\n", + " reward_function:\n", + " reward_components:\n", + " - type: DUMMY\n", + "\n", + " agent_settings:\n", + " flatten_obs: False\n", + "\"\"\"\n", + "custom_blue = yaml.safe_load(custom_blue_agent_yaml)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "with open(data_manipulation_config_path()) as f:\n", + " cfg = yaml.safe_load(f)\n", + " # removing all agents & adding the custom agent.\n", + " cfg['agents'] = {}\n", + " cfg['agents'] = custom_blue\n", + " \n", + "\n", + "blue_env = PrimaiteGymEnv(env_config=cfg)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Utility function for showing OBS changes between each time step.\n", + "\n", + "from deepdiff.diff import DeepDiff\n", + "\n", + "def display_obs_diffs(old, new, step_counter):\n", + " \"\"\"\n", + " Use DeepDiff to extract and display differences in old and new instances of\n", + " the observation space.\n", + "\n", + " :param old: observation space instance.\n", + " :param new: observation space instance.\n", + " :param step_counter: current step counter.\n", + " \"\"\"\n", + " print(\"\\nObservation space differences\")\n", + " print(\"-----------------------------\")\n", + " diff = DeepDiff(old, new)\n", + " print(f\"Step {step_counter}\")\n", + " for d,v in diff.get('values_changed', {}).items():\n", + " print(f\"{d}: {v['old_value']} -> {v['new_value']}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### **Command and Control** | Blue Agent Relevance | Observation Space\n", + "\n", + "This section demonstrates the impacts that each of that the C2 Beacon and the C2 Server's commands cause on the observation space (OBS)." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### **Command and Control** | OBS Impact | C2 Beacon | Installation & Configuration" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Resetting the environment and capturing the default observation space.\n", + "blue_env.reset()\n", + "default_obs, _, _, _, _ = blue_env.step(0)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Setting up the C2 Suite via the simulation API.\n", + "\n", + "client_1: Computer = blue_env.game.simulation.network.get_node_by_hostname(\"client_1\")\n", + "web_server: Server = blue_env.game.simulation.network.get_node_by_hostname(\"web_server\")\n", + "\n", + "# Installing the C2 Server.\n", + "client_1.software_manager.install(C2Server)\n", + "c2_server: C2Server = client_1.software_manager.software[\"C2Server\"]\n", + "c2_server.run()\n", + "\n", + "# Installing the C2 Beacon.\n", + "web_server.software_manager.install(C2Beacon)\n", + "c2_beacon: C2Beacon = web_server.software_manager.software[\"C2Beacon\"]\n", + "c2_beacon.configure(c2_server_ip_address=\"192.168.10.21\")\n", + "c2_beacon.establish()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Capturing the observation impacts of the previous code cell: C2 Suite setup.\n", + "c2_configuration_obs, _, _, _, _ = blue_env.step(0)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "display_obs_diffs(default_obs, c2_configuration_obs, blue_env.game.step_counter)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### **Command and Control** | OBS Impact | C2 Server | Terminal Command\n", + "\n", + "Using the C2 Server's ``TERMINAL`` command it is possible to install a ``RansomwareScript`` application onto the C2 Beacon's host.\n", + "\n", + "The below code cells perform this as well as capturing the OBS impacts.\n", + "\n", + "It's important to note that the ``TERMINAL`` command is not limited to just installing software." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Installing RansomwareScript via C2 Terminal Commands\n", + "ransomware_install_command = {\"commands\":[[\"software_manager\", \"application\", \"install\", \"RansomwareScript\"]],\n", + " \"username\": \"admin\",\n", + " \"password\": \"admin\"}\n", + "c2_server.send_command(C2Command.TERMINAL, command_options=ransomware_install_command)\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Configuring the RansomwareScript\n", + "ransomware_config = {\"server_ip_address\": \"192.168.1.14\", \"payload\": \"ENCRYPT\"}\n", + "c2_server.send_command(C2Command.RANSOMWARE_CONFIGURE, command_options=ransomware_config)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Capturing the observation impacts of the previous code cell: Ransomware installation & configuration.\n", + "c2_ransomware_obs, _, _, _, _ = blue_env.step(0)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "display_obs_diffs(default_obs, c2_ransomware_obs, env.game.step_counter)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### **Command and Control** | OBS Impact | C2 Server | Data Exfiltration\n", + "\n", + "Before encrypting the database.db file, the ``DATA_EXFILTRATION`` command can be used to copy the database.db file onto both the C2 Server and the C2 Beacon's file systems:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "exfil_options={\n", + " \"username\": \"admin\",\n", + " \"password\": \"admin\",\n", + " \"target_ip_address\": \"192.168.1.14\",\n", + " \"target_folder_name\": \"database\",\n", + " \"exfiltration_folder_name\": \"exfiltration_folder\",\n", + " \"target_file_name\": \"database.db\",\n", + "}" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "c2_server.send_command(given_command=C2Command.DATA_EXFILTRATION, command_options=exfil_options)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "c2_exfil_obs, _, _, _, _ = blue_env.step(0)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "display_obs_diffs(c2_ransomware_obs, c2_exfil_obs, env.game.step_counter)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### **Command and Control** | OBS Impact | C2 Server | Ransomware Commands\n", + "\n", + "The code cell below demonstrates the differences between the ransomware script installation obs and the impact of RansomwareScript upon the database." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Configuring the RansomwareScript\n", + "ransomware_config = {\"server_ip_address\": \"192.168.1.14\", \"payload\": \"ENCRYPT\"}\n", + "c2_server.send_command(C2Command.RANSOMWARE_CONFIGURE, command_options=ransomware_config)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Waiting for the ransomware to finish installing and then launching the RansomwareScript.\n", + "blue_env.step(0)\n", + "c2_server.send_command(C2Command.RANSOMWARE_LAUNCH, command_options={})" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Capturing the observation impacts of the previous code cell: Launching the RansomwareScript.\n", + "c2_final_obs, _, _, _, _ = blue_env.step(0)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "display_obs_diffs(c2_ransomware_obs, c2_final_obs, blue_env.game.step_counter)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### **Command and Control** | Blue Agent Relevance | Action Space\n", + "\n", + "The next section of this notebook will go over some potential blue agent actions that could be use to thwart the previously demonstrated attack." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# This method is used to simplify setting up the C2Server and the C2 Beacon.\n", + "def c2_setup(given_env: PrimaiteGymEnv):\n", + " client_1: Computer = given_env.game.simulation.network.get_node_by_hostname(\"client_1\")\n", + " web_server: Server = given_env.game.simulation.network.get_node_by_hostname(\"web_server\")\n", + "\n", + " client_1.software_manager.install(C2Server)\n", + " c2_server: C2Server = client_1.software_manager.software[\"C2Server\"]\n", + " c2_server.run()\n", + "\n", + " web_server.software_manager.install(C2Beacon)\n", + " c2_beacon: C2Beacon = web_server.software_manager.software[\"C2Beacon\"]\n", + " c2_beacon.configure(c2_server_ip_address=\"192.168.10.21\")\n", + " c2_beacon.establish()\n", + "\n", + " return given_env, c2_server, c2_beacon, client_1, web_server" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Removing the C2 Beacon.\n", + "\n", + "The simplest way a blue agent could prevent the C2 suite is by simply removing the C2 beacon from it's installation point. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "blue_env.reset()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Setting up the C2 Suite using the c2_setup method & capturing the OBS impacts\n", + "\n", + "blue_env, c2_server, c2_beacon, client_1, web_server = c2_setup(given_env=blue_env)\n", + "pre_blue_action_obs, _, _, _, _ = blue_env.step(0)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The code cell below uses the custom blue agent defined at the start of this section perform a NODE_APPLICATION_REMOVE on the C2 beacon:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Using CAOS ACTION: NODE_APPLICATION_REMOVE & capturing the OBS\n", + "post_blue_action_obs, _, _, _, _ = blue_env.step(1)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Which we can see after the effects of after stepping another timestep and looking at the web_servers software manager and the OBS differences." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "blue_env.step(0)\n", + "web_server.software_manager.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "display_obs_diffs(pre_blue_action_obs, post_blue_action_obs, blue_env.game.step_counter)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now we are unable to do so as the C2 Server has lost its connection to the C2 Beacon:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Attempting to install the C2 RansomwareScript\n", + "ransomware_install_command = {\"commands\":[[\"software_manager\", \"application\", \"install\", \"RansomwareScript\"]],\n", + " \"username\": \"admin\",\n", + " \"password\": \"admin\"}\n", + "\n", + "c2_server: C2Server = client_1.software_manager.software[\"C2Server\"]\n", + "c2_server.send_command(C2Command.TERMINAL, command_options=ransomware_install_command)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Shutting down the node infected with a C2 Beacon.\n", + "\n", + "Another way a blue agent can prevent the C2 suite is by shutting down the C2 beacon's host node. Whilst not as effective as the previous option, depending on the situation (such as multiple malicious applications) or other scenarios it may be more timestep efficient for a blue agent to shut down a node directly." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "blue_env.reset()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Setting up the C2 Suite using the c2_setup method & capturing the OBS impacts\n", + "\n", + "blue_env, c2_server, c2_beacon, client_1, web_server = c2_setup(given_env=blue_env)\n", + "pre_blue_action_obs, _, _, _, _ = blue_env.step(0)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The code cell below uses the custom blue agent defined at the start of this section to perform a ``NODE_SHUT_DOWN`` action on the web server." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Using CAOS ACTION: NODE_SHUT_DOWN & capturing the OBS\n", + "post_blue_action_obs, _, _, _, _ = blue_env.step(2)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Which we can see the effects of after another timestep and looking at the web server's operating state & the OBS differences." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "web_server = blue_env.game.simulation.network.get_node_by_hostname(\"web_server\")\n", + "print(web_server.operating_state)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "display_obs_diffs(pre_blue_action_obs, post_blue_action_obs, blue_env.game.step_counter)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Attempting to install the C2 RansomwareScript\n", + "ransomware_install_command = {\"commands\":[\"software_manager\", \"application\", \"install\", \"RansomwareScript\"],\n", + " \"username\": \"admin\",\n", + " \"password\": \"admin\"}\n", + "\n", + "c2_server: C2Server = client_1.software_manager.software[\"C2Server\"]\n", + "c2_server.send_command(C2Command.TERMINAL, command_options=ransomware_install_command)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Blocking C2 Traffic via ACL.\n", + "\n", + "Another potential option a blue agent could take is by placing an ACL rule which blocks traffic between the C2 Server and C2 Beacon.\n", + "\n", + "It's worth noting the potential effectiveness of this approach is connected to the current green agent traffic on the network. For example, if there are multiple green agents using the C2 Beacon's host node then blocking all traffic would lead to a negative reward. The same applies for the previous example." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "blue_env.reset()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Setting up the C2 Suite using the c2_setup method & capturing the OBS impacts\n", + "\n", + "blue_env, c2_server, c2_beacon, client_1, web_server = c2_setup(given_env=blue_env)\n", + "pre_blue_action_obs, _, _, _, _ = blue_env.step(0)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The code cell below uses the custom blue agent defined at the start of this section to perform a ROUTER_ACL_ADDRULE on router 1." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Using CAOS ACTION: ROUTER_ACL_ADDRULE & capturing the OBS\n", + "post_blue_action_obs, _, _, _, _ = blue_env.step(3)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Which we can see the effects of after another timestep and looking at router 1's ACLs and the OBS differences." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "router_1: Router = blue_env.game.simulation.network.get_node_by_hostname(\"router_1\")\n", + "router_1.acl.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now we can see that the C2 applications are unable to maintain connection - thus being unable to execute correctly." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "blue_env.step(0)\n", + "\n", + "# Attempting to install and execute the ransomware script\n", + "c2_server.send_command(C2Command.TERMINAL, command_options=ransomware_install_command)\n", + "c2_server.send_command(C2Command.RANSOMWARE_LAUNCH, command_options={})" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "router_1.acl.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Because of the ACL rule the C2 beacon never received the ransomware installation and execute commands from the C2 server:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "web_server.software_manager.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "database_server: Server = blue_env.game.simulation.network.get_node_by_hostname(\"database_server\")\n", + "database_server.software_manager.file_system.show(full=True)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "display_obs_diffs(pre_blue_action_obs, post_blue_action_obs, blue_env.game.step_counter)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## **Command and Control** | Configurability \n", + "\n", + "This section of the notebook demonstrates the C2 configuration options and their impact on the simulation layer and the game layer." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The table below is the currently offered C2 Beacon configuration options:\n", + "\n", + "|Configuration Option | Option Meaning |Default Option | Type | _Optional_ |\n", + "|---------------------|---------------------------------------------------------------------------|---------------|---------|------------|\n", + "|c2_server_ip_address | The IP Address of the C2 Server. (The C2 Server must be running) |_None_ |str (IP) | _No_ |\n", + "|keep_alive_frequency | How often should the C2 Beacon confirm it's connection in timesteps. |5 |Int | _Yes_ |\n", + "|masquerade_port | What port should the C2 traffic use? (TCP or UDP) |TCP |Str | _Yes_ |\n", + "|masquerade_protocol | What protocol should the C2 traffic masquerade as? (HTTP, FTP or DNS) |HTTP |Str | _Yes_ |\n", + "\n", + "The C2 Server currently does not offer any unique configuration options. The C2 Server aligns itself with the C2 Beacon's configuration options once connection is established." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "As demonstrated earlier, red agents can use the ``CONFIGURE_C2_BEACON`` action to configure these settings mid episode through the configuration options:\n", + "\n", + "``` YAML\n", + "...\n", + " action: CONFIGURE_C2_BEACON\n", + " options:\n", + " node_id: 0\n", + " config:\n", + " c2_server_ip_address: 192.168.10.21\n", + " keep_alive_frequency: 10\n", + " masquerade_protocol: TCP\n", + " masquerade_port: DNS\n", + "```" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### **Command and Control** | Configurability | C2 Server IP Address\n", + "\n", + "As with a majority of client and server based application configurations in primaite, the remote IP of a server must be supplied.\n", + "\n", + "In the case of the C2 Beacon, the C2 Server's IP address must be supplied before the C2 beacon will be able to perform any other actions (including ``APPLICATION EXECUTE``).\n", + "\n", + "If the network contains multiple C2 Servers then it's also possible to switch to a different C2 server mid-episode which is demonstrated in the below code cells." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "with open(data_manipulation_config_path()) as f:\n", + " cfg = yaml.safe_load(f)\n", + " # removing all agents & adding the custom agent.\n", + " cfg['agents'] = {}\n", + " cfg['agents'] = c2_agent_yaml\n", + " \n", + "\n", + "c2_config_env = PrimaiteGymEnv(env_config=cfg)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Installing the C2 Server on both client 1 and client 2." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "web_server: Server = c2_config_env.game.simulation.network.get_node_by_hostname(\"web_server\")\n", + "web_server.software_manager.install(C2Beacon)\n", + "c2_beacon: C2Beacon = web_server.software_manager.software[\"C2Beacon\"]\n", + "\n", + "client_1: Computer = c2_config_env.game.simulation.network.get_node_by_hostname(\"client_1\")\n", + "client_1.software_manager.install(C2Server)\n", + "c2_server_1: C2Server = client_1.software_manager.software[\"C2Server\"]\n", + "c2_server_1.run()\n", + "\n", + "client_2: Computer = c2_config_env.game.simulation.network.get_node_by_hostname(\"client_2\")\n", + "client_2.software_manager.install(C2Server)\n", + "c2_server_2: C2Server = client_2.software_manager.software[\"C2Server\"]\n", + "c2_server_2.run()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Configuring the C2 Beacon to establish connection to the C2 Server on client_1 (192.168.10.21)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "env.step(2) # Agent Action Equivalent to c2_beacon.configure(c2_server_ip_address=\"192.168.10.21\")\n", + "env.step(3) # Agent action Equivalent to c2_beacon.establish()\n", + "c2_beacon.show()\n", + "c2_server_1.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now reconfiguring the C2 Beacon to establish connection to the C2 Server on client_2 (192.168.10.22)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "env.step(9) # Equivalent of to c2_beacon.configure(c2_server_ip_address=\"192.168.10.22\")\n", + "env.step(3)\n", + "\n", + "c2_beacon.show()\n", + "c2_server_2.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "After six timesteps the client_1 server will recognise the C2 beacon's previous connection as dead and clear its connections. (This is dependent on the ``Keep Alive Frequency`` setting.)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "for i in range(6):\n", + " env.step(0)\n", + " \n", + "c2_server_1.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### **Command and Control** | Configurability | Keep Alive Frequency\n", + "\n", + "In order to confirm it's connection the C2 Beacon will send out a ``Keep Alive`` to the C2 Server and receive a keep alive back. \n", + "\n", + "By default, this occurs every 5 timesteps. However, this setting can be configured to be much more infrequent or as frequent as every timestep. \n", + "\n", + "The next set of code cells below demonstrate the impact that this setting has on blue agent observation space." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "with open(data_manipulation_config_path()) as f:\n", + " cfg = yaml.safe_load(f)\n", + " # removing all agents & adding the custom agent.\n", + " cfg['agents'] = {}\n", + " cfg['agents'] = custom_blue\n", + " cfg['agents'][0]['observation_space']['options']['components'][0]['options']['num_ports'] = 3\n", + " cfg['agents'][0]['observation_space']['options']['components'][0]['options']['monitored_traffic'].update({\"tcp\": [\"HTTP\",\"FTP\"]})\n", + " cfg['agents'][0]['observation_space']['options']['components'][0]['options']['monitored_traffic'].update({\"udp\": [\"DNS\"]})\n", + "\n", + "blue_config_env = PrimaiteGymEnv(env_config=cfg)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Performing the usual c2 setup:\n", + "blue_config_env, c2_server, c2_beacon, client_1, web_server = c2_setup(given_env=blue_config_env)\n", + "\n", + "# Flushing out the OBS impacts from setting up the C2 suite.\n", + "blue_config_env.step(0)\n", + "blue_config_env.step(0)\n", + "\n", + "# Capturing the 'default' obs (Post C2 installation and configuration):\n", + "default_obs, _, _, _, _ = blue_config_env.step(0)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The next code cells capture the obs impact of the default Keep Alive Frequency which is 5 timesteps:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "c2_beacon.configure(c2_server_ip_address=\"192.168.10.21\")\n", + "c2_beacon.establish()\n", + "c2_beacon.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The code cell below executes 10 timesteps and displays the differences between the default and the current timestep.\n", + "\n", + "You will notice that the only two timesteps displayed observation space differences. This is due to the C2 Suite confirming their connection through sending ``Keep Alive`` traffic across the network every 5 timesteps." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "for i in range(10):\n", + " keep_alive_obs, _, _, _, _ = blue_config_env.step(0)\n", + " display_obs_diffs(default_obs, keep_alive_obs, blue_config_env.game.step_counter)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Next, the code cells below configure the C2 Beacon to confirm connection on every timestep via changing the ``keep_alive_frequency`` to ``1``." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "c2_beacon.configure(c2_server_ip_address=\"192.168.10.21\", keep_alive_frequency=1)\n", + "c2_beacon.establish()\n", + "c2_beacon.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Demonstrating that the observation impacts of the Keep Alive can be seen on every timestep:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Comparing the OBS of the default frequency to a timestep frequency of 1 \n", + "for i in range(2):\n", + " keep_alive_obs, _, _, _, _ = blue_config_env.step(0)\n", + " display_obs_diffs(default_obs, keep_alive_obs, blue_config_env.game.step_counter)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Lastly, the keep_alive_frequency can also be used to configure the C2 Beacon to confirm connection less frequently. \n", + "\n", + "The code cells below demonstrate the impacts of changing the frequency rate to ``7`` timesteps." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "c2_beacon.configure(c2_server_ip_address=\"192.168.10.21\", keep_alive_frequency=7)\n", + "\n", + "# Comparing the OBS of the default frequency to a timestep frequency of 7\n", + "for i in range(7):\n", + " keep_alive_obs, _, _, _, _ = blue_config_env.step(0)\n", + " display_obs_diffs(default_obs, keep_alive_obs, blue_config_env.game.step_counter)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### **Command and Control** | Configurability | Masquerade Port & Masquerade Protocol\n", + "\n", + "The final configurable options are ``Masquerade Port`` & ``Masquerade Protocol``. These options can be used to control the networking IP Protocol and Port the C2 traffic is currently using.\n", + "\n", + "In the real world, adversaries take defensive steps to reduce the chance that an installed C2 Beacon is discovered. One of the most commonly used methods is to masquerade C2 traffic as other commonly used networking protocols.\n", + "\n", + "In primAITE, red agents can begin to simulate stealth behaviour by configuring C2 traffic to use different protocols mid episode or between episodes.\n", + "\n", + "Currently, red agent actions support the following port and protocol options:\n", + "\n", + "| Supported Ports | Supported Protocols |\n", + "|------------------|---------------------|\n", + "|``DNS`` | ``UDP`` |\n", + "|``FTP`` | ``TCP`` |\n", + "|``HTTP`` | |\n", + "\n", + "\n", + "\n", + "The next set of code cells will demonstrate the impact of this option from a blue agent perspective." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "blue_config_env.reset()\n", + "\n", + "# Performing the usual c2 setup:\n", + "blue_config_env, c2_server, c2_beacon, client_1, web_server = c2_setup(given_env=blue_config_env)\n", + "\n", + "blue_config_env.step(0)\n", + "\n", + "# Capturing the 'default' obs (Post C2 installation and configuration):\n", + "default_obs, _, _, _, _ = blue_config_env.step(0)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "By default, the C2 suite will masquerade a Web Browser, meaning C2 Traffic will opt to use ``TCP`` and ``HTTP`` (Port 80):" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Capturing default C2 Traffic \n", + "for i in range(3):\n", + " tcp_c2_obs, _, _, _, _ = blue_config_env.step(0)\n", + "\n", + "display_obs_diffs(default_obs, tcp_c2_obs, blue_config_env.game.step_counter)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "However, C2 Beacon can be configured to use UDP (``Masquerade Protocol``) and we can also configure the C2 Beacon to use a different Port (``Masquerade Port``) for example ``DNS``. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from primaite.simulator.network.transmission.network_layer import IPProtocol\n", + "from primaite.simulator.network.transmission.transport_layer import Port\n", + "# As we're configuring via the PrimAITE API we need to pass the actual IPProtocol/Port (Agents leverage the simulation via the game layer and thus can pass strings).\n", + "c2_beacon.configure(c2_server_ip_address=\"192.168.10.21\", masquerade_protocol=IPProtocol.UDP, masquerade_port=Port.DNS)\n", + "c2_beacon.establish()\n", + "c2_beacon.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Capturing UDP C2 Traffic\n", + "for i in range(5):\n", + " udp_c2_obs, _, _, _, _ = blue_config_env.step(0)\n", + "\n", + "display_obs_diffs(tcp_c2_obs, udp_c2_obs, blue_config_env.game.step_counter)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "venv", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.11" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/src/primaite/notebooks/Privilege-Escalation-and Data-Loss-Example.ipynb b/src/primaite/notebooks/Privilege-Escalation-and Data-Loss-Example.ipynb new file mode 100644 index 00000000..c751edfd --- /dev/null +++ b/src/primaite/notebooks/Privilege-Escalation-and Data-Loss-Example.ipynb @@ -0,0 +1,607 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Simulating Privilege Escalation and Data Loss Using SSH and ACLs Manipulation\n", + "\n", + "© Crown-owned copyright 2024, Defence Science and Technology Laboratory UK\n", + "\n", + "## Overview\n", + "\n", + "This Jupyter notebook demonstrates a cyber scenario focusing on internal privilege escalation and data loss through the manipulation of SSH access and Access Control Lists (ACLs). The scenario is designed to model and visualise how a disgruntled junior engineer might exploit internal network vulnerabilities and social engineering of account credentials to escalate privileges and cause significant data loss and disruption to services.\n", + "\n", + "## Scenario Description\n", + "\n", + "This simulation utilises the PrimAITE demo network, focussing specifically on five nodes:\n", + "\n", + "\n", + " \"Description\n", + "\n", + "\n", + "\n", + "- **SomeTech Developer PC (`some_tech_jnr_dev_pc`)**: The workstation used by the junior engineer.\n", + "- **SomeTech Core Router (`some_tech_rt`)**: A critical network device that controls access between nodes.\n", + "- **SomeTech PostgreSQL Database Server (`some_tech_db_srv`)**: Hosts the company’s critical database.\n", + "- **SomeTech Storage Server (`some_tech_storage_srv`)**: Stores important files and database backups.\n", + "- **SomeTech Web Server (`some_tech_web_srv`)**: Serves the company’s website.\n", + "\n", + "By default, the junior developer PC is restricted from connecting to the storage server via FTP or SSH due to ACL rules that permit only senior members of the engineering team to access these services.\n", + "\n", + "The goal of the scenario is to simulate how the junior engineer, after gaining unauthorised access to the core router, manipulates ACL rules to escalate privileges and delete critical data.\n", + "\n", + "### Key Actions Simulated\n", + "\n", + "1. **Privilege Escalation**: The junior engineer uses social engineering to obtain login credentials for the core router, SSHs into the router, and modifies the ACL rules to allow SSH access from their PC to the storage server.\n", + "2. **Remote Access**: The junior engineer then uses the newly gained SSH access to connect to the storage server from their PC. This step is crucial for executing further actions, such as deleting files.\n", + "3. **File Deletion**: With SSH access to the storage server, the engineer deletes the backup file from the storage server and subsequently removes critical data from the PostgreSQL database, bringing down the sometech.ai website.\n", + "4. **Website Impact Verification:** After the deletion of the database backup, the scenario checks the sometech.ai website's status to confirm it has been brought down due to the data loss.\n", + "5. **Database Restore Failure:** An attempt is made to restore the deleted backup, demonstrating that the restoration fails and highlighting the severity of the data loss.\n", + "\n", + "### Notes:\n", + "- The demo will utilise CAOS (Common Action and Observation Space) actions wherever they are available. For actions where a CAOS action does not yet exist, the action will be performed manually on the node/service.\n", + "- This notebook will be updated to incorporate new CAOS actions as they become supported." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## The Scenario" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "import yaml\n", + "\n", + "from primaite import PRIMAITE_PATHS\n", + "from primaite.game.game import PrimaiteGame\n", + "from primaite.simulator.network.hardware.nodes.host.computer import Computer\n", + "from primaite.simulator.network.hardware.nodes.network.router import Router\n", + "from primaite.simulator.network.hardware.nodes.host.server import Server\n", + "from primaite.simulator.system.applications.database_client import DatabaseClient\n", + "from primaite.simulator.system.applications.web_browser import WebBrowser\n", + "from primaite.simulator.system.services.database.database_service import DatabaseService" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Load the network configuration" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "path = PRIMAITE_PATHS.user_config_path / \"example_config\" / \"multi_lan_internet_network_example.yaml\"\n", + "\n", + "with open(path, \"r\") as file:\n", + " cfg = yaml.safe_load(file)\n", + "\n", + " game = PrimaiteGame.from_config(cfg)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Capture some of the nodes from the network to observe actions" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "some_tech_jnr_dev_pc: Computer = game.simulation.network.get_node_by_hostname(\"some_tech_jnr_dev_pc\")\n", + "some_tech_jnr_dev_db_client: DatabaseClient = some_tech_jnr_dev_pc.software_manager.software[\"DatabaseClient\"]\n", + "some_tech_jnr_dev_web_browser: WebBrowser = some_tech_jnr_dev_pc.software_manager.software[\"WebBrowser\"]\n", + "some_tech_rt: Router = game.simulation.network.get_node_by_hostname(\"some_tech_rt\")\n", + "some_tech_db_srv: Server = game.simulation.network.get_node_by_hostname(\"some_tech_db_srv\")\n", + "some_tech_db_service: DatabaseService = some_tech_db_srv.software_manager.software[\"DatabaseService\"]\n", + "some_tech_storage_srv: Server = game.simulation.network.get_node_by_hostname(\"some_tech_storage_srv\")\n", + "some_tech_web_srv: Server = game.simulation.network.get_node_by_hostname(\"some_tech_web_srv\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Perform a Database Backup and Inspect the Storage Server\n", + "\n", + "At this stage, a backup of the PostgreSQL database is created and the storage server’s file system is inspected. This step ensures that a backup file is present and correctly stored in the storage server before any further actions are taken. The inspection of the file system allows verification of the backup’s existence and health, establishing a baseline that will later be used to confirm the success of the subsequent deletion actions." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "some_tech_storage_srv.file_system.show(full=True)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "some_tech_db_service.backup_database()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "some_tech_storage_srv.file_system.show(full=True)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Extract the folder name containing the database backup file" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "db_backup_folder = [folder.name for folder in some_tech_storage_srv.file_system.folders.values() if folder.name != \"root\"][0]\n", + "db_backup_folder" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "tags": [] + }, + "source": [ + "## Check That the Junior Engineer Cannot SSH into the Storage Server\n", + "\n", + "This step verifies that the junior engineer is currently restricted from SSH access to the storage server. By attempting to establish an SSH connection from the junior engineer’s workstation to the storage server, this action confirms that the current ACL rules on the core router correctly prevents unauthorised access. It sets up the necessary conditions to later validate the effectiveness of the privilege escalation by demonstrating the initial access restrictions.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "caos_action = [\n", + " \"network\", \"node\", \"some_tech_jnr_dev_pc\", \n", + " \"service\", \"Terminal\", \"ssh_to_remote\", \"admin\", \"admin\", str(some_tech_storage_srv.network_interface[1].ip_address)\n", + "]\n", + "game.simulation.apply_request(caos_action)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Confirm That the Website Is Up by Executing the Web Browser on the Junior Engineer's Machine\n", + "\n", + "In this step, we verify that the sometech.ai website is operational before any malicious activities begin. By executing the web browser application on the junior engineer’s machine, we ensure that the website is accessible and functioning correctly. This establishes a baseline for the website’s status, allowing us to later assess the impact of the subsequent actions, such as database deletion, on the website's availability.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "caos_action = [\"network\", \"node\", \"some_tech_jnr_dev_pc\", \"application\", \"WebBrowser\", \"execute\"]\n", + "game.simulation.apply_request(caos_action)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "tags": [] + }, + "source": [ + "## Exploit Core Router to Add ACL for SSH Access\n", + "\n", + "At this point, the junior engineer exploits a vulnerability in the core router by obtaining the login credentials through social engineering. With SSH access to the core router, the engineer modifies the ACL rules to permit SSH connections from their machine to the storage server. This action is crucial as it will enable the engineer to remotely access the storage server and execute further malicious activities.\n", + "\n", + "Interestingly, if we inspect the `active_remote_sessions` on the SomeTech core routers `UserSessionManager`, we'll see an active session appear. This active session would pop up in the observation space." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "game.get_sim_state()[\"network\"][\"nodes\"][\"some_tech_rt\"][\"services\"][\"UserSessionManager\"][\"active_remote_sessions\"]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "caos_action = [\n", + " \"network\", \"node\", \"some_tech_jnr_dev_pc\", \n", + " \"service\", \"Terminal\", \"ssh_to_remote\", \"admin\", \"admin\", str(some_tech_rt.network_interface[4].ip_address)\n", + "]\n", + "game.simulation.apply_request(caos_action)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "game.get_sim_state()[\"network\"][\"nodes\"][\"some_tech_rt\"][\"services\"][\"UserSessionManager\"][\"active_remote_sessions\"]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Inspect the ACL Table Before Adding the New Rule\n", + "\n", + "Before making any changes, we first examine the current Access Control List (ACL) table on the core router. This inspection provides a snapshot of the existing rules that govern network traffic, including permissions and restrictions related to SSH access. Understanding this baseline is crucial for verifying the effect of new rules, ensuring that changes can be accurately assessed for their impact on network security and access controls.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "some_tech_rt.acl.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "caos_action = [\n", + " \"network\", \"node\", \"some_tech_jnr_dev_pc\", \n", + " \"service\", \"Terminal\", \"send_remote_command\", str(some_tech_rt.network_interface[4].ip_address),\n", + " {\n", + " \"command\": [\n", + " \"acl\", \"add_rule\", \"PERMIT\", \"TCP\",\n", + " str(some_tech_jnr_dev_pc.network_interface[1].ip_address), \"0.0.0.0\", \"SSH\",\n", + " str(some_tech_storage_srv.network_interface[1].ip_address), \"0.0.0.0\", \"SSH\",\n", + " 1\n", + " ]\n", + " }\n", + "]\n", + "\n", + "game.simulation.apply_request(caos_action)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Verify That the New ACL Rule Has Been Added\n", + "\n", + "After updating the ACL rules on the core router, we need to confirm that the new rule has been successfully applied. This verification involves inspecting the ACL table again to ensure that the new rule allowing SSH access from the junior engineer’s PC to the storage server is present. This step is critical to ensure that the modification was executed correctly and that the junior engineer now has the intended access." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "some_tech_rt.acl.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Terminate Remote Session on Core Router\n", + "\n", + "After successfully adding the ACL rule to allow SSH access to the storage server, the junior engineer terminates the remote session on the core router. The termination of the session is a strategic move to avoid leaving an active remote login open while maintaining the newly granted access privileges for future use." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "caos_action = [\n", + " \"network\", \"node\", \"some_tech_jnr_dev_pc\", \n", + " \"service\", \"Terminal\", \"remote_logoff\", str(some_tech_rt.network_interface[4].ip_address)\n", + "]\n", + "game.simulation.apply_request(caos_action)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Confirm the termination of the remote session" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "game.get_sim_state()[\"network\"][\"nodes\"][\"some_tech_rt\"][\"services\"][\"UserSessionManager\"][\"active_remote_sessions\"]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## SSH into Storage Server and Delete Database Backup\n", + "\n", + "With the newly added ACL rule, the junior engineer can now SSH into the storage server from their machine. The engineer proceeds to delete the critical database backup file stored on the server. This action is pivotal in the attack, as it directly impacts the availability of essential data and sets the stage for subsequent data loss and disruption of services.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "caos_action = [\n", + " \"network\", \"node\", \"some_tech_jnr_dev_pc\", \n", + " \"service\", \"Terminal\", \"ssh_to_remote\", \"admin\", \"admin\", str(some_tech_storage_srv.network_interface[1].ip_address)\n", + "]\n", + "game.simulation.apply_request(caos_action)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "caos_action = [\n", + " \"network\", \"node\", \"some_tech_jnr_dev_pc\", \n", + " \"service\", \"Terminal\", \"send_remote_command\", str(some_tech_storage_srv.network_interface[1].ip_address),\n", + " {\n", + " \"command\": [\n", + " \"file_system\", \"delete\", \"file\", db_backup_folder, \"database.db\"\n", + " ]\n", + " }\n", + "]\n", + "\n", + "game.simulation.apply_request(caos_action)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Verify that the database backup file has been deleted" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "some_tech_storage_srv.file_system.show(full=True)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Delete Critical Data from the PostgreSQL Database\n", + "\n", + "In this part of the scenario, the junior engineer manually interacts with the PostgreSQL database to delete critical data. The deletion of critical data from the database has significant implications, leading to the loss of essential information and affecting the availability of the sometech.ai website.\n", + "\n", + "* Since the CAOS framework does not support ad-hoc or dynamic SQL queries for database services, this action must be performed manually." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "##### Again, confirm that the sometech.ai website is up by executing the web browser on the junior engineer's machine" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "caos_action = [\"network\", \"node\", \"some_tech_jnr_dev_pc\", \"application\", \"WebBrowser\", \"execute\"]\n", + "game.simulation.apply_request(caos_action)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "##### Set the server IP address and open a new DB connection" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "some_tech_jnr_dev_db_client.server_ip_address = some_tech_db_srv.network_interface[1].ip_address\n", + "some_tech_jnr_dev_db_connection = some_tech_jnr_dev_db_client.get_new_connection()\n", + "some_tech_jnr_dev_db_connection" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "tags": [] + }, + "source": [ + "##### Send the DELETE query" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "some_tech_jnr_dev_db_connection.query(\"DELETE\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "##### Confirm that the actions have brought the sometech.ai website down" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "caos_action = [\"network\", \"node\", \"some_tech_jnr_dev_pc\", \"application\", \"WebBrowser\", \"execute\"]\n", + "game.simulation.apply_request(caos_action)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Attempt to Restore Database Backup\n", + "\n", + "In this final section, an attempt is made to restore the database backup that was deleted earlier. The action is performed using the `some_tech_db_service.restore_backup()` method. This will demonstrate the impact of the data loss and confirm that the backup restoration fails, highlighting the severity of the disruption caused." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "some_tech_db_service.restore_backup()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## End of Scenario Summary\n", + "\n", + "In this simulation, we modelled a cyber attack scenario where a disgruntled junior engineer exploits internal network vulnerabilities to escalate privileges, causing significant data loss and disruption of services. The following key actions were performed:\n", + "\n", + "1. **Privilege Escalation:** The junior engineer used social engineering to obtain the login credentials for the core router. They remotely accessed the router via SSH and modified the ACL rules to grant SSH access from their machine to the storage server.\n", + "\n", + "2. **Remote Access:** With the modified ACLs in place, the engineer was able to SSH into the storage server from their machine. This access enabled them to interact with the storage server and perform further actions.\n", + "\n", + "3. **File & Data Deletion:** The engineer used SSH remote access to delete a critical database backup file from the storage server. Subsequently, they executed a SQL command to delete critical data from the PostgreSQL database, which resulted in the disruption of the sometech.ai website.\n", + "\n", + "4. **Website Status Verification:** After the deletion of the database backup, the website's status was checked to confirm that it had been brought down due to the data loss.\n", + "\n", + "5. **Database Restore Failure:** An attempt to restore the deleted backup was made to demonstrate that the restoration process failed, highlighting the severity of the data loss.\n", + "\n", + "**Verification and Outcomes:**\n", + "\n", + "- **Initial State Verification:** The backup file was confirmed to be present on the storage server before any actions were taken. The junior engineer's inability to SSH into the storage server initially confirmed that ACL restrictions were in effect.\n", + "\n", + "- **Privilege Escalation Confirmation:** The successful modification of the ACL rules was verified by checking the router's ACL table.\n", + "\n", + "- **Remote Access Verification:** After the ACL modification, the engineer successfully SSH'd into the storage server from their PC. The file system inspection confirmed that the backup file was accessible and could be deleted.\n", + "\n", + "- **File Deletion Confirmation:** The deletion of the backup file was confirmed by inspecting the storage server's file system after the operation. The backup file was marked as deleted, validating that the deletion command was executed.\n", + "\n", + "- **Database and Website Impact:** The deletion of the database backup was followed by a DELETE query executed on the PostgreSQL database. The website's functionality was subsequently checked using a web browser, confirming that the sometech.ai website was down due to the data loss.\n", + "\n", + "- **Restore Attempt Verification:** An attempt to restore the deleted database backup was made, and it was confirmed that the restoration failed, highlighting the impact of the data deletion." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.12" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/src/primaite/notebooks/Terminal-Processing.ipynb b/src/primaite/notebooks/Terminal-Processing.ipynb new file mode 100644 index 00000000..fdf405a7 --- /dev/null +++ b/src/primaite/notebooks/Terminal-Processing.ipynb @@ -0,0 +1,224 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Terminal Processing\n", + "\n", + "© Crown-owned copyright 2024, Defence Science and Technology Laboratory UK" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This notebook serves as a guide on the functionality and use of the new Terminal simulation component.\n", + "\n", + "The Terminal service comes pre-installed on most Nodes (The exception being Switches, as these are currently dumb). " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from primaite.simulator.system.services.terminal.terminal import Terminal\n", + "from primaite.simulator.network.container import Network\n", + "from primaite.simulator.network.hardware.nodes.host.computer import Computer\n", + "from primaite.simulator.system.applications.red_applications.ransomware_script import RansomwareScript\n", + "from primaite.simulator.system.services.terminal.terminal import RemoteTerminalConnection\n", + "\n", + "def basic_network() -> Network:\n", + " \"\"\"Utility function for creating a default network to demonstrate Terminal functionality\"\"\"\n", + " network = Network()\n", + " node_a = Computer(hostname=\"node_a\", ip_address=\"192.168.0.10\", subnet_mask=\"255.255.255.0\", start_up_duration=0)\n", + " node_a.power_on()\n", + " node_b = Computer(hostname=\"node_b\", ip_address=\"192.168.0.11\", subnet_mask=\"255.255.255.0\", start_up_duration=0)\n", + " node_b.power_on()\n", + " network.connect(node_a.network_interface[1], node_b.network_interface[1])\n", + " return network" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The terminal can be accessed from a `Node` via the `software_manager` as demonstrated below. \n", + "\n", + "In the example, we have a basic network consisting of two computers, connected to form a basic network." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "network: Network = basic_network()\n", + "computer_a: Computer = network.get_node_by_hostname(\"node_a\")\n", + "terminal_a: Terminal = computer_a.software_manager.software.get(\"Terminal\")\n", + "computer_b: Computer = network.get_node_by_hostname(\"node_b\")\n", + "terminal_b: Terminal = computer_b.software_manager.software.get(\"Terminal\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "To be able to send commands from `node_a` to `node_b`, you will need to `login` to `node_b` first, using valid user credentials. In the example below, we are remotely logging in to the 'admin' account on `node_b`, from `node_a`. \n", + "If you are not logged in, any commands sent will be rejected by the remote.\n", + "\n", + "Remote Logins return a RemoteTerminalConnection object, which can be used for sending commands to the remote node. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Login to the remote (node_b) from local (node_a)\n", + "term_a_term_b_remote_connection: RemoteTerminalConnection = terminal_a.login(username=\"admin\", password=\"admin\", ip_address=\"192.168.0.11\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "You can view all active connections to a terminal through use of the `show()` method." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "terminal_b.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The new connection object allows us to forward commands to be executed on the target node. The example below demonstrates how you can remotely install an application on the target node." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "term_a_term_b_remote_connection.execute([\"software_manager\", \"application\", \"install\", \"RansomwareScript\"])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "computer_b.software_manager.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The code block below demonstrates how the Terminal class allows the user of `terminal_a`, on `computer_a`, to send a command to `computer_b` to create a downloads folder. \n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Display the current state of the file system on computer_b\n", + "computer_b.file_system.show()\n", + "\n", + "# Send command\n", + "term_a_term_b_remote_connection.execute([\"file_system\", \"create\", \"folder\", \"downloads\"])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The resultant call to `computer_b.file_system.show()` shows that the new folder has been created." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "computer_b.file_system.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "When finished, the connection can be closed by calling the `disconnect` function of the Remote Client object" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Display active connection\n", + "terminal_a.show()\n", + "terminal_b.show()\n", + "\n", + "term_a_term_b_remote_connection.disconnect()\n", + "\n", + "terminal_a.show()\n", + "terminal_b.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Disconnected Terminal sessions will no longer show in the node's Terminal connection list, but will be under the historic sessions in the `user_session_manager`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "computer_b.user_session_manager.show(include_historic=True, include_session_id=True)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": ".venv", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.11" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/src/primaite/notebooks/Training-an-RLLIB-MARL-System.ipynb b/src/primaite/notebooks/Training-an-RLLIB-MARL-System.ipynb index 28f08edd..19e95a95 100644 --- a/src/primaite/notebooks/Training-an-RLLIB-MARL-System.ipynb +++ b/src/primaite/notebooks/Training-an-RLLIB-MARL-System.ipynb @@ -62,6 +62,7 @@ " .environment(env=PrimaiteRayMARLEnv, env_config=cfg)\n", " .env_runners(num_env_runners=0)\n", " .training(train_batch_size=128)\n", + " .evaluation(evaluation_duration=1)\n", " )\n" ] }, @@ -82,6 +83,22 @@ "algo = config.build()\n", "results = algo.train()" ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Evaluate the results" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "eval = algo.evaluate()" + ] } ], "metadata": { diff --git a/src/primaite/notebooks/Training-an-RLLib-Agent.ipynb b/src/primaite/notebooks/Training-an-RLLib-Agent.ipynb index 9d870192..dbe8871c 100644 --- a/src/primaite/notebooks/Training-an-RLLib-Agent.ipynb +++ b/src/primaite/notebooks/Training-an-RLLib-Agent.ipynb @@ -55,6 +55,7 @@ " .environment(env=PrimaiteRayEnv, env_config=env_config)\n", " .env_runners(num_env_runners=0)\n", " .training(train_batch_size=128)\n", + " .evaluation(evaluation_duration=1)\n", ")\n" ] }, @@ -74,6 +75,22 @@ "algo = config.build()\n", "results = algo.train()\n" ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Evaluate the results" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "eval = algo.evaluate()" + ] } ], "metadata": { diff --git a/src/primaite/notebooks/Using-Episode-Schedules.ipynb b/src/primaite/notebooks/Using-Episode-Schedules.ipynb index 14012264..cb06e0f9 100644 --- a/src/primaite/notebooks/Using-Episode-Schedules.ipynb +++ b/src/primaite/notebooks/Using-Episode-Schedules.ipynb @@ -199,7 +199,7 @@ "metadata": {}, "source": [ "### Episode 0\n", - "Let' run the episodes to verify that the agents are changing as expected. In episode 0, there should be no green or red agents, just the defender blue agent." + "Let's run the episodes to verify that the agents are changing as expected. In episode 0, there should be no green or red agents, just the defender blue agent." ] }, { diff --git a/src/primaite/notebooks/_package_data/primaite_demo_network.png b/src/primaite/notebooks/_package_data/primaite_demo_network.png new file mode 100644 index 00000000..08a379f9 Binary files /dev/null and b/src/primaite/notebooks/_package_data/primaite_demo_network.png differ diff --git a/src/primaite/session/environment.py b/src/primaite/session/environment.py index a87f0cde..c66663e3 100644 --- a/src/primaite/session/environment.py +++ b/src/primaite/session/environment.py @@ -1,5 +1,7 @@ # © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK import json +import random +import sys from os import PathLike from typing import Any, Dict, Optional, SupportsFloat, Tuple, Union @@ -17,6 +19,36 @@ from primaite.simulator.system.core.packet_capture import PacketCapture _LOGGER = getLogger(__name__) +# Check torch is installed +try: + import torch as th +except ModuleNotFoundError: + _LOGGER.debug("Torch not available for importing") + + +def set_random_seed(seed: int) -> Union[None, int]: + """ + Set random number generators. + + :param seed: int + """ + if seed is None or seed == -1: + return None + elif seed < -1: + raise ValueError("Invalid random number seed") + # Seed python RNG + random.seed(seed) + # Seed numpy RNG + np.random.seed(seed) + # Seed the RNG for all devices (both CPU and CUDA) + # if torch not installed don't set random seed. + if sys.modules["torch"]: + th.manual_seed(seed) + th.backends.cudnn.deterministic = True + th.backends.cudnn.benchmark = False + + return seed + class PrimaiteGymEnv(gymnasium.Env): """ @@ -31,6 +63,9 @@ class PrimaiteGymEnv(gymnasium.Env): super().__init__() self.episode_scheduler: EpisodeScheduler = build_scheduler(env_config) """Object that returns a config corresponding to the current episode.""" + self.seed = self.episode_scheduler(0).get("game", {}).get("seed") + """Get RNG seed from config file. NB: Must be before game instantiation.""" + self.seed = set_random_seed(self.seed) self.io = PrimaiteIO.from_config(self.episode_scheduler(0).get("io_settings", {})) """Handles IO for the environment. This produces sys logs, agent logs, etc.""" self.game: PrimaiteGame = PrimaiteGame.from_config(self.episode_scheduler(0)) @@ -42,6 +77,8 @@ class PrimaiteGymEnv(gymnasium.Env): self.total_reward_per_episode: Dict[int, float] = {} """Average rewards of agents per episode.""" + _LOGGER.info(f"PrimaiteGymEnv RNG seed = {self.seed}") + def action_masks(self) -> np.ndarray: """ Return the action mask for the agent. @@ -108,6 +145,8 @@ class PrimaiteGymEnv(gymnasium.Env): f"Resetting environment, episode {self.episode_counter}, " f"avg. reward: {self.agent.reward_function.total_reward}" ) + if seed is not None: + set_random_seed(seed) self.total_reward_per_episode[self.episode_counter] = self.agent.reward_function.total_reward if self.io.settings.save_agent_actions: diff --git a/src/primaite/session/ray_envs.py b/src/primaite/session/ray_envs.py index 1adc324c..33c74b0e 100644 --- a/src/primaite/session/ray_envs.py +++ b/src/primaite/session/ray_envs.py @@ -63,6 +63,7 @@ class PrimaiteRayMARLEnv(MultiAgentEnv): def reset(self, *, seed: int = None, options: dict = None) -> Tuple[ObsType, Dict]: """Reset the environment.""" + super().reset() # Ensure PRNG seed is set everywhere rewards = {name: agent.reward_function.total_reward for name, agent in self.agents.items()} _LOGGER.info(f"Resetting environment, episode {self.episode_counter}, " f"avg. reward: {rewards}") @@ -176,6 +177,7 @@ class PrimaiteRayEnv(gymnasium.Env): def reset(self, *, seed: int = None, options: dict = None) -> Tuple[ObsType, Dict]: """Reset the environment.""" + super().reset() # Ensure PRNG seed is set everywhere if self.env.agent.action_masking: obs, *_ = self.env.reset(seed=seed) new_obs = {"action_mask": self.env.action_masks(), "observations": obs} diff --git a/src/primaite/simulator/network/airspace.py b/src/primaite/simulator/network/airspace.py index 9c736383..cdb01514 100644 --- a/src/primaite/simulator/network/airspace.py +++ b/src/primaite/simulator/network/airspace.py @@ -60,13 +60,13 @@ class AirSpaceFrequency(Enum): @property def maximum_data_rate_bps(self) -> float: """ - Retrieves the maximum data transmission rate in bits per second (bps) for the frequency. + Retrieves the maximum data transmission rate in bits per second (bps). - The maximum rates are predefined for known frequencies: - - For WIFI_2_4, it returns 100,000,000 bps (100 Mbps). - - For WIFI_5, it returns 500,000,000 bps (500 Mbps). + The maximum rates are predefined for frequencies.: + - WIFI 2.4 supports 100,000,000 bps + - WIFI 5 supports 500,000,000 bps - :return: The maximum data rate in bits per second. If the frequency is not recognized, returns 0.0. + :return: The maximum data rate in bits per second. """ if self == AirSpaceFrequency.WIFI_2_4: return 100_000_000.0 # 100 Megabits per second diff --git a/src/primaite/simulator/network/hardware/base.py b/src/primaite/simulator/network/hardware/base.py index 7a127601..ef2d47c3 100644 --- a/src/primaite/simulator/network/hardware/base.py +++ b/src/primaite/simulator/network/hardware/base.py @@ -6,12 +6,11 @@ import secrets from abc import ABC, abstractmethod from ipaddress import IPv4Address, IPv4Network from pathlib import Path -from typing import Any, Dict, Optional, TypeVar, Union +from typing import Any, ClassVar, Dict, List, Optional, Type, TypeVar, Union from prettytable import MARKDOWN, PrettyTable -from pydantic import BaseModel, Field +from pydantic import BaseModel, Field, validate_call -import primaite.simulator.network.nmne from primaite import getLogger from primaite.exceptions import NetworkError from primaite.interface.request import RequestResponse @@ -20,17 +19,10 @@ from primaite.simulator.core import RequestFormat, RequestManager, RequestPermis from primaite.simulator.domain.account import Account from primaite.simulator.file_system.file_system import FileSystem from primaite.simulator.network.hardware.node_operating_state import NodeOperatingState -from primaite.simulator.network.nmne import ( - CAPTURE_BY_DIRECTION, - CAPTURE_BY_IP_ADDRESS, - CAPTURE_BY_KEYWORD, - CAPTURE_BY_PORT, - CAPTURE_BY_PROTOCOL, - CAPTURE_NMNE, - NMNE_CAPTURE_KEYWORDS, -) +from primaite.simulator.network.nmne import NMNEConfig from primaite.simulator.network.transmission.data_link_layer import Frame from primaite.simulator.network.transmission.network_layer import IPProtocol +from primaite.simulator.network.transmission.transport_layer import Port from primaite.simulator.system.applications.application import Application from primaite.simulator.system.core.packet_capture import PacketCapture from primaite.simulator.system.core.session_manager import SessionManager @@ -38,7 +30,8 @@ from primaite.simulator.system.core.software_manager import SoftwareManager from primaite.simulator.system.core.sys_log import SysLog from primaite.simulator.system.processes.process import Process from primaite.simulator.system.services.service import Service -from primaite.simulator.system.software import IOSoftware +from primaite.simulator.system.services.terminal.terminal import Terminal +from primaite.simulator.system.software import IOSoftware, Software from primaite.utils.converters import convert_dict_enum_keys_to_enum_values from primaite.utils.validators import IPV4Address @@ -108,8 +101,11 @@ class NetworkInterface(SimComponent, ABC): pcap: Optional[PacketCapture] = None "A PacketCapture instance for capturing and analysing packets passing through this interface." + nmne_config: ClassVar[NMNEConfig] = NMNEConfig() + "A dataclass defining malicious network events to be captured." + nmne: Dict = Field(default_factory=lambda: {}) - "A dict containing details of the number of malicious network events captured." + "A dict containing details of the number of malicious events captured." traffic: Dict = Field(default_factory=lambda: {}) "A dict containing details of the inbound and outbound traffic by port and protocol." @@ -167,8 +163,8 @@ class NetworkInterface(SimComponent, ABC): "enabled": self.enabled, } ) - if CAPTURE_NMNE: - state.update({"nmne": {k: v for k, v in self.nmne.items()}}) + if self.nmne_config and self.nmne_config.capture_nmne: + state.update({"nmne": self.nmne}) state.update({"traffic": convert_dict_enum_keys_to_enum_values(self.traffic)}) return state @@ -201,7 +197,7 @@ class NetworkInterface(SimComponent, ABC): :param inbound: Boolean indicating if the frame direction is inbound. Defaults to True. """ # Exit function if NMNE capturing is disabled - if not CAPTURE_NMNE: + if not (self.nmne_config and self.nmne_config.capture_nmne): return # Initialise basic frame data variables @@ -222,27 +218,27 @@ class NetworkInterface(SimComponent, ABC): frame_str = str(frame.payload) # Proceed only if any NMNE keyword is present in the frame payload - if any(keyword in frame_str for keyword in NMNE_CAPTURE_KEYWORDS): + if any(keyword in frame_str for keyword in self.nmne_config.nmne_capture_keywords): # Start with the root of the NMNE capture structure current_level = self.nmne # Update NMNE structure based on enabled settings - if CAPTURE_BY_DIRECTION: + if self.nmne_config.capture_by_direction: # Set or get the dictionary for the current direction current_level = current_level.setdefault("direction", {}) current_level = current_level.setdefault(direction, {}) - if CAPTURE_BY_IP_ADDRESS: + if self.nmne_config.capture_by_ip_address: # Set or get the dictionary for the current IP address current_level = current_level.setdefault("ip_address", {}) current_level = current_level.setdefault(ip_address, {}) - if CAPTURE_BY_PROTOCOL: + if self.nmne_config.capture_by_protocol: # Set or get the dictionary for the current protocol current_level = current_level.setdefault("protocol", {}) current_level = current_level.setdefault(protocol, {}) - if CAPTURE_BY_PORT: + if self.nmne_config.capture_by_port: # Set or get the dictionary for the current port current_level = current_level.setdefault("port", {}) current_level = current_level.setdefault(port, {}) @@ -251,8 +247,8 @@ class NetworkInterface(SimComponent, ABC): keyword_level = current_level.setdefault("keywords", {}) # Increment the count for detected keywords in the payload - if CAPTURE_BY_KEYWORD: - for keyword in NMNE_CAPTURE_KEYWORDS: + if self.nmne_config.capture_by_keyword: + for keyword in self.nmne_config.nmne_capture_keywords: if keyword in frame_str: # Update the count for each keyword found keyword_level[keyword] = keyword_level.get(keyword, 0) + 1 @@ -794,6 +790,685 @@ class Link(SimComponent): self.current_load = 0.0 +class User(SimComponent): + """ + Represents a user in the PrimAITE system. + + :ivar username: The username of the user + :ivar password: The password of the user + :ivar disabled: Boolean flag indicating whether the user is disabled + :ivar is_admin: Boolean flag indicating whether the user has admin privileges + """ + + username: str + """The username of the user""" + + password: str + """The password of the user""" + + disabled: bool = False + """Boolean flag indicating whether the user is disabled""" + + is_admin: bool = False + """Boolean flag indicating whether the user has admin privileges""" + + num_of_logins: int = 0 + """Counts the number of the User has logged in""" + + def describe_state(self) -> Dict: + """ + Returns a dictionary representing the current state of the user. + + :return: A dict containing the state of the user + """ + return self.model_dump() + + +class UserManager(Service): + """ + Manages users within the PrimAITE system, handling creation, authentication, and administration. + + :param users: A dictionary of all users by their usernames + :param admins: A dictionary of admin users by their usernames + :param disabled_admins: A dictionary of currently disabled admin users by their usernames + """ + + users: Dict[str, User] = {} + + def __init__(self, **kwargs): + """ + Initializes a UserManager instanc. + + :param username: The username for the default admin user + :param password: The password for the default admin user + """ + kwargs["name"] = "UserManager" + kwargs["port"] = Port.NONE + kwargs["protocol"] = IPProtocol.NONE + super().__init__(**kwargs) + + self.start() + + def _init_request_manager(self) -> RequestManager: + """ + Initialise the request manager. + + More information in user guide and docstring for SimComponent._init_request_manager. + """ + rm = super()._init_request_manager() + + # todo add doc about requeest schemas + rm.add_request( + "change_password", + RequestType( + func=lambda request, context: RequestResponse.from_bool( + self.change_user_password(username=request[0], current_password=request[1], new_password=request[2]) + ) + ), + ) + return rm + + def describe_state(self) -> Dict: + """ + Returns the state of the UserManager along with the number of users and admins. + + :return: A dict containing detailed state information + """ + state = super().describe_state() + state.update({"total_users": len(self.users), "total_admins": len(self.admins) + len(self.disabled_admins)}) + state["users"] = {k: v.describe_state() for k, v in self.users.items()} + return state + + def show(self, markdown: bool = False): + """ + Display the Users. + + :param markdown: Whether to display the table in Markdown format or not. Default is `False`. + """ + table = PrettyTable(["Username", "Admin", "Disabled"]) + if markdown: + table.set_style(MARKDOWN) + table.align = "l" + table.title = f"{self.sys_log.hostname} User Manager" + for user in self.users.values(): + table.add_row([user.username, user.is_admin, user.disabled]) + print(table.get_string(sortby="Username")) + + @property + def non_admins(self) -> Dict[str, User]: + """ + Returns a dictionary of all enabled non-admin users. + + :return: A dictionary with usernames as keys and User objects as values for non-admin, non-disabled users. + """ + return {k: v for k, v in self.users.items() if not v.is_admin and not v.disabled} + + @property + def disabled_non_admins(self) -> Dict[str, User]: + """ + Returns a dictionary of all disabled non-admin users. + + :return: A dictionary with usernames as keys and User objects as values for non-admin, disabled users. + """ + return {k: v for k, v in self.users.items() if not v.is_admin and v.disabled} + + @property + def admins(self) -> Dict[str, User]: + """ + Returns a dictionary of all enabled admin users. + + :return: A dictionary with usernames as keys and User objects as values for admin, non-disabled users. + """ + return {k: v for k, v in self.users.items() if v.is_admin and not v.disabled} + + @property + def disabled_admins(self) -> Dict[str, User]: + """ + Returns a dictionary of all disabled admin users. + + :return: A dictionary with usernames as keys and User objects as values for admin, disabled users. + """ + return {k: v for k, v in self.users.items() if v.is_admin and v.disabled} + + def install(self) -> None: + """Setup default user during first-time installation.""" + self.add_user(username="admin", password="admin", is_admin=True, bypass_can_perform_action=True) + + def _is_last_admin(self, username: str) -> bool: + return username in self.admins and len(self.admins) == 1 + + def add_user( + self, username: str, password: str, is_admin: bool = False, bypass_can_perform_action: bool = False + ) -> bool: + """ + Adds a new user to the system. + + :param username: The username for the new user + :param password: The password for the new user + :param is_admin: Flag indicating if the new user is an admin + :return: True if user was successfully added, False otherwise + """ + if not bypass_can_perform_action and not self._can_perform_action(): + return False + if username in self.users: + self.sys_log.info(f"{self.name}: Failed to create new user {username} as this user name already exists") + return False + user = User(username=username, password=password, is_admin=is_admin) + self.users[username] = user + self.sys_log.info(f"{self.name}: Added new {'admin' if is_admin else 'user'}: {username}") + return True + + def authenticate_user(self, username: str, password: str) -> Optional[User]: + """ + Authenticates a user's login attempt. + + :param username: The username of the user trying to log in + :param password: The password provided by the user + :return: The User object if authentication is successful, None otherwise + """ + if not self._can_perform_action(): + return None + user = self.users.get(username) + if user and not user.disabled and user.password == password: + self.sys_log.info(f"{self.name}: User authenticated: {username}") + return user + self.sys_log.info(f"{self.name}: Authentication failed for: {username}") + return None + + def change_user_password(self, username: str, current_password: str, new_password: str) -> bool: + """ + Changes a user's password. + + :param username: The username of the user changing their password + :param current_password: The current password of the user + :param new_password: The new password for the user + :return: True if the password was changed successfully, False otherwise + """ + if not self._can_perform_action(): + return False + user = self.users.get(username) + if user and user.password == current_password: + user.password = new_password + self.sys_log.info(f"{self.name}: Password changed for {username}") + self._user_session_manager._logout_user(user=user) + return True + self.sys_log.info(f"{self.name}: Password change failed for {username}") + return False + + def disable_user(self, username: str) -> bool: + """ + Disables a user account, preventing them from logging in. + + :param username: The username of the user to disable + :return: True if the user was disabled successfully, False otherwise + """ + if not self._can_perform_action(): + return False + if username in self.users and not self.users[username].disabled: + if self._is_last_admin(username): + self.sys_log.info(f"{self.name}: Cannot disable User {username} as they are the only enabled admin") + return False + self.users[username].disabled = True + self.sys_log.info(f"{self.name}: User disabled: {username}") + return True + self.sys_log.info(f"{self.name}: Failed to disable user: {username}") + return False + + def enable_user(self, username: str) -> bool: + """ + Enables a previously disabled user account. + + :param username: The username of the user to enable + :return: True if the user was enabled successfully, False otherwise + """ + if username in self.users and self.users[username].disabled: + self.users[username].disabled = False + self.sys_log.info(f"{self.name}: User enabled: {username}") + return True + self.sys_log.info(f"{self.name}: Failed to enable user: {username}") + return False + + @property + def _user_session_manager(self) -> "UserSessionManager": + return self.software_manager.software["UserSessionManager"] # noqa + + +class UserSession(SimComponent): + """ + Represents a user session on the Node. + + This class manages the state of a user session, including the user, session start, last active step, + and end step. It also indicates whether the session is local. + + :ivar user: The user associated with this session. + :ivar start_step: The timestep when the session was started. + :ivar last_active_step: The last timestep when the session was active. + :ivar end_step: The timestep when the session ended, if applicable. + :ivar local: Indicates if the session is local. Defaults to True. + """ + + user: User + """The user associated with this session.""" + + start_step: int + """The timestep when the session was started.""" + + last_active_step: int + """The last timestep when the session was active.""" + + end_step: Optional[int] = None + """The timestep when the session ended, if applicable.""" + + local: bool = True + """Indicates if the session is a local session or a remote session. Defaults to True as a local session.""" + + @classmethod + def create(cls, user: User, timestep: int) -> UserSession: + """ + Creates a new instance of UserSession. + + This class method initialises a user session with the given user and timestep. + + :param user: The user associated with this session. + :param timestep: The timestep when the session is created. + :return: An instance of UserSession. + """ + user.num_of_logins += 1 + return UserSession(user=user, start_step=timestep, last_active_step=timestep) + + def describe_state(self) -> Dict: + """ + Describes the current state of the user session. + + :return: A dictionary representing the state of the user session. + """ + return self.model_dump() + + +class RemoteUserSession(UserSession): + """ + Represents a remote user session on the Node. + + This class extends the UserSession class to include additional attributes and methods specific to remote sessions. + + :ivar remote_ip_address: The IP address of the remote user. + :ivar local: Indicates that this is not a local session. Always set to False. + """ + + remote_ip_address: IPV4Address + """The IP address of the remote user.""" + + local: bool = False + """Indicates that this is not a local session. Always set to False.""" + + @classmethod + def create(cls, user: User, timestep: int, remote_ip_address: IPV4Address) -> RemoteUserSession: # noqa + """ + Creates a new instance of RemoteUserSession. + + This class method initialises a remote user session with the given user, timestep, and remote IP address. + + :param user: The user associated with this session. + :param timestep: The timestep when the session is created. + :param remote_ip_address: The IP address of the remote user. + :return: An instance of RemoteUserSession. + """ + return RemoteUserSession( + user=user, start_step=timestep, last_active_step=timestep, remote_ip_address=remote_ip_address + ) + + def describe_state(self) -> Dict: + """ + Describes the current state of the remote user session. + + This method extends the base describe_state method to include the remote IP address. + + :return: A dictionary representing the state of the remote user session. + """ + state = super().describe_state() + state["remote_ip_address"] = str(self.remote_ip_address) + return state + + +class UserSessionManager(Service): + """ + Manages user sessions on a Node, including local and remote sessions. + + This class handles authentication, session management, and session timeouts for users interacting with the Node. + """ + + local_session: Optional[UserSession] = None + """The current local user session, if any.""" + + remote_sessions: Dict[str, RemoteUserSession] = {} + """A dictionary of active remote user sessions.""" + + historic_sessions: List[UserSession] = Field(default_factory=list) + """A list of historic user sessions.""" + + local_session_timeout_steps: int = 30 + """The number of steps before a local session times out due to inactivity.""" + + remote_session_timeout_steps: int = 30 + """The number of steps before a remote session times out due to inactivity.""" + + max_remote_sessions: int = 3 + """The maximum number of concurrent remote sessions allowed.""" + + current_timestep: int = 0 + """The current timestep in the simulation.""" + + def __init__(self, **kwargs): + """ + Initializes a UserSessionManager instance. + + :param username: The username for the default admin user + :param password: The password for the default admin user + """ + kwargs["name"] = "UserSessionManager" + kwargs["port"] = Port.NONE + kwargs["protocol"] = IPProtocol.NONE + super().__init__(**kwargs) + self.start() + + def _init_request_manager(self) -> RequestManager: + """ + Initialise the request manager. + + More information in user guide and docstring for SimComponent._init_request_manager. + """ + rm = super()._init_request_manager() + + def _remote_login(request: RequestFormat, context: Dict) -> RequestResponse: + """Request should take the form [username, password, remote_ip_address].""" + username, password, remote_ip_address = request + response = RequestResponse.from_bool(self.remote_login(username, password, remote_ip_address)) + response.data = {"remote_hostname": self.parent.hostname, "username": username} + return response + + rm.add_request("remote_login", RequestType(func=_remote_login)) + + rm.add_request( + "remote_logout", + RequestType( + func=lambda request, context: RequestResponse.from_bool( + self.remote_logout(remote_session_id=request[0]) + ) + ), + ) + return rm + + def show(self, markdown: bool = False, include_session_id: bool = False, include_historic: bool = False): + """ + Displays a table of the user sessions on the Node. + + :param markdown: Whether to display the table in markdown format. + :param include_session_id: Whether to include session IDs in the table. + :param include_historic: Whether to include historic sessions in the table. + """ + headers = ["Session ID", "Username", "Type", "Remote IP", "Start Step", "Step Last Active", "End Step"] + + if not include_session_id: + headers = headers[1:] + + table = PrettyTable(headers) + + if markdown: + table.set_style(MARKDOWN) + table.align = "l" + table.title = f"{self.parent.hostname} User Sessions" + + def _add_session_to_table(user_session: UserSession): + """ + Adds a user session to the table for display. + + This helper function determines whether the session is local or remote and formats the session data + accordingly. It then adds the session data to the table. + + :param user_session: The user session to add to the table. + """ + session_type = "local" + remote_ip = "" + if isinstance(user_session, RemoteUserSession): + session_type = "remote" + remote_ip = str(user_session.remote_ip_address) + data = [ + user_session.uuid, + user_session.user.username, + session_type, + remote_ip, + user_session.start_step, + user_session.last_active_step, + user_session.end_step if user_session.end_step else "", + ] + if not include_session_id: + data = data[1:] + table.add_row(data) + + if self.local_session is not None: + _add_session_to_table(self.local_session) + + for user_session in self.remote_sessions.values(): + _add_session_to_table(user_session) + + if include_historic: + for user_session in self.historic_sessions: + _add_session_to_table(user_session) + + print(table.get_string(sortby="Step Last Active", reversesort=True)) + + def describe_state(self) -> Dict: + """ + Describes the current state of the UserSessionManager. + + :return: A dictionary representing the current state. + """ + state = super().describe_state() + state["current_local_user"] = None if not self.local_session else self.local_session.user.username + state["active_remote_sessions"] = list(self.remote_sessions.keys()) + return state + + @property + def _user_manager(self) -> UserManager: + """ + Returns the UserManager instance. + + :return: The UserManager instance. + """ + return self.software_manager.software["UserManager"] # noqa + + def pre_timestep(self, timestep: int) -> None: + """Apply any pre-timestep logic that helps make sure we have the correct observations.""" + self.current_timestep = timestep + inactive_sessions: list = [] + if self.local_session: + if self.local_session.last_active_step + self.local_session_timeout_steps <= timestep: + inactive_sessions.append(self.local_session) + + for session in self.remote_sessions: + remote_session = self.remote_sessions[session] + if remote_session.last_active_step + self.remote_session_timeout_steps <= timestep: + inactive_sessions.append(remote_session) + + for sessions in inactive_sessions: + self._timeout_session(sessions) + + def _timeout_session(self, session: UserSession) -> None: + """ + Handles session timeout logic. + + :param session: The session to be timed out. + """ + session.end_step = self.current_timestep + session_identity = session.user.username + if session.local: + self.local_session = None + session_type = "Local" + else: + self.remote_sessions.pop(session.uuid) + session_type = "Remote" + session_identity = f"{session_identity} {session.remote_ip_address}" + self.parent.terminal._connections.pop(session.uuid) + software_manager: SoftwareManager = self.software_manager + software_manager.send_payload_to_session_manager( + payload={"type": "user_timeout", "connection_id": session.uuid}, + dest_port=Port.SSH, + dest_ip_address=session.remote_ip_address, + ) + + self.sys_log.info(f"{self.name}: {session_type} {session_identity} session timeout due to inactivity") + + @property + def remote_session_limit_reached(self) -> bool: + """ + Checks if the maximum number of remote sessions has been reached. + + :return: True if the limit is reached, otherwise False. + """ + return len(self.remote_sessions) >= self.max_remote_sessions + + def validate_remote_session_uuid(self, remote_session_id: str) -> bool: + """ + Validates if a given remote session ID exists. + + :param remote_session_id: The remote session ID to validate. + :return: True if the session ID exists, otherwise False. + """ + return remote_session_id in self.remote_sessions + + def _login( + self, username: str, password: str, local: bool = True, remote_ip_address: Optional[IPv4Address] = None + ) -> Optional[str]: + """ + Logs a user in either locally or remotely. + + :param username: The username of the account. + :param password: The password of the account. + :param local: Whether the login is local or remote. + :param remote_ip_address: The remote IP address for remote login. + :return: The session ID if login is successful, otherwise None. + """ + if not self._can_perform_action(): + return None + + user = self._user_manager.authenticate_user(username=username, password=password) + + if not user: + self.sys_log.info(f"{self.name}: Incorrect username or password") + return None + + session_id = None + if local: + create_new_session = True + if self.local_session: + if self.local_session.user != user: + # logout the current user + self.local_logout() + else: + # not required as existing logged-in user attempting to re-login + create_new_session = False + + if create_new_session: + self.local_session = UserSession.create(user=user, timestep=self.current_timestep) + + session_id = self.local_session.uuid + else: + if not self.remote_session_limit_reached: + remote_session = RemoteUserSession.create( + user=user, timestep=self.current_timestep, remote_ip_address=remote_ip_address + ) + session_id = remote_session.uuid + self.remote_sessions[session_id] = remote_session + self.sys_log.info(f"{self.name}: User {user.username} logged in") + return session_id + + def local_login(self, username: str, password: str) -> Optional[str]: + """ + Logs a user in locally. + + :param username: The username of the account. + :param password: The password of the account. + :return: The session ID if login is successful, otherwise None. + """ + return self._login(username=username, password=password, local=True) + + @validate_call() + def remote_login(self, username: str, password: str, remote_ip_address: IPV4Address) -> Optional[str]: + """ + Logs a user in remotely. + + :param username: The username of the account. + :param password: The password of the account. + :param remote_ip_address: The remote IP address for the remote login. + :return: The session ID if login is successful, otherwise None. + """ + return self._login(username=username, password=password, local=False, remote_ip_address=remote_ip_address) + + def _logout(self, local: bool = True, remote_session_id: Optional[str] = None) -> bool: + """ + Logs a user out either locally or remotely. + + :param local: Whether the logout is local or remote. + :param remote_session_id: The remote session ID for remote logout. + :return: True if logout successful, otherwise False. + """ + if not self._can_perform_action(): + return False + session = None + if local and self.local_session: + session = self.local_session + session.end_step = self.current_timestep + self.local_session = None + + if not local and remote_session_id: + self.parent.terminal._disconnect(remote_session_id) + session = self.remote_sessions.pop(remote_session_id) + if session: + self.historic_sessions.append(session) + self.sys_log.info(f"{self.name}: User {session.user.username} logged out") + return True + return False + + def local_logout(self) -> bool: + """ + Logs out the current local user. + + :return: True if logout successful, otherwise False. + """ + return self._logout(local=True) + + def remote_logout(self, remote_session_id: str) -> bool: + """ + Logs out a remote user by session ID. + + :param remote_session_id: The remote session ID. + :return: True if logout successful, otherwise False. + """ + return self._logout(local=False, remote_session_id=remote_session_id) + + def _logout_user(self, user: Union[str, User]) -> bool: + """End a user session by username or user object.""" + if isinstance(user, str): + user = self._user_manager.users[user] # grab user object from username + for sess_id, session in self.remote_sessions.items(): + if session.user is user: + self._logout(local=False, remote_session_id=sess_id) + return True + if self.local_user_logged_in and self.local_session.user is user: + self.local_logout() + return True + return False + + @property + def local_user_logged_in(self) -> bool: + """ + Checks if a local user is currently logged in. + + :return: True if a local user is logged in, otherwise False. + """ + return self.local_session is not None + + class Node(SimComponent): """ A basic Node class that represents a node on the network. @@ -861,11 +1536,14 @@ class Node(SimComponent): red_scan_countdown: int = 0 "Time steps until reveal to red scan is complete." + SYSTEM_SOFTWARE: ClassVar[Dict[str, Type[Software]]] = {} + "Base system software that must be preinstalled." + def __init__(self, **kwargs): """ Initialize the Node with various components and managers. - This method initializes the ARP cache, ICMP handler, session manager, and software manager if they are not + This method initialises the ARP cache, ICMP handler, session manager, and software manager if they are not provided. """ if not kwargs.get("sys_log"): @@ -885,9 +1563,45 @@ class Node(SimComponent): dns_server=kwargs.get("dns_server"), ) super().__init__(**kwargs) + self._install_system_software() self.session_manager.node = self self.session_manager.software_manager = self.software_manager - self._install_system_software() + + @property + def user_manager(self) -> Optional[UserManager]: + """The Nodes User Manager.""" + return self.software_manager.software.get("UserManager") # noqa + + @property + def user_session_manager(self) -> Optional[UserSessionManager]: + """The Nodes User Session Manager.""" + return self.software_manager.software.get("UserSessionManager") # noqa + + @property + def terminal(self) -> Optional[Terminal]: + """The Nodes Terminal.""" + return self.software_manager.software.get("Terminal") + + def local_login(self, username: str, password: str) -> Optional[str]: + """ + Attempt to log in to the node uas a local user. + + This method attempts to authenticate a local user with the given username and password. If successful, it + returns a session token. If authentication fails, it returns None. + + :param username: The username of the account attempting to log in. + :param password: The password of the account attempting to log in. + :return: A session token if the login is successful, otherwise None. + """ + return self.user_session_manager.local_login(username, password) + + def local_logout(self) -> None: + """ + Log out the current local user from the node. + + This method ends the current local user's session and invalidates the session token. + """ + return self.user_session_manager.local_logout() def ip_is_network_interface(self, ip_address: IPv4Address, enabled_only: bool = False) -> bool: """ @@ -942,7 +1656,7 @@ class Node(SimComponent): @property def fail_message(self) -> str: """Message that is reported when a request is rejected by this validator.""" - return f"Cannot perform request on node '{self.node.hostname}' because it is not turned on." + return f"Cannot perform request on node '{self.node.hostname}' because it is not powered on." class _NodeIsOffValidator(RequestPermissionValidator): """ @@ -1091,10 +1805,6 @@ class Node(SimComponent): return rm - def _install_system_software(self): - """Install System Software - software that is usually provided with the OS.""" - pass - def describe_state(self) -> Dict: """ Produce a dictionary describing the current state of this object. @@ -1173,7 +1883,7 @@ class Node(SimComponent): ip_address, network_interface.speed, "Enabled" if network_interface.enabled else "Disabled", - network_interface.nmne if primaite.simulator.network.nmne.CAPTURE_NMNE else "Disabled", + network_interface.nmne if network_interface.nmne_config.capture_nmne else "Disabled", ] ) print(table) @@ -1455,74 +2165,6 @@ class Node(SimComponent): else: return - def install_service(self, service: Service) -> None: - """ - Install a service on this node. - - :param service: Service instance that has not been installed on any node yet. - :type service: Service - """ - if service in self: - _LOGGER.warning(f"Can't add service {service.name} to node {self.hostname}. It's already installed.") - return - self.services[service.uuid] = service - service.parent = self - service.install() # Perform any additional setup, such as creating files for this service on the node. - self.sys_log.info(f"Installed service {service.name}") - _LOGGER.debug(f"Added service {service.name} to node {self.hostname}") - self._service_request_manager.add_request(service.name, RequestType(func=service._request_manager)) - - def uninstall_service(self, service: Service) -> None: - """ - Uninstall and completely remove service from this node. - - :param service: Service object that is currently associated with this node. - :type service: Service - """ - if service not in self: - _LOGGER.warning(f"Can't remove service {service.name} from node {self.hostname}. It's not installed.") - return - service.uninstall() # Perform additional teardown, such as removing files or restarting the machine. - self.services.pop(service.uuid) - service.parent = None - self.sys_log.info(f"Uninstalled service {service.name}") - self._service_request_manager.remove_request(service.name) - - def install_application(self, application: Application) -> None: - """ - Install an application on this node. - - :param application: Application instance that has not been installed on any node yet. - :type application: Application - """ - if application in self: - _LOGGER.warning( - f"Can't add application {application.name} to node {self.hostname}. It's already installed." - ) - return - self.applications[application.uuid] = application - application.parent = self - self.sys_log.info(f"Installed application {application.name}") - _LOGGER.debug(f"Added application {application.name} to node {self.hostname}") - self._application_request_manager.add_request(application.name, RequestType(func=application._request_manager)) - - def uninstall_application(self, application: Application) -> None: - """ - Uninstall and completely remove application from this node. - - :param application: Application object that is currently associated with this node. - :type application: Application - """ - if application not in self: - _LOGGER.warning( - f"Can't remove application {application.name} from node {self.hostname}. It's not installed." - ) - return - self.applications.pop(application.uuid) - application.parent = None - self.sys_log.info(f"Uninstalled application {application.name}") - self._application_request_manager.remove_request(application.name) - def _shut_down_actions(self): """Actions to perform when the node is shut down.""" # Turn off all the services in the node @@ -1551,6 +2193,11 @@ class Node(SimComponent): # for process_id in self.processes: # self.processes[process_id] + def _install_system_software(self) -> None: + """Preinstall required software.""" + for _, software_class in self.SYSTEM_SOFTWARE.items(): + self.software_manager.install(software_class) + def __contains__(self, item: Any) -> bool: if isinstance(item, Service): return item.uuid in self.services diff --git a/src/primaite/simulator/network/hardware/nodes/host/host_node.py b/src/primaite/simulator/network/hardware/nodes/host/host_node.py index fdb28339..c197d30b 100644 --- a/src/primaite/simulator/network/hardware/nodes/host/host_node.py +++ b/src/primaite/simulator/network/hardware/nodes/host/host_node.py @@ -5,7 +5,13 @@ from ipaddress import IPv4Address from typing import Any, ClassVar, Dict, Optional from primaite import getLogger -from primaite.simulator.network.hardware.base import IPWiredNetworkInterface, Link, Node +from primaite.simulator.network.hardware.base import ( + IPWiredNetworkInterface, + Link, + Node, + UserManager, + UserSessionManager, +) from primaite.simulator.network.hardware.node_operating_state import NodeOperatingState from primaite.simulator.network.transmission.data_link_layer import Frame from primaite.simulator.system.applications.application import ApplicationOperatingState @@ -15,6 +21,7 @@ from primaite.simulator.system.services.arp.arp import ARP, ARPPacket from primaite.simulator.system.services.dns.dns_client import DNSClient from primaite.simulator.system.services.icmp.icmp import ICMP from primaite.simulator.system.services.ntp.ntp_client import NTPClient +from primaite.simulator.system.services.terminal.terminal import Terminal from primaite.utils.validators import IPV4Address _LOGGER = getLogger(__name__) @@ -292,6 +299,7 @@ class HostNode(Node): * DNS (Domain Name System) Client: Resolves domain names to IP addresses. * FTP (File Transfer Protocol) Client: Enables file transfers between the host and FTP servers. * NTP (Network Time Protocol) Client: Synchronizes the system clock with NTP servers. + * Terminal Client: Handles SSH requests between HostNode and external components. Applications: ------------ @@ -306,6 +314,9 @@ class HostNode(Node): "NTPClient": NTPClient, "WebBrowser": WebBrowser, "NMAP": NMAP, + "UserSessionManager": UserSessionManager, + "UserManager": UserManager, + "Terminal": Terminal, } """List of system software that is automatically installed on nodes.""" @@ -338,18 +349,6 @@ class HostNode(Node): """ return self.software_manager.software.get("ARP") - def _install_system_software(self): - """ - Installs the system software and network services typically found on an operating system. - - This method equips the host with essential network services and applications, preparing it for various - network-related tasks and operations. - """ - for _, software_class in self.SYSTEM_SOFTWARE.items(): - self.software_manager.install(software_class) - - super()._install_system_software() - def default_gateway_hello(self): """ Sends a hello message to the default gateway to establish connectivity and resolve the gateway's MAC address. diff --git a/src/primaite/simulator/network/hardware/nodes/network/router.py b/src/primaite/simulator/network/hardware/nodes/network/router.py index 61b7b96a..ceb91695 100644 --- a/src/primaite/simulator/network/hardware/nodes/network/router.py +++ b/src/primaite/simulator/network/hardware/nodes/network/router.py @@ -4,14 +4,14 @@ from __future__ import annotations import secrets from enum import Enum from ipaddress import IPv4Address, IPv4Network -from typing import Any, Dict, List, Optional, Tuple, Union +from typing import Any, ClassVar, Dict, List, Optional, Tuple, Union from prettytable import MARKDOWN, PrettyTable from pydantic import validate_call from primaite.interface.request import RequestResponse from primaite.simulator.core import RequestManager, RequestType, SimComponent -from primaite.simulator.network.hardware.base import IPWiredNetworkInterface +from primaite.simulator.network.hardware.base import IPWiredNetworkInterface, UserManager, UserSessionManager from primaite.simulator.network.hardware.node_operating_state import NodeOperatingState from primaite.simulator.network.hardware.nodes.network.network_node import NetworkNode from primaite.simulator.network.protocols.arp import ARPPacket @@ -24,6 +24,7 @@ from primaite.simulator.system.core.session_manager import SessionManager from primaite.simulator.system.core.sys_log import SysLog from primaite.simulator.system.services.arp.arp import ARP from primaite.simulator.system.services.icmp.icmp import ICMP +from primaite.simulator.system.services.terminal.terminal import Terminal from primaite.utils.validators import IPV4Address @@ -1200,6 +1201,12 @@ class Router(NetworkNode): RouteTable, RouterARP, and RouterICMP services. """ + SYSTEM_SOFTWARE: ClassVar[Dict] = { + "UserSessionManager": UserSessionManager, + "UserManager": UserManager, + "Terminal": Terminal, + } + num_ports: int network_interfaces: Dict[str, RouterInterface] = {} "The Router Interfaces on the node." @@ -1235,6 +1242,7 @@ class Router(NetworkNode): resolution within the network. These services are crucial for the router's operation, enabling it to manage network traffic efficiently. """ + super()._install_system_software() self.software_manager.install(RouterICMP) icmp: RouterICMP = self.software_manager.icmp # noqa icmp.router = self diff --git a/src/primaite/simulator/network/hardware/nodes/network/switch.py b/src/primaite/simulator/network/hardware/nodes/network/switch.py index 1a7da2e7..4324ac94 100644 --- a/src/primaite/simulator/network/hardware/nodes/network/switch.py +++ b/src/primaite/simulator/network/hardware/nodes/network/switch.py @@ -108,6 +108,9 @@ class Switch(NetworkNode): for i in range(1, self.num_ports + 1): self.connect_nic(SwitchPort()) + def _install_system_software(self): + pass + def show(self, markdown: bool = False): """ Prints a table of the SwitchPorts on the Switch. diff --git a/src/primaite/simulator/network/nmne.py b/src/primaite/simulator/network/nmne.py index 5c0c657b..c9cff5de 100644 --- a/src/primaite/simulator/network/nmne.py +++ b/src/primaite/simulator/network/nmne.py @@ -1,48 +1,25 @@ # © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK -from typing import Dict, Final, List +from typing import List -CAPTURE_NMNE: bool = True -"""Indicates whether Malicious Network Events (MNEs) should be captured. Default is True.""" - -NMNE_CAPTURE_KEYWORDS: List[str] = [] -"""List of keywords to identify malicious network events.""" - -# TODO: Remove final and make configurable after example layout when the NICObservation creates nmne structure dynamically -CAPTURE_BY_DIRECTION: Final[bool] = True -"""Flag to determine if captures should be organized by traffic direction (inbound/outbound).""" -CAPTURE_BY_IP_ADDRESS: Final[bool] = False -"""Flag to determine if captures should be organized by source or destination IP address.""" -CAPTURE_BY_PROTOCOL: Final[bool] = False -"""Flag to determine if captures should be organized by network protocol (e.g., TCP, UDP).""" -CAPTURE_BY_PORT: Final[bool] = False -"""Flag to determine if captures should be organized by source or destination port.""" -CAPTURE_BY_KEYWORD: Final[bool] = False -"""Flag to determine if captures should be filtered and categorised based on specific keywords.""" +from pydantic import BaseModel, ConfigDict -def set_nmne_config(nmne_config: Dict): - """ - Sets the configuration for capturing Malicious Network Events (MNEs) based on a provided dictionary. +class NMNEConfig(BaseModel): + """Store all the information to perform NMNE operations.""" - This function updates global settings related to NMNE capture, including whether to capture NMNEs and what - keywords to use for identifying NMNEs. + model_config = ConfigDict(extra="forbid") - The function ensures that the settings are updated only if they are provided in the `nmne_config` dictionary, - and maintains type integrity by checking the types of the provided values. - - :param nmne_config: A dictionary containing the NMNE configuration settings. Possible keys include: - "capture_nmne" (bool) to indicate whether NMNEs should be captured, "nmne_capture_keywords" (list of strings) - to specify keywords for NMNE identification. - """ - global NMNE_CAPTURE_KEYWORDS - global CAPTURE_NMNE - - # Update the NMNE capture flag, defaulting to False if not specified or if the type is incorrect - CAPTURE_NMNE = nmne_config.get("capture_nmne", False) - if not isinstance(CAPTURE_NMNE, bool): - CAPTURE_NMNE = True # Revert to default True if the provided value is not a boolean - - # Update the NMNE capture keywords, appending new keywords if provided - NMNE_CAPTURE_KEYWORDS += nmne_config.get("nmne_capture_keywords", []) - if not isinstance(NMNE_CAPTURE_KEYWORDS, list): - NMNE_CAPTURE_KEYWORDS = [] # Reset to empty list if the provided value is not a list + capture_nmne: bool = False + """Indicates whether Malicious Network Events (MNEs) should be captured.""" + nmne_capture_keywords: List[str] = [] + """List of keywords to identify malicious network events.""" + capture_by_direction: bool = True + """Captures should be organized by traffic direction (inbound/outbound).""" + capture_by_ip_address: bool = False + """Captures should be organized by source or destination IP address.""" + capture_by_protocol: bool = False + """Captures should be organized by network protocol (e.g., TCP, UDP).""" + capture_by_port: bool = False + """Captures should be organized by source or destination port.""" + capture_by_keyword: bool = False + """Captures should be filtered and categorised based on specific keywords.""" diff --git a/src/primaite/simulator/network/protocols/icmp.py b/src/primaite/simulator/network/protocols/icmp.py index 743e2375..9f0626f0 100644 --- a/src/primaite/simulator/network/protocols/icmp.py +++ b/src/primaite/simulator/network/protocols/icmp.py @@ -4,7 +4,7 @@ from enum import Enum from typing import Union from pydantic import BaseModel, field_validator, validate_call -from pydantic_core.core_schema import FieldValidationInfo +from pydantic_core.core_schema import ValidationInfo from primaite import getLogger @@ -96,7 +96,7 @@ class ICMPPacket(BaseModel): @field_validator("icmp_code") # noqa @classmethod - def _icmp_type_must_have_icmp_code(cls, v: int, info: FieldValidationInfo) -> int: + def _icmp_type_must_have_icmp_code(cls, v: int, info: ValidationInfo) -> int: """Validates the icmp_type and icmp_code.""" icmp_type = info.data["icmp_type"] if get_icmp_type_code_description(icmp_type, v): diff --git a/src/primaite/simulator/network/protocols/masquerade.py b/src/primaite/simulator/network/protocols/masquerade.py new file mode 100644 index 00000000..e2a7b6a0 --- /dev/null +++ b/src/primaite/simulator/network/protocols/masquerade.py @@ -0,0 +1,23 @@ +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +from enum import Enum +from typing import Optional + +from primaite.simulator.network.protocols.packet import DataPacket + + +class MasqueradePacket(DataPacket): + """Represents an generic malicious packet that is masquerading as another protocol.""" + + masquerade_protocol: Enum # The 'Masquerade' protocol that is currently in use + + masquerade_port: Enum # The 'Masquerade' port that is currently in use + + +class C2Packet(MasqueradePacket): + """Represents C2 suite communications packets.""" + + payload_type: Enum # The type of C2 traffic (e.g keep alive, command or command out) + + command: Optional[Enum] = None # Used to pass the actual C2 Command in C2 INPUT + + keep_alive_frequency: int diff --git a/src/primaite/simulator/network/protocols/ssh.py b/src/primaite/simulator/network/protocols/ssh.py new file mode 100644 index 00000000..be7f842f --- /dev/null +++ b/src/primaite/simulator/network/protocols/ssh.py @@ -0,0 +1,89 @@ +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK + +from enum import IntEnum +from typing import Optional + +from primaite.interface.request import RequestResponse +from primaite.simulator.network.protocols.packet import DataPacket + + +class SSHTransportMessage(IntEnum): + """ + Enum list of Transport layer messages that can be handled by the simulation. + + Each msg value is equivalent to the real-world. + """ + + SSH_MSG_USERAUTH_REQUEST = 50 + """Requests User Authentication.""" + + SSH_MSG_USERAUTH_FAILURE = 51 + """Indicates User Authentication failed.""" + + SSH_MSG_USERAUTH_SUCCESS = 52 + """Indicates User Authentication was successful.""" + + SSH_MSG_SERVICE_REQUEST = 24 + """Requests a service - such as executing a command.""" + + # These two msgs are invented for primAITE however are modelled on reality + + SSH_MSG_SERVICE_FAILED = 25 + """Indicates that the requested service failed.""" + + SSH_MSG_SERVICE_SUCCESS = 26 + """Indicates that the requested service was successful.""" + + +class SSHConnectionMessage(IntEnum): + """Int Enum list of all SSH's connection protocol messages that can be handled by the simulation.""" + + SSH_MSG_CHANNEL_OPEN = 80 + """Requests an open channel - Used in combination with SSH_MSG_USERAUTH_REQUEST.""" + + SSH_MSG_CHANNEL_OPEN_CONFIRMATION = 81 + """Confirms an open channel.""" + + SSH_MSG_CHANNEL_OPEN_FAILED = 82 + """Indicates that channel opening failure.""" + + SSH_MSG_CHANNEL_DATA = 84 + """Indicates that data is being sent through the channel.""" + + SSH_MSG_CHANNEL_CLOSE = 87 + """Closes the channel.""" + + +class SSHUserCredentials(DataPacket): + """Hold Username and Password in SSH Packets.""" + + username: str + """Username for login""" + + password: str + """Password for login""" + + +class SSHPacket(DataPacket): + """Represents an SSHPacket.""" + + transport_message: SSHTransportMessage + """Message Transport Type""" + + connection_message: SSHConnectionMessage + """Message Connection Status""" + + user_account: Optional[SSHUserCredentials] = None + """User Account Credentials if passed""" + + connection_request_uuid: Optional[str] = None + """Connection Request UUID used when establishing a remote connection""" + + connection_uuid: Optional[str] = None + """Connection UUID used when validating a remote connection""" + + ssh_output: Optional[RequestResponse] = None + """RequestResponse from Request Manager""" + + ssh_command: Optional[list] = None + """Request String""" diff --git a/src/primaite/simulator/system/applications/application.py b/src/primaite/simulator/system/applications/application.py index dc16a725..741f491d 100644 --- a/src/primaite/simulator/system/applications/application.py +++ b/src/primaite/simulator/system/applications/application.py @@ -214,3 +214,21 @@ class Application(IOSoftware): f"Cannot perform request on application '{self.application.name}' because it is not in the " f"{self.state.name} state." ) + + def _can_perform_network_action(self) -> bool: + """ + Checks if the application can perform outbound network actions. + + First confirms application suitability via the can_perform_action method. + Then confirms that the host has an enabled NIC that can be used for outbound traffic. + + :return: True if outbound network actions can be performed, otherwise False. + :rtype bool: + """ + if not super()._can_perform_action(): + return False + + for nic in self.software_manager.node.network_interface.values(): + if nic.enabled: + return True + return False diff --git a/src/primaite/simulator/system/applications/database_client.py b/src/primaite/simulator/system/applications/database_client.py index 06d22126..3f80c745 100644 --- a/src/primaite/simulator/system/applications/database_client.py +++ b/src/primaite/simulator/system/applications/database_client.py @@ -2,7 +2,7 @@ from __future__ import annotations from ipaddress import IPv4Address -from typing import Any, Dict, Optional +from typing import Any, Dict, Optional, Union from uuid import uuid4 from prettytable import MARKDOWN, PrettyTable @@ -54,6 +54,12 @@ class DatabaseClientConnection(BaseModel): if self.client and self.is_active: self.client._disconnect(self.connection_id) # noqa + def __str__(self) -> str: + return f"{self.__class__.__name__}(connection_id='{self.connection_id}', is_active={self.is_active})" + + def __repr__(self) -> str: + return str(self) + class DatabaseClient(Application, identifier="DatabaseClient"): """ @@ -67,7 +73,6 @@ class DatabaseClient(Application, identifier="DatabaseClient"): server_ip_address: Optional[IPv4Address] = None server_password: Optional[str] = None - _last_connection_successful: Optional[bool] = None _query_success_tracker: Dict[str, bool] = {} """Keep track of connections that were established or verified during this step. Used for rewards.""" last_query_response: Optional[Dict] = None @@ -76,7 +81,7 @@ class DatabaseClient(Application, identifier="DatabaseClient"): """Connection ID to the Database Server.""" client_connections: Dict[str, DatabaseClientConnection] = {} """Keep track of active connections to Database Server.""" - _client_connection_requests: Dict[str, Optional[str]] = {} + _client_connection_requests: Dict[str, Optional[Union[str, DatabaseClientConnection]]] = {} """Dictionary of connection requests to Database Server.""" connected: bool = False """Boolean Value for whether connected to DB Server.""" @@ -129,8 +134,6 @@ class DatabaseClient(Application, identifier="DatabaseClient"): :return: A dictionary representing the current state. """ state = super().describe_state() - # list of connections that were established or verified during this step. - state["last_connection_successful"] = self._last_connection_successful return state def show(self, markdown: bool = False): @@ -187,7 +190,7 @@ class DatabaseClient(Application, identifier="DatabaseClient"): return False return self._query("SELECT * FROM pg_stat_activity", connection_id=connection_id) - def _check_client_connection(self, connection_id: str) -> bool: + def _validate_client_connection_request(self, connection_id: str) -> bool: """Check that client_connection_id is valid.""" return True if connection_id in self._client_connection_requests else False @@ -211,23 +214,28 @@ class DatabaseClient(Application, identifier="DatabaseClient"): :type: is_reattempt: Optional[bool] """ if is_reattempt: - valid_connection = self._check_client_connection(connection_id=connection_request_id) - if valid_connection: + valid_connection_request = self._validate_client_connection_request(connection_id=connection_request_id) + if valid_connection_request: database_client_connection = self._client_connection_requests.pop(connection_request_id) - self.sys_log.info( - f"{self.name}: DatabaseClient connection to {server_ip_address} authorised." - f"Connection Request ID was {connection_request_id}." - ) - self.connected = True - self._last_connection_successful = True - return database_client_connection + if isinstance(database_client_connection, DatabaseClientConnection): + self.sys_log.info( + f"{self.name}: Connection request ({connection_request_id}) to {server_ip_address} authorised. " + f"Using connection id {database_client_connection}" + ) + self.connected = True + return database_client_connection + else: + self.sys_log.info( + f"{self.name}: Connection request ({connection_request_id}) to {server_ip_address} declined" + ) + return None else: - self.sys_log.warning( - f"{self.name}: DatabaseClient connection to {server_ip_address} declined." - f"Connection Request ID was {connection_request_id}." + self.sys_log.info( + f"{self.name}: Connection request ({connection_request_id}) to {server_ip_address} declined " + f"due to unknown client-side connection request id" ) - self._last_connection_successful = False return None + payload = {"type": "connect_request", "password": password, "connection_request_id": connection_request_id} software_manager: SoftwareManager = self.software_manager software_manager.send_payload_to_session_manager( @@ -300,9 +308,14 @@ class DatabaseClient(Application, identifier="DatabaseClient"): """ if not self._can_perform_action(): return None + connection_request_id = str(uuid4()) self._client_connection_requests[connection_request_id] = None + self.sys_log.info( + f"{self.name}: Sending new connection request ({connection_request_id}) to {self.server_ip_address}" + ) + return self._connect( server_ip_address=self.server_ip_address, password=self.server_password, @@ -339,10 +352,8 @@ class DatabaseClient(Application, identifier="DatabaseClient"): success = self._query_success_tracker.get(query_id) if success: self.sys_log.info(f"{self.name}: Query successful {sql}") - self._last_connection_successful = True return True self.sys_log.error(f"{self.name}: Unable to run query {sql}") - self._last_connection_successful = False return False else: software_manager: SoftwareManager = self.software_manager diff --git a/src/primaite/simulator/system/applications/red_applications/c2/__init__.py b/src/primaite/simulator/system/applications/red_applications/c2/__init__.py new file mode 100644 index 00000000..60e39743 --- /dev/null +++ b/src/primaite/simulator/system/applications/red_applications/c2/__init__.py @@ -0,0 +1,64 @@ +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +from typing import Optional, Union + +from pydantic import BaseModel, Field, field_validator, ValidationInfo + +from primaite.interface.request import RequestFormat + + +class CommandOpts(BaseModel): + """A C2 Pydantic Schema acting as a base class for all C2 Commands.""" + + @field_validator("payload", "exfiltration_folder_name", "ip_address", mode="before", check_fields=False) + @classmethod + def not_none(cls, v: str, info: ValidationInfo) -> int: + """If None is passed, use the default value instead.""" + if v is None: + return cls.model_fields[info.field_name].default + return v + + +class RansomwareOpts(CommandOpts): + """A Pydantic Schema for the Ransomware Configuration command options.""" + + server_ip_address: str + """The IP Address of the target database that the RansomwareScript will attack.""" + + payload: str = Field(default="ENCRYPT") + """The malicious payload to be used to attack the target database.""" + + +class RemoteOpts(CommandOpts): + """A base C2 Pydantic Schema for all C2 Commands that require a terminal connection.""" + + ip_address: Optional[str] = Field(default=None) + """The IP address of a remote host. If this field defaults to None then a local session is used.""" + + username: str + """A Username of a valid user account. Used to login into both remote and local hosts.""" + + password: str + """A Password of a valid user account. Used to login into both remote and local hosts.""" + + +class ExfilOpts(RemoteOpts): + """A Pydantic Schema for the C2 Data Exfiltration command options.""" + + target_ip_address: str + """The IP address of the target host that will be the target of the exfiltration.""" + + target_file_name: str + """The name of the file that is attempting to be exfiltrated.""" + + target_folder_name: str + """The name of the remote folder which contains the target file.""" + + exfiltration_folder_name: str = Field(default="exfiltration_folder") + """The name of C2 Suite folder used to store the target file. Defaults to ``exfiltration_folder``""" + + +class TerminalOpts(RemoteOpts): + """A Pydantic Schema for the C2 Terminal command options.""" + + commands: Union[list[RequestFormat], RequestFormat] + """A list or individual Terminal Command. Please refer to the RequestResponse system for further info.""" diff --git a/src/primaite/simulator/system/applications/red_applications/c2/abstract_c2.py b/src/primaite/simulator/system/applications/red_applications/c2/abstract_c2.py new file mode 100644 index 00000000..5d4cc8e0 --- /dev/null +++ b/src/primaite/simulator/system/applications/red_applications/c2/abstract_c2.py @@ -0,0 +1,488 @@ +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +from abc import abstractmethod +from enum import Enum +from ipaddress import IPv4Address +from typing import Dict, Optional, Union + +from pydantic import BaseModel, Field, validate_call + +from primaite.interface.request import RequestResponse +from primaite.simulator.file_system.file_system import FileSystem, Folder +from primaite.simulator.network.protocols.masquerade import C2Packet +from primaite.simulator.network.transmission.network_layer import IPProtocol +from primaite.simulator.network.transmission.transport_layer import Port +from primaite.simulator.system.applications.application import Application, ApplicationOperatingState +from primaite.simulator.system.core.session_manager import Session +from primaite.simulator.system.services.ftp.ftp_client import FTPClient +from primaite.simulator.system.services.ftp.ftp_server import FTPServer +from primaite.simulator.system.services.service import ServiceOperatingState +from primaite.simulator.system.software import SoftwareHealthState + + +class C2Command(Enum): + """Enumerations representing the different commands the C2 suite currently supports.""" + + RANSOMWARE_CONFIGURE = "Ransomware Configure" + "Instructs the c2 beacon to configure the ransomware with the provided options." + + RANSOMWARE_LAUNCH = "Ransomware Launch" + "Instructs the c2 beacon to execute the installed ransomware." + + DATA_EXFILTRATION = "Data Exfiltration" + "Instructs the c2 beacon to attempt to return a file to the C2 Server." + + TERMINAL = "Terminal" + "Instructs the c2 beacon to execute the provided terminal command." + + +class C2Payload(Enum): + """Represents the different types of command and control payloads.""" + + KEEP_ALIVE = "keep_alive" + """C2 Keep Alive payload. Used by the C2 beacon and C2 Server to confirm their connection.""" + + INPUT = "input_command" + """C2 Input Command payload. Used by the C2 Server to send a command to the c2 beacon.""" + + OUTPUT = "output_command" + """C2 Output Command. Used by the C2 Beacon to send the results of a Input command to the c2 server.""" + + +class AbstractC2(Application, identifier="AbstractC2"): + """ + An abstract command and control (c2) application. + + Extends the application class to provide base functionality for c2 suite applications + such as c2 beacons and c2 servers. + + Provides the base methods for handling ``Keep Alive`` connections, configuring masquerade ports and protocols + as well as providing the abstract methods for sending, receiving and parsing commands. + + Defaults to masquerading as HTTP (Port 80) via TCP. + + Please refer to the Command-&-Control notebook for an in-depth example of the C2 Suite. + """ + + c2_connection_active: bool = False + """Indicates if the c2 server and c2 beacon are currently connected.""" + + c2_remote_connection: IPv4Address = None + """The IPv4 Address of the remote c2 connection. (Either the IP of the beacon or the server).""" + + c2_session: Session = None + """The currently active session that the C2 Traffic is using. Set after establishing connection.""" + + keep_alive_inactivity: int = 0 + """Indicates how many timesteps since the last time the c2 application received a keep alive.""" + + class _C2Opts(BaseModel): + """A Pydantic Schema for the different C2 configuration options.""" + + keep_alive_frequency: int = Field(default=5, ge=1) + """The frequency at which ``Keep Alive`` packets are sent to the C2 Server from the C2 Beacon.""" + + masquerade_protocol: IPProtocol = Field(default=IPProtocol.TCP) + """The currently chosen protocol that the C2 traffic is masquerading as. Defaults as TCP.""" + + masquerade_port: Port = Field(default=Port.HTTP) + """The currently chosen port that the C2 traffic is masquerading as. Defaults at HTTP.""" + + c2_config: _C2Opts = _C2Opts() + """ + Holds the current configuration settings of the C2 Suite. + + The C2 beacon initialise this class through it's internal configure method. + + The C2 Server when receiving a keep alive will initialise it's own configuration + to match that of the configuration settings passed in the keep alive through _resolve keep alive. + + If the C2 Beacon is reconfigured then a new keep alive is set which causes the + C2 beacon to reconfigure it's configuration settings. + """ + + def _craft_packet( + self, c2_payload: C2Payload, c2_command: Optional[C2Command] = None, command_options: Optional[Dict] = {} + ) -> C2Packet: + """ + Creates and returns a Masquerade Packet using the parameters given. + + The packet uses the current c2 configuration and parameters given + to construct the base networking information such as the masquerade + protocol/port. Additionally all C2 Traffic packets pass the currently + in use C2 configuration. This ensures that the all C2 applications + can keep their configuration in sync. + + :param c2_payload: The type of C2 Traffic ot be sent + :type c2_payload: C2Payload + :param c2_command: The C2 command to be sent to the C2 Beacon. + :type c2_command: C2Command. + :param command_options: The relevant C2 Beacon parameters.F + :type command_options: Dict + :return: Returns the construct C2Packet + :rtype: C2Packet + """ + constructed_packet = C2Packet( + masquerade_protocol=self.c2_config.masquerade_protocol, + masquerade_port=self.c2_config.masquerade_port, + keep_alive_frequency=self.c2_config.keep_alive_frequency, + payload_type=c2_payload, + command=c2_command, + payload=command_options, + ) + return constructed_packet + + def describe_state(self) -> Dict: + """ + Describe the state of the C2 application. + + :return: A dictionary representation of the C2 application's state. + :rtype: Dict + """ + return super().describe_state() + + def __init__(self, **kwargs): + """Initialise the C2 applications to by default listen for HTTP traffic.""" + kwargs["listen_on_ports"] = {Port.HTTP, Port.FTP, Port.DNS} + kwargs["port"] = Port.NONE + kwargs["protocol"] = IPProtocol.TCP + super().__init__(**kwargs) + + @property + def _host_ftp_client(self) -> Optional[FTPClient]: + """Return the FTPClient that is installed C2 Application's host. + + This method confirms that the FTP Client is functional via the ._can_perform_action + method. If the FTP Client service is not in a suitable state (e.g disabled/pause) + then this method will return None. + + (The FTP Client service is installed by default) + + :return: An FTPClient object is successful, else None + :rtype: union[FTPClient, None] + """ + ftp_client: Union[FTPClient, None] = self.software_manager.software.get("FTPClient") + if ftp_client is None: + self.sys_log.warning(f"{self.__class__.__name__}: No FTPClient. Attempting to install.") + self.software_manager.install(FTPClient) + ftp_client = self.software_manager.software.get("FTPClient") + + # Force start if the service is stopped. + if ftp_client.operating_state == ServiceOperatingState.STOPPED: + if not ftp_client.start(): + self.sys_log.warning(f"{self.__class__.__name__}: cannot start the FTP Client.") + + if not ftp_client._can_perform_action(): + self.sys_log.error(f"{self.__class__.__name__}: is unable to use the FTP service on its host.") + return + + return ftp_client + + @property + def _host_ftp_server(self) -> Optional[FTPServer]: + """ + Returns the FTP Server that is installed C2 Application's host. + + If a FTPServer is not installed then this method will attempt to install one. + + :return: An FTPServer object is successful, else None + :rtype: Optional[FTPServer] + """ + ftp_server: Optional[FTPServer] = self.software_manager.software.get("FTPServer") + if ftp_server is None: + self.sys_log.warning(f"{self.__class__.__name__}:No FTPServer installed. Attempting to install FTPServer.") + self.software_manager.install(FTPServer) + ftp_server = self.software_manager.software.get("FTPServer") + + # Force start if the service is stopped. + if ftp_server.operating_state == ServiceOperatingState.STOPPED: + if not ftp_server.start(): + self.sys_log.warning(f"{self.__class__.__name__}: cannot start the FTP Server.") + + if not ftp_server._can_perform_action(): + self.sys_log.error(f"{self.__class__.__name__}: is unable use FTP Server service on its host.") + return + + return ftp_server + + # Getter property for the get_exfiltration_folder method () + @property + def _host_file_system(self) -> FileSystem: + """Return the C2 Host's filesystem (Used for exfiltration related commands) .""" + host_file_system: FileSystem = self.software_manager.file_system + if host_file_system is None: + self.sys_log.error(f"{self.__class__.__name__}: does not seem to have a file system!") + return host_file_system + + def get_exfiltration_folder(self, folder_name: Optional[str] = "exfiltration_folder") -> Optional[Folder]: + """Return a folder used for storing exfiltrated data. Otherwise returns None.""" + if self._host_file_system is None: + return + exfiltration_folder: Union[Folder, None] = self._host_file_system.get_folder(folder_name) + if exfiltration_folder is None: + self.sys_log.info(f"{self.__class__.__name__}: Creating a exfiltration folder.") + return self._host_file_system.create_folder(folder_name=folder_name) + + return exfiltration_folder + + # Validate call ensures we are only handling Masquerade Packets. + @validate_call + def _handle_c2_payload(self, payload: C2Packet, session_id: Optional[str] = None) -> bool: + """Handles masquerade payloads for both c2 beacons and c2 servers. + + Currently, the C2 application suite can handle the following payloads: + + KEEP ALIVE: + Establishes or confirms connection from the C2 Beacon to the C2 server. + Sent by both C2 beacons and C2 Servers. + + INPUT: + Contains a c2 command which must be executed by the C2 beacon. + Sent by C2 Servers and received by C2 Beacons. + + OUTPUT: + Contains the output of a c2 command which must be returned to the C2 Server. + Sent by C2 Beacons and received by C2 Servers + + The payload is passed to a different method dependant on the payload type. + + :param payload: The C2 Payload to be parsed and handled. + :return: True if the c2 payload was handled successfully, False otherwise. + :rtype: Bool + """ + if payload.payload_type == C2Payload.KEEP_ALIVE: + self.sys_log.info(f"{self.name} received a KEEP ALIVE payload.") + return self._handle_keep_alive(payload, session_id) + + elif payload.payload_type == C2Payload.INPUT: + self.sys_log.info(f"{self.name} received an INPUT COMMAND payload.") + return self._handle_command_input(payload, session_id) + + elif payload.payload_type == C2Payload.OUTPUT: + self.sys_log.info(f"{self.name} received an OUTPUT COMMAND payload.") + return self._handle_command_output(payload) + + else: + self.sys_log.warning( + f"{self.name} received an unexpected c2 payload:{payload.payload_type}. Dropping Packet." + ) + return False + + @abstractmethod + def _handle_command_output(payload): + """Abstract Method: Used in C2 server to parse and receive the output of commands sent to the c2 beacon.""" + pass + + @abstractmethod + def _handle_command_input(payload): + """Abstract Method: Used in C2 beacon to parse and handle commands received from the c2 server.""" + pass + + @abstractmethod + def _handle_keep_alive(self, payload: C2Packet, session_id: Optional[str]) -> bool: + """Abstract Method: Each C2 suite handles ``C2Payload.KEEP_ALIVE`` differently.""" + pass + + # from_network_interface=from_network_interface + def receive(self, payload: any, session_id: Optional[str] = None, **kwargs) -> bool: + """Receives masquerade packets. Used by both c2 server and c2 beacon. + + Defining the `Receive` method so that the application can receive packets via the session manager. + These packets are then immediately handed to ._handle_c2_payload. + + :param payload: The Masquerade Packet to be received. + :type payload: C2Packet + :param session_id: The transport session_id that the payload is originating from. + :type session_id: str + :return: Returns a bool if the traffic was received correctly (See _handle_c2_payload.) + :rtype: bool + """ + if not isinstance(payload, C2Packet): + self.sys_log.warning(f"{self.name}: Payload is not an C2Packet") + self.sys_log.debug(f"{self.name}: {payload}") + return False + + return self._handle_c2_payload(payload, session_id) + + def _send_keep_alive(self, session_id: Optional[str]) -> bool: + """Sends a C2 keep alive payload to the self.remote_connection IPv4 Address. + + Used by both the c2 client and the s2 server for establishing and confirming connection. + This method also contains some additional validation to ensure that the C2 applications + are correctly configured before sending any traffic. + + :param session_id: The transport session_id that the payload is originating from. + :type session_id: str + :returns: Returns True if a send alive was successfully sent. False otherwise. + :rtype bool: + """ + # Checking that the c2 application is capable of connecting to remote. + # Purely a safety guard clause. + if not (connection_status := self._check_connection()[0]): + self.sys_log.warning( + f"{self.name}: Unable to send keep alive due to c2 connection status: {connection_status}." + ) + return False + + # Passing our current C2 configuration in remain in sync. + keep_alive_packet = self._craft_packet(c2_payload=C2Payload.KEEP_ALIVE) + + # Sending the keep alive via the .send() method (as with all other applications.) + if self.send( + payload=keep_alive_packet, + dest_ip_address=self.c2_remote_connection, + dest_port=self.c2_config.masquerade_port, + ip_protocol=self.c2_config.masquerade_protocol, + session_id=session_id, + ): + # Setting the keep_alive_sent guard condition to True. This is used to prevent packet storms. + # This prevents the _resolve_keep_alive method from calling this method again (until the next timestep.) + self.keep_alive_sent = True + self.sys_log.info(f"{self.name}: Keep Alive sent to {self.c2_remote_connection}") + self.sys_log.debug( + f"{self.name}: Keep Alive sent to {self.c2_remote_connection} " + f"Masquerade Port: {self.c2_config.masquerade_port} " + f"Masquerade Protocol: {self.c2_config.masquerade_protocol} " + ) + return True + else: + self.sys_log.warning( + f"{self.name}: Failed to send a Keep Alive. The node may be unable to access networking resources." + ) + return False + + def _resolve_keep_alive(self, payload: C2Packet, session_id: Optional[str]) -> bool: + """ + Parses the Masquerade Port/Protocol within the received Keep Alive packet. + + Used to dynamically set the Masquerade Port and Protocol based on incoming traffic. + + Returns True on successfully extracting and configuring the masquerade port/protocols. + Returns False otherwise. + + :param payload: The Keep Alive payload received. + :type payload: C2Packet + :param session_id: The transport session_id that the payload is originating from. + :type session_id: str + :return: True on successful configuration, false otherwise. + :rtype: bool + """ + # Validating that they are valid Enums. + if not isinstance(payload.masquerade_port, Port) or not isinstance(payload.masquerade_protocol, IPProtocol): + self.sys_log.warning( + f"{self.name}: Received invalid Masquerade Values within Keep Alive." + f"Port: {payload.masquerade_port} Protocol: {payload.masquerade_protocol}." + ) + return False + + # Updating the C2 Configuration attribute. + + self.c2_config.masquerade_port = payload.masquerade_port + self.c2_config.masquerade_protocol = payload.masquerade_protocol + self.c2_config.keep_alive_frequency = payload.keep_alive_frequency + + self.sys_log.debug( + f"{self.name}: C2 Config Resolved Config from Keep Alive:" + f"Masquerade Port: {self.c2_config.masquerade_port}" + f"Masquerade Protocol: {self.c2_config.masquerade_protocol}" + f"Keep Alive Frequency: {self.c2_config.keep_alive_frequency}" + ) + + # This statement is intended to catch on the C2 Application that is listening for connection. + if self.c2_remote_connection is None: + self.sys_log.debug(f"{self.name}: Attempting to configure remote C2 connection based off received output.") + self.c2_remote_connection = IPv4Address(self.c2_session.with_ip_address) + + self.c2_connection_active = True # Sets the connection to active + self.keep_alive_inactivity = 0 # Sets the keep alive inactivity to zero + + return True + + def _reset_c2_connection(self) -> None: + """ + Resets all currently established C2 communications to their default setting. + + This method is called once a C2 application considers their remote connection + severed and reverts back to default settings. Worth noting that that this will + revert any non-default configuration that a user/agent may have set. + """ + self.c2_connection_active = False + self.c2_session = None + self.keep_alive_inactivity = 0 + self.keep_alive_frequency = 5 + self.c2_remote_connection = None + self.c2_config.masquerade_port = Port.HTTP + self.c2_config.masquerade_protocol = IPProtocol.TCP + + @abstractmethod + def _confirm_remote_connection(self, timestep: int) -> bool: + """ + Abstract method - Confirms the suitability of the current C2 application remote connection. + + Each application will have perform different behaviour to confirm the remote connection. + + :return: Boolean. True if remote connection is confirmed, false otherwise. + """ + + def apply_timestep(self, timestep: int) -> None: + """Apply a timestep to the c2_server & c2 beacon. + + Used to keep track of when the c2 server should consider a beacon dead + and set it's c2_remote_connection attribute to false. + + 1. Each timestep the keep_alive_inactivity is increased. + + 2. If the keep alive inactivity eclipses that of the keep alive frequency then another keep alive is sent. + + 3. If a keep alive response packet is received then the ``keep_alive_inactivity`` attribute is reset. + + Therefore, if ``keep_alive_inactivity`` attribute is not 0 after a keep alive is sent + then the connection is considered severed and c2 beacon will shut down. + + :param timestep: The current timestep of the simulation. + :type timestep: Int + :return bool: Returns false if connection was lost. Returns True if connection is active or re-established. + :rtype bool: + """ + if ( + self.operating_state is ApplicationOperatingState.RUNNING + and self.health_state_actual is SoftwareHealthState.GOOD + and self.c2_connection_active is True + ): + self.keep_alive_inactivity += 1 + self._confirm_remote_connection(timestep) + return super().apply_timestep(timestep=timestep) + + def _check_connection(self) -> tuple[bool, RequestResponse]: + """ + Validation method: Checks that the C2 application is capable of sending C2 Command input/output. + + Performs a series of connection validation to ensure that the C2 application is capable of + sending and responding to the remote c2 connection. This method is used to confirm connection + before carrying out Agent Commands hence why this method also returns a tuple + containing both a success boolean as well as RequestResponse. + + :return: A tuple containing a boolean True/False and a corresponding Request Response + :rtype: tuple[bool, RequestResponse] + """ + if not self._can_perform_network_action(): + self.sys_log.warning(f"{self.name}: Unable to make leverage networking resources. Rejecting Command.") + return ( + False, + RequestResponse( + status="failure", data={"Reason": "Unable to access networking resources. Unable to send command."} + ), + ) + + if self.c2_remote_connection is None: + self.sys_log.warning(f"{self.name}: C2 Application has yet to establish connection. Rejecting command.") + return ( + False, + RequestResponse( + status="failure", + data={"Reason": "C2 Application has yet to establish connection. Unable to send command."}, + ), + ) + return ( + True, + RequestResponse(status="success", data={"Reason": "C2 Application is able to send connections."}), + ) diff --git a/src/primaite/simulator/system/applications/red_applications/c2/c2_beacon.py b/src/primaite/simulator/system/applications/red_applications/c2/c2_beacon.py new file mode 100644 index 00000000..fa0271e5 --- /dev/null +++ b/src/primaite/simulator/system/applications/red_applications/c2/c2_beacon.py @@ -0,0 +1,636 @@ +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +from enum import Enum +from ipaddress import IPv4Address +from typing import Dict, Optional + +from prettytable import MARKDOWN, PrettyTable +from pydantic import validate_call + +from primaite.interface.request import RequestFormat, RequestResponse +from primaite.simulator.core import RequestManager, RequestType +from primaite.simulator.network.protocols.masquerade import C2Packet +from primaite.simulator.network.transmission.network_layer import IPProtocol +from primaite.simulator.network.transmission.transport_layer import Port +from primaite.simulator.system.applications.red_applications.c2 import ExfilOpts, RansomwareOpts, TerminalOpts +from primaite.simulator.system.applications.red_applications.c2.abstract_c2 import AbstractC2, C2Command, C2Payload +from primaite.simulator.system.applications.red_applications.ransomware_script import RansomwareScript +from primaite.simulator.system.services.terminal.terminal import Terminal, TerminalClientConnection + + +class C2Beacon(AbstractC2, identifier="C2Beacon"): + """ + C2 Beacon Application. + + Represents a vendor generic C2 beacon is used in conjunction with the C2 Server + to simulate malicious communications and infrastructure within primAITE. + + Must be configured with the C2 Server's IP Address upon installation. + Please refer to the _configure method for further information. + + Extends the Abstract C2 application to include the following: + + 1. Receiving commands from the C2 Server (Command input) + 2. Leveraging the terminal application to execute requests (dependent on the command given) + 3. Sending the RequestResponse back to the C2 Server (Command output) + + Please refer to the Command-&-Control notebook for an in-depth example of the C2 Suite. + """ + + keep_alive_attempted: bool = False + """Indicates if a keep alive has been attempted to be sent this timestep. Used to prevent packet storms.""" + + terminal_session: TerminalClientConnection = None + "The currently in use terminal session." + + @property + def _host_terminal(self) -> Optional[Terminal]: + """Return the Terminal that is installed on the same machine as the C2 Beacon.""" + host_terminal: Terminal = self.software_manager.software.get("Terminal") + if host_terminal is None: + self.sys_log.warning(f"{self.__class__.__name__} cannot find a terminal on its host.") + return host_terminal + + @property + def _host_ransomware_script(self) -> RansomwareScript: + """Return the RansomwareScript that is installed on the same machine as the C2 Beacon.""" + ransomware_script: RansomwareScript = self.software_manager.software.get("RansomwareScript") + if ransomware_script is None: + self.sys_log.warning(f"{self.__class__.__name__} cannot find installed ransomware on its host.") + return ransomware_script + + def _set_terminal_session(self, username: str, password: str, ip_address: Optional[IPv4Address] = None) -> bool: + """ + Attempts to create and a terminal session using the parameters given. + + If an IP Address is passed then this method will attempt to create a remote terminal + session. Otherwise a local terminal session will be created. + + :return: Returns true if a terminal session was successfully set. False otherwise. + :rtype: Bool + """ + self.terminal_session is None + host_terminal: Terminal = self._host_terminal + self.terminal_session = host_terminal.login(username=username, password=password, ip_address=ip_address) + return self.terminal_session is not None + + def _init_request_manager(self) -> RequestManager: + """ + Initialise the request manager. + + More information in user guide and docstring for SimComponent._init_request_manager. + """ + rm = super()._init_request_manager() + rm.add_request( + name="execute", + request_type=RequestType(func=lambda request, context: RequestResponse.from_bool(self.establish())), + ) + + def _configure(request: RequestFormat, context: Dict) -> RequestResponse: + """ + Request for configuring the C2 Beacon. + + :param request: Request with one element containing a dict of parameters for the configure method. + :type request: RequestFormat + :param context: additional context for resolving this action, currently unused + :type context: dict + :return: RequestResponse object with a success code reflecting whether the configuration could be applied. + :rtype: RequestResponse + """ + c2_remote_ip = request[-1].get("c2_server_ip_address") + if c2_remote_ip is None: + self.sys_log.error(f"{self.name}: Did not receive C2 Server IP in configuration parameters.") + RequestResponse( + status="failure", data={"No C2 Server IP given to C2 beacon. Unable to configure C2 Beacon"} + ) + + c2_remote_ip = IPv4Address(c2_remote_ip) + frequency = request[-1].get("keep_alive_frequency") + protocol = request[-1].get("masquerade_protocol") + port = request[-1].get("masquerade_port") + + return RequestResponse.from_bool( + self.configure( + c2_server_ip_address=c2_remote_ip, + keep_alive_frequency=frequency, + masquerade_protocol=IPProtocol[protocol], + masquerade_port=Port[port], + ) + ) + + rm.add_request("configure", request_type=RequestType(func=_configure)) + return rm + + def __init__(self, **kwargs): + kwargs["name"] = "C2Beacon" + super().__init__(**kwargs) + + # Configure is practically setter method for the ``c2.config`` attribute that also ties into the request manager. + @validate_call + def configure( + self, + c2_server_ip_address: IPv4Address = None, + keep_alive_frequency: int = 5, + masquerade_protocol: Enum = IPProtocol.TCP, + masquerade_port: Enum = Port.HTTP, + ) -> bool: + """ + Configures the C2 beacon to communicate with the C2 server. + + The C2 Beacon has four different configuration options which can be used to + modify the networking behaviour between the C2 Server and the C2 Beacon. + + Configuration Option | Option Meaning + ---------------------|------------------------ + c2_server_ip_address | The IP Address of the C2 Server. (The C2 Server must be running) + keep_alive_frequency | How often should the C2 Beacon confirm it's connection in timesteps. + masquerade_protocol | What protocol should the C2 traffic masquerade as? (HTTP, FTP or DNS) + masquerade_port | What port should the C2 traffic use? (TCP or UDP) + + These configuration options are used to reassign the fields in the inherited inner class + ``c2_config``. + + If a connection is already in progress then this method also sends a keep alive to the C2 + Server in order for the C2 Server to sync with the new configuration settings. + + :param c2_server_ip_address: The IP Address of the C2 Server. Used to establish connection. + :type c2_server_ip_address: IPv4Address + :param keep_alive_frequency: The frequency (timesteps) at which the C2 beacon will send keep alive(s). + :type keep_alive_frequency: Int + :param masquerade_protocol: The Protocol that C2 Traffic will masquerade as. Defaults to TCP. + :type masquerade_protocol: Enum (IPProtocol) + :param masquerade_port: The Port that the C2 Traffic will masquerade as. Defaults to FTP. + :type masquerade_port: Enum (Port) + :return: Returns True if the configuration was successful, False otherwise. + """ + self.c2_remote_connection = IPv4Address(c2_server_ip_address) + self.c2_config.keep_alive_frequency = keep_alive_frequency + self.c2_config.masquerade_port = masquerade_port + self.c2_config.masquerade_protocol = masquerade_protocol + self.sys_log.info( + f"{self.name}: Configured {self.name} with remote C2 server connection: {c2_server_ip_address=}." + ) + self.sys_log.debug( + f"{self.name}: configured with the following settings:" + f"Remote C2 Server: {c2_server_ip_address}" + f"Keep Alive Frequency {keep_alive_frequency}" + f"Masquerade Protocol: {masquerade_protocol}" + f"Masquerade Port: {masquerade_port}" + ) + # Send a keep alive to the C2 Server if we already have a keep alive. + if self.c2_connection_active is True: + self.sys_log.info(f"{self.name}: Updating C2 Server with updated C2 configuration.") + return self._send_keep_alive(self.c2_session.uuid if not None else None) + return True + + def establish(self) -> bool: + """Establishes connection to the C2 server via a send alive. The C2 Beacon must already be configured.""" + if self.c2_remote_connection is None: + self.sys_log.info(f"{self.name}: Failed to establish connection. C2 Beacon has not been configured.") + return False + self.run() + self.num_executions += 1 + # Creates a new session if using the establish method. + return self._send_keep_alive(session_id=None) + + def _handle_command_input(self, payload: C2Packet, session_id: Optional[str]) -> bool: + """ + Handles the parsing of C2 Commands from C2 Traffic (Masquerade Packets). + + Dependant the C2 Command parsed from the payload, the following methods are called and returned: + + C2 Command | Internal Method + ---------------------|------------------------ + RANSOMWARE_CONFIGURE | self._command_ransomware_config() + RANSOMWARE_LAUNCH | self._command_ransomware_launch() + DATA_EXFILTRATION | self._command_data_exfiltration() + TERMINAL | self._command_terminal() + + Please see each method individually for further information regarding + the implementation of these commands. + + :param payload: The INPUT C2 Payload + :type payload: C2Packet + :return: The Request Response provided by the terminal execute method. + :rtype Request Response: + """ + command = payload.command + if not isinstance(command, C2Command): + self.sys_log.warning(f"{self.name}: Received unexpected C2 command. Unable to resolve command") + return self._return_command_output( + command_output=RequestResponse( + status="failure", + data={"Reason": "C2 Beacon received unexpected C2Command. Unable to resolve command."}, + ), + session_id=session_id, + ) + + if command == C2Command.RANSOMWARE_CONFIGURE: + self.sys_log.info(f"{self.name}: Received a ransomware configuration C2 command.") + return self._return_command_output( + command_output=self._command_ransomware_config(payload), session_id=session_id + ) + + elif command == C2Command.RANSOMWARE_LAUNCH: + self.sys_log.info(f"{self.name}: Received a ransomware launch C2 command.") + return self._return_command_output( + command_output=self._command_ransomware_launch(payload), session_id=session_id + ) + + elif command == C2Command.TERMINAL: + self.sys_log.info(f"{self.name}: Received a terminal C2 command.") + return self._return_command_output(command_output=self._command_terminal(payload), session_id=session_id) + + elif command == C2Command.DATA_EXFILTRATION: + self.sys_log.info(f"{self.name}: Received a Data Exfiltration C2 command.") + return self._return_command_output( + command_output=self._command_data_exfiltration(payload), session_id=session_id + ) + + else: + self.sys_log.error(f"{self.name}: Received an C2 command: {command} but was unable to resolve command.") + return self._return_command_output( + RequestResponse(status="failure", data={"Reason": "Unexpected Behaviour. Unable to resolve command."}) + ) + + def _return_command_output(self, command_output: RequestResponse, session_id: Optional[str] = None) -> bool: + """Responsible for responding to the C2 Server with the output of the given command. + + :param command_output: The RequestResponse returned by the terminal application's execute method. + :type command_output: Request Response + :param session_id: The current session established with the C2 Server. + :type session_id: Str + """ + output_packet = self._craft_packet(c2_payload=C2Payload.OUTPUT, command_options=command_output) + if self.send( + payload=output_packet, + dest_ip_address=self.c2_remote_connection, + dest_port=self.c2_config.masquerade_port, + ip_protocol=self.c2_config.masquerade_protocol, + session_id=session_id, + ): + self.sys_log.info(f"{self.name}: Command output sent to {self.c2_remote_connection}") + self.sys_log.debug( + f"{self.name}: on {self.c2_config.masquerade_port} via {self.c2_config.masquerade_protocol}" + ) + return True + else: + self.sys_log.warning( + f"{self.name}: failed to send a output packet. The node may be unable to access the network." + ) + return False + + def _command_ransomware_config(self, payload: C2Packet) -> RequestResponse: + """ + C2 Command: Ransomware Configuration. + + Calls the locally installed RansomwareScript application's configure method + and passes the given parameters. + + The class attribute self._host_ransomware_script will return None if the host + does not have an instance of the RansomwareScript. + + :payload C2Packet: The incoming INPUT command. + :type Masquerade Packet: C2Packet. + :return: Returns the Request Response returned by the Terminal execute method. + :rtype: Request Response + """ + command_opts = RansomwareOpts.model_validate(payload.payload) + if self._host_ransomware_script is None: + return RequestResponse( + status="failure", + data={"Reason": "Cannot find any instances of a RansomwareScript. Have you installed one?"}, + ) + return RequestResponse.from_bool( + self._host_ransomware_script.configure( + server_ip_address=command_opts.server_ip_address, payload=command_opts.payload + ) + ) + + def _command_ransomware_launch(self, payload: C2Packet) -> RequestResponse: + """ + C2 Command: Ransomware Launch. + + Uses the RansomwareScript's public method .attack() to carry out the + ransomware attack and uses the .from_bool method to return a RequestResponse + + :payload C2Packet: The incoming INPUT command. + :type Masquerade Packet: C2Packet. + :return: Returns the Request Response returned by the Terminal execute method. + :rtype: Request Response + """ + if self._host_ransomware_script is None: + return RequestResponse( + status="failure", + data={"Reason": "Cannot find any instances of a RansomwareScript. Have you installed one?"}, + ) + return RequestResponse.from_bool(self._host_ransomware_script.attack()) + + def _command_data_exfiltration(self, payload: C2Packet) -> RequestResponse: + """ + C2 Command: Data Exfiltration. + + Uses the FTP Client & Server services to perform the data exfiltration. + + This command instructs the C2 Beacon to ssh into the target ip + and execute a command which causes the FTPClient service to send a + + target file will be moved from the target IP address onto the C2 Beacon's host + file system. + + However, if no IP is given, then the command will move the target file from this + machine onto the C2 server. (This logic is performed on the C2) + + :payload C2Packet: The incoming INPUT command. + :type Masquerade Packet: C2Packet. + :return: Returns a tuple containing Request Response returned by the Terminal execute method. + :rtype: Request Response + """ + if self._host_ftp_server is None: + self.sys_log.warning(f"{self.name}: C2 Beacon unable to the FTP Server. Unable to resolve command.") + return RequestResponse( + status="failure", + data={"Reason": "Cannot find any instances of both a FTP Server & Client. Are they installed?"}, + ) + + command_opts = ExfilOpts.model_validate(payload.payload) + + # Setting up the terminal session and the ftp server + if not self._set_terminal_session( + username=command_opts.username, password=command_opts.password, ip_address=command_opts.target_ip_address + ): + return RequestResponse( + status="failure", data={"Reason": "Cannot create a terminal session. Are the credentials correct?"} + ) + + # Using the terminal to start the FTP Client on the remote machine. + self.terminal_session.execute(command=["service", "start", "FTPClient"]) + + # Need to supply to the FTP Client the C2 Beacon's host IP. + host_network_interfaces = self.software_manager.node.network_interfaces + local_ip = host_network_interfaces.get(next(iter(host_network_interfaces))).ip_address + + # Creating the FTP creation options. + ftp_opts = { + "dest_ip_address": str(local_ip), + "src_folder_name": command_opts.target_folder_name, + "src_file_name": command_opts.target_file_name, + "dest_folder_name": command_opts.exfiltration_folder_name, + "dest_file_name": command_opts.target_file_name, + } + + attempt_exfiltration: tuple[bool, RequestResponse] = self._perform_exfiltration(ftp_opts) + + if attempt_exfiltration[0] is False: + self.sys_log.error(f"{self.name}: File Exfiltration Attempt Failed: {attempt_exfiltration[1].data}") + return attempt_exfiltration[1] + + # Sending the transferred target data back to the C2 Server to successfully exfiltrate the data out the network. + + return RequestResponse.from_bool( + self._host_ftp_client.send_file( + dest_ip_address=self.c2_remote_connection, + src_folder_name=command_opts.exfiltration_folder_name, # The Exfil folder is inherited attribute. + src_file_name=command_opts.target_file_name, + dest_folder_name=command_opts.exfiltration_folder_name, + dest_file_name=command_opts.target_file_name, + ) + ) + + def _perform_exfiltration(self, ftp_opts: dict) -> tuple[bool, RequestResponse]: + """ + Attempts to exfiltrate a target file from a target using the parameters given. + + Uses the current terminal_session to send a command to the + remote host's FTP Client passing the ExfilOpts as command options. + + This will instruct the FTP client to send the target file to the + dest_ip_address's destination folder. + + This method assumes that the following: + 1. The self.terminal_session is the remote target. + 2. The target has a functioning FTP Client Service. + + + :ExfilOpts: A Pydantic model containing the require configuration options + :type ExfilOpts: ExfilOpts + :return: Returns a tuple containing a success boolean and a Request Response.. + :rtype: tuple[bool, RequestResponse + """ + # Creating the exfiltration folder . + exfiltration_folder = self.get_exfiltration_folder(ftp_opts.get("dest_folder_name")) + + # Using the terminal to send the target data back to the C2 Beacon. + exfil_response: RequestResponse = RequestResponse.from_bool( + self.terminal_session.execute(command=["service", "FTPClient", "send", ftp_opts]) + ) + + # Validating that we successfully received the target data. + + if exfil_response.status == "failure": + self.sys_log.warning(f"{self.name}: Remote connection failure. failed to transfer the target data via FTP.") + return [False, exfil_response] + + # Target file: + target_file: str = ftp_opts.get("src_file_name") + + if exfiltration_folder.get_file(target_file) is None: + self.sys_log.warning( + f"{self.name}: Unable to locate exfiltrated file on local filesystem. " + f"Perhaps the file transfer failed?" + ) + return [ + False, + RequestResponse(status="failure", data={"reason": "Unable to locate exfiltrated data on file system."}), + ] + + if self._host_ftp_client is None: + self.sys_log.warning(f"{self.name}: C2 Beacon unable to the FTP Server. Unable to resolve command.") + return [ + False, + RequestResponse( + status="failure", + data={"Reason": "Cannot find any instances of both a FTP Server & Client. Are they installed?"}, + ), + ] + + return [ + True, + RequestResponse( + status="success", + data={"Reason": "Located the target file on local file system. Data exfiltration successful."}, + ), + ] + + def _command_terminal(self, payload: C2Packet) -> RequestResponse: + """ + C2 Command: Terminal. + + Creates a request that executes a terminal command. + This request is then sent to the terminal service in order to be executed. + + :payload C2Packet: The incoming INPUT command. + :type Masquerade Packet: C2Packet. + :return: Returns the Request Response returned by the Terminal execute method. + :rtype: Request Response + """ + command_opts = TerminalOpts.model_validate(payload.payload) + + if self._host_terminal is None: + return RequestResponse( + status="failure", + data={"Reason": "Host does not seem to have terminal installed. Unable to resolve command."}, + ) + + terminal_output: Dict[int, RequestResponse] = {} + + # Creating a remote terminal session if given an IP Address, otherwise using a local terminal session. + if not self._set_terminal_session( + username=command_opts.username, password=command_opts.password, ip_address=command_opts.ip_address + ): + return RequestResponse( + status="failure", + data={"Reason": "Cannot create a terminal session. Are the credentials correct?"}, + ) + + # Converts a singular terminal command: [RequestFormat] into a list with one element [[RequestFormat]] + # Checks the first element - if this element is a str then there must be multiple commands. + command_opts.commands = ( + [command_opts.commands] if isinstance(command_opts.commands[0], str) else command_opts.commands + ) + + for index, given_command in enumerate(command_opts.commands): + # A try catch exception ladder was used but was considered not the best approach + # as it can end up obscuring visibility of actual bugs (Not the expected ones) and was a temporary solution. + # TODO: Refactor + add further validation to ensure that a request is correct. (maybe a pydantic method?) + terminal_output[index] = self.terminal_session.execute(given_command) + + # Reset our remote terminal session. + self.terminal_session is None + return RequestResponse(status="success", data=terminal_output) + + def _handle_keep_alive(self, payload: C2Packet, session_id: Optional[str]) -> bool: + """ + Handles receiving and sending keep alive payloads. This method is only called if we receive a keep alive. + + In the C2 Beacon implementation of this method the c2 connection active boolean + is set to true and the keep alive inactivity is reset only after sending a keep alive + as wel as receiving a response back from the C2 Server. + + This is because the C2 Server is the listener and thus will only ever receive packets from + the C2 Beacon rather than the other way around. (The C2 Beacon is akin to a reverse shell) + + Therefore, we need a response back from the listener (C2 Server) + before the C2 beacon is able to confirm it's connection. + + Returns False if a keep alive was unable to be sent. + Returns True if a keep alive was successfully sent or already has been sent this timestep. + + :return: True if successfully handled, false otherwise. + :rtype: Bool + """ + self.sys_log.info(f"{self.name}: Keep Alive Received from {self.c2_remote_connection}.") + + # Using this guard clause to prevent packet storms and recognise that we've achieved a connection. + # This guard clause triggers on the c2 suite that establishes connection. + if self.keep_alive_attempted is True: + self.c2_connection_active = True # Sets the connection to active + self.keep_alive_inactivity = 0 # Sets the keep alive inactivity to zero + self.c2_session = self.software_manager.session_manager.sessions_by_uuid[session_id] + + # We set keep alive_attempted here to show that we've achieved connection. + self.keep_alive_attempted = False + self.sys_log.warning(f"{self.name}: Connection successfully Established with C2 Server.") + return True + + # If we've reached this part of the method then we've received a keep alive but haven't sent a reply. + # Therefore we also need to configure the masquerade attributes based off the keep alive sent. + if self._resolve_keep_alive(payload, session_id) is False: + self.sys_log.warning(f"{self.name}: Keep Alive Could not be resolved correctly. Refusing Keep Alive.") + return False + + self.keep_alive_attempted = True + # If this method returns true then we have sent successfully sent a keep alive. + return self._send_keep_alive(session_id) + + def _confirm_remote_connection(self, timestep: int) -> bool: + """Checks the suitability of the current C2 Server connection. + + If a connection cannot be confirmed then this method will return false otherwise true. + + :param timestep: The current timestep of the simulation. + :type timestep: Int + :return: Returns False if connection was lost. Returns True if connection is active or re-established. + :rtype bool: + """ + self.keep_alive_attempted = False # Resetting keep alive sent. + if self.keep_alive_inactivity == self.c2_config.keep_alive_frequency: + self.sys_log.info( + f"{self.name}: Attempting to Send Keep Alive to {self.c2_remote_connection} at timestep {timestep}." + ) + self._send_keep_alive(session_id=self.c2_session.uuid) + if self.keep_alive_inactivity != 0: + self.sys_log.warning( + f"{self.name}: Did not receive keep alive from c2 Server. Connection considered severed." + ) + self._reset_c2_connection() + self.close() + return False + return True + + # Defining this abstract method from Abstract C2 + def _handle_command_output(self, payload: C2Packet): + """C2 Beacons currently does not need to handle output commands coming from the C2 Servers.""" + self.sys_log.warning(f"{self.name}: C2 Beacon received an unexpected OUTPUT payload: {payload}.") + pass + + def show(self, markdown: bool = False): + """ + Prints a table of the current status of the C2 Beacon. + + Displays the current values of the following C2 attributes: + + ``C2 Connection Active``: + If the C2 Beacon is currently connected to the C2 Server + + ``C2 Remote Connection``: + The IP of the C2 Server. (Configured by upon installation) + + ``Keep Alive Inactivity``: + How many timesteps have occurred since the last keep alive. + + ``Keep Alive Frequency``: + How often should the C2 Beacon attempt a keep alive? + + ``Current Masquerade Protocol``: + The current protocol that the C2 Traffic is using. (e.g TCP/UDP) + + ``Current Masquerade Port``: + The current port that the C2 Traffic is using. (e.g HTTP (Port 80)) + + :param markdown: If True, outputs the table in markdown format. Default is False. + """ + table = PrettyTable( + [ + "C2 Connection Active", + "C2 Remote Connection", + "Keep Alive Inactivity", + "Keep Alive Frequency", + "Current Masquerade Protocol", + "Current Masquerade Port", + ] + ) + if markdown: + table.set_style(MARKDOWN) + table.align = "l" + table.title = f"{self.name} Running Status" + table.add_row( + [ + self.c2_connection_active, + self.c2_remote_connection, + self.keep_alive_inactivity, + self.c2_config.keep_alive_frequency, + self.c2_config.masquerade_protocol, + self.c2_config.masquerade_port, + ] + ) + print(table) diff --git a/src/primaite/simulator/system/applications/red_applications/c2/c2_server.py b/src/primaite/simulator/system/applications/red_applications/c2/c2_server.py new file mode 100644 index 00000000..f948d696 --- /dev/null +++ b/src/primaite/simulator/system/applications/red_applications/c2/c2_server.py @@ -0,0 +1,396 @@ +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +from typing import Dict, Optional + +from prettytable import MARKDOWN, PrettyTable +from pydantic import validate_call + +from primaite.interface.request import RequestFormat, RequestResponse +from primaite.simulator.core import RequestManager, RequestType +from primaite.simulator.network.protocols.masquerade import C2Packet +from primaite.simulator.system.applications.red_applications.c2 import ( + CommandOpts, + ExfilOpts, + RansomwareOpts, + TerminalOpts, +) +from primaite.simulator.system.applications.red_applications.c2.abstract_c2 import AbstractC2, C2Command, C2Payload + + +class C2Server(AbstractC2, identifier="C2Server"): + """ + C2 Server Application. + + Represents a vendor generic C2 Server used in conjunction with the C2 beacon + to simulate malicious communications and infrastructure within primAITE. + + The C2 Server must be installed and be in a running state before it's able to receive + red agent actions and send commands to the C2 beacon. + + Extends the Abstract C2 application to include the following: + + 1. Sending commands to the C2 Beacon. (Command input) + 2. Parsing terminal RequestResponses back to the Agent. + + Please refer to the Command-&-Control notebook for an in-depth example of the C2 Suite. + """ + + current_command_output: RequestResponse = None + """The Request Response by the last command send. This attribute is updated by the method _handle_command_output.""" + + def _init_request_manager(self) -> RequestManager: + """ + Initialise the request manager. + + More information in user guide and docstring for SimComponent._init_request_manager. + """ + rm = super()._init_request_manager() + + def _configure_ransomware_action(request: RequestFormat, context: Dict) -> RequestResponse: + """Requests - Sends a RANSOMWARE_CONFIGURE C2Command to the C2 Beacon with the given parameters. + + :param request: Request with one element containing a dict of parameters for the configure method. + :type request: RequestFormat + :param context: additional context for resolving this action, currently unused + :type context: dict + :return: RequestResponse object with a success code reflecting whether the configuration could be applied. + :rtype: RequestResponse + """ + command_payload = { + "server_ip_address": request[-1].get("server_ip_address"), + "payload": request[-1].get("payload"), + } + return self.send_command(given_command=C2Command.RANSOMWARE_CONFIGURE, command_options=command_payload) + + def _launch_ransomware_action(request: RequestFormat, context: Dict) -> RequestResponse: + """Agent Action - Sends a RANSOMWARE_LAUNCH C2Command to the C2 Beacon with the given parameters. + + :param request: Request with one element containing a dict of parameters for the configure method. + :type request: RequestFormat + :param context: additional context for resolving this action, currently unused + :type context: dict + :return: RequestResponse object with a success code reflecting whether the ransomware was launched. + :rtype: RequestResponse + """ + return self.send_command(given_command=C2Command.RANSOMWARE_LAUNCH, command_options={}) + + def _data_exfiltration_action(request: RequestFormat, context: Dict) -> RequestResponse: + """Agent Action - Sends a Data Exfiltration C2Command to the C2 Beacon with the given parameters. + + :param request: Request with one element containing a dict of parameters for the configure method. + :type request: RequestFormat + :param context: additional context for resolving this action, currently unused + :type context: dict + :return: RequestResponse object with a success code reflecting whether the ransomware was launched. + :rtype: RequestResponse + """ + command_payload = request[-1] + return self.send_command(given_command=C2Command.DATA_EXFILTRATION, command_options=command_payload) + + def _remote_terminal_action(request: RequestFormat, context: Dict) -> RequestResponse: + """Agent Action - Sends a TERMINAL C2Command to the C2 Beacon with the given parameters. + + :param request: Request with one element containing a dict of parameters for the configure method. + :type request: RequestFormat + :param context: additional context for resolving this action, currently unused + :type context: dict + :return: RequestResponse object with a success code reflecting whether the ransomware was launched. + :rtype: RequestResponse + """ + command_payload = request[-1] + return self.send_command(given_command=C2Command.TERMINAL, command_options=command_payload) + + rm.add_request( + name="ransomware_configure", + request_type=RequestType(func=_configure_ransomware_action), + ) + rm.add_request( + name="ransomware_launch", + request_type=RequestType(func=_launch_ransomware_action), + ) + rm.add_request( + name="terminal_command", + request_type=RequestType(func=_remote_terminal_action), + ) + rm.add_request( + name="exfiltrate", + request_type=RequestType(func=_data_exfiltration_action), + ) + return rm + + def __init__(self, **kwargs): + kwargs["name"] = "C2Server" + super().__init__(**kwargs) + self.run() + + def _handle_command_output(self, payload: C2Packet) -> bool: + """ + Handles the parsing of C2 Command Output from C2 Traffic (Masquerade Packets). + + Parses the Request Response from the given C2Packet's payload attribute (Inherited from Data packet). + This RequestResponse is then stored in the C2 Server class attribute self.current_command_output. + + If the payload attribute does not contain a RequestResponse, then an error will be raised in syslog and + the self.current_command_output is updated to reflect the error. + + :param payload: The OUTPUT C2 Payload + :type payload: C2Packet + :return: Returns True if the self.current_command_output was updated, false otherwise. + :rtype Bool: + """ + self.sys_log.info(f"{self.name}: Received command response from C2 Beacon: {payload}.") + command_output = payload.payload + if not isinstance(command_output, RequestResponse): + self.sys_log.warning(f"{self.name}: C2 Server received invalid command response: {command_output}.") + self.current_command_output = RequestResponse( + status="failure", data={"Reason": "Received unexpected C2 Response."} + ) + return False + + self.current_command_output = command_output + return True + + def _handle_keep_alive(self, payload: C2Packet, session_id: Optional[str]) -> bool: + """ + Handles receiving and sending keep alive payloads. This method is only called if we receive a keep alive. + + Abstract method inherited from abstract C2. + + In the C2 Server implementation of this method the following logic is performed: + + 1. The ``self.c2_connection_active`` is set to True. (Indicates that we're received a connection) + 2. The received keep alive (Payload parameter) is then resolved by _resolve_keep_alive. + 3. After the keep alive is resolved, a keep alive is sent back to confirm connection. + + This is because the C2 Server is the listener and thus will only ever receive packets from + the C2 Beacon rather than the other way around. + + The C2 Beacon/Server communication is akin to that of a real-world reverse shells. + + Returns False if a keep alive was unable to be sent. + Returns True if a keep alive was successfully sent or already has been sent this timestep. + + :param payload: The Keep Alive payload received. + :type payload: C2Packet + :param session_id: The transport session_id that the payload originates from. + :type session_id: str + :return: True if the keep alive was successfully handled, false otherwise. + :rtype: Bool + """ + self.sys_log.info(f"{self.name}: Keep Alive Received. Attempting to resolve the remote connection details.") + + self.c2_connection_active = True # Sets the connection to active + self.c2_session = self.software_manager.session_manager.sessions_by_uuid[session_id] + + if self._resolve_keep_alive(payload, session_id) == False: + self.sys_log.warning(f"{self.name}: Keep Alive Could not be resolved correctly. Refusing Keep Alive.") + return False + + self.sys_log.info(f"{self.name}: Remote connection successfully established: {self.c2_remote_connection}.") + self.sys_log.debug(f"{self.name}: Attempting to send Keep Alive response back to {self.c2_remote_connection}.") + + # If this method returns true then we have sent successfully sent a keep alive response back. + return self._send_keep_alive(session_id) + + @validate_call + def send_command(self, given_command: C2Command, command_options: Dict) -> RequestResponse: + """ + Sends a C2 command to the C2 Beacon using the given parameters. + + C2 Command | Command Synopsis + ---------------------|------------------------ + RANSOMWARE_CONFIGURE | Configures an installed ransomware script based on the passed parameters. + RANSOMWARE_LAUNCH | Launches the installed ransomware script. + DATA_EXFILTRATION | Utilises the FTP Service to exfiltrate data back to the C2 Server. + TERMINAL | Executes a command via the terminal installed on the C2 Beacons Host. + + Currently, these commands leverage the pre-existing capability of other applications. + However, the commands are sent via the network rather than the game layer which + grants more opportunity to the blue agent to prevent attacks. + + Additionally, future editions of primAITE may expand the C2 repertoire to allow for + more complex red agent behaviour such as establishing further fall back channels + or introduce red applications that are only installable via C2 Servers. (T1105) + + For more information on the impact of these commands please refer to the terminal + and the ransomware applications. + + :param given_command: The C2 command to be sent to the C2 Beacon. + :type given_command: C2Command. + :param command_options: The relevant C2 Beacon parameters. + :type command_options: Dict + :return: Returns the Request Response of the C2 Beacon's host terminal service execute method. + :rtype: RequestResponse + """ + if not isinstance(given_command, C2Command): + self.sys_log.warning(f"{self.name}: Received unexpected C2 command. Unable to send command.") + return RequestResponse( + status="failure", data={"Reason": "Received unexpected C2Command. Unable to send command."} + ) + + connection_status: tuple[bool, RequestResponse] = self._check_connection() + + if connection_status[0] is False: + return connection_status[1] + + setup_success, command_options = self._command_setup(given_command, command_options) + + if setup_success is False: + self.sys_log.warning( + f"{self.name}: Failed to perform necessary C2 Server setup for given command: {given_command}." + ) + return RequestResponse( + status="failure", data={"Reason": "Failed to perform necessary C2 Server setup for given command."} + ) + + self.sys_log.info(f"{self.name}: Attempting to send command {given_command}.") + command_packet = self._craft_packet( + c2_payload=C2Payload.INPUT, c2_command=given_command, command_options=command_options.model_dump() + ) + + if self.send( + payload=command_packet, + dest_ip_address=self.c2_remote_connection, + session_id=self.c2_session.uuid, + dest_port=self.c2_config.masquerade_port, + ip_protocol=self.c2_config.masquerade_protocol, + ): + self.sys_log.info(f"{self.name}: Successfully sent {given_command}.") + self.sys_log.info(f"{self.name}: Awaiting command response {given_command}.") + + # If the command output was handled currently, the self.current_command_output will contain the RequestResponse. + if self.current_command_output is None: + return RequestResponse( + status="failure", data={"Reason": "Command sent to the C2 Beacon but no response was ever received."} + ) + return self.current_command_output + + def _command_setup(self, given_command: C2Command, command_options: dict) -> tuple[bool, CommandOpts]: + """ + Performs any necessary C2 Server setup needed to perform certain commands. + + This includes any option validation and any other required setup. + + The following table details any C2 Server prequisites for following commands. + + C2 Command | Command Service/Application Requirements + ---------------------|----------------------------------------- + RANSOMWARE_CONFIGURE | N/A + RANSOMWARE_LAUNCH | N/A + DATA_EXFILTRATION | FTP Server & File system folder + TERMINAL | N/A + + Currently, only the data exfiltration command require the C2 Server + to perform any necessary setup. Specifically, the Data Exfiltration command requires + the C2 Server to have an running FTP Server service as well as a folder for + storing any exfiltrated data. + + :param given_command: Any C2 Command. + :type given_command: C2Command. + :param command_options: The relevant command parameters. + :type command_options: Dict + :returns: Tuple containing a success bool if the setup was successful and the validated c2 opts. + :rtype: tuple[bool, CommandOpts] + """ + server_setup_success: bool = True + + if given_command == C2Command.DATA_EXFILTRATION: # Data exfiltration setup + # Validating command options + command_options = ExfilOpts.model_validate(command_options) + if self._host_ftp_server is None: + self.sys_log.warning(f"{self.name}: Unable to setup the FTP Server for data exfiltration") + server_setup_success = False + + if self.get_exfiltration_folder(command_options.exfiltration_folder_name) is None: + self.sys_log.warning(f"{self.name}: Unable to create a folder for storing exfiltration data.") + server_setup_success = False + + if given_command == C2Command.TERMINAL: + # Validating command options + command_options = TerminalOpts.model_validate(command_options) + + if given_command == C2Command.RANSOMWARE_CONFIGURE: + # Validating command options + command_options = RansomwareOpts.model_validate(command_options) + + if given_command == C2Command.RANSOMWARE_LAUNCH: + # Validating command options + command_options = CommandOpts.model_validate(command_options) + + return [server_setup_success, command_options] + + def _confirm_remote_connection(self, timestep: int) -> bool: + """Checks the suitability of the current C2 Beacon connection. + + Inherited Abstract Method. + + If a C2 Server has not received a keep alive within the current set + keep alive frequency (self._keep_alive_frequency) then the C2 beacons + connection is considered dead and any commands will be rejected. + + This method is called on each timestep (Called by .apply_timestep) + + :param timestep: The current timestep of the simulation. + :type timestep: Int + :return: Returns False if the C2 beacon is considered dead. Otherwise True. + :rtype bool: + """ + if self.keep_alive_inactivity > self.c2_config.keep_alive_frequency: + self.sys_log.info(f"{self.name}: C2 Beacon connection considered dead due to inactivity.") + self.sys_log.debug( + f"{self.name}: Did not receive expected keep alive connection from {self.c2_remote_connection}" + f"{self.name}: Expected at timestep: {timestep} due to frequency: {self.c2_config.keep_alive_frequency}" + f"{self.name}: Last Keep Alive received at {(timestep - self.keep_alive_inactivity)}" + ) + self._reset_c2_connection() + return False + return True + + # Abstract method inherited from abstract C2. + # C2 Servers do not currently receive any input commands from the C2 beacon. + def _handle_command_input(self, payload: C2Packet) -> None: + """Defining this method (Abstract method inherited from abstract C2) in order to instantiate the class. + + C2 Servers currently do not receive input commands coming from the C2 Beacons. + + :param payload: The incoming C2Packet + :type payload: C2Packet. + """ + self.sys_log.warning(f"{self.name}: C2 Server received an unexpected INPUT payload: {payload}") + pass + + def show(self, markdown: bool = False): + """ + Prints a table of the current C2 attributes on a C2 Server. + + Displays the current values of the following C2 attributes: + + ``C2 Connection Active``: + If the C2 Server has established connection with a C2 Beacon. + + ``C2 Remote Connection``: + The IP of the C2 Beacon. (Configured by upon receiving a keep alive.) + + ``Current Masquerade Protocol``: + The current protocol that the C2 Traffic is using. (e.g TCP/UDP) + + ``Current Masquerade Port``: + The current port that the C2 Traffic is using. (e.g HTTP (Port 80)) + + :param markdown: If True, outputs the table in markdown format. Default is False. + """ + table = PrettyTable( + ["C2 Connection Active", "C2 Remote Connection", "Current Masquerade Protocol", "Current Masquerade Port"] + ) + if markdown: + table.set_style(MARKDOWN) + table.align = "l" + table.title = f"{self.name} Running Status" + table.add_row( + [ + self.c2_connection_active, + self.c2_remote_connection, + self.c2_config.masquerade_protocol, + self.c2_config.masquerade_port, + ] + ) + print(table) diff --git a/src/primaite/simulator/system/applications/red_applications/ransomware_script.py b/src/primaite/simulator/system/applications/red_applications/ransomware_script.py index 77a6bf2c..2046affc 100644 --- a/src/primaite/simulator/system/applications/red_applications/ransomware_script.py +++ b/src/primaite/simulator/system/applications/red_applications/ransomware_script.py @@ -2,6 +2,8 @@ from ipaddress import IPv4Address from typing import Dict, Optional +from prettytable import MARKDOWN, PrettyTable + from primaite.interface.request import RequestFormat, RequestResponse from primaite.simulator.core import RequestManager, RequestType from primaite.simulator.network.transmission.network_layer import IPProtocol @@ -169,3 +171,25 @@ class RansomwareScript(Application, identifier="RansomwareScript"): else: self.sys_log.warning("Attack Attempted to launch too quickly") return False + + def show(self, markdown: bool = False): + """ + Prints a table of the current status of the Ransomware Script. + + Displays the current values of the following Ransomware Attributes: + + ``server_ip_address`: + The IP of the target database. + + ``payload``: + The payload (type of attack) to be sent to the database. + + :param markdown: If True, outputs the table in markdown format. Default is False. + """ + table = PrettyTable(["Target Server IP Address", "Payload"]) + if markdown: + table.set_style(MARKDOWN) + table.align = "l" + table.title = f"{self.name} Running Status" + table.add_row([self.server_ip_address, self.payload]) + print(table) diff --git a/src/primaite/simulator/system/core/software_manager.py b/src/primaite/simulator/system/core/software_manager.py index e2266c2d..d45611ed 100644 --- a/src/primaite/simulator/system/core/software_manager.py +++ b/src/primaite/simulator/system/core/software_manager.py @@ -1,9 +1,11 @@ # © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +from copy import deepcopy from ipaddress import IPv4Address, IPv4Network from typing import Any, Dict, List, Optional, Tuple, TYPE_CHECKING, Union from prettytable import MARKDOWN, PrettyTable +from primaite.simulator.core import RequestType from primaite.simulator.file_system.file_system import FileSystem from primaite.simulator.network.transmission.data_link_layer import Frame from primaite.simulator.network.transmission.network_layer import IPProtocol @@ -20,9 +22,7 @@ if TYPE_CHECKING: from primaite.simulator.system.services.arp.arp import ARP from primaite.simulator.system.services.icmp.icmp import ICMP -from typing import Type, TypeVar - -IOSoftwareClass = TypeVar("IOSoftwareClass", bound=IOSoftware) +from typing import Type class SoftwareManager: @@ -51,7 +51,7 @@ class SoftwareManager: self.node = parent_node self.session_manager = session_manager self.software: Dict[str, Union[Service, Application]] = {} - self._software_class_to_name_map: Dict[Type[IOSoftwareClass], str] = {} + self._software_class_to_name_map: Dict[Type[IOSoftware], str] = {} self.port_protocol_mapping: Dict[Tuple[Port, IPProtocol], Union[Service, Application]] = {} self.sys_log: SysLog = sys_log self.file_system: FileSystem = file_system @@ -77,6 +77,8 @@ class SoftwareManager: for software in self.port_protocol_mapping.values(): if software.operating_state in {ApplicationOperatingState.RUNNING, ServiceOperatingState.RUNNING}: open_ports.append(software.port) + if software.listen_on_ports: + open_ports += list(software.listen_on_ports) return open_ports def check_port_is_open(self, port: Port, protocol: IPProtocol) -> bool: @@ -104,33 +106,38 @@ class SoftwareManager: return True return False - def install(self, software_class: Type[IOSoftwareClass]): + def install(self, software_class: Type[IOSoftware], **install_kwargs): """ Install an Application or Service. :param software_class: The software class. """ - # TODO: Software manager and node itself both have an install method. Need to refactor to have more logical - # separation of concerns. if software_class in self._software_class_to_name_map: self.sys_log.warning(f"Cannot install {software_class} as it is already installed") return software = software_class( - software_manager=self, sys_log=self.sys_log, file_system=self.file_system, dns_server=self.dns_server + software_manager=self, + sys_log=self.sys_log, + file_system=self.file_system, + dns_server=self.dns_server, + **install_kwargs, ) + software.parent = self.node if isinstance(software, Application): - software.install() + self.node.applications[software.uuid] = software + self.node._application_request_manager.add_request( + software.name, RequestType(func=software._request_manager) + ) + elif isinstance(software, Service): + self.node.services[software.uuid] = software + self.node._service_request_manager.add_request(software.name, RequestType(func=software._request_manager)) + software.install() software.software_manager = self self.software[software.name] = software self.port_protocol_mapping[(software.port, software.protocol)] = software if isinstance(software, Application): software.operating_state = ApplicationOperatingState.CLOSED - - # add the software to the node's registry after it has been fully initialized - if isinstance(software, Service): - self.node.install_service(software) - elif isinstance(software, Application): - self.node.install_application(software) + self.node.sys_log.info(f"Installed {software.name}") def uninstall(self, software_name: str): """ @@ -138,25 +145,31 @@ class SoftwareManager: :param software_name: The software name. """ - if software_name in self.software: - self.software[software_name].uninstall() - software = self.software.pop(software_name) # noqa - if isinstance(software, Application): - self.node.uninstall_application(software) - elif isinstance(software, Service): - self.node.uninstall_service(software) - for key, value in self.port_protocol_mapping.items(): - if value.name == software_name: - self.port_protocol_mapping.pop(key) - break - for key, value in self._software_class_to_name_map.items(): - if value == software_name: - self._software_class_to_name_map.pop(key) - break - del software - self.sys_log.info(f"Uninstalled {software_name}") + if software_name not in self.software: + self.sys_log.error(f"Cannot uninstall {software_name} as it is not installed") return - self.sys_log.error(f"Cannot uninstall {software_name} as it is not installed") + + self.software[software_name].uninstall() + software = self.software.pop(software_name) # noqa + if isinstance(software, Application): + self.node.applications.pop(software.uuid) + self.node._application_request_manager.remove_request(software.name) + elif isinstance(software, Service): + self.node.services.pop(software.uuid) + software.uninstall() + self.node._service_request_manager.remove_request(software.name) + software.parent = None + for key, value in self.port_protocol_mapping.items(): + if value.name == software_name: + self.port_protocol_mapping.pop(key) + break + for key, value in self._software_class_to_name_map.items(): + if value == software_name: + self._software_class_to_name_map.pop(key) + break + del software + self.sys_log.info(f"Uninstalled {software_name}") + return def send_internal_payload(self, target_software: str, payload: Any): """ @@ -213,7 +226,9 @@ class SoftwareManager: frame: Frame, ): """ - Receive a payload from the SessionManager and forward it to the corresponding service or application. + Receive a payload from the SessionManager and forward it to the corresponding service or applications. + + This function handles both software assigned a specific port, and software listening in on other ports. :param payload: The payload being received. :param session: The transport session the payload originates from. @@ -221,14 +236,25 @@ class SoftwareManager: if payload.__class__.__name__ == "PortScanPayload": self.software.get("NMAP").receive(payload=payload, session_id=session_id) return - receiver: Optional[Union[Service, Application]] = self.port_protocol_mapping.get((port, protocol), None) - if receiver: - receiver.receive( + main_receiver = self.port_protocol_mapping.get((port, protocol), None) + if main_receiver: + main_receiver.receive( payload=payload, session_id=session_id, from_network_interface=from_network_interface, frame=frame ) - else: + listening_receivers = [ + software + for software in self.software.values() + if port in software.listen_on_ports and software != main_receiver + ] + for receiver in listening_receivers: + receiver.receive( + payload=deepcopy(payload), + session_id=session_id, + from_network_interface=from_network_interface, + frame=frame, + ) + if not main_receiver and not listening_receivers: self.sys_log.warning(f"No service or application found for port {port} and protocol {protocol}") - pass def show(self, markdown: bool = False): """ diff --git a/src/primaite/simulator/system/services/access/__init__.py b/src/primaite/simulator/system/services/access/__init__.py new file mode 100644 index 00000000..be6c00e7 --- /dev/null +++ b/src/primaite/simulator/system/services/access/__init__.py @@ -0,0 +1 @@ +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK diff --git a/src/primaite/simulator/system/services/access/user_manager.py b/src/primaite/simulator/system/services/access/user_manager.py new file mode 100644 index 00000000..be6c00e7 --- /dev/null +++ b/src/primaite/simulator/system/services/access/user_manager.py @@ -0,0 +1 @@ +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK diff --git a/src/primaite/simulator/system/services/access/user_session_manager.py b/src/primaite/simulator/system/services/access/user_session_manager.py new file mode 100644 index 00000000..be6c00e7 --- /dev/null +++ b/src/primaite/simulator/system/services/access/user_session_manager.py @@ -0,0 +1 @@ +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK diff --git a/src/primaite/simulator/system/services/database/database_service.py b/src/primaite/simulator/system/services/database/database_service.py index 22ae0ff3..b38e87b4 100644 --- a/src/primaite/simulator/system/services/database/database_service.py +++ b/src/primaite/simulator/system/services/database/database_service.py @@ -191,12 +191,16 @@ class DatabaseService(Service): :return: Response to connection request containing success info. :rtype: Dict[str, Union[int, Dict[str, bool]]] """ + self.sys_log.info(f"{self.name}: Processing new connection request ({connection_request_id}) from {src_ip}") status_code = 500 # Default internal server error connection_id = None if self.operating_state == ServiceOperatingState.RUNNING: status_code = 503 # service unavailable if self.health_state_actual == SoftwareHealthState.OVERWHELMED: - self.sys_log.error(f"{self.name}: Connect request for {src_ip=} declined. Service is at capacity.") + self.sys_log.info( + f"{self.name}: Connection request ({connection_request_id}) from {src_ip} declined, service is at " + f"capacity." + ) if self.health_state_actual in [ SoftwareHealthState.GOOD, SoftwareHealthState.FIXING, @@ -208,12 +212,16 @@ class DatabaseService(Service): # try to create connection if not self.add_connection(connection_id=connection_id, session_id=session_id): status_code = 500 - self.sys_log.warning(f"{self.name}: Connect request for {connection_id=} declined") - else: - self.sys_log.info(f"{self.name}: Connect request for {connection_id=} authorised") + self.sys_log.info( + f"{self.name}: Connection request ({connection_request_id}) from {src_ip} declined, " + f"returning status code 500" + ) else: status_code = 401 # Unauthorised - self.sys_log.warning(f"{self.name}: Connect request for {connection_id=} declined") + self.sys_log.info( + f"{self.name}: Connection request ({connection_request_id}) from {src_ip} unauthorised " + f"(incorrect password), returning status code 401" + ) else: status_code = 404 # service not found return { @@ -377,6 +385,8 @@ class DatabaseService(Service): ) else: result = {"status_code": 401, "type": "sql"} + else: + self.sys_log.info(f"{self.name}: Ignoring payload as it is not a Database payload") self.send(payload=result, session_id=session_id) return True diff --git a/src/primaite/simulator/system/services/ftp/ftp_client.py b/src/primaite/simulator/system/services/ftp/ftp_client.py index 28a591dd..f823e42c 100644 --- a/src/primaite/simulator/system/services/ftp/ftp_client.py +++ b/src/primaite/simulator/system/services/ftp/ftp_client.py @@ -1,8 +1,10 @@ # © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from ipaddress import IPv4Address -from typing import Optional +from typing import Dict, Optional from primaite import getLogger +from primaite.interface.request import RequestFormat, RequestResponse +from primaite.simulator.core import RequestManager, RequestType from primaite.simulator.file_system.file_system import File from primaite.simulator.network.protocols.ftp import FTPCommand, FTPPacket, FTPStatusCode from primaite.simulator.network.transmission.network_layer import IPProtocol @@ -28,6 +30,58 @@ class FTPClient(FTPServiceABC): super().__init__(**kwargs) self.start() + def _init_request_manager(self) -> RequestManager: + """ + Initialise the request manager. + + More information in user guide and docstring for SimComponent._init_request_manager. + """ + rm = super()._init_request_manager() + + def _send_data_request(request: RequestFormat, context: Dict) -> RequestResponse: + """ + Request for sending data via the ftp_client using the request options parameters. + + :param request: Request with one element containing a dict of parameters for the send method. + :type request: RequestFormat + :param context: additional context for resolving this action, currently unused + :type context: dict + :return: RequestResponse object with a success code reflecting whether the configuration could be applied. + :rtype: RequestResponse + """ + dest_ip = request[-1].get("dest_ip_address") + dest_ip = None if dest_ip is None else IPv4Address(dest_ip) + + # Missing FTP Options results is an automatic failure. + src_folder = request[-1].get("src_folder_name", None) + src_file_name = request[-1].get("src_file_name", None) + dest_folder = request[-1].get("dest_folder_name", None) + dest_file_name = request[-1].get("dest_file_name", None) + + if not self.file_system.access_file(folder_name=src_folder, file_name=src_file_name): + self.sys_log.debug( + f"{self.name}: Received a FTP Request to transfer file: {src_file_name} to Remote IP: {dest_ip}." + ) + return RequestResponse( + status="failure", + data={ + "reason": "Unable to locate given file on local file system. Perhaps given options are invalid?" + }, + ) + + return RequestResponse.from_bool( + self.send_file( + dest_ip_address=dest_ip, + src_folder_name=src_folder, + src_file_name=src_file_name, + dest_folder_name=dest_folder, + dest_file_name=dest_file_name, + ) + ) + + rm.add_request("send", request_type=RequestType(func=_send_data_request)), + return rm + def _process_ftp_command(self, payload: FTPPacket, session_id: Optional[str] = None, **kwargs) -> FTPPacket: """ Process the command in the FTP Packet. diff --git a/src/primaite/simulator/system/services/terminal/__init__.py b/src/primaite/simulator/system/services/terminal/__init__.py new file mode 100644 index 00000000..be6c00e7 --- /dev/null +++ b/src/primaite/simulator/system/services/terminal/__init__.py @@ -0,0 +1 @@ +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK diff --git a/src/primaite/simulator/system/services/terminal/terminal.py b/src/primaite/simulator/system/services/terminal/terminal.py new file mode 100644 index 00000000..e98e8555 --- /dev/null +++ b/src/primaite/simulator/system/services/terminal/terminal.py @@ -0,0 +1,545 @@ +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +from __future__ import annotations + +from abc import abstractmethod +from datetime import datetime +from ipaddress import IPv4Address +from typing import Any, Dict, List, Optional, Union +from uuid import uuid4 + +from pydantic import BaseModel + +from primaite.interface.request import RequestFormat, RequestResponse +from primaite.simulator.core import RequestManager, RequestType +from primaite.simulator.network.protocols.ssh import ( + SSHConnectionMessage, + SSHPacket, + SSHTransportMessage, + SSHUserCredentials, +) +from primaite.simulator.network.transmission.network_layer import IPProtocol +from primaite.simulator.network.transmission.transport_layer import Port +from primaite.simulator.system.core.software_manager import SoftwareManager +from primaite.simulator.system.services.service import Service, ServiceOperatingState + + +# TODO 2824: Since remote terminal connections and remote user sessions are the same thing, we could refactor +# the terminal to leverage the user session manager's list. This way we avoid potential bugs and code ducplication +class TerminalClientConnection(BaseModel): + """ + TerminalClientConnection Class. + + This class is used to record current User Connections to the Terminal class. + """ + + parent_terminal: Terminal + """The parent Node that this connection was created on.""" + + ssh_session_id: str = None + """Session ID that connection is linked to, used for sending commands via session manager.""" + + connection_uuid: str = None + """Connection UUID""" + + connection_request_id: str = None + """Connection request ID""" + + time: datetime = None + """Timestamp connection was created.""" + + ip_address: IPv4Address + """Source IP of Connection""" + + is_active: bool = True + """Flag to state whether the connection is active or not""" + + def __str__(self) -> str: + return f"{self.__class__.__name__}(connection_id: '{self.connection_uuid}, ip_address: {self.ip_address}')" + + def __repr__(self) -> str: + return self.__str__() + + def __getitem__(self, key: Any) -> Any: + return getattr(self, key) + + @property + def client(self) -> Optional[Terminal]: + """The Terminal that holds this connection.""" + return self.parent_terminal + + def disconnect(self) -> bool: + """Disconnect the session.""" + return self.parent_terminal._disconnect(connection_uuid=self.connection_uuid) + + @abstractmethod + def execute(self, command: Any) -> bool: + """Execute a given command.""" + pass + + +class LocalTerminalConnection(TerminalClientConnection): + """ + LocalTerminalConnectionClass. + + This class represents a local terminal when connected. + """ + + ip_address: str = "Local Connection" + + def execute(self, command: Any) -> Optional[RequestResponse]: + """Execute a given command on local Terminal.""" + if self.parent_terminal.operating_state != ServiceOperatingState.RUNNING: + self.parent_terminal.sys_log.warning("Cannot process command as system not running") + return None + if not self.is_active: + self.parent_terminal.sys_log.warning("Connection inactive, cannot execute") + return None + return self.parent_terminal.execute(command) + + +class RemoteTerminalConnection(TerminalClientConnection): + """ + RemoteTerminalConnection Class. + + This class acts as broker between the terminal and remote. + + """ + + def execute(self, command: Any) -> bool: + """Execute a given command on the remote Terminal.""" + if self.parent_terminal.operating_state != ServiceOperatingState.RUNNING: + self.parent_terminal.sys_log.warning("Cannot process command as system not running") + return False + if not self.is_active: + self.parent_terminal.sys_log.warning("Connection inactive, cannot execute") + return False + # Send command to remote terminal to process. + + transport_message: SSHTransportMessage = SSHTransportMessage.SSH_MSG_SERVICE_REQUEST + connection_message: SSHConnectionMessage = SSHConnectionMessage.SSH_MSG_CHANNEL_DATA + + payload: SSHPacket = SSHPacket( + transport_message=transport_message, + connection_message=connection_message, + connection_request_uuid=self.connection_request_id, + connection_uuid=self.connection_uuid, + ssh_command=command, + ) + + return self.parent_terminal.send(payload=payload, session_id=self.ssh_session_id) + + +class Terminal(Service): + """Class used to simulate a generic terminal service. Can be interacted with by other terminals via SSH.""" + + _client_connection_requests: Dict[str, Optional[Union[str, TerminalClientConnection]]] = {} + """Dictionary of connect requests made to remote nodes.""" + + def __init__(self, **kwargs): + kwargs["name"] = "Terminal" + kwargs["port"] = Port.SSH + kwargs["protocol"] = IPProtocol.TCP + super().__init__(**kwargs) + + def describe_state(self) -> Dict: + """ + Produce a dictionary describing the current state of this object. + + Please see :py:meth:`primaite.simulator.core.SimComponent.describe_state` for a more detailed explanation. + + :return: Current state of this object and child objects. + :rtype: Dict + """ + state = super().describe_state() + return state + + def show(self, markdown: bool = False): + """ + Display the remote connections to this terminal instance in tabular format. + + :param markdown: Whether to display the table in Markdown format or not. Default is `False`. + """ + self.show_connections(markdown=markdown) + + def _init_request_manager(self) -> RequestManager: + """Initialise Request manager.""" + rm = super()._init_request_manager() + + def _remote_login(request: RequestFormat, context: Dict) -> RequestResponse: + login = self._send_remote_login(username=request[0], password=request[1], ip_address=request[2]) + if login: + return RequestResponse( + status="success", + data={ + "ip_address": str(login.ip_address), + "username": request[0], + }, + ) + else: + return RequestResponse(status="failure", data={}) + + rm.add_request( + "ssh_to_remote", + request_type=RequestType(func=_remote_login), + ) + + def _remote_logoff(request: RequestFormat, context: Dict) -> RequestResponse: + """Logoff from remote connection.""" + ip_address = IPv4Address(request[0]) + remote_connection = self._get_connection_from_ip(ip_address=ip_address) + if remote_connection: + outcome = self._disconnect(remote_connection.connection_uuid) + if outcome: + return RequestResponse(status="success", data={}) + + return RequestResponse(status="failure", data={}) + + rm.add_request("remote_logoff", request_type=RequestType(func=_remote_logoff)) + + def remote_execute_request(request: RequestFormat, context: Dict) -> RequestResponse: + """Execute an instruction.""" + ip_address: IPv4Address = IPv4Address(request[0]) + command: str = request[1]["command"] + remote_connection = self._get_connection_from_ip(ip_address=ip_address) + if remote_connection: + outcome = remote_connection.execute(command) + if outcome: + return RequestResponse( + status="success", + data={}, + ) + else: + return RequestResponse( + status="failure", + data={}, + ) + + rm.add_request( + "send_remote_command", + request_type=RequestType(func=remote_execute_request), + ) + + return rm + + def execute(self, command: List[Any]) -> Optional[RequestResponse]: + """Execute a passed ssh command via the request manager.""" + return self.parent.apply_request(command) + + def _get_connection_from_ip(self, ip_address: IPv4Address) -> Optional[RemoteTerminalConnection]: + """Find Remote Terminal Connection from a given IP.""" + for connection in self._connections.values(): + if connection.ip_address == ip_address: + return connection + + def _create_local_connection(self, connection_uuid: str, session_id: str) -> TerminalClientConnection: + """Create a new connection object and amend to list of active connections. + + :param connection_uuid: Connection ID of the new local connection + :param session_id: Session ID of the new local connection + :return: TerminalClientConnection object + """ + new_connection = LocalTerminalConnection( + parent_terminal=self, + connection_uuid=connection_uuid, + ssh_session_id=session_id, + time=datetime.now(), + ) + self._connections[connection_uuid] = new_connection + self._client_connection_requests[connection_uuid] = new_connection + + return new_connection + + def login( + self, username: str, password: str, ip_address: Optional[IPv4Address] = None + ) -> Optional[TerminalClientConnection]: + """Login to the terminal. Will attempt a remote login if ip_address is given, else local. + + :param: username: Username used to connect to the remote node. + :type: username: str + + :param: password: Password used to connect to the remote node + :type: password: str + + :param: ip_address: Target Node IP address for login attempt. If None, login is assumed local. + :type: ip_address: Optional[IPv4Address] + """ + if self.operating_state != ServiceOperatingState.RUNNING: + self.sys_log.warning(f"{self.name}: Cannot login as service is not running.") + return None + if ip_address: + # Assuming that if IP is passed we are connecting to remote + return self._send_remote_login(username=username, password=password, ip_address=ip_address) + else: + return self._process_local_login(username=username, password=password) + + def _process_local_login(self, username: str, password: str) -> Optional[TerminalClientConnection]: + """Local session login to terminal. + + :param username: Username for login. + :param password: Password for login. + :return: boolean, True if successful, else False + """ + # TODO: Un-comment this when UserSessionManager is merged. + connection_uuid = self.parent.user_session_manager.local_login(username=username, password=password) + if connection_uuid: + self.sys_log.info(f"{self.name}: Login request authorised, connection uuid: {connection_uuid}") + # Add new local session to list of connections and return + return self._create_local_connection(connection_uuid=connection_uuid, session_id="Local_Connection") + else: + self.sys_log.warning(f"{self.name}: Login failed, incorrect Username or Password") + return None + + def _validate_client_connection_request(self, connection_id: str) -> bool: + """Check that client_connection_id is valid.""" + return connection_id in self._client_connection_requests + + def _check_client_connection(self, connection_id: str) -> bool: + """Check that client_connection_id is valid.""" + if not self.parent.user_session_manager.validate_remote_session_uuid(connection_id): + self._disconnect(connection_id) + return False + return connection_id in self._connections + + def _send_remote_login( + self, + username: str, + password: str, + ip_address: IPv4Address, + connection_request_id: Optional[str] = None, + is_reattempt: bool = False, + ) -> Optional[RemoteTerminalConnection]: + """Send a remote login attempt and connect to Node. + + :param: username: Username used to connect to the remote node. + :type: username: str + :param: password: Password used to connect to the remote node + :type: password: str + :param: ip_address: Target Node IP address for login attempt. + :type: ip_address: IPv4Address + :param: connection_request_id: Connection Request ID, if not provided, a new one is generated + :type: connection_request_id: Optional[str] + :param: is_reattempt: True if the request has been reattempted. Default False. + :type: is_reattempt: Optional[bool] + :return: RemoteTerminalConnection: Connection Object for sending further commands if successful, else False. + """ + connection_request_id = connection_request_id or str(uuid4()) + if is_reattempt: + valid_connection_request = self._validate_client_connection_request(connection_id=connection_request_id) + if valid_connection_request: + remote_terminal_connection = self._client_connection_requests.pop(connection_request_id) + if isinstance(remote_terminal_connection, RemoteTerminalConnection): + self.sys_log.info(f"{self.name}: Remote Connection to {ip_address} authorised.") + return remote_terminal_connection + else: + self.sys_log.warning(f"{self.name}: Connection request {connection_request_id} declined") + return None + else: + self.sys_log.warning(f"{self.name}: Remote connection to {ip_address} declined.") + return None + + self.sys_log.info( + f"{self.name}: Sending Remote login attempt to {ip_address}. Connection_id is {connection_request_id}" + ) + transport_message: SSHTransportMessage = SSHTransportMessage.SSH_MSG_USERAUTH_REQUEST + connection_message: SSHConnectionMessage = SSHConnectionMessage.SSH_MSG_CHANNEL_DATA + user_details: SSHUserCredentials = SSHUserCredentials(username=username, password=password) + + payload_contents = { + "type": "login_request", + "username": username, + "password": password, + "connection_request_id": connection_request_id, + } + + payload: SSHPacket = SSHPacket( + payload=payload_contents, + transport_message=transport_message, + connection_message=connection_message, + user_account=user_details, + connection_request_uuid=connection_request_id, + ) + + software_manager: SoftwareManager = self.software_manager + software_manager.send_payload_to_session_manager( + payload=payload, dest_ip_address=ip_address, dest_port=self.port + ) + return self._send_remote_login( + username=username, + password=password, + ip_address=ip_address, + is_reattempt=True, + connection_request_id=connection_request_id, + ) + + def _create_remote_connection( + self, connection_id: str, connection_request_id: str, session_id: str, source_ip: str + ) -> None: + """Create a new TerminalClientConnection Object. + + :param: connection_request_id: Connection Request ID + :type: connection_request_id: str + + :param: session_id: Session ID of connection. + :type: session_id: str + """ + client_connection = RemoteTerminalConnection( + parent_terminal=self, + ssh_session_id=session_id, + connection_uuid=connection_id, + ip_address=source_ip, + connection_request_id=connection_request_id, + time=datetime.now(), + ) + self._connections[connection_id] = client_connection + self._client_connection_requests[connection_request_id] = client_connection + + def receive(self, session_id: str, payload: Union[SSHPacket, Dict], **kwargs) -> bool: + """ + Receive a payload from the Software Manager. + + :param payload: A payload to receive. + :param session_id: The session id the payload relates to. + :return: True. + """ + source_ip = kwargs["frame"].ip.src_ip_address + self.sys_log.info(f"{self.name}: Received payload: {payload}. Source: {source_ip}") + if isinstance(payload, SSHPacket): + if payload.transport_message == SSHTransportMessage.SSH_MSG_USERAUTH_REQUEST: + # validate & add connection + # TODO: uncomment this as part of 2781 + username = payload.user_account.username + password = payload.user_account.password + connection_id = self.parent.user_session_manager.remote_login( + username=username, password=password, remote_ip_address=source_ip + ) + if connection_id: + connection_request_id = payload.connection_request_uuid + self.sys_log.info(f"{self.name}: Connection authorised, session_id: {session_id}") + self._create_remote_connection( + connection_id=connection_id, + connection_request_id=connection_request_id, + session_id=session_id, + source_ip=source_ip, + ) + + transport_message = SSHTransportMessage.SSH_MSG_USERAUTH_SUCCESS + connection_message = SSHConnectionMessage.SSH_MSG_CHANNEL_DATA + + payload_contents = { + "type": "login_success", + "username": username, + "password": password, + "connection_request_id": connection_request_id, + "connection_id": connection_id, + } + payload: SSHPacket = SSHPacket( + payload=payload_contents, + transport_message=transport_message, + connection_message=connection_message, + connection_request_uuid=connection_request_id, + connection_uuid=connection_id, + ) + + software_manager: SoftwareManager = self.software_manager + software_manager.send_payload_to_session_manager( + payload=payload, dest_port=self.port, session_id=session_id + ) + elif payload.transport_message == SSHTransportMessage.SSH_MSG_USERAUTH_SUCCESS: + self.sys_log.info(f"{self.name}: Login Successful") + self._create_remote_connection( + connection_id=payload.connection_uuid, + connection_request_id=payload.connection_request_uuid, + session_id=session_id, + source_ip=source_ip, + ) + + elif payload.transport_message == SSHTransportMessage.SSH_MSG_SERVICE_REQUEST: + # Requesting a command to be executed + self.sys_log.info(f"{self.name}: Received command to execute") + command = payload.ssh_command + valid_connection = self._check_client_connection(payload.connection_uuid) + if valid_connection: + remote_session = self.software_manager.node.user_session_manager.remote_sessions.get( + payload.connection_uuid + ) + remote_session.last_active_step = self.software_manager.node.user_session_manager.current_timestep + self.execute(command) + return True + else: + self.sys_log.error( + f"{self.name}: Connection UUID:{payload.connection_uuid} is not valid. Rejecting Command." + ) + + if isinstance(payload, dict) and payload.get("type"): + if payload["type"] == "disconnect": + connection_id = payload["connection_id"] + valid_id = self._check_client_connection(connection_id) + if valid_id: + self.sys_log.info(f"{self.name}: Received disconnect command for {connection_id=} from remote.") + self._disconnect(payload["connection_id"]) + self.parent.user_session_manager.remote_logout(remote_session_id=connection_id) + else: + self.sys_log.error(f"{self.name}: No Active connection held for received connection ID.") + + if payload["type"] == "user_timeout": + connection_id = payload["connection_id"] + valid_id = connection_id in self._connections + if valid_id: + connection = self._connections.pop(connection_id) + connection.is_active = False + self.sys_log.info(f"{self.name}: Connection {connection_id} disconnected due to inactivity.") + else: + self.sys_log.error(f"{self.name}: Connection {connection_id} is invalid.") + + return True + + def _disconnect(self, connection_uuid: str) -> bool: + """Disconnect connection. + + :param connection_uuid: Connection ID that we want to disconnect. + :return True if successful, False otherwise. + """ + # TODO: Handle the possibility of attempting to disconnect + if not self._connections: + self.sys_log.warning(f"{self.name}: No remote connection present") + return False + + connection = self._connections.pop(connection_uuid, None) + if not connection: + return False + connection.is_active = False + + if isinstance(connection, RemoteTerminalConnection): + # Send disconnect command via software manager + session_id = connection.ssh_session_id + + software_manager: SoftwareManager = self.software_manager + software_manager.send_payload_to_session_manager( + payload={"type": "disconnect", "connection_id": connection_uuid}, + dest_port=self.port, + session_id=session_id, + ) + self.sys_log.info(f"{self.name}: Disconnected {connection_uuid}") + return True + + elif isinstance(connection, LocalTerminalConnection): + self.parent.user_session_manager.local_logout() + return True + + def send( + self, payload: SSHPacket, dest_ip_address: Optional[IPv4Address] = None, session_id: Optional[str] = None + ) -> bool: + """ + Send a payload out from the Terminal. + + :param payload: The payload to be sent. + :param dest_up_address: The IP address of the payload destination. + """ + if self.operating_state != ServiceOperatingState.RUNNING: + self.sys_log.warning(f"{self.name}: Cannot send commands when Operating state is {self.operating_state}!") + return False + + self.sys_log.debug(f"{self.name}: Sending payload: {payload}") + return super().send( + payload=payload, dest_ip_address=dest_ip_address, dest_port=self.port, session_id=session_id + ) diff --git a/src/primaite/simulator/system/services/web_server/web_server.py b/src/primaite/simulator/system/services/web_server/web_server.py index 6f6fa335..4fc64e1f 100644 --- a/src/primaite/simulator/system/services/web_server/web_server.py +++ b/src/primaite/simulator/system/services/web_server/web_server.py @@ -1,6 +1,6 @@ # © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from ipaddress import IPv4Address -from typing import Any, Dict, Optional +from typing import Any, Dict, List, Optional from urllib.parse import urlparse from primaite import getLogger @@ -22,7 +22,7 @@ _LOGGER = getLogger(__name__) class WebServer(Service): """Class used to represent a Web Server Service in simulation.""" - last_response_status_code: Optional[HttpStatusCode] = None + response_codes_this_timestep: List[HttpStatusCode] = [] def describe_state(self) -> Dict: """ @@ -34,11 +34,19 @@ class WebServer(Service): :rtype: Dict """ state = super().describe_state() - state["last_response_status_code"] = ( - self.last_response_status_code.value if isinstance(self.last_response_status_code, HttpStatusCode) else None - ) + state["response_codes_this_timestep"] = [code.value for code in self.response_codes_this_timestep] return state + def pre_timestep(self, timestep: int) -> None: + """ + Logic to execute at the start of the timestep - clear the observation-related attributes. + + :param timestep: the current timestep in the episode. + :type timestep: int + """ + self.response_codes_this_timestep = [] + return super().pre_timestep(timestep) + def __init__(self, **kwargs): kwargs["name"] = "WebServer" kwargs["protocol"] = IPProtocol.TCP @@ -89,7 +97,7 @@ class WebServer(Service): self.send(payload=response, session_id=session_id) # return true if response is OK - self.last_response_status_code = response.status_code + self.response_codes_this_timestep.append(response.status_code) return response.status_code == HttpStatusCode.OK def _handle_get_request(self, payload: HttpRequestPacket) -> HttpResponsePacket: diff --git a/src/primaite/simulator/system/software.py b/src/primaite/simulator/system/software.py index 7ea67dcd..f1d1b9a1 100644 --- a/src/primaite/simulator/system/software.py +++ b/src/primaite/simulator/system/software.py @@ -4,9 +4,10 @@ from abc import abstractmethod from datetime import datetime from enum import Enum from ipaddress import IPv4Address, IPv4Network -from typing import Any, Dict, Optional, TYPE_CHECKING, Union +from typing import Any, Dict, Optional, Set, TYPE_CHECKING, Union from prettytable import MARKDOWN, PrettyTable +from pydantic import Field from primaite.interface.request import RequestResponse from primaite.simulator.core import RequestManager, RequestType, SimComponent @@ -252,6 +253,8 @@ class IOSoftware(Software): "Indicates if the software uses UDP protocol for communication. Default is True." port: Port "The port to which the software is connected." + listen_on_ports: Set[Port] = Field(default_factory=set) + "The set of ports to listen on." protocol: IPProtocol "The IP Protocol the Software operates on." _connections: Dict[str, Dict] = {} @@ -291,7 +294,7 @@ class IOSoftware(Software): """ if self.software_manager and self.software_manager.node.operating_state != NodeOperatingState.ON: self.software_manager.node.sys_log.error( - f"{self.name} Error: {self.software_manager.node.hostname} is not online." + f"{self.name} Error: {self.software_manager.node.hostname} is not powered on." ) return False return True @@ -313,7 +316,7 @@ class IOSoftware(Software): # if over or at capacity, set to overwhelmed if len(self._connections) >= self.max_sessions: self.set_health_state(SoftwareHealthState.OVERWHELMED) - self.sys_log.warning(f"{self.name}: Connect request for {connection_id=} declined. Service is at capacity.") + self.sys_log.warning(f"{self.name}: Connection request ({connection_id}) declined. Service is at capacity.") return False else: # if service was previously overwhelmed, set to good because there is enough space for connections @@ -330,11 +333,11 @@ class IOSoftware(Software): "ip_address": session_details.with_ip_address if session_details else None, "time": datetime.now(), } - self.sys_log.info(f"{self.name}: Connect request for {connection_id=} authorised") + self.sys_log.info(f"{self.name}: Connection request ({connection_id}) authorised") return True # connection with given id already exists self.sys_log.warning( - f"{self.name}: Connect request for {connection_id=} declined. Connection already exists." + f"{self.name}: Connection request ({connection_id}) declined. Connection already exists." ) return False diff --git a/tests/assets/configs/bad_primaite_session.yaml b/tests/assets/configs/bad_primaite_session.yaml index 8cbd3ae9..c83cadc8 100644 --- a/tests/assets/configs/bad_primaite_session.yaml +++ b/tests/assets/configs/bad_primaite_session.yaml @@ -99,7 +99,7 @@ agents: num_files: 1 num_nics: 2 include_num_access: false - include_nmne: true + include_nmne: false routers: - hostname: router_1 num_ports: 0 diff --git a/tests/assets/configs/basic_c2_setup.yaml b/tests/assets/configs/basic_c2_setup.yaml new file mode 100644 index 00000000..0cae2ba0 --- /dev/null +++ b/tests/assets/configs/basic_c2_setup.yaml @@ -0,0 +1,76 @@ +# Basic Switched network +# +# -------------- -------------- -------------- +# | node_a |------| switch_1 |------| node_b | +# -------------- -------------- -------------- +# +io_settings: + save_step_metadata: false + save_pcap_logs: true + save_sys_logs: true + sys_log_level: WARNING + agent_log_level: INFO + save_agent_logs: true + write_agent_log_to_terminal: True + + +game: + max_episode_length: 256 + ports: + - ARP + - DNS + - HTTP + - POSTGRES_SERVER + protocols: + - ICMP + - TCP + - UDP + +simulation: + network: + nodes: + + - type: switch + hostname: switch_1 + num_ports: 8 + + - hostname: node_a + type: computer + ip_address: 192.168.10.21 + subnet_mask: 255.255.255.0 + default_gateway: 192.168.10.1 + applications: + - type: C2Server + options: + listen_on_ports: + - 80 + - 53 + - 21 + - hostname: node_b + type: computer + ip_address: 192.168.10.22 + subnet_mask: 255.255.255.0 + default_gateway: 192.168.10.1 + applications: + - type: C2Beacon + options: + c2_server_ip_address: 192.168.10.21 + keep_alive_frequency: 5 + masquerade_protocol: TCP + masquerade_port: HTTP + listen_on_ports: + - 80 + - 53 + - 21 + + links: + - endpoint_a_hostname: switch_1 + endpoint_a_port: 1 + endpoint_b_hostname: node_a + endpoint_b_port: 1 + bandwidth: 200 + - endpoint_a_hostname: switch_1 + endpoint_a_port: 2 + endpoint_b_hostname: node_b + endpoint_b_port: 1 + bandwidth: 200 diff --git a/tests/assets/configs/basic_node_with_software_listening_ports.yaml b/tests/assets/configs/basic_node_with_software_listening_ports.yaml new file mode 100644 index 00000000..53eee87f --- /dev/null +++ b/tests/assets/configs/basic_node_with_software_listening_ports.yaml @@ -0,0 +1,39 @@ +io_settings: + save_step_metadata: false + save_pcap_logs: true + save_sys_logs: true + sys_log_level: WARNING + agent_log_level: INFO + save_agent_logs: true + write_agent_log_to_terminal: True + + +game: + max_episode_length: 256 + ports: + - ARP + protocols: + - ICMP + - UDP + + +simulation: + network: + nodes: + - hostname: client + type: computer + ip_address: 192.168.10.11 + subnet_mask: 255.255.255.0 + default_gateway: 192.168.10.1 + services: + - type: DatabaseService + options: + backup_server_ip: 10.10.1.12 + listen_on_ports: + - 631 + applications: + - type: WebBrowser + options: + target_url: http://sometech.ai + listen_on_ports: + - SMB diff --git a/tests/assets/configs/basic_node_with_users.yaml b/tests/assets/configs/basic_node_with_users.yaml new file mode 100644 index 00000000..064519dd --- /dev/null +++ b/tests/assets/configs/basic_node_with_users.yaml @@ -0,0 +1,34 @@ +io_settings: + save_step_metadata: false + save_pcap_logs: true + save_sys_logs: true + sys_log_level: WARNING + agent_log_level: INFO + save_agent_logs: true + write_agent_log_to_terminal: True + + +game: + max_episode_length: 256 + ports: + - ARP + protocols: + - ICMP + - UDP + + +simulation: + network: + nodes: + - hostname: client_1 + type: computer + ip_address: 192.168.10.11 + subnet_mask: 255.255.255.0 + default_gateway: 192.168.10.1 + users: + - username: jane.doe + password: '1234' + is_admin: true + - username: john.doe + password: password_1 + is_admin: false diff --git a/tests/assets/configs/basic_switched_network.yaml b/tests/assets/configs/basic_switched_network.yaml index 69187fa3..fed0f52d 100644 --- a/tests/assets/configs/basic_switched_network.yaml +++ b/tests/assets/configs/basic_switched_network.yaml @@ -92,7 +92,7 @@ agents: - NONE tcp: - DNS - include_nmne: true + include_nmne: false routers: - hostname: router_1 num_ports: 0 diff --git a/tests/assets/configs/data_manipulation.yaml b/tests/assets/configs/data_manipulation.yaml new file mode 100644 index 00000000..97442903 --- /dev/null +++ b/tests/assets/configs/data_manipulation.yaml @@ -0,0 +1,942 @@ +io_settings: + save_agent_actions: true + save_step_metadata: false + save_pcap_logs: false + save_sys_logs: false + sys_log_level: WARNING + + +game: + max_episode_length: 128 + ports: + - HTTP + - POSTGRES_SERVER + protocols: + - ICMP + - TCP + - UDP + thresholds: + nmne: + high: 10 + medium: 5 + low: 0 + +agents: + - ref: client_2_green_user + team: GREEN + type: ProbabilisticAgent + agent_settings: + action_probabilities: + 0: 0.3 + 1: 0.6 + 2: 0.1 + observation_space: null + action_space: + action_list: + - type: DONOTHING + - type: NODE_APPLICATION_EXECUTE + options: + nodes: + - node_name: client_2 + applications: + - application_name: WebBrowser + - application_name: DatabaseClient + max_folders_per_node: 1 + max_files_per_folder: 1 + max_services_per_node: 1 + max_applications_per_node: 2 + action_map: + 0: + action: DONOTHING + options: {} + 1: + action: NODE_APPLICATION_EXECUTE + options: + node_id: 0 + application_id: 0 + 2: + action: NODE_APPLICATION_EXECUTE + options: + node_id: 0 + application_id: 1 + + reward_function: + reward_components: + - type: WEBPAGE_UNAVAILABLE_PENALTY + weight: 0.25 + options: + node_hostname: client_2 + - type: GREEN_ADMIN_DATABASE_UNREACHABLE_PENALTY + weight: 0.05 + options: + node_hostname: client_2 + + - ref: client_1_green_user + team: GREEN + type: ProbabilisticAgent + agent_settings: + action_probabilities: + 0: 0.3 + 1: 0.6 + 2: 0.1 + observation_space: null + action_space: + action_list: + - type: DONOTHING + - type: NODE_APPLICATION_EXECUTE + options: + nodes: + - node_name: client_1 + applications: + - application_name: WebBrowser + - application_name: DatabaseClient + max_folders_per_node: 1 + max_files_per_folder: 1 + max_services_per_node: 1 + max_applications_per_node: 2 + action_map: + 0: + action: DONOTHING + options: {} + 1: + action: NODE_APPLICATION_EXECUTE + options: + node_id: 0 + application_id: 0 + 2: + action: NODE_APPLICATION_EXECUTE + options: + node_id: 0 + application_id: 1 + + reward_function: + reward_components: + - type: WEBPAGE_UNAVAILABLE_PENALTY + weight: 0.25 + options: + node_hostname: client_1 + - type: GREEN_ADMIN_DATABASE_UNREACHABLE_PENALTY + weight: 0.05 + options: + node_hostname: client_1 + + + + + + - ref: data_manipulation_attacker + team: RED + type: RedDatabaseCorruptingAgent + + observation_space: null + + action_space: + action_list: + - type: DONOTHING + - type: NODE_APPLICATION_EXECUTE + options: + nodes: + - node_name: client_1 + applications: + - application_name: DataManipulationBot + - node_name: client_2 + applications: + - application_name: DataManipulationBot + max_folders_per_node: 1 + max_files_per_folder: 1 + max_services_per_node: 1 + + reward_function: + reward_components: + - type: DUMMY + + agent_settings: # options specific to this particular agent type, basically args of __init__(self) + start_settings: + start_step: 25 + frequency: 20 + variance: 5 + + - ref: defender + team: BLUE + type: ProxyAgent + + observation_space: + type: CUSTOM + options: + components: + - type: NODES + label: NODES + options: + hosts: + - hostname: domain_controller + - hostname: web_server + services: + - service_name: WebServer + - hostname: database_server + folders: + - folder_name: database + files: + - file_name: database.db + - hostname: backup_server + - hostname: security_suite + - hostname: client_1 + - hostname: client_2 + num_services: 1 + num_applications: 0 + num_folders: 1 + num_files: 1 + num_nics: 2 + include_num_access: false + include_nmne: true + monitored_traffic: + icmp: + - NONE + tcp: + - DNS + routers: + - hostname: router_1 + num_ports: 0 + ip_list: + - 192.168.1.10 + - 192.168.1.12 + - 192.168.1.14 + - 192.168.1.16 + - 192.168.1.110 + - 192.168.10.21 + - 192.168.10.22 + - 192.168.10.110 + wildcard_list: + - 0.0.0.1 + port_list: + - 80 + - 5432 + protocol_list: + - ICMP + - TCP + - UDP + num_rules: 10 + + - type: LINKS + label: LINKS + options: + link_references: + - router_1:eth-1<->switch_1:eth-8 + - router_1:eth-2<->switch_2:eth-8 + - switch_1:eth-1<->domain_controller:eth-1 + - switch_1:eth-2<->web_server:eth-1 + - switch_1:eth-3<->database_server:eth-1 + - switch_1:eth-4<->backup_server:eth-1 + - switch_1:eth-7<->security_suite:eth-1 + - switch_2:eth-1<->client_1:eth-1 + - switch_2:eth-2<->client_2:eth-1 + - switch_2:eth-7<->security_suite:eth-2 + - type: "NONE" + label: ICS + options: {} + + action_space: + action_list: + - type: DONOTHING + - type: NODE_SERVICE_SCAN + - type: NODE_SERVICE_STOP + - type: NODE_SERVICE_START + - type: NODE_SERVICE_PAUSE + - type: NODE_SERVICE_RESUME + - type: NODE_SERVICE_RESTART + - type: NODE_SERVICE_DISABLE + - type: NODE_SERVICE_ENABLE + - type: NODE_SERVICE_FIX + - type: NODE_FILE_SCAN + - type: NODE_FILE_CHECKHASH + - type: NODE_FILE_DELETE + - type: NODE_FILE_REPAIR + - type: NODE_FILE_RESTORE + - type: NODE_FOLDER_SCAN + - type: NODE_FOLDER_CHECKHASH + - type: NODE_FOLDER_REPAIR + - type: NODE_FOLDER_RESTORE + - type: NODE_OS_SCAN + - type: NODE_SHUTDOWN + - type: NODE_STARTUP + - type: NODE_RESET + - type: ROUTER_ACL_ADDRULE + - type: ROUTER_ACL_REMOVERULE + - type: HOST_NIC_ENABLE + - type: HOST_NIC_DISABLE + + action_map: + 0: + action: DONOTHING + options: {} + # scan webapp service + 1: + action: NODE_SERVICE_SCAN + options: + node_id: 1 + service_id: 0 + # stop webapp service + 2: + action: NODE_SERVICE_STOP + options: + node_id: 1 + service_id: 0 + # start webapp service + 3: + action: "NODE_SERVICE_START" + options: + node_id: 1 + service_id: 0 + 4: + action: "NODE_SERVICE_PAUSE" + options: + node_id: 1 + service_id: 0 + 5: + action: "NODE_SERVICE_RESUME" + options: + node_id: 1 + service_id: 0 + 6: + action: "NODE_SERVICE_RESTART" + options: + node_id: 1 + service_id: 0 + 7: + action: "NODE_SERVICE_DISABLE" + options: + node_id: 1 + service_id: 0 + 8: + action: "NODE_SERVICE_ENABLE" + options: + node_id: 1 + service_id: 0 + 9: # check database.db file + action: "NODE_FILE_SCAN" + options: + node_id: 2 + folder_id: 0 + file_id: 0 + 10: + action: "NODE_FILE_CHECKHASH" # CHECKHASH replaced by SCAN - but the behaviour is the same in this context. + options: + node_id: 2 + folder_id: 0 + file_id: 0 + 11: + action: "NODE_FILE_DELETE" + options: + node_id: 2 + folder_id: 0 + file_id: 0 + 12: + action: "NODE_FILE_REPAIR" + options: + node_id: 2 + folder_id: 0 + file_id: 0 + 13: + action: "NODE_SERVICE_FIX" + options: + node_id: 2 + service_id: 0 + 14: + action: "NODE_FOLDER_SCAN" + options: + node_id: 2 + folder_id: 0 + 15: + action: "NODE_FOLDER_CHECKHASH" # CHECKHASH replaced by SCAN - but the behaviour is the same in this context. + options: + node_id: 2 + folder_id: 0 + 16: + action: "NODE_FOLDER_REPAIR" + options: + node_id: 2 + folder_id: 0 + 17: + action: "NODE_FOLDER_RESTORE" + options: + node_id: 2 + folder_id: 0 + 18: + action: "NODE_OS_SCAN" + options: + node_id: 0 + 19: + action: "NODE_SHUTDOWN" + options: + node_id: 0 + 20: + action: NODE_STARTUP + options: + node_id: 0 + 21: + action: NODE_RESET + options: + node_id: 0 + 22: + action: "NODE_OS_SCAN" + options: + node_id: 1 + 23: + action: "NODE_SHUTDOWN" + options: + node_id: 1 + 24: + action: NODE_STARTUP + options: + node_id: 1 + 25: + action: NODE_RESET + options: + node_id: 1 + 26: # old action num: 18 + action: "NODE_OS_SCAN" + options: + node_id: 2 + 27: + action: "NODE_SHUTDOWN" + options: + node_id: 2 + 28: + action: NODE_STARTUP + options: + node_id: 2 + 29: + action: NODE_RESET + options: + node_id: 2 + 30: + action: "NODE_OS_SCAN" + options: + node_id: 3 + 31: + action: "NODE_SHUTDOWN" + options: + node_id: 3 + 32: + action: NODE_STARTUP + options: + node_id: 3 + 33: + action: NODE_RESET + options: + node_id: 3 + 34: + action: "NODE_OS_SCAN" + options: + node_id: 4 + 35: + action: "NODE_SHUTDOWN" + options: + node_id: 4 + 36: + action: NODE_STARTUP + options: + node_id: 4 + 37: + action: NODE_RESET + options: + node_id: 4 + 38: + action: "NODE_OS_SCAN" + options: + node_id: 5 + 39: # old action num: 19 # shutdown client 1 + action: "NODE_SHUTDOWN" + options: + node_id: 5 + 40: # old action num: 20 + action: NODE_STARTUP + options: + node_id: 5 + 41: # old action num: 21 + action: NODE_RESET + options: + node_id: 5 + 42: + action: "NODE_OS_SCAN" + options: + node_id: 6 + 43: + action: "NODE_SHUTDOWN" + options: + node_id: 6 + 44: + action: NODE_STARTUP + options: + node_id: 6 + 45: + action: NODE_RESET + options: + node_id: 6 + + 46: # old action num: 22 # "ACL: ADDRULE - Block outgoing traffic from client 1" + action: "ROUTER_ACL_ADDRULE" + options: + target_router: router_1 + position: 1 + permission: 2 + source_ip_id: 7 # client 1 + dest_ip_id: 1 # ALL + source_port_id: 1 + dest_port_id: 1 + protocol_id: 1 + source_wildcard_id: 0 + dest_wildcard_id: 0 + 47: # old action num: 23 # "ACL: ADDRULE - Block outgoing traffic from client 2" + action: "ROUTER_ACL_ADDRULE" + options: + target_router: router_1 + position: 2 + permission: 2 + source_ip_id: 8 # client 2 + dest_ip_id: 1 # ALL + source_port_id: 1 + dest_port_id: 1 + protocol_id: 1 + source_wildcard_id: 0 + dest_wildcard_id: 0 + 48: # old action num: 24 # block tcp traffic from client 1 to web app + action: "ROUTER_ACL_ADDRULE" + options: + target_router: router_1 + position: 3 + permission: 2 + source_ip_id: 7 # client 1 + dest_ip_id: 3 # web server + source_port_id: 1 + dest_port_id: 1 + protocol_id: 3 + source_wildcard_id: 0 + dest_wildcard_id: 0 + 49: # old action num: 25 # block tcp traffic from client 2 to web app + action: "ROUTER_ACL_ADDRULE" + options: + target_router: router_1 + position: 4 + permission: 2 + source_ip_id: 8 # client 2 + dest_ip_id: 3 # web server + source_port_id: 1 + dest_port_id: 1 + protocol_id: 3 + source_wildcard_id: 0 + dest_wildcard_id: 0 + 50: # old action num: 26 + action: "ROUTER_ACL_ADDRULE" + options: + target_router: router_1 + position: 5 + permission: 2 + source_ip_id: 7 # client 1 + dest_ip_id: 4 # database + source_port_id: 1 + dest_port_id: 1 + protocol_id: 3 + source_wildcard_id: 0 + dest_wildcard_id: 0 + 51: # old action num: 27 + action: "ROUTER_ACL_ADDRULE" + options: + target_router: router_1 + position: 6 + permission: 2 + source_ip_id: 8 # client 2 + dest_ip_id: 4 # database + source_port_id: 1 + dest_port_id: 1 + protocol_id: 3 + source_wildcard_id: 0 + dest_wildcard_id: 0 + 52: # old action num: 28 + action: "ROUTER_ACL_REMOVERULE" + options: + target_router: router_1 + position: 0 + 53: # old action num: 29 + action: "ROUTER_ACL_REMOVERULE" + options: + target_router: router_1 + position: 1 + 54: # old action num: 30 + action: "ROUTER_ACL_REMOVERULE" + options: + target_router: router_1 + position: 2 + 55: # old action num: 31 + action: "ROUTER_ACL_REMOVERULE" + options: + target_router: router_1 + position: 3 + 56: # old action num: 32 + action: "ROUTER_ACL_REMOVERULE" + options: + target_router: router_1 + position: 4 + 57: # old action num: 33 + action: "ROUTER_ACL_REMOVERULE" + options: + target_router: router_1 + position: 5 + 58: # old action num: 34 + action: "ROUTER_ACL_REMOVERULE" + options: + target_router: router_1 + position: 6 + 59: # old action num: 35 + action: "ROUTER_ACL_REMOVERULE" + options: + target_router: router_1 + position: 7 + 60: # old action num: 36 + action: "ROUTER_ACL_REMOVERULE" + options: + target_router: router_1 + position: 8 + 61: # old action num: 37 + action: "ROUTER_ACL_REMOVERULE" + options: + target_router: router_1 + position: 9 + 62: # old action num: 38 + action: "HOST_NIC_DISABLE" + options: + node_id: 0 + nic_id: 0 + 63: # old action num: 39 + action: "HOST_NIC_ENABLE" + options: + node_id: 0 + nic_id: 0 + 64: # old action num: 40 + action: "HOST_NIC_DISABLE" + options: + node_id: 1 + nic_id: 0 + 65: # old action num: 41 + action: "HOST_NIC_ENABLE" + options: + node_id: 1 + nic_id: 0 + 66: # old action num: 42 + action: "HOST_NIC_DISABLE" + options: + node_id: 2 + nic_id: 0 + 67: # old action num: 43 + action: "HOST_NIC_ENABLE" + options: + node_id: 2 + nic_id: 0 + 68: # old action num: 44 + action: "HOST_NIC_DISABLE" + options: + node_id: 3 + nic_id: 0 + 69: # old action num: 45 + action: "HOST_NIC_ENABLE" + options: + node_id: 3 + nic_id: 0 + 70: # old action num: 46 + action: "HOST_NIC_DISABLE" + options: + node_id: 4 + nic_id: 0 + 71: # old action num: 47 + action: "HOST_NIC_ENABLE" + options: + node_id: 4 + nic_id: 0 + 72: # old action num: 48 + action: "HOST_NIC_DISABLE" + options: + node_id: 4 + nic_id: 1 + 73: # old action num: 49 + action: "HOST_NIC_ENABLE" + options: + node_id: 4 + nic_id: 1 + 74: # old action num: 50 + action: "HOST_NIC_DISABLE" + options: + node_id: 5 + nic_id: 0 + 75: # old action num: 51 + action: "HOST_NIC_ENABLE" + options: + node_id: 5 + nic_id: 0 + 76: # old action num: 52 + action: "HOST_NIC_DISABLE" + options: + node_id: 6 + nic_id: 0 + 77: # old action num: 53 + action: "HOST_NIC_ENABLE" + options: + node_id: 6 + nic_id: 0 + + + + options: + nodes: + - node_name: domain_controller + - node_name: web_server + applications: + - application_name: DatabaseClient + services: + - service_name: WebServer + - node_name: database_server + folders: + - folder_name: database + files: + - file_name: database.db + services: + - service_name: DatabaseService + - node_name: backup_server + - node_name: security_suite + - node_name: client_1 + - node_name: client_2 + + max_folders_per_node: 2 + max_files_per_folder: 2 + max_services_per_node: 2 + max_nics_per_node: 8 + max_acl_rules: 10 + ip_list: + - 192.168.1.10 + - 192.168.1.12 + - 192.168.1.14 + - 192.168.1.16 + - 192.168.1.110 + - 192.168.10.21 + - 192.168.10.22 + - 192.168.10.110 + + + reward_function: + reward_components: + - type: DATABASE_FILE_INTEGRITY + weight: 0.40 + options: + node_hostname: database_server + folder_name: database + file_name: database.db + + - type: SHARED_REWARD + weight: 1.0 + options: + agent_name: client_1_green_user + + - type: SHARED_REWARD + weight: 1.0 + options: + agent_name: client_2_green_user + + agent_settings: + flatten_obs: true + action_masking: true + + + + + +simulation: + network: + nmne_config: + capture_nmne: true + nmne_capture_keywords: + - DELETE + nodes: + + - hostname: router_1 + type: router + num_ports: 5 + ports: + 1: + ip_address: 192.168.1.1 + subnet_mask: 255.255.255.0 + 2: + ip_address: 192.168.10.1 + subnet_mask: 255.255.255.0 + acl: + 18: + action: PERMIT + src_port: POSTGRES_SERVER + dst_port: POSTGRES_SERVER + 19: + action: PERMIT + src_port: DNS + dst_port: DNS + 20: + action: PERMIT + src_port: FTP + dst_port: FTP + 21: + action: PERMIT + src_port: HTTP + dst_port: HTTP + 22: + action: PERMIT + src_port: ARP + dst_port: ARP + 23: + action: PERMIT + protocol: ICMP + + - hostname: switch_1 + type: switch + num_ports: 8 + + - hostname: switch_2 + type: switch + num_ports: 8 + + - hostname: domain_controller + type: server + ip_address: 192.168.1.10 + subnet_mask: 255.255.255.0 + default_gateway: 192.168.1.1 + services: + - type: DNSServer + options: + domain_mapping: + arcd.com: 192.168.1.12 # web server + + - hostname: web_server + type: server + ip_address: 192.168.1.12 + subnet_mask: 255.255.255.0 + default_gateway: 192.168.1.1 + dns_server: 192.168.1.10 + services: + - type: WebServer + applications: + - type: DatabaseClient + options: + db_server_ip: 192.168.1.14 + + + - hostname: database_server + type: server + ip_address: 192.168.1.14 + subnet_mask: 255.255.255.0 + default_gateway: 192.168.1.1 + dns_server: 192.168.1.10 + services: + - type: DatabaseService + options: + backup_server_ip: 192.168.1.16 + - type: FTPClient + + - hostname: backup_server + type: server + ip_address: 192.168.1.16 + subnet_mask: 255.255.255.0 + default_gateway: 192.168.1.1 + dns_server: 192.168.1.10 + services: + - type: FTPServer + + - hostname: security_suite + type: server + ip_address: 192.168.1.110 + subnet_mask: 255.255.255.0 + default_gateway: 192.168.1.1 + dns_server: 192.168.1.10 + network_interfaces: + 2: # unfortunately this number is currently meaningless, they're just added in order and take up the next available slot + ip_address: 192.168.10.110 + subnet_mask: 255.255.255.0 + + - hostname: client_1 + type: computer + ip_address: 192.168.10.21 + subnet_mask: 255.255.255.0 + default_gateway: 192.168.10.1 + dns_server: 192.168.1.10 + applications: + - type: DataManipulationBot + options: + port_scan_p_of_success: 0.8 + data_manipulation_p_of_success: 0.8 + payload: "DELETE" + server_ip: 192.168.1.14 + - type: WebBrowser + options: + target_url: http://arcd.com/users/ + - type: DatabaseClient + options: + db_server_ip: 192.168.1.14 + services: + - type: DNSClient + + - hostname: client_2 + type: computer + ip_address: 192.168.10.22 + subnet_mask: 255.255.255.0 + default_gateway: 192.168.10.1 + dns_server: 192.168.1.10 + applications: + - type: WebBrowser + options: + target_url: http://arcd.com/users/ + - type: DataManipulationBot + options: + port_scan_p_of_success: 0.8 + data_manipulation_p_of_success: 0.8 + payload: "DELETE" + server_ip: 192.168.1.14 + - type: DatabaseClient + options: + db_server_ip: 192.168.1.14 + services: + - type: DNSClient + + links: + - endpoint_a_hostname: router_1 + endpoint_a_port: 1 + endpoint_b_hostname: switch_1 + endpoint_b_port: 8 + - endpoint_a_hostname: router_1 + endpoint_a_port: 2 + endpoint_b_hostname: switch_2 + endpoint_b_port: 8 + - endpoint_a_hostname: switch_1 + endpoint_a_port: 1 + endpoint_b_hostname: domain_controller + endpoint_b_port: 1 + - endpoint_a_hostname: switch_1 + endpoint_a_port: 2 + endpoint_b_hostname: web_server + endpoint_b_port: 1 + - endpoint_a_hostname: switch_1 + endpoint_a_port: 3 + endpoint_b_hostname: database_server + endpoint_b_port: 1 + - endpoint_a_hostname: switch_1 + endpoint_a_port: 4 + endpoint_b_hostname: backup_server + endpoint_b_port: 1 + - endpoint_a_hostname: switch_1 + endpoint_a_port: 7 + endpoint_b_hostname: security_suite + endpoint_b_port: 1 + - endpoint_a_hostname: switch_2 + endpoint_a_port: 1 + endpoint_b_hostname: client_1 + endpoint_b_port: 1 + - endpoint_a_hostname: switch_2 + endpoint_a_port: 2 + endpoint_b_hostname: client_2 + endpoint_b_port: 1 + - endpoint_a_hostname: switch_2 + endpoint_a_port: 7 + endpoint_b_hostname: security_suite + endpoint_b_port: 2 diff --git a/tests/assets/configs/eval_only_primaite_session.yaml b/tests/assets/configs/eval_only_primaite_session.yaml index de861dcc..3d60eb6e 100644 --- a/tests/assets/configs/eval_only_primaite_session.yaml +++ b/tests/assets/configs/eval_only_primaite_session.yaml @@ -111,7 +111,7 @@ agents: num_files: 1 num_nics: 2 include_num_access: false - include_nmne: true + include_nmne: false routers: - hostname: router_1 num_ports: 0 diff --git a/tests/assets/configs/firewall_actions_network.yaml b/tests/assets/configs/firewall_actions_network.yaml index fd5b1bf8..2292616d 100644 --- a/tests/assets/configs/firewall_actions_network.yaml +++ b/tests/assets/configs/firewall_actions_network.yaml @@ -68,7 +68,7 @@ agents: num_files: 1 num_nics: 2 include_num_access: false - include_nmne: true + include_nmne: false routers: - hostname: router_1 num_ports: 0 diff --git a/tests/assets/configs/fix_duration_one_item.yaml b/tests/assets/configs/fix_duration_one_item.yaml index 59bc15f9..bd0fb61f 100644 --- a/tests/assets/configs/fix_duration_one_item.yaml +++ b/tests/assets/configs/fix_duration_one_item.yaml @@ -89,7 +89,7 @@ agents: - NONE tcp: - DNS - include_nmne: true + include_nmne: false routers: - hostname: router_1 num_ports: 0 diff --git a/tests/assets/configs/scenario_with_placeholders/scenario.yaml b/tests/assets/configs/scenario_with_placeholders/scenario.yaml index 81848b2d..ef930a1a 100644 --- a/tests/assets/configs/scenario_with_placeholders/scenario.yaml +++ b/tests/assets/configs/scenario_with_placeholders/scenario.yaml @@ -44,7 +44,7 @@ agents: num_files: 1 num_nics: 1 include_num_access: false - include_nmne: true + include_nmne: false - type: LINKS label: LINKS diff --git a/tests/assets/configs/software_fix_duration.yaml b/tests/assets/configs/software_fix_duration.yaml index 1acb05a9..1a28258b 100644 --- a/tests/assets/configs/software_fix_duration.yaml +++ b/tests/assets/configs/software_fix_duration.yaml @@ -89,7 +89,7 @@ agents: - NONE tcp: - DNS - include_nmne: true + include_nmne: false routers: - hostname: router_1 num_ports: 0 diff --git a/tests/assets/configs/test_primaite_session.yaml b/tests/assets/configs/test_primaite_session.yaml index eb8103e8..27cfa240 100644 --- a/tests/assets/configs/test_primaite_session.yaml +++ b/tests/assets/configs/test_primaite_session.yaml @@ -120,7 +120,7 @@ agents: num_files: 1 num_nics: 2 include_num_access: false - include_nmne: true + include_nmne: false routers: - hostname: router_1 num_ports: 0 diff --git a/tests/conftest.py b/tests/conftest.py index 54519e2b..1bbff8f2 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -30,21 +30,21 @@ from primaite.simulator.system.services.service import Service from primaite.simulator.system.services.web_server.web_server import WebServer from tests import TEST_ASSETS_ROOT -rayinit(local_mode=True) +rayinit() ACTION_SPACE_NODE_VALUES = 1 ACTION_SPACE_NODE_ACTION_VALUES = 1 _LOGGER = getLogger(__name__) -class TestService(Service): +class DummyService(Service): """Test Service class""" def describe_state(self) -> Dict: return super().describe_state() def __init__(self, **kwargs): - kwargs["name"] = "TestService" + kwargs["name"] = "DummyService" kwargs["port"] = Port.HTTP kwargs["protocol"] = IPProtocol.TCP super().__init__(**kwargs) @@ -75,15 +75,15 @@ def uc2_network() -> Network: @pytest.fixture(scope="function") -def service(file_system) -> TestService: - return TestService( - name="TestService", port=Port.ARP, file_system=file_system, sys_log=SysLog(hostname="test_service") +def service(file_system) -> DummyService: + return DummyService( + name="DummyService", port=Port.ARP, file_system=file_system, sys_log=SysLog(hostname="dummy_service") ) @pytest.fixture(scope="function") def service_class(): - return TestService + return DummyService @pytest.fixture(scope="function") @@ -458,6 +458,15 @@ def game_and_agent(): {"type": "HOST_NIC_DISABLE"}, {"type": "NETWORK_PORT_ENABLE"}, {"type": "NETWORK_PORT_DISABLE"}, + {"type": "CONFIGURE_C2_BEACON"}, + {"type": "C2_SERVER_RANSOMWARE_LAUNCH"}, + {"type": "C2_SERVER_RANSOMWARE_CONFIGURE"}, + {"type": "C2_SERVER_TERMINAL_COMMAND"}, + {"type": "C2_SERVER_DATA_EXFILTRATE"}, + {"type": "NODE_ACCOUNTS_CHANGE_PASSWORD"}, + {"type": "SSH_TO_REMOTE"}, + {"type": "SESSIONS_REMOTE_LOGOFF"}, + {"type": "NODE_SEND_REMOTE_COMMAND"}, ] action_space = ActionManager( @@ -468,12 +477,14 @@ def game_and_agent(): "applications": [ {"application_name": "WebBrowser"}, {"application_name": "DoSBot"}, + {"application_name": "C2Server"}, ], "folders": [{"folder_name": "downloads", "files": [{"file_name": "cat.png"}]}], }, { "node_name": "server_1", "services": [{"service_name": "DNSServer"}], + "applications": [{"application_name": "C2Beacon"}], }, {"node_name": "server_2", "services": [{"service_name": "WebServer"}]}, {"node_name": "router"}, @@ -481,7 +492,7 @@ def game_and_agent(): max_folders_per_node=2, max_files_per_folder=2, max_services_per_node=2, - max_applications_per_node=2, + max_applications_per_node=3, max_nics_per_node=2, max_acl_rules=10, protocols=["TCP", "UDP", "ICMP"], diff --git a/tests/e2e_integration_tests/action_masking/test_agents_use_action_masks.py b/tests/e2e_integration_tests/action_masking/test_agents_use_action_masks.py index 745e280b..addf6dca 100644 --- a/tests/e2e_integration_tests/action_masking/test_agents_use_action_masks.py +++ b/tests/e2e_integration_tests/action_masking/test_agents_use_action_masks.py @@ -1,6 +1,7 @@ # © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from typing import Dict +import pytest import yaml from ray.rllib.algorithms.ppo import PPOConfig from ray.rllib.core.rl_module.marl_module import MultiAgentRLModuleSpec @@ -100,6 +101,7 @@ def test_ray_single_agent_action_masking(monkeypatch): monkeypatch.undo() +@pytest.mark.xfail(reason="Fails due to being flaky when run in CI.") def test_ray_multi_agent_action_masking(monkeypatch): """Check that Ray agents never take invalid actions when using MARL.""" with open(MARL_PATH, "r") as f: diff --git a/tests/integration_tests/component_creation/test_action_integration.py b/tests/integration_tests/component_creation/test_action_integration.py index a6f09436..7bdc80fc 100644 --- a/tests/integration_tests/component_creation/test_action_integration.py +++ b/tests/integration_tests/component_creation/test_action_integration.py @@ -22,8 +22,7 @@ def test_passing_actions_down(monkeypatch) -> None: for n in [pc1, pc2, srv, s1]: sim.network.add_node(n) - database_service = DatabaseService(file_system=srv.file_system) - srv.install_service(database_service) + srv.software_manager.install(DatabaseService) downloads_folder = pc1.file_system.create_folder("downloads") pc1.file_system.create_file("bermuda_triangle.png", folder_name="downloads") diff --git a/tests/integration_tests/game_layer/actions/test_c2_suite_actions.py b/tests/integration_tests/game_layer/actions/test_c2_suite_actions.py new file mode 100644 index 00000000..806ce063 --- /dev/null +++ b/tests/integration_tests/game_layer/actions/test_c2_suite_actions.py @@ -0,0 +1,203 @@ +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +from ipaddress import IPv4Address +from typing import Tuple + +import pytest + +from primaite.game.agent.interface import ProxyAgent +from primaite.game.game import PrimaiteGame +from primaite.simulator.file_system.file_system_item_abc import FileSystemItemHealthStatus +from primaite.simulator.network.hardware.base import UserManager +from primaite.simulator.network.hardware.nodes.host.computer import Computer +from primaite.simulator.network.hardware.nodes.host.server import Server +from primaite.simulator.network.hardware.nodes.network.router import ACLAction +from primaite.simulator.network.transmission.transport_layer import Port +from primaite.simulator.system.applications.red_applications.c2.c2_beacon import C2Beacon +from primaite.simulator.system.applications.red_applications.c2.c2_server import C2Command, C2Server +from primaite.simulator.system.services.database.database_service import DatabaseService +from primaite.simulator.system.services.ftp.ftp_client import FTPClient +from primaite.simulator.system.services.ftp.ftp_server import FTPServer +from primaite.simulator.system.services.service import ServiceOperatingState + + +@pytest.fixture +def game_and_agent_fixture(game_and_agent): + """Create a game with a simple agent that can be controlled by the tests.""" + game, agent = game_and_agent + + router = game.simulation.network.get_node_by_hostname("router") + router.acl.add_rule(action=ACLAction.PERMIT, src_port=Port.HTTP, dst_port=Port.HTTP, position=4) + router.acl.add_rule(action=ACLAction.PERMIT, src_port=Port.DNS, dst_port=Port.DNS, position=5) + router.acl.add_rule(action=ACLAction.PERMIT, src_port=Port.FTP, dst_port=Port.FTP, position=6) + + c2_server_host = game.simulation.network.get_node_by_hostname("client_1") + c2_server_host.software_manager.install(software_class=C2Server) + c2_server: C2Server = c2_server_host.software_manager.software["C2Server"] + c2_server.run() + + return (game, agent) + + +def test_c2_beacon_default(game_and_agent_fixture: Tuple[PrimaiteGame, ProxyAgent]): + """Tests that a Red Agent can install, configure and establish a C2 Beacon (default params).""" + game, agent = game_and_agent_fixture + + # Installing C2 Beacon on Server_1 + server_1: Server = game.simulation.network.get_node_by_hostname("server_1") + + action = ( + "NODE_APPLICATION_INSTALL", + {"node_id": 1, "application_name": "C2Beacon"}, + ) + agent.store_action(action) + game.step() + assert agent.history[-1].response.status == "success" + + action = ( + "CONFIGURE_C2_BEACON", + { + "node_id": 1, + "config": { + "c2_server_ip_address": "10.0.1.2", + "keep_alive_frequency": 5, + "masquerade_protocol": "TCP", + "masquerade_port": "HTTP", + }, + }, + ) + agent.store_action(action) + game.step() + assert agent.history[-1].response.status == "success" + + action = ( + "NODE_APPLICATION_EXECUTE", + {"node_id": 1, "application_id": 0}, + ) + agent.store_action(action) + game.step() + assert agent.history[-1].response.status == "success" + + # Asserting that we've confirmed our connection + c2_beacon: C2Beacon = server_1.software_manager.software["C2Beacon"] + assert c2_beacon.c2_connection_active == True + + +def test_c2_server_ransomware(game_and_agent_fixture: Tuple[PrimaiteGame, ProxyAgent]): + """Tests that a Red Agent can install a RansomwareScript, Configure and launch all via C2 Server actions.""" + game, agent = game_and_agent_fixture + + # Installing a C2 Beacon on server_1 + server_1: Server = game.simulation.network.get_node_by_hostname("server_1") + server_1.software_manager.install(C2Beacon) + + # Installing a database on Server_2 for the ransomware to attack + server_2: Server = game.simulation.network.get_node_by_hostname("server_2") + server_2.software_manager.install(DatabaseService) + server_2.software_manager.software["DatabaseService"].start() + # Configuring the C2 to connect to client 1 (C2 Server) + c2_beacon: C2Beacon = server_1.software_manager.software["C2Beacon"] + c2_beacon.configure(c2_server_ip_address=IPv4Address("10.0.1.2")) + c2_beacon.establish() + assert c2_beacon.c2_connection_active == True + + # C2 Action 1: Installing the RansomwareScript & Database client via Terminal + + action = ( + "C2_SERVER_TERMINAL_COMMAND", + { + "node_id": 0, + "ip_address": None, + "account": { + "username": "admin", + "password": "admin", + }, + "commands": [ + ["software_manager", "application", "install", "RansomwareScript"], + ["software_manager", "application", "install", "DatabaseClient"], + ], + }, + ) + agent.store_action(action) + game.step() + assert agent.history[-1].response.status == "success" + + action = ( + "C2_SERVER_RANSOMWARE_CONFIGURE", + { + "node_id": 0, + "config": {"server_ip_address": "10.0.2.3", "payload": "ENCRYPT"}, + }, + ) + agent.store_action(action) + game.step() + assert agent.history[-1].response.status == "success" + + # Stepping a few timesteps to allow for the RansowmareScript to finish installing. + + action = ("DONOTHING", {}) + agent.store_action(action) + game.step() + game.step() + game.step() + + action = ( + "C2_SERVER_RANSOMWARE_LAUNCH", + { + "node_id": 0, + }, + ) + agent.store_action(action) + game.step() + assert agent.history[-1].response.status == "success" + + database_file = server_2.software_manager.file_system.get_file("database", "database.db") + assert database_file.health_status == FileSystemItemHealthStatus.CORRUPT + + +def test_c2_server_data_exfiltration(game_and_agent_fixture: Tuple[PrimaiteGame, ProxyAgent]): + """Tests that a Red Agent can extract a database.db file via C2 Server actions.""" + game, agent = game_and_agent_fixture + + # Installing a C2 Beacon on server_1 + server_1: Server = game.simulation.network.get_node_by_hostname("server_1") + server_1.software_manager.install(C2Beacon) + + # Installing a database on Server_2 (creates a database.db file.) + server_2: Server = game.simulation.network.get_node_by_hostname("server_2") + server_2.software_manager.install(DatabaseService) + server_2.software_manager.software["DatabaseService"].start() + + # Configuring the C2 to connect to client 1 (C2 Server) + c2_beacon: C2Beacon = server_1.software_manager.software["C2Beacon"] + c2_beacon.configure(c2_server_ip_address=IPv4Address("10.0.1.2")) + c2_beacon.establish() + assert c2_beacon.c2_connection_active == True + + # Selecting a target file to steal: database.db + # Server 2 ip : 10.0.2.3 + database_file = server_2.software_manager.file_system.get_file(folder_name="database", file_name="database.db") + assert database_file is not None + + # C2 Action: Data exfiltrate. + + action = ( + "C2_SERVER_DATA_EXFILTRATE", + { + "node_id": 0, + "target_file_name": "database.db", + "target_folder_name": "database", + "exfiltration_folder_name": "spoils", + "target_ip_address": "10.0.2.3", + "account": { + "username": "admin", + "password": "admin", + }, + }, + ) + agent.store_action(action) + game.step() + + assert server_1.file_system.access_file(folder_name="spoils", file_name="database.db") + + client_1 = game.simulation.network.get_node_by_hostname("client_1") + assert client_1.file_system.access_file(folder_name="spoils", file_name="database.db") diff --git a/tests/integration_tests/game_layer/actions/test_terminal_actions.py b/tests/integration_tests/game_layer/actions/test_terminal_actions.py new file mode 100644 index 00000000..d011c1e8 --- /dev/null +++ b/tests/integration_tests/game_layer/actions/test_terminal_actions.py @@ -0,0 +1,166 @@ +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +from typing import Tuple + +import pytest + +from primaite.game.agent.interface import ProxyAgent +from primaite.game.game import PrimaiteGame +from primaite.simulator.network.hardware.base import UserManager +from primaite.simulator.network.hardware.nodes.host.computer import Computer +from primaite.simulator.network.hardware.nodes.host.server import Server +from primaite.simulator.network.hardware.nodes.network.router import ACLAction +from primaite.simulator.network.transmission.transport_layer import Port +from primaite.simulator.system.services.service import ServiceOperatingState +from primaite.simulator.system.services.terminal.terminal import RemoteTerminalConnection + + +@pytest.fixture +def game_and_agent_fixture(game_and_agent): + """Create a game with a simple agent that can be controlled by the tests.""" + game, agent = game_and_agent + + router = game.simulation.network.get_node_by_hostname("router") + router.acl.add_rule(action=ACLAction.PERMIT, src_port=Port.SSH, dst_port=Port.SSH, position=4) + + return (game, agent) + + +def test_remote_login(game_and_agent_fixture: Tuple[PrimaiteGame, ProxyAgent]): + game, agent = game_and_agent_fixture + + server_1: Server = game.simulation.network.get_node_by_hostname("server_1") + client_1 = game.simulation.network.get_node_by_hostname("client_1") + + # create a new user account on server_1 that will be logged into remotely + server_1_usm: UserManager = server_1.software_manager.software["UserManager"] + server_1_usm.add_user("user123", "password", is_admin=True) + + action = ( + "SSH_TO_REMOTE", + { + "node_id": 0, + "username": "user123", + "password": "password", + "remote_ip": str(server_1.network_interface[1].ip_address), + }, + ) + agent.store_action(action) + game.step() + assert agent.history[-1].response.status == "success" + + connection_established = False + for conn_str, conn_obj in client_1.terminal.connections.items(): + conn_obj: RemoteTerminalConnection + if conn_obj.ip_address == server_1.network_interface[1].ip_address: + connection_established = True + if not connection_established: + pytest.fail("Remote SSH connection could not be established") + + +def test_remote_login_wrong_password(game_and_agent_fixture: Tuple[PrimaiteGame, ProxyAgent]): + game, agent = game_and_agent_fixture + + server_1: Server = game.simulation.network.get_node_by_hostname("server_1") + client_1 = game.simulation.network.get_node_by_hostname("client_1") + + # create a new user account on server_1 that will be logged into remotely + server_1_usm: UserManager = server_1.software_manager.software["UserManager"] + server_1_usm.add_user("user123", "password", is_admin=True) + + action = ( + "SSH_TO_REMOTE", + { + "node_id": 0, + "username": "user123", + "password": "wrong_password", + "remote_ip": str(server_1.network_interface[1].ip_address), + }, + ) + agent.store_action(action) + game.step() + assert agent.history[-1].response.status == "failure" + + connection_established = False + for conn_str, conn_obj in client_1.terminal.connections.items(): + conn_obj: RemoteTerminalConnection + if conn_obj.ip_address == server_1.network_interface[1].ip_address: + connection_established = True + if connection_established: + pytest.fail("Remote SSH connection was established despite wrong password") + + +def test_remote_login_change_password(game_and_agent_fixture: Tuple[PrimaiteGame, ProxyAgent]): + game, agent = game_and_agent_fixture + + server_1: Server = game.simulation.network.get_node_by_hostname("server_1") + client_1 = game.simulation.network.get_node_by_hostname("client_1") + + # create a new user account on server_1 that will be logged into remotely + server_1_um: UserManager = server_1.software_manager.software["UserManager"] + server_1_um.add_user("user123", "password", is_admin=True) + + action = ( + "NODE_ACCOUNTS_CHANGE_PASSWORD", + { + "node_id": 1, # server_1 + "username": "user123", + "current_password": "password", + "new_password": "different_password", + }, + ) + agent.store_action(action) + game.step() + assert agent.history[-1].response.status == "success" + assert server_1_um.users["user123"].password == "different_password" + + +def test_change_password_logs_out_user(game_and_agent_fixture: Tuple[PrimaiteGame, ProxyAgent]): + game, agent = game_and_agent_fixture + + server_1: Server = game.simulation.network.get_node_by_hostname("server_1") + client_1 = game.simulation.network.get_node_by_hostname("client_1") + + # create a new user account on server_1 that will be logged into remotely + server_1_usm: UserManager = server_1.software_manager.software["UserManager"] + server_1_usm.add_user("user123", "password", is_admin=True) + + # Log in remotely + action = ( + "SSH_TO_REMOTE", + { + "node_id": 0, + "username": "user123", + "password": "password", + "remote_ip": str(server_1.network_interface[1].ip_address), + }, + ) + agent.store_action(action) + game.step() + + # Change password + action = ( + "NODE_ACCOUNTS_CHANGE_PASSWORD", + { + "node_id": 1, # server_1 + "username": "user123", + "current_password": "password", + "new_password": "different_password", + }, + ) + agent.store_action(action) + game.step() + + # Assert that the user cannot execute an action + action = ( + "NODE_SEND_REMOTE_COMMAND", + { + "node_id": 0, + "remote_ip": str(server_1.network_interface[1].ip_address), + "command": ["file_system", "create", "file", "folder123", "doggo.pdf", False], + }, + ) + agent.store_action(action) + game.step() + + assert server_1.file_system.get_folder("folder123") is None + assert server_1.file_system.get_file("folder123", "doggo.pdf") is None diff --git a/tests/integration_tests/game_layer/observations/test_file_system_observations.py b/tests/integration_tests/game_layer/observations/test_file_system_observations.py index 1031dcb0..e2ab2990 100644 --- a/tests/integration_tests/game_layer/observations/test_file_system_observations.py +++ b/tests/integration_tests/game_layer/observations/test_file_system_observations.py @@ -26,6 +26,7 @@ def test_file_observation(simulation): dog_file_obs = FileObservation( where=["network", "nodes", pc.hostname, "file_system", "folders", "root", "files", "dog.png"], include_num_access=False, + file_system_requires_scan=True, ) assert dog_file_obs.space["health_status"] == spaces.Discrete(6) @@ -53,6 +54,7 @@ def test_folder_observation(simulation): root_folder_obs = FolderObservation( where=["network", "nodes", pc.hostname, "file_system", "folders", "test_folder"], include_num_access=False, + file_system_requires_scan=True, num_files=1, files=[], ) diff --git a/tests/integration_tests/game_layer/observations/test_firewall_observation.py b/tests/integration_tests/game_layer/observations/test_firewall_observation.py index 99417e33..34a37f5e 100644 --- a/tests/integration_tests/game_layer/observations/test_firewall_observation.py +++ b/tests/integration_tests/game_layer/observations/test_firewall_observation.py @@ -33,6 +33,7 @@ def test_firewall_observation(): wildcard_list=["0.0.0.255", "0.0.0.1"], port_list=["HTTP", "DNS"], protocol_list=["TCP"], + include_users=False, ) observation = firewall_observation.observe(firewall.describe_state()) diff --git a/tests/integration_tests/game_layer/observations/test_nic_observations.py b/tests/integration_tests/game_layer/observations/test_nic_observations.py index 88dd2bd5..ef789ba7 100644 --- a/tests/integration_tests/game_layer/observations/test_nic_observations.py +++ b/tests/integration_tests/game_layer/observations/test_nic_observations.py @@ -9,9 +9,11 @@ from gymnasium import spaces from primaite.game.agent.interface import ProxyAgent from primaite.game.agent.observations.nic_observations import NICObservation from primaite.game.game import PrimaiteGame +from primaite.simulator.network.hardware.base import NetworkInterface from primaite.simulator.network.hardware.nodes.host.computer import Computer from primaite.simulator.network.hardware.nodes.host.host_node import NIC from primaite.simulator.network.hardware.nodes.host.server import Server +from primaite.simulator.network.nmne import NMNEConfig from primaite.simulator.sim_container import Simulation from primaite.simulator.system.applications.database_client import DatabaseClient from primaite.simulator.system.applications.web_browser import WebBrowser @@ -75,6 +77,18 @@ def test_nic(simulation): nic_obs = NICObservation(where=["network", "nodes", pc.hostname, "NICs", 1], include_nmne=True) + # Set the NMNE configuration to capture DELETE/ENCRYPT queries as MNEs + nmne_config = { + "capture_nmne": True, # Enable the capture of MNEs + "nmne_capture_keywords": [ + "DELETE", + "ENCRYPT", + ], # Specify "DELETE/ENCRYPT" SQL command as a keyword for MNE detection + } + + # Apply the NMNE configuration settings + NetworkInterface.nmne_config = NMNEConfig(**nmne_config) + assert nic_obs.space["nic_status"] == spaces.Discrete(3) assert nic_obs.space["NMNE"]["inbound"] == spaces.Discrete(4) assert nic_obs.space["NMNE"]["outbound"] == spaces.Discrete(4) @@ -144,7 +158,7 @@ def test_nic_monitored_traffic(simulation): pc2: Computer = simulation.network.get_node_by_hostname("client_2") nic_obs = NICObservation( - where=["network", "nodes", pc.hostname, "NICs", 1], include_nmne=True, monitored_traffic=monitored_traffic + where=["network", "nodes", pc.hostname, "NICs", 1], include_nmne=False, monitored_traffic=monitored_traffic ) simulation.pre_timestep(0) # apply timestep to whole sim diff --git a/tests/integration_tests/game_layer/observations/test_node_observations.py b/tests/integration_tests/game_layer/observations/test_node_observations.py index 8a36ea5c..69d9f106 100644 --- a/tests/integration_tests/game_layer/observations/test_node_observations.py +++ b/tests/integration_tests/game_layer/observations/test_node_observations.py @@ -38,6 +38,8 @@ def test_host_observation(simulation): applications=[], folders=[], network_interfaces=[], + file_system_requires_scan=True, + include_users=False, ) assert host_obs.space["operating_status"] == spaces.Discrete(5) diff --git a/tests/integration_tests/game_layer/observations/test_router_observation.py b/tests/integration_tests/game_layer/observations/test_router_observation.py index c534307f..48d29cfb 100644 --- a/tests/integration_tests/game_layer/observations/test_router_observation.py +++ b/tests/integration_tests/game_layer/observations/test_router_observation.py @@ -27,7 +27,7 @@ def test_router_observation(): port_list=["HTTP", "DNS"], protocol_list=["TCP"], ) - router_observation = RouterObservation(where=[], ports=ports, num_ports=8, acl=acl) + router_observation = RouterObservation(where=[], ports=ports, num_ports=8, acl=acl, include_users=False) # Observe the state using the RouterObservation instance observed_output = router_observation.observe(router.describe_state()) diff --git a/tests/integration_tests/game_layer/observations/test_user_observations.py b/tests/integration_tests/game_layer/observations/test_user_observations.py new file mode 100644 index 00000000..ca5e2543 --- /dev/null +++ b/tests/integration_tests/game_layer/observations/test_user_observations.py @@ -0,0 +1,89 @@ +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +import pytest + +from primaite.session.environment import PrimaiteGymEnv +from primaite.simulator.network.hardware.nodes.network.router import ACLAction, Router +from primaite.simulator.network.transmission.transport_layer import Port +from tests import TEST_ASSETS_ROOT + +DATA_MANIPULATION_CONFIG = TEST_ASSETS_ROOT / "configs" / "data_manipulation.yaml" + + +@pytest.fixture +def env_with_ssh() -> PrimaiteGymEnv: + """Build data manipulation environment with SSH port open on router.""" + env = PrimaiteGymEnv(DATA_MANIPULATION_CONFIG) + env.agent.flatten_obs = False + router: Router = env.game.simulation.network.get_node_by_hostname("router_1") + router.acl.add_rule(ACLAction.PERMIT, src_port=Port.SSH, dst_port=Port.SSH, position=3) + return env + + +def extract_login_numbers_from_obs(obs): + """Traverse the observation dictionary and return number of user sessions for all nodes.""" + login_nums = {} + for node_name, node_obs in obs["NODES"].items(): + login_nums[node_name] = node_obs.get("users") + return login_nums + + +class TestUserObservations: + """Test that the RouterObservation, FirewallObservation, and HostObservation have the correct number of logins.""" + + def test_no_sessions_at_episode_start(self, env_with_ssh): + """Test that all of the login observations start at 0 before any logins occur.""" + obs, *_ = env_with_ssh.step(0) + logins_obs = extract_login_numbers_from_obs(obs) + for o in logins_obs.values(): + assert o["local_login"] == 0 + assert o["remote_sessions"] == 0 + + def test_single_login(self, env_with_ssh: PrimaiteGymEnv): + """Test that performing a remote login increases the remote_sessions observation by 1.""" + client_1 = env_with_ssh.game.simulation.network.get_node_by_hostname("client_1") + client_1.terminal._send_remote_login("admin", "admin", "192.168.1.14") # connect to database server via ssh + obs, *_ = env_with_ssh.step(0) + logins_obs = extract_login_numbers_from_obs(obs) + db_srv_logins_obs = logins_obs.pop("HOST2") # this is the index of db server + assert db_srv_logins_obs["local_login"] == 0 + assert db_srv_logins_obs["remote_sessions"] == 1 + for o in logins_obs.values(): # the remaining obs after popping HOST2 + assert o["local_login"] == 0 + assert o["remote_sessions"] == 0 + + def test_logout(self, env_with_ssh: PrimaiteGymEnv): + """Test that remote_sessions observation correctly decreases upon logout.""" + client_1 = env_with_ssh.game.simulation.network.get_node_by_hostname("client_1") + client_1.terminal._send_remote_login("admin", "admin", "192.168.1.14") # connect to database server via ssh + db_srv = env_with_ssh.game.simulation.network.get_node_by_hostname("database_server") + db_srv.user_manager.change_user_password("admin", "admin", "different_pass") # changing password logs out user + + obs, *_ = env_with_ssh.step(0) + logins_obs = extract_login_numbers_from_obs(obs) + for o in logins_obs.values(): + assert o["local_login"] == 0 + assert o["remote_sessions"] == 0 + + def test_max_observable_sessions(self, env_with_ssh: PrimaiteGymEnv): + """Log in from 5 remote places and check that only a max of 3 is shown in the observation.""" + MAX_OBSERVABLE_SESSIONS = 3 + # Right now this is hardcoded as 3 in HostObservation, FirewallObservation, and RouterObservation + obs, *_ = env_with_ssh.step(0) + logins_obs = extract_login_numbers_from_obs(obs) + db_srv_logins_obs = logins_obs.pop("HOST2") # this is the index of db server + + db_srv = env_with_ssh.game.simulation.network.get_node_by_hostname("database_server") + db_srv.user_session_manager.remote_session_timeout_steps = 20 + db_srv.user_session_manager.max_remote_sessions = 5 + node_names = ("client_1", "client_2", "backup_server", "security_suite", "domain_controller") + + for i, node_name in enumerate(node_names): + node = env_with_ssh.game.simulation.network.get_node_by_hostname(node_name) + node.terminal._send_remote_login("admin", "admin", "192.168.1.14") + + obs, *_ = env_with_ssh.step(0) + logins_obs = extract_login_numbers_from_obs(obs) + db_srv_logins_obs = logins_obs.pop("HOST2") # this is the index of db server + + assert db_srv_logins_obs["remote_sessions"] == min(MAX_OBSERVABLE_SESSIONS, i + 1) + assert len(db_srv.user_session_manager.remote_sessions) == i + 1 diff --git a/tests/integration_tests/game_layer/test_RNG_seed.py b/tests/integration_tests/game_layer/test_RNG_seed.py new file mode 100644 index 00000000..0c6d567d --- /dev/null +++ b/tests/integration_tests/game_layer/test_RNG_seed.py @@ -0,0 +1,50 @@ +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +from pprint import pprint + +import pytest +import yaml + +from primaite.config.load import data_manipulation_config_path +from primaite.game.agent.interface import AgentHistoryItem +from primaite.session.environment import PrimaiteGymEnv + + +@pytest.fixture() +def create_env(): + with open(data_manipulation_config_path(), "r") as f: + cfg = yaml.safe_load(f) + + env = PrimaiteGymEnv(env_config=cfg) + return env + + +def test_rng_seed_set(create_env): + """Test with RNG seed set.""" + env = create_env + env.reset(seed=3) + for i in range(100): + env.step(0) + a = [item.timestep for item in env.game.agents["client_2_green_user"].history if item.action != "DONOTHING"] + + env.reset(seed=3) + for i in range(100): + env.step(0) + b = [item.timestep for item in env.game.agents["client_2_green_user"].history if item.action != "DONOTHING"] + + assert a == b + + +def test_rng_seed_unset(create_env): + """Test with no RNG seed.""" + env = create_env + env.reset() + for i in range(100): + env.step(0) + a = [item.timestep for item in env.game.agents["client_2_green_user"].history if item.action != "DONOTHING"] + + env.reset() + for i in range(100): + env.step(0) + b = [item.timestep for item in env.game.agents["client_2_green_user"].history if item.action != "DONOTHING"] + + assert a != b diff --git a/tests/integration_tests/game_layer/test_observations.py b/tests/integration_tests/game_layer/test_observations.py index ff83c532..d5679007 100644 --- a/tests/integration_tests/game_layer/test_observations.py +++ b/tests/integration_tests/game_layer/test_observations.py @@ -17,6 +17,7 @@ def test_file_observation(): dog_file_obs = FileObservation( where=["network", "nodes", pc.hostname, "file_system", "folders", "root", "files", "dog.png"], include_num_access=False, + file_system_requires_scan=True, ) assert dog_file_obs.observe(state) == {"health_status": 1} assert dog_file_obs.space == spaces.Dict({"health_status": spaces.Discrete(6)}) diff --git a/tests/integration_tests/game_layer/test_rewards.py b/tests/integration_tests/game_layer/test_rewards.py index 2bf551c8..58783d70 100644 --- a/tests/integration_tests/game_layer/test_rewards.py +++ b/tests/integration_tests/game_layer/test_rewards.py @@ -12,6 +12,7 @@ from primaite.simulator.network.hardware.nodes.network.router import ACLAction, from primaite.simulator.network.transmission.network_layer import IPProtocol from primaite.simulator.network.transmission.transport_layer import Port from primaite.simulator.system.applications.database_client import DatabaseClient +from primaite.simulator.system.applications.web_browser import WebBrowser from primaite.simulator.system.services.database.database_service import DatabaseService from tests import TEST_ASSETS_ROOT from tests.conftest import ControlledAgent @@ -19,32 +20,30 @@ from tests.conftest import ControlledAgent def test_WebpageUnavailablePenalty(game_and_agent): """Test that we get the right reward for failing to fetch a website.""" + # set up the scenario, configure the web browser to the correct url game, agent = game_and_agent agent: ControlledAgent comp = WebpageUnavailablePenalty(node_hostname="client_1") - - agent.reward_function.register_component(comp, 0.7) - action = ("DONOTHING", {}) - agent.store_action(action) - game.step() - - # client 1 has not attempted to fetch webpage yet! - assert agent.reward_function.current_reward == 0.0 - client_1 = game.simulation.network.get_node_by_hostname("client_1") - browser = client_1.software_manager.software.get("WebBrowser") + browser: WebBrowser = client_1.software_manager.software.get("WebBrowser") browser.run() browser.target_url = "http://www.example.com" - assert browser.get_webpage() - action = ("DONOTHING", {}) - agent.store_action(action) + agent.reward_function.register_component(comp, 0.7) + + # Check that before trying to fetch the webpage, the reward is 0.0 + agent.store_action(("DONOTHING", {})) + game.step() + assert agent.reward_function.current_reward == 0.0 + + # Check that successfully fetching the webpage yields a reward of 0.7 + agent.store_action(("NODE_APPLICATION_EXECUTE", {"node_id": 0, "application_id": 0})) game.step() assert agent.reward_function.current_reward == 0.7 + # Block the web traffic, check that failing to fetch the webpage yields a reward of -0.7 router: Router = game.simulation.network.get_node_by_hostname("router") router.acl.add_rule(action=ACLAction.DENY, protocol=IPProtocol.TCP, src_port=Port.HTTP, dst_port=Port.HTTP) - assert not browser.get_webpage() - agent.store_action(action) + agent.store_action(("NODE_APPLICATION_EXECUTE", {"node_id": 0, "application_id": 0})) game.step() assert agent.reward_function.current_reward == -0.7 @@ -70,35 +69,29 @@ def test_uc2_rewards(game_and_agent): comp = GreenAdminDatabaseUnreachablePenalty("client_1") - response = db_client.apply_request( - [ - "execute", - ] - ) + request = ["network", "node", "client_1", "application", "DatabaseClient", "execute"] + response = game.simulation.apply_request(request) state = game.get_sim_state() - reward_value = comp.calculate( - state, - last_action_response=AgentHistoryItem( - timestep=0, action="NODE_APPLICATION_EXECUTE", parameters={}, request=["execute"], response=response - ), + ahi = AgentHistoryItem( + timestep=0, action="NODE_APPLICATION_EXECUTE", parameters={}, request=request, response=response ) + reward_value = comp.calculate(state, last_action_response=ahi) assert reward_value == 1.0 + assert ahi.reward_info == {"connection_attempt_status": "success"} router.acl.remove_rule(position=2) - db_client.apply_request( - [ - "execute", - ] - ) + response = game.simulation.apply_request(request) state = game.get_sim_state() + ahi = AgentHistoryItem( + timestep=0, action="NODE_APPLICATION_EXECUTE", parameters={}, request=request, response=response + ) reward_value = comp.calculate( state, - last_action_response=AgentHistoryItem( - timestep=0, action="NODE_APPLICATION_EXECUTE", parameters={}, request=["execute"], response=response - ), + last_action_response=ahi, ) assert reward_value == -1.0 + assert ahi.reward_info == {"connection_attempt_status": "failure"} def test_shared_reward(): diff --git a/tests/integration_tests/network/test_capture_nmne.py b/tests/integration_tests/network/test_capture_nmne.py index a8f1f245..debf5b1c 100644 --- a/tests/integration_tests/network/test_capture_nmne.py +++ b/tests/integration_tests/network/test_capture_nmne.py @@ -1,12 +1,14 @@ # © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from primaite.game.agent.observations.nic_observations import NICObservation +from primaite.simulator.network.container import Network +from primaite.simulator.network.hardware.nodes.host.host_node import NIC from primaite.simulator.network.hardware.nodes.host.server import Server -from primaite.simulator.network.nmne import set_nmne_config +from primaite.simulator.network.nmne import NMNEConfig from primaite.simulator.sim_container import Simulation from primaite.simulator.system.applications.database_client import DatabaseClient, DatabaseClientConnection -def test_capture_nmne(uc2_network): +def test_capture_nmne(uc2_network: Network): """ Conducts a test to verify that Malicious Network Events (MNEs) are correctly captured. @@ -33,7 +35,7 @@ def test_capture_nmne(uc2_network): } # Apply the NMNE configuration settings - set_nmne_config(nmne_config) + NIC.nmne_config = NMNEConfig(**nmne_config) # Assert that initially, there are no captured MNEs on both web and database servers assert web_server_nic.nmne == {} @@ -82,7 +84,7 @@ def test_capture_nmne(uc2_network): assert db_server_nic.nmne == {"direction": {"inbound": {"keywords": {"*": 3}}}} -def test_describe_state_nmne(uc2_network): +def test_describe_state_nmne(uc2_network: Network): """ Conducts a test to verify that Malicious Network Events (MNEs) are correctly represented in the nic state. @@ -110,7 +112,7 @@ def test_describe_state_nmne(uc2_network): } # Apply the NMNE configuration settings - set_nmne_config(nmne_config) + NIC.nmne_config = NMNEConfig(**nmne_config) # Assert that initially, there are no captured MNEs on both web and database servers web_server_nic_state = web_server_nic.describe_state() @@ -190,7 +192,7 @@ def test_describe_state_nmne(uc2_network): assert db_server_nic_state["nmne"] == {"direction": {"inbound": {"keywords": {"*": 4}}}} -def test_capture_nmne_observations(uc2_network): +def test_capture_nmne_observations(uc2_network: Network): """ Tests the NICObservation class's functionality within a simulated network environment. @@ -219,7 +221,7 @@ def test_capture_nmne_observations(uc2_network): } # Apply the NMNE configuration settings - set_nmne_config(nmne_config) + NIC.nmne_config = NMNEConfig(**nmne_config) # Define observations for the NICs of the database and web servers db_server_nic_obs = NICObservation(where=["network", "nodes", "database_server", "NICs", 1], include_nmne=True) diff --git a/tests/integration_tests/network/test_users_creation_from_config.py b/tests/integration_tests/network/test_users_creation_from_config.py new file mode 100644 index 00000000..8cd3b037 --- /dev/null +++ b/tests/integration_tests/network/test_users_creation_from_config.py @@ -0,0 +1,26 @@ +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +import yaml + +from primaite.game.game import PrimaiteGame +from primaite.simulator.network.hardware.base import UserManager +from tests import TEST_ASSETS_ROOT + + +def test_users_from_config(): + config_path = TEST_ASSETS_ROOT / "configs" / "basic_node_with_users.yaml" + + with open(config_path, "r") as f: + config_dict = yaml.safe_load(f) + network = PrimaiteGame.from_config(cfg=config_dict).simulation.network + + client_1 = network.get_node_by_hostname("client_1") + + user_manager: UserManager = client_1.software_manager.software["UserManager"] + + assert len(user_manager.users) == 3 + + assert user_manager.users["jane.doe"].password == "1234" + assert user_manager.users["jane.doe"].is_admin + + assert user_manager.users["john.doe"].password == "password_1" + assert not user_manager.users["john.doe"].is_admin diff --git a/tests/integration_tests/system/red_applications/test_c2_suite_integration.py b/tests/integration_tests/system/red_applications/test_c2_suite_integration.py new file mode 100644 index 00000000..9d12f2cf --- /dev/null +++ b/tests/integration_tests/system/red_applications/test_c2_suite_integration.py @@ -0,0 +1,554 @@ +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +from ipaddress import IPv4Address +from typing import Tuple + +import pytest +import yaml + +from primaite.game.agent.interface import ProxyAgent +from primaite.game.game import PrimaiteGame +from primaite.simulator.file_system.file_system_item_abc import FileSystemItemHealthStatus +from primaite.simulator.network.container import Network +from primaite.simulator.network.hardware.nodes.host.computer import Computer +from primaite.simulator.network.hardware.nodes.host.server import Server +from primaite.simulator.network.hardware.nodes.network.router import AccessControlList, ACLAction, Router +from primaite.simulator.network.hardware.nodes.network.switch import Switch +from primaite.simulator.network.transmission.network_layer import IPProtocol +from primaite.simulator.network.transmission.transport_layer import Port +from primaite.simulator.system.applications.application import ApplicationOperatingState +from primaite.simulator.system.applications.database_client import DatabaseClient +from primaite.simulator.system.applications.red_applications.c2.c2_beacon import C2Beacon +from primaite.simulator.system.applications.red_applications.c2.c2_server import C2Command, C2Server +from primaite.simulator.system.applications.red_applications.ransomware_script import RansomwareScript +from primaite.simulator.system.services.database.database_service import DatabaseService +from primaite.simulator.system.services.dns.dns_server import DNSServer +from primaite.simulator.system.services.ftp.ftp_client import FTPClient +from primaite.simulator.system.services.ftp.ftp_server import FTPServer +from primaite.simulator.system.services.web_server.web_server import WebServer +from tests import TEST_ASSETS_ROOT + + +@pytest.fixture(scope="function") +def basic_network() -> Network: + network = Network() + + # Creating two generic nodes for the C2 Server and the C2 Beacon. + + node_a = Computer( + hostname="node_a", + ip_address="192.168.0.2", + subnet_mask="255.255.255.252", + default_gateway="192.168.0.1", + start_up_duration=0, + ) + node_a.power_on() + node_a.software_manager.get_open_ports() + node_a.software_manager.install(software_class=C2Server) + + node_b = Computer( + hostname="node_b", + ip_address="192.168.255.2", + subnet_mask="255.255.255.248", + default_gateway="192.168.255.1", + start_up_duration=0, + ) + + node_b.power_on() + node_b.software_manager.install(software_class=C2Beacon) + + # Creating a generic computer for testing remote terminal connections. + node_c = Computer( + hostname="node_c", + ip_address="192.168.255.3", + subnet_mask="255.255.255.248", + default_gateway="192.168.255.1", + start_up_duration=0, + ) + node_c.power_on() + + # Creating a router to sit between node 1 and node 2. + router = Router(hostname="router", num_ports=3, start_up_duration=0) + # Default allow all. + router.acl.add_rule(action=ACLAction.PERMIT) + router.power_on() + # Creating switches for each client. + switch_1 = Switch(hostname="switch_1", num_ports=6, start_up_duration=0) + switch_1.power_on() + + # Connecting the switches to the router. + router.configure_port(port=1, ip_address="192.168.0.1", subnet_mask="255.255.255.252") + network.connect(endpoint_a=router.network_interface[1], endpoint_b=switch_1.network_interface[6]) + + switch_2 = Switch(hostname="switch_2", num_ports=6, start_up_duration=0) + switch_2.power_on() + + network.connect(endpoint_a=router.network_interface[2], endpoint_b=switch_2.network_interface[6]) + router.configure_port(port=2, ip_address="192.168.255.1", subnet_mask="255.255.255.248") + + router.enable_port(1) + router.enable_port(2) + + # Connecting the node to each switch + network.connect(node_a.network_interface[1], switch_1.network_interface[1]) + network.connect(node_b.network_interface[1], switch_2.network_interface[1]) + network.connect(node_c.network_interface[1], switch_2.network_interface[2]) + + return network + + +def setup_c2(given_network: Network): + """Installs the C2 Beacon & Server, configures and then returns.""" + computer_a: Computer = given_network.get_node_by_hostname("node_a") + c2_server: C2Server = computer_a.software_manager.software.get("C2Server") + computer_a.software_manager.install(DatabaseService) + computer_a.software_manager.software["DatabaseService"].start() + + computer_b: Computer = given_network.get_node_by_hostname("node_b") + c2_beacon: C2Beacon = computer_b.software_manager.software.get("C2Beacon") + computer_b.software_manager.install(DatabaseClient) + computer_b.software_manager.software["DatabaseClient"].configure(server_ip_address=IPv4Address("192.168.0.2")) + computer_b.software_manager.software["DatabaseClient"].run() + + c2_beacon.configure(c2_server_ip_address="192.168.0.2", keep_alive_frequency=2) + c2_server.run() + c2_beacon.establish() + + return given_network, computer_a, c2_server, computer_b, c2_beacon + + +def test_c2_suite_setup_receive(basic_network): + """Test that C2 Beacon can successfully establish connection with the C2 Server.""" + network: Network = basic_network + network, computer_a, c2_server, computer_b, c2_beacon = setup_c2(network) + # Asserting that the c2 beacon has established a c2 connection + assert c2_beacon.c2_connection_active is True + + # Asserting that the c2 server has established a c2 connection. + assert c2_server.c2_connection_active is True + assert c2_server.c2_remote_connection == IPv4Address("192.168.255.2") + + for i in range(50): + network.apply_timestep(i) + + assert c2_beacon.c2_connection_active is True + assert c2_server.c2_connection_active is True + + +def test_c2_suite_keep_alive_inactivity(basic_network): + """Tests that C2 Beacon disconnects from the C2 Server after inactivity.""" + network: Network = basic_network + network, computer_a, c2_server, computer_b, c2_beacon = setup_c2(network) + + c2_beacon.apply_timestep(0) + assert c2_beacon.keep_alive_inactivity == 1 + + # Keep Alive successfully sent and received upon the 2nd timestep. + c2_beacon.apply_timestep(1) + assert c2_beacon.keep_alive_inactivity == 0 + assert c2_beacon.c2_connection_active == True + + # Now we turn off the c2 server (Thus preventing a keep alive) + c2_server.close() + c2_beacon.apply_timestep(2) + + assert c2_beacon.keep_alive_inactivity == 1 + + c2_beacon.apply_timestep(3) + + # C2 Beacon resets it's connections back to default. + assert c2_beacon.keep_alive_inactivity == 0 + assert c2_beacon.c2_connection_active == False + assert c2_beacon.operating_state == ApplicationOperatingState.CLOSED + + +def test_c2_suite_configure_request(basic_network): + """Tests that the request system can be used to successfully setup a c2 suite.""" + network: Network = basic_network + network, computer_a, c2_server, computer_b, c2_beacon = setup_c2(network) + + # Testing Via Requests: + c2_server.run() + network.apply_timestep(0) + + c2_beacon_config = { + "c2_server_ip_address": "192.168.0.2", + "keep_alive_frequency": 5, + "masquerade_protocol": "TCP", + "masquerade_port": "HTTP", + } + + network.apply_request(["node", "node_b", "application", "C2Beacon", "configure", c2_beacon_config]) + network.apply_timestep(0) + network.apply_request(["node", "node_b", "application", "C2Beacon", "execute"]) + + assert c2_beacon.c2_connection_active is True + assert c2_server.c2_connection_active is True + assert c2_server.c2_remote_connection == IPv4Address("192.168.255.2") + + +def test_c2_suite_ransomware_commands(basic_network): + """Tests the Ransomware commands can be used to configure & launch ransomware via Requests.""" + # Setting up the network: + network: Network = basic_network + network, computer_a, c2_server, computer_b, c2_beacon = setup_c2(network) + + # Testing Via Requests: + computer_b.software_manager.install(software_class=RansomwareScript) + ransomware_config = {"server_ip_address": "192.168.0.2"} + network.apply_request(["node", "node_a", "application", "C2Server", "ransomware_configure", ransomware_config]) + + ransomware_script: RansomwareScript = computer_b.software_manager.software["RansomwareScript"] + + assert ransomware_script.server_ip_address == "192.168.0.2" + + network.apply_request(["node", "node_a", "application", "C2Server", "ransomware_launch"]) + + database_file = computer_a.software_manager.file_system.get_file("database", "database.db") + + assert database_file.health_status == FileSystemItemHealthStatus.CORRUPT + + +def test_c2_suite_acl_block(basic_network): + """Tests that C2 Beacon disconnects from the C2 Server after blocking ACL rules.""" + + network: Network = basic_network + network, computer_a, c2_server, computer_b, c2_beacon = setup_c2(network) + computer_b.software_manager.install(software_class=RansomwareScript) + ransomware_config = {"server_ip_address": "192.168.0.2"} + + router: Router = network.get_node_by_hostname("router") + + c2_beacon.apply_timestep(0) + assert c2_beacon.keep_alive_inactivity == 1 + + # Keep Alive successfully sent and received upon the 2nd timestep. + c2_beacon.apply_timestep(1) + assert c2_beacon.keep_alive_inactivity == 0 + assert c2_beacon.c2_connection_active == True + + # Now we add a HTTP blocking acl (Thus preventing a keep alive) + router.acl.add_rule(action=ACLAction.DENY, src_port=Port.HTTP, dst_port=Port.HTTP, position=0) + + c2_beacon.apply_timestep(2) + c2_beacon.apply_timestep(3) + + # C2 Beacon resets after unable to maintain contact. + + assert c2_beacon.keep_alive_inactivity == 0 + assert c2_beacon.c2_connection_active == False + assert c2_beacon.operating_state == ApplicationOperatingState.CLOSED + + +def test_c2_suite_terminal_command_file_creation(basic_network): + """Tests the C2 Terminal command can be used on local and remote.""" + network: Network = basic_network + network, computer_a, c2_server, computer_b, c2_beacon = setup_c2(network) + computer_c: Computer = network.get_node_by_hostname("node_c") + + # Asserting to demonstrate that the test files don't exist: + assert ( + computer_c.software_manager.file_system.access_file(folder_name="test_folder", file_name="test_file") == False + ) + + assert ( + computer_b.software_manager.file_system.access_file(folder_name="test_folder", file_name="test_file") == False + ) + + # Testing that we can create the test file and folders via the terminal command (Local C2 Terminal). + + # Local file/folder creation commands. + folder_create_command = { + "commands": ["file_system", "create", "folder", "test_folder"], + "username": "admin", + "password": "admin", + "ip_address": None, + } + c2_server.send_command(C2Command.TERMINAL, command_options=folder_create_command) + + file_create_command = { + "commands": ["file_system", "create", "file", "test_folder", "test_file", "True"], + "username": "admin", + "password": "admin", + "ip_address": None, + } + c2_server.send_command(C2Command.TERMINAL, command_options=file_create_command) + + assert computer_b.software_manager.file_system.access_file(folder_name="test_folder", file_name="test_file") == True + assert c2_beacon.terminal_session is not None + + # Testing that we can create the same test file/folders via on node 3 via a remote terminal. + file_remote_create_command = { + "commands": [ + ["file_system", "create", "folder", "test_folder"], + ["file_system", "create", "file", "test_folder", "test_file", "True"], + ], + "username": "admin", + "password": "admin", + "ip_address": "192.168.255.3", + } + + c2_server.send_command(C2Command.TERMINAL, command_options=file_remote_create_command) + + assert computer_c.software_manager.file_system.access_file(folder_name="test_folder", file_name="test_file") == True + assert c2_beacon.terminal_session is not None + + +def test_c2_suite_acl_bypass(basic_network): + """Tests that C2 Beacon can be reconfigured to connect C2 Server to bypass blocking ACL rules. + + 1. This Test first configures a router to block HTTP traffic and asserts the following: + 1. C2 Beacon and C2 Server are unable to maintain connection + 2. Traffic is confirmed to be blocked by the ACL rule. + + 2. Next the C2 Beacon is re-configured to use FTP which is permitted by the ACL and asserts the following; + 1. The C2 Beacon and C2 Server re-establish connection + 2. The ACL rule has not prevent any further traffic. + 3. A test file create command is sent & it's output confirmed + + 3. The ACL is then re-configured to block FTP traffic and asserts the following: + 1. C2 Beacon and C2 Server are unable to maintain connection + 2. Traffic is confirmed to be blocked by the ACL rule. + + 4. Next the C2 Beacon is re-configured to use HTTP which is permitted by the ACL and asserts the following; + 1. The C2 Beacon and C2 Server re-establish connection + 2. The ACL rule has not prevent any further traffic. + 3. A test file create command is sent & it's output confirmed + """ + + network: Network = basic_network + network, computer_a, c2_server, computer_b, c2_beacon = setup_c2(network) + router: Router = network.get_node_by_hostname("router") + + ################ Confirm Default Setup ######################### + + # Permitting all HTTP & FTP traffic + router.acl.add_rule(action=ACLAction.PERMIT, src_port=Port.HTTP, dst_port=Port.HTTP, position=0) + router.acl.add_rule(action=ACLAction.PERMIT, src_port=Port.FTP, dst_port=Port.FTP, position=1) + + c2_beacon.apply_timestep(0) + assert c2_beacon.keep_alive_inactivity == 1 + + # Keep Alive successfully sent and received upon the 2nd timestep. + c2_beacon.apply_timestep(1) + + assert c2_beacon.keep_alive_inactivity == 0 + assert c2_beacon.c2_connection_active == True + + ################ Denying HTTP Traffic ######################### + + # Now we add a HTTP blocking acl (Thus preventing a keep alive) + router.acl.add_rule(action=ACLAction.DENY, src_port=Port.HTTP, dst_port=Port.HTTP, position=0) + blocking_acl: AccessControlList = router.acl.acl[0] + + # Asserts to show the C2 Suite is unable to maintain connection: + + network.apply_timestep(2) + network.apply_timestep(3) + + c2_packets_blocked = blocking_acl.match_count + assert c2_packets_blocked != 0 + assert c2_beacon.c2_connection_active is False + + # Stepping one more time to confirm that the C2 server drops its connection + network.apply_timestep(4) + assert c2_server.c2_connection_active is False + + ################ Configuring C2 to use FTP ##################### + + # Reconfiguring the c2 beacon to now use FTP + c2_beacon.configure( + c2_server_ip_address="192.168.0.2", + keep_alive_frequency=2, + masquerade_port=Port.FTP, + masquerade_protocol=IPProtocol.TCP, + ) + + c2_beacon.establish() + + ################ Confirming connection via FTP ##################### + + # Confirming we've re-established connection + + assert c2_beacon.c2_connection_active is True + assert c2_server.c2_connection_active is True + + # Confirming that we can send commands: + + ftp_file_create_command = { + "commands": [ + ["file_system", "create", "folder", "test_folder"], + ["file_system", "create", "file", "test_folder", "ftp_test_file", "True"], + ], + "username": "admin", + "password": "admin", + "ip_address": None, + } + c2_server.send_command(C2Command.TERMINAL, command_options=ftp_file_create_command) + assert ( + computer_b.software_manager.file_system.access_file(folder_name="test_folder", file_name="ftp_test_file") + == True + ) + + # Confirming we can maintain connection + + # Stepping twenty timesteps in the network + i = 4 # We're already at the 4th timestep (starting at timestep 4) + + for i in range(20): + network.apply_timestep(i) + + # Confirming HTTP ACL ineffectiveness (C2 Bypass) + + # Asserting that the ACL hasn't caught more traffic and the c2 connection is still active + assert c2_packets_blocked == blocking_acl.match_count + assert c2_server.c2_connection_active is True + assert c2_beacon.c2_connection_active is True + + ################ Denying FTP Traffic & Enable HTTP ######################### + + # Blocking FTP and re-permitting HTTP: + router.acl.add_rule(action=ACLAction.PERMIT, src_port=Port.HTTP, dst_port=Port.HTTP, position=0) + router.acl.add_rule(action=ACLAction.DENY, src_port=Port.FTP, dst_port=Port.FTP, position=1) + blocking_acl: AccessControlList = router.acl.acl[1] + + # Asserts to show the C2 Suite is unable to maintain connection: + + network.apply_timestep(25) + network.apply_timestep(26) + + c2_packets_blocked = blocking_acl.match_count + assert c2_packets_blocked != 0 + assert c2_beacon.c2_connection_active is False + + # Stepping one more time to confirm that the C2 server drops its connection + network.apply_timestep(27) + assert c2_server.c2_connection_active is False + + ################ Configuring C2 to use HTTP ##################### + + # Reconfiguring the c2 beacon to now use HTTP Again + c2_beacon.configure( + c2_server_ip_address="192.168.0.2", + keep_alive_frequency=2, + masquerade_port=Port.HTTP, + masquerade_protocol=IPProtocol.TCP, + ) + + c2_beacon.establish() + + ################ Confirming connection via HTTP ##################### + + # Confirming we've re-established connection + + assert c2_beacon.c2_connection_active is True + assert c2_server.c2_connection_active is True + + # Confirming that we can send commands + + http_folder_create_command = { + "commands": ["file_system", "create", "folder", "test_folder"], + "username": "admin", + "password": "admin", + "ip_address": None, + } + c2_server.send_command(C2Command.TERMINAL, command_options=http_folder_create_command) + http_file_create_command = { + "commands": ["file_system", "create", "file", "test_folder", "http_test_file", "true"], + "username": "admin", + "password": "admin", + "ip_address": None, + } + c2_server.send_command(C2Command.TERMINAL, command_options=http_file_create_command) + assert ( + computer_b.software_manager.file_system.access_file(folder_name="test_folder", file_name="http_test_file") + == True + ) + + assert c2_beacon.c2_connection_active is True + assert c2_server.c2_connection_active is True + + # Confirming we can maintain connection + + # Stepping twenty timesteps in the network + i = 28 # We're already at the 28th timestep + + for i in range(20): + network.apply_timestep(i) + + # Confirming FTP ACL ineffectiveness (C2 Bypass) + + # Asserting that the ACL hasn't caught more traffic and the c2 connection is still active + assert c2_packets_blocked == blocking_acl.match_count + assert c2_server.c2_connection_active is True + assert c2_beacon.c2_connection_active is True + + +def test_c2_suite_yaml(): + """Tests that the C2 Suite is can be configured correctly via the Yaml.""" + with open(TEST_ASSETS_ROOT / "configs" / "basic_c2_setup.yaml") as f: + cfg = yaml.safe_load(f) + game = PrimaiteGame.from_config(cfg) + + yaml_network = game.simulation.network + computer_a: Computer = yaml_network.get_node_by_hostname("node_a") + c2_server: C2Server = computer_a.software_manager.software.get("C2Server") + + computer_b: Computer = yaml_network.get_node_by_hostname("node_b") + c2_beacon: C2Beacon = computer_b.software_manager.software.get("C2Beacon") + + assert c2_server.operating_state == ApplicationOperatingState.RUNNING + + assert c2_beacon.c2_remote_connection == IPv4Address("192.168.10.21") + + c2_beacon.establish() + + # Asserting that the c2 beacon has established a c2 connection + assert c2_beacon.c2_connection_active is True + # Asserting that the c2 server has established a c2 connection. + assert c2_server.c2_connection_active is True + assert c2_server.c2_remote_connection == IPv4Address("192.168.10.22") + + for i in range(50): + yaml_network.apply_timestep(i) + + assert c2_beacon.c2_connection_active is True + assert c2_server.c2_connection_active is True + + +def test_c2_suite_file_extraction(basic_network): + """Test that C2 Beacon can successfully exfiltrate a target file.""" + network: Network = basic_network + network, computer_a, c2_server, computer_b, c2_beacon = setup_c2(network) + # Asserting that the c2 beacon has established a c2 connection + assert c2_beacon.c2_connection_active is True + + # Asserting that the c2 server has established a c2 connection. + assert c2_server.c2_connection_active is True + assert c2_server.c2_remote_connection == IPv4Address("192.168.255.2") + + # Creating the target file on computer_c + computer_c: Computer = network.get_node_by_hostname("node_c") + computer_c.file_system.create_folder("important_files") + computer_c.file_system.create_file(file_name="secret.txt", folder_name="important_files") + assert computer_c.file_system.access_file(folder_name="important_files", file_name="secret.txt") + + # Installing an FTP Server on the same node as C2 Beacon via the terminal: + + # Attempting to exfiltrate secret.txt from computer c to the C2 Server + c2_server.send_command( + given_command=C2Command.DATA_EXFILTRATION, + command_options={ + "username": "admin", + "password": "admin", + "target_ip_address": "192.168.255.3", + "target_folder_name": "important_files", + "exfiltration_folder_name": "yoinked_files", + "target_file_name": "secret.txt", + }, + ) + + # Asserting that C2 Beacon has managed to get the file + assert c2_beacon._host_file_system.access_file(folder_name="yoinked_files", file_name="secret.txt") + + # Asserting that the C2 Beacon can relay it back to the C2 Server + assert c2_server._host_file_system.access_file(folder_name="yoinked_files", file_name="secret.txt") diff --git a/tests/integration_tests/system/red_applications/test_data_manipulation_bot_and_server.py b/tests/integration_tests/system/red_applications/test_data_manipulation_bot_and_server.py index a01cffbe..2e87578d 100644 --- a/tests/integration_tests/system/red_applications/test_data_manipulation_bot_and_server.py +++ b/tests/integration_tests/system/red_applications/test_data_manipulation_bot_and_server.py @@ -146,7 +146,6 @@ def test_data_manipulation_disrupts_green_agent_connection(data_manipulation_db_ assert db_server_service.db_file.health_status is FileSystemItemHealthStatus.GOOD assert green_db_connection.query("SELECT") - assert green_db_client.last_query_response.get("status_code") == 200 data_manipulation_bot.port_scan_p_of_success = 1 data_manipulation_bot.data_manipulation_p_of_success = 1 @@ -155,4 +154,3 @@ def test_data_manipulation_disrupts_green_agent_connection(data_manipulation_db_ assert db_server_service.db_file.health_status is FileSystemItemHealthStatus.COMPROMISED assert green_db_connection.query("SELECT") is False - assert green_db_client.last_query_response.get("status_code") != 200 diff --git a/tests/integration_tests/system/red_applications/test_ransomware_script.py b/tests/integration_tests/system/red_applications/test_ransomware_script.py index 2e3a0b1c..97abafb5 100644 --- a/tests/integration_tests/system/red_applications/test_ransomware_script.py +++ b/tests/integration_tests/system/red_applications/test_ransomware_script.py @@ -103,7 +103,7 @@ def test_ransomware_script_attack(ransomware_script_and_db_server): def test_ransomware_disrupts_green_agent_connection(ransomware_script_db_server_green_client): - """Test to see show that the database service still operate""" + """Test to show that the database service still operates after corruption""" network: Network = ransomware_script_db_server_green_client client_1: Computer = network.get_node_by_hostname("client_1") @@ -111,17 +111,18 @@ def test_ransomware_disrupts_green_agent_connection(ransomware_script_db_server_ client_2: Computer = network.get_node_by_hostname("client_2") green_db_client: DatabaseClient = client_2.software_manager.software.get("DatabaseClient") + green_db_client.connect() green_db_client_connection: DatabaseClientConnection = green_db_client.get_new_connection() server: Server = network.get_node_by_hostname("server_1") db_server_service: DatabaseService = server.software_manager.software.get("DatabaseService") assert db_server_service.db_file.health_status is FileSystemItemHealthStatus.GOOD - assert green_db_client_connection.query("SELECT") - assert green_db_client.last_query_response.get("status_code") == 200 + assert green_db_client.query("SELECT") is True ransomware_script_application.attack() + network.apply_timestep(0) + assert db_server_service.db_file.health_status is FileSystemItemHealthStatus.CORRUPT - assert green_db_client_connection.query("SELECT") is True - assert green_db_client.last_query_response.get("status_code") == 200 + assert green_db_client.query("SELECT") is True # Still operates but now the data field of response is empty diff --git a/tests/integration_tests/system/test_ftp_client_server.py b/tests/integration_tests/system/test_ftp_client_server.py index fcd13609..22c5d484 100644 --- a/tests/integration_tests/system/test_ftp_client_server.py +++ b/tests/integration_tests/system/test_ftp_client_server.py @@ -3,6 +3,7 @@ from typing import Tuple import pytest +from primaite.interface.request import RequestResponse from primaite.simulator.network.hardware.nodes.host.computer import Computer from primaite.simulator.network.hardware.nodes.host.server import Server from primaite.simulator.system.services.ftp.ftp_client import FTPClient @@ -105,3 +106,65 @@ def test_ftp_client_tries_to_connect_to_offline_server(ftp_client_and_ftp_server # client should have retrieved the file assert ftp_client.file_system.get_file(folder_name="downloads", file_name="test_file.txt") is None + + +def test_ftp_client_store_file_in_server_via_request_success(ftp_client_and_ftp_server): + """ + Test checks to see if the client can successfully store files in the backup server via the request manager. + """ + ftp_client, computer, ftp_server, server = ftp_client_and_ftp_server + + assert ftp_client.operating_state == ServiceOperatingState.RUNNING + assert ftp_server.operating_state == ServiceOperatingState.RUNNING + + # create file on ftp client + ftp_client.file_system.create_file(file_name="test_file.txt") + + ftp_opts = { + "src_folder_name": "root", + "src_file_name": "test_file.txt", + "dest_folder_name": "client_1_backup", + "dest_file_name": "test_file.txt", + "dest_ip_address": server.network_interfaces.get(next(iter(server.network_interfaces))).ip_address, + } + + ftp_client.apply_request(["send", ftp_opts]) + + assert ftp_server.file_system.get_file(folder_name="client_1_backup", file_name="test_file.txt") + + +def test_ftp_client_store_file_in_server_via_request_failure(ftp_client_and_ftp_server): + """ + Test checks to see if the client fails to store files in the backup server via the request manager. + """ + ftp_client, computer, ftp_server, server = ftp_client_and_ftp_server + + assert ftp_client.operating_state == ServiceOperatingState.RUNNING + assert ftp_server.operating_state == ServiceOperatingState.RUNNING + + # create file on ftp client + ftp_client.file_system.create_file(file_name="test_file.txt") + + # Purposefully misconfigured FTP Options + ftp_opts = { + "src_folder_name": "root", + "dest_ip_address": server.network_interfaces.get(next(iter(server.network_interfaces))).ip_address, + } + + request_response: RequestResponse = ftp_client.apply_request(["send", ftp_opts]) + + assert request_response.status == "failure" + + # Purposefully misconfigured FTP Options + + ftp_opts = { + "src_folder_name": "root", + "src_file_name": "not_a_real_file.txt", + "dest_folder_name": "client_1_backup", + "dest_file_name": "test_file.txt", + "dest_ip_address": server.network_interfaces.get(next(iter(server.network_interfaces))).ip_address, + } + + request_response: RequestResponse = ftp_client.apply_request(["send", ftp_opts]) + + assert request_response.status == "failure" diff --git a/tests/integration_tests/system/test_service_listening_on_ports.py b/tests/integration_tests/system/test_service_listening_on_ports.py new file mode 100644 index 00000000..fd502a70 --- /dev/null +++ b/tests/integration_tests/system/test_service_listening_on_ports.py @@ -0,0 +1,84 @@ +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +from typing import Any, Dict, List, Set + +import yaml +from pydantic import Field + +from primaite.game.game import PrimaiteGame +from primaite.simulator.network.hardware.nodes.host.computer import Computer +from primaite.simulator.network.transmission.network_layer import IPProtocol +from primaite.simulator.network.transmission.transport_layer import Port +from primaite.simulator.system.applications.database_client import DatabaseClient +from primaite.simulator.system.services.database.database_service import DatabaseService +from primaite.simulator.system.services.service import Service +from tests import TEST_ASSETS_ROOT + + +class _DatabaseListener(Service): + name: str = "DatabaseListener" + protocol: IPProtocol = IPProtocol.TCP + port: Port = Port.NONE + listen_on_ports: Set[Port] = {Port.POSTGRES_SERVER} + payloads_received: List[Any] = Field(default_factory=list) + + def receive(self, payload: Any, session_id: str, **kwargs) -> bool: + self.payloads_received.append(payload) + self.sys_log.info(f"{self.name}: received payload {payload}") + return True + + def describe_state(self) -> Dict: + return super().describe_state() + + +def test_http_listener(client_server): + computer, server = client_server + + server.software_manager.install(DatabaseService) + server_db = server.software_manager.software["DatabaseService"] + server_db.start() + + server.software_manager.install(_DatabaseListener) + server_db_listener: _DatabaseListener = server.software_manager.software["DatabaseListener"] + server_db_listener.start() + + computer.software_manager.install(DatabaseClient) + computer_db_client: DatabaseClient = computer.software_manager.software["DatabaseClient"] + + computer_db_client.run() + computer_db_client.server_ip_address = server.network_interface[1].ip_address + + assert len(server_db_listener.payloads_received) == 0 + computer.session_manager.receive_payload_from_software_manager( + payload="masquerade as Database traffic", + dst_ip_address=server.network_interface[1].ip_address, + dst_port=Port.POSTGRES_SERVER, + ip_protocol=IPProtocol.TCP, + ) + + assert len(server_db_listener.payloads_received) == 1 + + db_connection = computer_db_client.get_new_connection() + + assert db_connection + + assert len(server_db_listener.payloads_received) == 2 + + assert db_connection.query("SELECT") + + assert len(server_db_listener.payloads_received) == 3 + + +def test_set_listen_on_ports_from_config(): + config_path = TEST_ASSETS_ROOT / "configs" / "basic_node_with_software_listening_ports.yaml" + + with open(config_path, "r") as f: + config_dict = yaml.safe_load(f) + network = PrimaiteGame.from_config(cfg=config_dict).simulation.network + + client: Computer = network.get_node_by_hostname("client") + assert Port.SMB in client.software_manager.get_open_ports() + assert Port.IPP in client.software_manager.get_open_ports() + + web_browser = client.software_manager.software["WebBrowser"] + + assert not web_browser.listen_on_ports.difference({Port.SMB, Port.IPP}) diff --git a/tests/integration_tests/system/test_service_on_node.py b/tests/integration_tests/system/test_service_on_node.py index 15dbaf1d..cf9728ce 100644 --- a/tests/integration_tests/system/test_service_on_node.py +++ b/tests/integration_tests/system/test_service_on_node.py @@ -23,7 +23,7 @@ def populated_node( server.power_on() server.software_manager.install(service_class) - service = server.software_manager.software.get("TestService") + service = server.software_manager.software.get("DummyService") service.start() return server, service @@ -42,7 +42,7 @@ def test_service_on_offline_node(service_class): computer.power_on() computer.software_manager.install(service_class) - service: Service = computer.software_manager.software.get("TestService") + service: Service = computer.software_manager.software.get("DummyService") computer.power_off() diff --git a/tests/integration_tests/system/test_user_session_manager_logins.py b/tests/integration_tests/system/test_user_session_manager_logins.py new file mode 100644 index 00000000..4318530c --- /dev/null +++ b/tests/integration_tests/system/test_user_session_manager_logins.py @@ -0,0 +1,274 @@ +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +from typing import Tuple +from uuid import uuid4 + +import pytest + +from primaite.simulator.network.container import Network +from primaite.simulator.network.hardware.base import User +from primaite.simulator.network.hardware.nodes.host.computer import Computer +from primaite.simulator.network.hardware.nodes.host.server import Server + + +@pytest.fixture(scope="function") +def client_server_network() -> Tuple[Computer, Server, Network]: + network = Network() + + client = Computer( + hostname="client", + ip_address="192.168.1.2", + subnet_mask="255.255.255.0", + default_gateway="192.168.1.1", + start_up_duration=0, + ) + client.power_on() + + server = Server( + hostname="server", + ip_address="192.168.1.3", + subnet_mask="255.255.255.0", + default_gateway="192.168.1.1", + start_up_duration=0, + ) + server.power_on() + + network.connect(client.network_interface[1], server.network_interface[1]) + + return client, server, network + + +def test_local_login_success(client_server_network): + client, server, network = client_server_network + + assert not client.user_session_manager.local_user_logged_in + + client.user_session_manager.local_login(username="admin", password="admin") + + assert client.user_session_manager.local_user_logged_in + + +def test_login_count_increases(client_server_network): + client, server, network = client_server_network + + admin_user: User = client.user_manager.users["admin"] + + assert admin_user.num_of_logins == 0 + + client.user_session_manager.local_login(username="admin", password="admin") + + assert admin_user.num_of_logins == 1 + + client.user_session_manager.local_login(username="admin", password="admin") + + # shouldn't change as user is already logged in + assert admin_user.num_of_logins == 1 + + client.user_session_manager.local_logout() + + client.user_session_manager.local_login(username="admin", password="admin") + + assert admin_user.num_of_logins == 2 + + +def test_local_login_failure(client_server_network): + client, server, network = client_server_network + + assert not client.user_session_manager.local_user_logged_in + + client.user_session_manager.local_login(username="jane.doe", password="12345") + + assert not client.user_session_manager.local_user_logged_in + + +def test_new_user_local_login_success(client_server_network): + client, server, network = client_server_network + + assert not client.user_session_manager.local_user_logged_in + + client.user_manager.add_user(username="jane.doe", password="12345") + + client.user_session_manager.local_login(username="jane.doe", password="12345") + + assert client.user_session_manager.local_user_logged_in + + +def test_new_local_login_clears_previous_login(client_server_network): + client, server, network = client_server_network + + assert not client.user_session_manager.local_user_logged_in + + current_session_id = client.user_session_manager.local_login(username="admin", password="admin") + + assert client.user_session_manager.local_user_logged_in + + assert client.user_session_manager.local_session.user.username == "admin" + + client.user_manager.add_user(username="jane.doe", password="12345") + + new_session_id = client.user_session_manager.local_login(username="jane.doe", password="12345") + + assert client.user_session_manager.local_user_logged_in + + assert client.user_session_manager.local_session.user.username == "jane.doe" + + assert new_session_id != current_session_id + + +def test_new_local_login_attempt_same_uses_persists(client_server_network): + client, server, network = client_server_network + + assert not client.user_session_manager.local_user_logged_in + + current_session_id = client.user_session_manager.local_login(username="admin", password="admin") + + assert client.user_session_manager.local_user_logged_in + + assert client.user_session_manager.local_session.user.username == "admin" + + new_session_id = client.user_session_manager.local_login(username="admin", password="admin") + + assert client.user_session_manager.local_user_logged_in + + assert client.user_session_manager.local_session.user.username == "admin" + + assert new_session_id == current_session_id + + +def test_remote_login_success(client_server_network): + # partial test for now until we get the terminal application in so that amn actual remote connection can be made + client, server, network = client_server_network + + assert not server.user_session_manager.remote_sessions + + remote_session_id = server.user_session_manager.remote_login( + username="admin", password="admin", remote_ip_address="192.168.1.10" + ) + + assert server.user_session_manager.validate_remote_session_uuid(remote_session_id) + + server.user_session_manager.remote_logout(remote_session_id) + + assert not server.user_session_manager.validate_remote_session_uuid(remote_session_id) + + +def test_remote_login_failure(client_server_network): + # partial test for now until we get the terminal application in so that amn actual remote connection can be made + client, server, network = client_server_network + + assert not server.user_session_manager.remote_sessions + + remote_session_id = server.user_session_manager.remote_login( + username="jane.doe", password="12345", remote_ip_address="192.168.1.10" + ) + + assert not server.user_session_manager.validate_remote_session_uuid(remote_session_id) + + +def test_new_user_remote_login_success(client_server_network): + client, server, network = client_server_network + + server.user_manager.add_user(username="jane.doe", password="12345") + + remote_session_id = server.user_session_manager.remote_login( + username="jane.doe", password="12345", remote_ip_address="192.168.1.10" + ) + + assert server.user_session_manager.validate_remote_session_uuid(remote_session_id) + + server.user_session_manager.remote_logout(remote_session_id) + + assert not server.user_session_manager.validate_remote_session_uuid(remote_session_id) + + +def test_max_remote_sessions_same_user(client_server_network): + client, server, network = client_server_network + + remote_session_ids = [ + server.user_session_manager.remote_login(username="admin", password="admin", remote_ip_address="192.168.1.10") + for _ in range(server.user_session_manager.max_remote_sessions) + ] + + assert all([server.user_session_manager.validate_remote_session_uuid(id) for id in remote_session_ids]) + + +def test_max_remote_sessions_different_users(client_server_network): + client, server, network = client_server_network + + remote_session_ids = [] + + for i in range(server.user_session_manager.max_remote_sessions): + username = str(uuid4()) + password = "12345" + server.user_manager.add_user(username=username, password=password) + + remote_session_ids.append( + server.user_session_manager.remote_login( + username=username, password=password, remote_ip_address="192.168.1.10" + ) + ) + + assert all([server.user_session_manager.validate_remote_session_uuid(id) for id in remote_session_ids]) + + +def test_max_remote_sessions_limit_reached(client_server_network): + client, server, network = client_server_network + + remote_session_ids = [ + server.user_session_manager.remote_login(username="admin", password="admin", remote_ip_address="192.168.1.10") + for _ in range(server.user_session_manager.max_remote_sessions) + ] + + assert all([server.user_session_manager.validate_remote_session_uuid(id) for id in remote_session_ids]) + + assert len(server.user_session_manager.remote_sessions) == server.user_session_manager.max_remote_sessions + + fourth_attempt_session_id = server.user_session_manager.remote_login( + username="admin", password="admin", remote_ip_address="192.168.1.10" + ) + + assert not server.user_session_manager.validate_remote_session_uuid(fourth_attempt_session_id) + + assert all([server.user_session_manager.validate_remote_session_uuid(id) for id in remote_session_ids]) + + +def test_single_remote_logout_others_persist(client_server_network): + client, server, network = client_server_network + + server.user_manager.add_user(username="jane.doe", password="12345") + server.user_manager.add_user(username="john.doe", password="12345") + + admin_session_id = server.user_session_manager.remote_login( + username="admin", password="admin", remote_ip_address="192.168.1.10" + ) + + jane_session_id = server.user_session_manager.remote_login( + username="jane.doe", password="12345", remote_ip_address="192.168.1.10" + ) + + john_session_id = server.user_session_manager.remote_login( + username="john.doe", password="12345", remote_ip_address="192.168.1.10" + ) + + server.user_session_manager.remote_logout(admin_session_id) + + assert not server.user_session_manager.validate_remote_session_uuid(admin_session_id) + + assert server.user_session_manager.validate_remote_session_uuid(jane_session_id) + + assert server.user_session_manager.validate_remote_session_uuid(john_session_id) + + server.user_session_manager.remote_logout(jane_session_id) + + assert not server.user_session_manager.validate_remote_session_uuid(admin_session_id) + + assert not server.user_session_manager.validate_remote_session_uuid(jane_session_id) + + assert server.user_session_manager.validate_remote_session_uuid(john_session_id) + + server.user_session_manager.remote_logout(john_session_id) + + assert not server.user_session_manager.validate_remote_session_uuid(admin_session_id) + + assert not server.user_session_manager.validate_remote_session_uuid(jane_session_id) + + assert not server.user_session_manager.validate_remote_session_uuid(john_session_id) diff --git a/tests/integration_tests/test_simulation/test_request_response.py b/tests/integration_tests/test_simulation/test_request_response.py index a9f0b58d..95634cf1 100644 --- a/tests/integration_tests/test_simulation/test_request_response.py +++ b/tests/integration_tests/test_simulation/test_request_response.py @@ -13,7 +13,7 @@ from primaite.simulator.network.hardware.node_operating_state import NodeOperati from primaite.simulator.network.hardware.nodes.host.host_node import HostNode from primaite.simulator.network.hardware.nodes.network.router import ACLAction, Router from primaite.simulator.network.transmission.transport_layer import Port -from tests.conftest import DummyApplication, TestService +from tests.conftest import DummyApplication, DummyService def test_successful_node_file_system_creation_request(example_network): @@ -61,7 +61,7 @@ def test_successful_application_requests(example_network): def test_successful_service_requests(example_network): net = example_network server_1 = net.get_node_by_hostname("server_1") - server_1.software_manager.install(TestService) + server_1.software_manager.install(DummyService) # Careful: the order here is important, for example we cannot run "stop" unless we run "start" first for verb in [ @@ -77,7 +77,7 @@ def test_successful_service_requests(example_network): "scan", "fix", ]: - resp_1 = net.apply_request(["node", "server_1", "service", "TestService", verb]) + resp_1 = net.apply_request(["node", "server_1", "service", "DummyService", verb]) assert resp_1 == RequestResponse(status="success", data={}) server_1.apply_timestep(timestep=1) server_1.apply_timestep(timestep=1) diff --git a/tests/unit_tests/_primaite/_game/_agent/test_observations.py b/tests/unit_tests/_primaite/_game/_agent/test_observations.py new file mode 100644 index 00000000..7f590685 --- /dev/null +++ b/tests/unit_tests/_primaite/_game/_agent/test_observations.py @@ -0,0 +1,132 @@ +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +from typing import List + +import pytest +import yaml + +from primaite.game.agent.observations import ObservationManager +from primaite.game.agent.observations.file_system_observations import FileObservation, FolderObservation +from primaite.game.agent.observations.host_observations import HostObservation + + +class TestFileSystemRequiresScan: + @pytest.mark.parametrize( + ("yaml_option_string", "expected_val"), + ( + ("file_system_requires_scan: true", True), + ("file_system_requires_scan: false", False), + (" ", True), + ), + ) + def test_obs_config(self, yaml_option_string, expected_val): + """Check that the default behaviour is to set FileSystemRequiresScan to True.""" + obs_cfg_yaml = f""" + type: CUSTOM + options: + components: + - type: NODES + label: NODES + options: + hosts: + - hostname: domain_controller + - hostname: web_server + services: + - service_name: WebServer + - hostname: database_server + folders: + - folder_name: database + files: + - file_name: database.db + - hostname: backup_server + - hostname: security_suite + - hostname: client_1 + - hostname: client_2 + num_services: 1 + num_applications: 0 + num_folders: 1 + num_files: 1 + num_nics: 2 + include_num_access: false + {yaml_option_string} + include_nmne: true + monitored_traffic: + icmp: + - NONE + tcp: + - DNS + routers: + - hostname: router_1 + num_ports: 0 + ip_list: + - 192.168.1.10 + - 192.168.1.12 + - 192.168.1.14 + - 192.168.1.16 + - 192.168.1.110 + - 192.168.10.21 + - 192.168.10.22 + - 192.168.10.110 + wildcard_list: + - 0.0.0.1 + port_list: + - 80 + - 5432 + protocol_list: + - ICMP + - TCP + - UDP + num_rules: 10 + + - type: LINKS + label: LINKS + options: + link_references: + - router_1:eth-1<->switch_1:eth-8 + - router_1:eth-2<->switch_2:eth-8 + - switch_1:eth-1<->domain_controller:eth-1 + - switch_1:eth-2<->web_server:eth-1 + - switch_1:eth-3<->database_server:eth-1 + - switch_1:eth-4<->backup_server:eth-1 + - switch_1:eth-7<->security_suite:eth-1 + - switch_2:eth-1<->client_1:eth-1 + - switch_2:eth-2<->client_2:eth-1 + - switch_2:eth-7<->security_suite:eth-2 + - type: "NONE" + label: ICS + options: {{}} + + """ + + cfg = yaml.safe_load(obs_cfg_yaml) + manager = ObservationManager.from_config(cfg) + + hosts: List[HostObservation] = manager.obs.components["NODES"].hosts + for i, host in enumerate(hosts): + folders: List[FolderObservation] = host.folders + for j, folder in enumerate(folders): + assert folder.file_system_requires_scan == expected_val # Make sure folders require scan by default + files: List[FileObservation] = folder.files + for k, file in enumerate(files): + assert file.file_system_requires_scan == expected_val + + def test_file_require_scan(self): + file_state = {"health_status": 3, "visible_status": 1} + + obs_requiring_scan = FileObservation([], include_num_access=False, file_system_requires_scan=True) + assert obs_requiring_scan.observe(file_state)["health_status"] == 1 + + obs_not_requiring_scan = FileObservation([], include_num_access=False, file_system_requires_scan=False) + assert obs_not_requiring_scan.observe(file_state)["health_status"] == 3 + + def test_folder_require_scan(self): + folder_state = {"health_status": 3, "visible_status": 1} + + obs_requiring_scan = FolderObservation( + [], files=[], num_files=0, include_num_access=False, file_system_requires_scan=True + ) + assert obs_requiring_scan.observe(folder_state)["health_status"] == 1 + + obs_not_requiring_scan = FolderObservation( + [], files=[], num_files=0, include_num_access=False, file_system_requires_scan=False + ) + assert obs_not_requiring_scan.observe(folder_state)["health_status"] == 3 diff --git a/tests/unit_tests/_primaite/_game/_agent/test_probabilistic_agent.py b/tests/unit_tests/_primaite/_game/_agent/test_probabilistic_agent.py index f3b3c6eb..ec18f1fb 100644 --- a/tests/unit_tests/_primaite/_game/_agent/test_probabilistic_agent.py +++ b/tests/unit_tests/_primaite/_game/_agent/test_probabilistic_agent.py @@ -62,7 +62,6 @@ def test_probabilistic_agent(): reward_function=reward_function, settings={ "action_probabilities": {0: P_DO_NOTHING, 1: P_NODE_APPLICATION_EXECUTE, 2: P_NODE_FILE_DELETE}, - "random_seed": 120, }, ) diff --git a/tests/unit_tests/_primaite/_game/_agent/test_sticky_rewards.py b/tests/unit_tests/_primaite/_game/_agent/test_sticky_rewards.py new file mode 100644 index 00000000..58f0fcc1 --- /dev/null +++ b/tests/unit_tests/_primaite/_game/_agent/test_sticky_rewards.py @@ -0,0 +1,299 @@ +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK + +from primaite.game.agent.interface import AgentHistoryItem +from primaite.game.agent.rewards import ( + GreenAdminDatabaseUnreachablePenalty, + WebpageUnavailablePenalty, + WebServer404Penalty, +) +from primaite.interface.request import RequestResponse + + +class TestWebServer404PenaltySticky: + def test_non_sticky(self): + reward = WebServer404Penalty("computer", "WebService", sticky=False) + + # no response codes yet, reward is 0 + codes = [] + state = { + "network": {"nodes": {"computer": {"services": {"WebService": {"response_codes_this_timestep": codes}}}}} + } + last_action_response = None + assert reward.calculate(state, last_action_response) == 0 + + # update codes (by reference), 200 response code is now present + codes.append(200) + assert reward.calculate(state, last_action_response) == 1.0 + + # THE IMPORTANT BIT + # update codes (by reference), to make it empty again, reward goes back to 0 + codes.pop() + assert reward.calculate(state, last_action_response) == 0.0 + + # update codes (by reference), 404 response code is now present, reward = -1.0 + codes.append(404) + assert reward.calculate(state, last_action_response) == -1.0 + + # don't update codes, it still has just a 404, check the reward is -1.0 again + assert reward.calculate(state, last_action_response) == -1.0 + + def test_sticky(self): + reward = WebServer404Penalty("computer", "WebService", sticky=True) + + # no response codes yet, reward is 0 + codes = [] + state = { + "network": {"nodes": {"computer": {"services": {"WebService": {"response_codes_this_timestep": codes}}}}} + } + last_action_response = None + assert reward.calculate(state, last_action_response) == 0 + + # update codes (by reference), 200 response code is now present + codes.append(200) + assert reward.calculate(state, last_action_response) == 1.0 + + # THE IMPORTANT BIT + # update codes (by reference), to make it empty again, reward remains at 1.0 because it's sticky + codes.pop() + assert reward.calculate(state, last_action_response) == 1.0 + + # update codes (by reference), 404 response code is now present, reward = -1.0 + codes.append(404) + assert reward.calculate(state, last_action_response) == -1.0 + + # don't update codes, it still has just a 404, check the reward is -1.0 again + assert reward.calculate(state, last_action_response) == -1.0 + + +class TestWebpageUnavailabilitySticky: + def test_non_sticky(self): + reward = WebpageUnavailablePenalty("computer", sticky=False) + + # no response codes yet, reward is 0 + action, params, request = "DO_NOTHING", {}, ["DONOTHING"] + response = RequestResponse(status="success", data={}) + browser_history = [] + state = {"network": {"nodes": {"computer": {"applications": {"WebBrowser": {"history": browser_history}}}}}} + last_action_response = AgentHistoryItem( + timestep=0, action=action, parameters=params, request=request, response=response + ) + assert reward.calculate(state, last_action_response) == 0 + + # agent did a successful fetch + action = "NODE_APPLICATION_EXECUTE" + params = {"node_id": 0, "application_id": 0} + request = ["network", "node", "computer", "application", "WebBrowser", "execute"] + response = RequestResponse(status="success", data={}) + browser_history.append({"outcome": 200}) + state = {"network": {"nodes": {"computer": {"applications": {"WebBrowser": {"history": browser_history}}}}}} + last_action_response = AgentHistoryItem( + timestep=0, action=action, parameters=params, request=request, response=response + ) + assert reward.calculate(state, last_action_response) == 1.0 + + # THE IMPORTANT BIT + # agent did nothing, because reward is not sticky, it goes back to 0 + action, params, request = "DO_NOTHING", {}, ["DONOTHING"] + response = RequestResponse(status="success", data={}) + browser_history = [] + state = {"network": {"nodes": {"computer": {"applications": {"WebBrowser": {"history": browser_history}}}}}} + last_action_response = AgentHistoryItem( + timestep=0, action=action, parameters=params, request=request, response=response + ) + assert reward.calculate(state, last_action_response) == 0.0 + + # agent fails to fetch, get a -1.0 reward + action = "NODE_APPLICATION_EXECUTE" + params = {"node_id": 0, "application_id": 0} + request = ["network", "node", "computer", "application", "WebBrowser", "execute"] + response = RequestResponse(status="failure", data={}) + browser_history.append({"outcome": 404}) + state = {"network": {"nodes": {"computer": {"applications": {"WebBrowser": {"history": browser_history}}}}}} + last_action_response = AgentHistoryItem( + timestep=0, action=action, parameters=params, request=request, response=response + ) + assert reward.calculate(state, last_action_response) == -1.0 + + # agent fails again to fetch, get a -1.0 reward again + action = "NODE_APPLICATION_EXECUTE" + params = {"node_id": 0, "application_id": 0} + request = ["network", "node", "computer", "application", "WebBrowser", "execute"] + response = RequestResponse(status="failure", data={}) + browser_history.append({"outcome": 404}) + state = {"network": {"nodes": {"computer": {"applications": {"WebBrowser": {"history": browser_history}}}}}} + last_action_response = AgentHistoryItem( + timestep=0, action=action, parameters=params, request=request, response=response + ) + assert reward.calculate(state, last_action_response) == -1.0 + + def test_sticky(self): + reward = WebpageUnavailablePenalty("computer", sticky=True) + + # no response codes yet, reward is 0 + action, params, request = "DO_NOTHING", {}, ["DONOTHING"] + response = RequestResponse(status="success", data={}) + browser_history = [] + state = {"network": {"nodes": {"computer": {"applications": {"WebBrowser": {"history": browser_history}}}}}} + last_action_response = AgentHistoryItem( + timestep=0, action=action, parameters=params, request=request, response=response + ) + assert reward.calculate(state, last_action_response) == 0 + + # agent did a successful fetch + action = "NODE_APPLICATION_EXECUTE" + params = {"node_id": 0, "application_id": 0} + request = ["network", "node", "computer", "application", "WebBrowser", "execute"] + response = RequestResponse(status="success", data={}) + browser_history.append({"outcome": 200}) + state = {"network": {"nodes": {"computer": {"applications": {"WebBrowser": {"history": browser_history}}}}}} + last_action_response = AgentHistoryItem( + timestep=0, action=action, parameters=params, request=request, response=response + ) + assert reward.calculate(state, last_action_response) == 1.0 + + # THE IMPORTANT BIT + # agent did nothing, because reward is sticky, it stays at 1.0 + action, params, request = "DO_NOTHING", {}, ["DONOTHING"] + response = RequestResponse(status="success", data={}) + state = {"network": {"nodes": {"computer": {"applications": {"WebBrowser": {"history": browser_history}}}}}} + last_action_response = AgentHistoryItem( + timestep=0, action=action, parameters=params, request=request, response=response + ) + assert reward.calculate(state, last_action_response) == 1.0 + + # agent fails to fetch, get a -1.0 reward + action = "NODE_APPLICATION_EXECUTE" + params = {"node_id": 0, "application_id": 0} + request = ["network", "node", "computer", "application", "WebBrowser", "execute"] + response = RequestResponse(status="failure", data={}) + browser_history.append({"outcome": 404}) + state = {"network": {"nodes": {"computer": {"applications": {"WebBrowser": {"history": browser_history}}}}}} + last_action_response = AgentHistoryItem( + timestep=0, action=action, parameters=params, request=request, response=response + ) + assert reward.calculate(state, last_action_response) == -1.0 + + # agent fails again to fetch, get a -1.0 reward again + action = "NODE_APPLICATION_EXECUTE" + params = {"node_id": 0, "application_id": 0} + request = ["network", "node", "computer", "application", "WebBrowser", "execute"] + response = RequestResponse(status="failure", data={}) + browser_history.append({"outcome": 404}) + state = {"network": {"nodes": {"computer": {"applications": {"WebBrowser": {"history": browser_history}}}}}} + last_action_response = AgentHistoryItem( + timestep=0, action=action, parameters=params, request=request, response=response + ) + assert reward.calculate(state, last_action_response) == -1.0 + + +class TestGreenAdminDatabaseUnreachableSticky: + def test_non_sticky(self): + reward = GreenAdminDatabaseUnreachablePenalty("computer", sticky=False) + + # no response codes yet, reward is 0 + action, params, request = "DO_NOTHING", {}, ["DONOTHING"] + response = RequestResponse(status="success", data={}) + state = {"network": {"nodes": {"computer": {"applications": {"DatabaseClient": {}}}}}} + last_action_response = AgentHistoryItem( + timestep=0, action=action, parameters=params, request=request, response=response + ) + assert reward.calculate(state, last_action_response) == 0 + + # agent did a successful fetch + action = "NODE_APPLICATION_EXECUTE" + params = {"node_id": 0, "application_id": 0} + request = ["network", "node", "computer", "application", "DatabaseClient", "execute"] + response = RequestResponse(status="success", data={}) + state = {"network": {"nodes": {"computer": {"applications": {"DatabaseClient": {}}}}}} + last_action_response = AgentHistoryItem( + timestep=0, action=action, parameters=params, request=request, response=response + ) + assert reward.calculate(state, last_action_response) == 1.0 + + # THE IMPORTANT BIT + # agent did nothing, because reward is not sticky, it goes back to 0 + action, params, request = "DO_NOTHING", {}, ["DONOTHING"] + response = RequestResponse(status="success", data={}) + browser_history = [] + state = {"network": {"nodes": {"computer": {"applications": {"DatabaseClient": {}}}}}} + last_action_response = AgentHistoryItem( + timestep=0, action=action, parameters=params, request=request, response=response + ) + assert reward.calculate(state, last_action_response) == 0.0 + + # agent fails to fetch, get a -1.0 reward + action = "NODE_APPLICATION_EXECUTE" + params = {"node_id": 0, "application_id": 0} + request = ["network", "node", "computer", "application", "DatabaseClient", "execute"] + response = RequestResponse(status="failure", data={}) + state = {"network": {"nodes": {"computer": {"applications": {"DatabaseClient": {}}}}}} + last_action_response = AgentHistoryItem( + timestep=0, action=action, parameters=params, request=request, response=response + ) + assert reward.calculate(state, last_action_response) == -1.0 + + # agent fails again to fetch, get a -1.0 reward again + action = "NODE_APPLICATION_EXECUTE" + params = {"node_id": 0, "application_id": 0} + request = ["network", "node", "computer", "application", "DatabaseClient", "execute"] + response = RequestResponse(status="failure", data={}) + state = {"network": {"nodes": {"computer": {"applications": {"DatabaseClient": {}}}}}} + last_action_response = AgentHistoryItem( + timestep=0, action=action, parameters=params, request=request, response=response + ) + assert reward.calculate(state, last_action_response) == -1.0 + + def test_sticky(self): + reward = GreenAdminDatabaseUnreachablePenalty("computer", sticky=True) + + # no response codes yet, reward is 0 + action, params, request = "DO_NOTHING", {}, ["DONOTHING"] + response = RequestResponse(status="success", data={}) + state = {"network": {"nodes": {"computer": {"applications": {"DatabaseClient": {}}}}}} + last_action_response = AgentHistoryItem( + timestep=0, action=action, parameters=params, request=request, response=response + ) + assert reward.calculate(state, last_action_response) == 0 + + # agent did a successful fetch + action = "NODE_APPLICATION_EXECUTE" + params = {"node_id": 0, "application_id": 0} + request = ["network", "node", "computer", "application", "DatabaseClient", "execute"] + response = RequestResponse(status="success", data={}) + state = {"network": {"nodes": {"computer": {"applications": {"DatabaseClient": {}}}}}} + last_action_response = AgentHistoryItem( + timestep=0, action=action, parameters=params, request=request, response=response + ) + assert reward.calculate(state, last_action_response) == 1.0 + + # THE IMPORTANT BIT + # agent did nothing, because reward is not sticky, it goes back to 0 + action, params, request = "DO_NOTHING", {}, ["DONOTHING"] + response = RequestResponse(status="success", data={}) + state = {"network": {"nodes": {"computer": {"applications": {"DatabaseClient": {}}}}}} + last_action_response = AgentHistoryItem( + timestep=0, action=action, parameters=params, request=request, response=response + ) + assert reward.calculate(state, last_action_response) == 1.0 + + # agent fails to fetch, get a -1.0 reward + action = "NODE_APPLICATION_EXECUTE" + params = {"node_id": 0, "application_id": 0} + request = ["network", "node", "computer", "application", "DatabaseClient", "execute"] + response = RequestResponse(status="failure", data={}) + state = {"network": {"nodes": {"computer": {"applications": {"DatabaseClient": {}}}}}} + last_action_response = AgentHistoryItem( + timestep=0, action=action, parameters=params, request=request, response=response + ) + assert reward.calculate(state, last_action_response) == -1.0 + + # agent fails again to fetch, get a -1.0 reward again + action = "NODE_APPLICATION_EXECUTE" + params = {"node_id": 0, "application_id": 0} + request = ["network", "node", "computer", "application", "DatabaseClient", "execute"] + response = RequestResponse(status="failure", data={}) + state = {"network": {"nodes": {"computer": {"applications": {"DatabaseClient": {}}}}}} + last_action_response = AgentHistoryItem( + timestep=0, action=action, parameters=params, request=request, response=response + ) + assert reward.calculate(state, last_action_response) == -1.0 diff --git a/tests/unit_tests/_primaite/_simulator/_network/_hardware/test_node_actions.py b/tests/unit_tests/_primaite/_simulator/_network/_hardware/test_node_actions.py index 9b37ac80..44c5c781 100644 --- a/tests/unit_tests/_primaite/_simulator/_network/_hardware/test_node_actions.py +++ b/tests/unit_tests/_primaite/_simulator/_network/_hardware/test_node_actions.py @@ -7,6 +7,7 @@ from primaite.simulator.file_system.folder import Folder from primaite.simulator.network.hardware.base import Node, NodeOperatingState from primaite.simulator.network.hardware.nodes.host.computer import Computer from primaite.simulator.system.software import SoftwareHealthState +from tests.conftest import DummyApplication, DummyService @pytest.fixture @@ -47,7 +48,7 @@ def test_node_shutdown(node): assert node.operating_state == NodeOperatingState.OFF -def test_node_os_scan(node, service, application): +def test_node_os_scan(node): """Test OS Scanning.""" node.operating_state = NodeOperatingState.ON @@ -55,13 +56,15 @@ def test_node_os_scan(node, service, application): # TODO implement processes # add services to node + node.software_manager.install(DummyService) + service = node.software_manager.software.get("DummyService") service.set_health_state(SoftwareHealthState.COMPROMISED) - node.install_service(service=service) assert service.health_state_visible == SoftwareHealthState.UNUSED # add application to node + node.software_manager.install(DummyApplication) + application = node.software_manager.software.get("DummyApplication") application.set_health_state(SoftwareHealthState.COMPROMISED) - node.install_application(application=application) assert application.health_state_visible == SoftwareHealthState.UNUSED # add folder and file to node @@ -91,7 +94,7 @@ def test_node_os_scan(node, service, application): assert file2.visible_health_status == FileSystemItemHealthStatus.CORRUPT -def test_node_red_scan(node, service, application): +def test_node_red_scan(node): """Test revealing to red""" node.operating_state = NodeOperatingState.ON @@ -99,12 +102,14 @@ def test_node_red_scan(node, service, application): # TODO implement processes # add services to node - node.install_service(service=service) + node.software_manager.install(DummyService) + service = node.software_manager.software.get("DummyService") assert service.revealed_to_red is False # add application to node + node.software_manager.install(DummyApplication) + application = node.software_manager.software.get("DummyApplication") application.set_health_state(SoftwareHealthState.COMPROMISED) - node.install_application(application=application) assert application.revealed_to_red is False # add folder and file to node diff --git a/tests/unit_tests/_primaite/_simulator/_system/_applications/_red_applications/test_c2_suite.py b/tests/unit_tests/_primaite/_simulator/_system/_applications/_red_applications/test_c2_suite.py new file mode 100644 index 00000000..885a3cb6 --- /dev/null +++ b/tests/unit_tests/_primaite/_simulator/_system/_applications/_red_applications/test_c2_suite.py @@ -0,0 +1,248 @@ +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +import pytest + +from primaite.simulator.network.container import Network +from primaite.simulator.network.hardware.nodes.host.computer import Computer +from primaite.simulator.network.hardware.nodes.host.server import Server +from primaite.simulator.network.transmission.network_layer import IPProtocol +from primaite.simulator.network.transmission.transport_layer import Port +from primaite.simulator.system.applications.application import ApplicationOperatingState +from primaite.simulator.system.applications.red_applications.c2.c2_beacon import C2Beacon +from primaite.simulator.system.applications.red_applications.c2.c2_server import C2Command, C2Server + + +@pytest.fixture(scope="function") +def basic_c2_network() -> Network: + network = Network() + + # Creating two generic nodes for the C2 Server and the C2 Beacon. + + computer_a = Computer( + hostname="computer_a", + ip_address="192.168.0.1", + subnet_mask="255.255.255.252", + start_up_duration=0, + ) + computer_a.power_on() + computer_a.software_manager.install(software_class=C2Server) + + computer_b = Computer( + hostname="computer_b", ip_address="192.168.0.2", subnet_mask="255.255.255.252", start_up_duration=0 + ) + + computer_b.power_on() + computer_b.software_manager.install(software_class=C2Beacon) + + network.connect(endpoint_a=computer_a.network_interface[1], endpoint_b=computer_b.network_interface[1]) + return network + + +def setup_c2(given_network: Network): + """Installs the C2 Beacon & Server, configures and then returns.""" + network: Network = given_network + + computer_a: Computer = network.get_node_by_hostname("computer_a") + computer_b: Computer = network.get_node_by_hostname("computer_b") + + c2_beacon: C2Beacon = computer_b.software_manager.software.get("C2Beacon") + c2_server: C2Server = computer_a.software_manager.software.get("C2Server") + + c2_beacon.configure(c2_server_ip_address="192.168.0.1", keep_alive_frequency=2) + c2_server.run() + c2_beacon.establish() + + return network, computer_a, c2_server, computer_b, c2_beacon + + +def test_c2_handle_server_disconnect(basic_c2_network): + """Tests that the C2 suite will be able handle the c2 server application closing.""" + network: Network = basic_c2_network + network, computer_a, c2_server, computer_b, c2_beacon = setup_c2(network) + + assert c2_beacon.c2_connection_active is True + + ##### C2 Server disconnecting. + + # Closing the C2 Server + c2_server.close() + + # Applying 10 timesteps to trigger C2 beacon keep alive + + for i in range(10): + network.apply_timestep(i) + + assert c2_beacon.c2_connection_active is False + assert c2_beacon.operating_state is ApplicationOperatingState.CLOSED + + # C2 Beacon disconnected. + + network: Network = basic_c2_network + + network, computer_a, c2_server, computer_b, c2_beacon = setup_c2(network) + + +def test_c2_handle_beacon_disconnect(basic_c2_network): + """Tests that the C2 suite will be able handle the c2 beacon application closing.""" + network: Network = basic_c2_network + network, computer_a, c2_server, computer_b, c2_beacon = setup_c2(network) + + assert c2_server.c2_connection_active is True + + # Closing the C2 beacon + + c2_beacon.close() + + assert c2_beacon.operating_state is ApplicationOperatingState.CLOSED + + # Attempting a simple C2 Server command: + file_create_command = { + "commands": [["file_system", "create", "folder", "test_folder"]], + "username": "admin", + "password": "admin", + "ip_address": None, + } + + command_request_response = c2_server.send_command(C2Command.TERMINAL, command_options=file_create_command) + + assert command_request_response.status == "failure" + + # Despite the command failing - The C2 Server will still consider the beacon alive + # Until it does not respond within the keep alive frequency set in the last keep_alive. + assert c2_server.c2_connection_active is True + + # Stepping 6 timesteps in order for the C2 server to consider the beacon dead. + for i in range(6): + network.apply_timestep(i) + + assert c2_server.c2_connection_active is False + + +def test_c2_handle_switching_port(basic_c2_network): + """Tests that the C2 suite will be able handle switching destination/src port.""" + network: Network = basic_c2_network + + network, computer_a, c2_server, computer_b, c2_beacon = setup_c2(network) + + # Asserting that the c2 applications have established a c2 connection + assert c2_beacon.c2_connection_active is True + assert c2_server.c2_connection_active is True + + # Assert to confirm that both the C2 server and the C2 beacon are configured correctly. + assert c2_beacon.c2_config.keep_alive_frequency is 2 + assert c2_beacon.c2_config.masquerade_port is Port.HTTP + assert c2_beacon.c2_config.masquerade_protocol is IPProtocol.TCP + + assert c2_server.c2_config.keep_alive_frequency is 2 + assert c2_server.c2_config.masquerade_port is Port.HTTP + assert c2_server.c2_config.masquerade_protocol is IPProtocol.TCP + + # Configuring the C2 Beacon. + c2_beacon.configure( + c2_server_ip_address="192.168.0.1", + keep_alive_frequency=2, + masquerade_port=Port.FTP, + masquerade_protocol=IPProtocol.TCP, + ) + + # Asserting that the c2 applications have established a c2 connection + assert c2_beacon.c2_connection_active is True + assert c2_server.c2_connection_active is True + + # Assert to confirm that both the C2 server and the C2 beacon + # Have reconfigured their C2 settings. + assert c2_beacon.c2_config.masquerade_port is Port.FTP + assert c2_beacon.c2_config.masquerade_protocol is IPProtocol.TCP + + assert c2_server.c2_config.masquerade_port is Port.FTP + assert c2_server.c2_config.masquerade_protocol is IPProtocol.TCP + + +def test_c2_handle_switching_frequency(basic_c2_network): + """Tests that the C2 suite will be able handle switching keep alive frequency.""" + network: Network = basic_c2_network + + network, computer_a, c2_server, computer_b, c2_beacon = setup_c2(network) + + # Asserting that the c2 beacon has established a c2 connection + assert c2_beacon.c2_connection_active is True + network: Network = basic_c2_network + + network, computer_a, c2_server, computer_b, c2_beacon = setup_c2(network) + + # Asserting that the c2 applications have established a c2 connection + assert c2_beacon.c2_connection_active is True + assert c2_server.c2_connection_active is True + + # Assert to confirm that both the C2 server and the C2 beacon are configured correctly. + assert c2_beacon.c2_config.keep_alive_frequency is 2 + assert c2_server.c2_config.keep_alive_frequency is 2 + + # Configuring the C2 Beacon. + c2_beacon.configure(c2_server_ip_address="192.168.0.1", keep_alive_frequency=10) + + # Asserting that the c2 applications have established a c2 connection + assert c2_beacon.c2_connection_active is True + assert c2_server.c2_connection_active is True + + # Assert to confirm that both the C2 server and the C2 beacon + # Have reconfigured their C2 settings. + assert c2_beacon.c2_config.keep_alive_frequency is 10 + assert c2_server.c2_config.keep_alive_frequency is 10 + + # Now skipping 9 time steps to confirm keep alive inactivity + for i in range(9): + network.apply_timestep(i) + + # If the keep alive reconfiguration failed then the keep alive inactivity could never reach 9 + # As another keep alive would have already been sent. + assert c2_beacon.keep_alive_inactivity is 9 + assert c2_server.keep_alive_inactivity is 9 + + network.apply_timestep(10) + + assert c2_beacon.keep_alive_inactivity is 0 + assert c2_server.keep_alive_inactivity is 0 + + +def test_c2_handles_1_timestep_keep_alive(basic_c2_network): + """Tests that the C2 suite will be able handle a C2 Beacon will a keep alive of 1 timestep.""" + network: Network = basic_c2_network + + network, computer_a, c2_server, computer_b, c2_beacon = setup_c2(network) + + c2_beacon.configure(c2_server_ip_address="192.168.0.1", keep_alive_frequency=1) + c2_server.run() + c2_beacon.establish() + + for i in range(50): + network.apply_timestep(i) + + assert c2_beacon.c2_connection_active is True + assert c2_server.c2_connection_active is True + + +def test_c2_exfil_folder(basic_c2_network): + """Tests that the C2 suite correctly default and setup their exfiltration_folders.""" + network: Network = basic_c2_network + + network, computer_a, c2_server, computer_b, c2_beacon = setup_c2(network) + + c2_beacon.get_exfiltration_folder() + c2_server.get_exfiltration_folder() + assert c2_beacon.file_system.get_folder("exfiltration_folder") + assert c2_server.file_system.get_folder("exfiltration_folder") + + c2_server.file_system.create_file(folder_name="test_folder", file_name="test_file") + + # asserting to check that by default the c2 exfil will use "exfiltration_folder" + exfil_options = { + "username": "admin", + "password": "admin", + "target_ip_address": "192.168.0.1", + "target_folder_name": "test_folder", + "exfiltration_folder_name": None, + "target_file_name": "test_file", + } + c2_server.send_command(given_command=C2Command.DATA_EXFILTRATION, command_options=exfil_options) + + assert c2_beacon.file_system.get_file(folder_name="exfiltration_folder", file_name="test_file") diff --git a/tests/unit_tests/_primaite/_simulator/_system/_services/test_terminal.py b/tests/unit_tests/_primaite/_simulator/_system/_services/test_terminal.py new file mode 100644 index 00000000..41858b90 --- /dev/null +++ b/tests/unit_tests/_primaite/_simulator/_system/_services/test_terminal.py @@ -0,0 +1,406 @@ +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +from typing import Tuple +from uuid import uuid4 + +import pytest + +from primaite.game.agent.interface import ProxyAgent +from primaite.game.game import PrimaiteGame +from primaite.simulator.network.container import Network +from primaite.simulator.network.hardware.nodes.host.computer import Computer +from primaite.simulator.network.hardware.nodes.host.server import Server +from primaite.simulator.network.hardware.nodes.network.router import ACLAction, Router +from primaite.simulator.network.hardware.nodes.network.switch import Switch +from primaite.simulator.network.hardware.nodes.network.wireless_router import WirelessRouter +from primaite.simulator.network.protocols.ssh import ( + SSHConnectionMessage, + SSHPacket, + SSHTransportMessage, + SSHUserCredentials, +) +from primaite.simulator.network.transmission.network_layer import IPProtocol +from primaite.simulator.network.transmission.transport_layer import Port +from primaite.simulator.system.applications.red_applications.ransomware_script import RansomwareScript +from primaite.simulator.system.services.dns.dns_server import DNSServer +from primaite.simulator.system.services.service import ServiceOperatingState +from primaite.simulator.system.services.terminal.terminal import RemoteTerminalConnection, Terminal +from primaite.simulator.system.services.web_server.web_server import WebServer + + +@pytest.fixture(scope="function") +def terminal_on_computer() -> Tuple[Terminal, Computer]: + computer: Computer = Computer( + hostname="node_a", ip_address="192.168.0.10", subnet_mask="255.255.255.0", start_up_duration=0 + ) + computer.power_on() + terminal: Terminal = computer.software_manager.software.get("Terminal") + + return terminal, computer + + +@pytest.fixture(scope="function") +def basic_network() -> Network: + network = Network() + node_a = Computer(hostname="node_a", ip_address="192.168.0.10", subnet_mask="255.255.255.0", start_up_duration=0) + node_a.power_on() + node_a.software_manager.get_open_ports() + + node_b = Computer(hostname="node_b", ip_address="192.168.0.11", subnet_mask="255.255.255.0", start_up_duration=0) + node_b.power_on() + network.connect(node_a.network_interface[1], node_b.network_interface[1]) + + return network + + +@pytest.fixture(scope="function") +def wireless_wan_network(): + network = Network() + + # Configure PC A + pc_a = Computer( + hostname="pc_a", + ip_address="192.168.0.2", + subnet_mask="255.255.255.0", + default_gateway="192.168.0.1", + start_up_duration=0, + ) + pc_a.power_on() + network.add_node(pc_a) + + # Configure Router 1 + router_1 = WirelessRouter(hostname="router_1", start_up_duration=0, airspace=network.airspace) + router_1.power_on() + network.add_node(router_1) + + # Configure the connection between PC A and Router 1 port 2 + router_1.configure_router_interface("192.168.0.1", "255.255.255.0") + network.connect(pc_a.network_interface[1], router_1.network_interface[2]) + + # Configure Router 1 ACLs + router_1.acl.add_rule(action=ACLAction.PERMIT, src_port=Port.ARP, dst_port=Port.ARP, position=22) + router_1.acl.add_rule(action=ACLAction.PERMIT, protocol=IPProtocol.ICMP, position=23) + + # add ACL rule to allow SSH traffic + router_1.acl.add_rule(action=ACLAction.PERMIT, src_port=Port.SSH, dst_port=Port.SSH, position=21) + + # Configure PC B + pc_b = Computer( + hostname="pc_b", + ip_address="192.168.2.2", + subnet_mask="255.255.255.0", + default_gateway="192.168.2.1", + start_up_duration=0, + ) + pc_b.power_on() + network.add_node(pc_b) + + # Configure Router 2 + router_2 = WirelessRouter(hostname="router_2", start_up_duration=0, airspace=network.airspace) + router_2.power_on() + network.add_node(router_2) + + # Configure the connection between PC B and Router 2 port 2 + router_2.configure_router_interface("192.168.2.1", "255.255.255.0") + network.connect(pc_b.network_interface[1], router_2.network_interface[2]) + + # Configure Router 2 ACLs + + # Configure the wireless connection between Router 1 port 1 and Router 2 port 1 + router_1.configure_wireless_access_point("192.168.1.1", "255.255.255.0") + router_2.configure_wireless_access_point("192.168.1.2", "255.255.255.0") + + router_1.route_table.add_route( + address="192.168.2.0", subnet_mask="255.255.255.0", next_hop_ip_address="192.168.1.2" + ) + + # Configure Route from Router 2 to PC A subnet + router_2.route_table.add_route( + address="192.168.0.2", subnet_mask="255.255.255.0", next_hop_ip_address="192.168.1.1" + ) + + return pc_a, pc_b, router_1, router_2 + + +@pytest.fixture +def game_and_agent_fixture(game_and_agent): + """Create a game with a simple agent that can be controlled by the tests.""" + game, agent = game_and_agent + + client_1: Computer = game.simulation.network.get_node_by_hostname("client_1") + client_1.start_up_duration = 3 + + return game, agent + + +def test_terminal_creation(terminal_on_computer): + terminal, computer = terminal_on_computer + terminal.describe_state() + + +def test_terminal_install_default(): + """Terminal should be auto installed onto Nodes""" + computer = Computer(hostname="node_a", ip_address="192.168.0.10", subnet_mask="255.255.255.0", start_up_duration=0) + computer.power_on() + + assert computer.software_manager.software.get("Terminal") + + +def test_terminal_not_on_switch(): + """Ensure terminal does not auto-install to switch""" + test_switch = Switch(hostname="Test") + + assert not test_switch.software_manager.software.get("Terminal") + + +def test_terminal_send(basic_network): + """Test that Terminal can send valid commands.""" + network: Network = basic_network + computer_a: Computer = network.get_node_by_hostname("node_a") + terminal_a: Terminal = computer_a.software_manager.software.get("Terminal") + computer_b: Computer = network.get_node_by_hostname("node_b") + + payload: SSHPacket = SSHPacket( + payload="Test_Payload", + transport_message=SSHTransportMessage.SSH_MSG_USERAUTH_REQUEST, + connection_message=SSHConnectionMessage.SSH_MSG_CHANNEL_DATA, + user_account=SSHUserCredentials(username="username", password="password"), + connection_request_uuid=str(uuid4()), + ) + + assert terminal_a.send(payload=payload, dest_ip_address=computer_b.network_interface[1].ip_address) + + +def test_terminal_receive(basic_network): + """Test that terminal can receive and process commands""" + network: Network = basic_network + computer_a: Computer = network.get_node_by_hostname("node_a") + terminal_a: Terminal = computer_a.software_manager.software.get("Terminal") + computer_b: Computer = network.get_node_by_hostname("node_b") + folder_name = "Downloads" + + payload: SSHPacket = SSHPacket( + payload=["file_system", "create", "folder", folder_name], + transport_message=SSHTransportMessage.SSH_MSG_SERVICE_REQUEST, + connection_message=SSHConnectionMessage.SSH_MSG_CHANNEL_OPEN, + ) + + term_a_on_node_b: RemoteTerminalConnection = terminal_a.login( + username="admin", password="admin", ip_address="192.168.0.11" + ) + + term_a_on_node_b.execute(["file_system", "create", "folder", folder_name]) + + # Assert that the Folder has been correctly created + assert computer_b.file_system.get_folder(folder_name) + + +def test_terminal_install(basic_network): + """Test that Terminal can successfully process an INSTALL request""" + network: Network = basic_network + computer_a: Computer = network.get_node_by_hostname("node_a") + terminal_a: Terminal = computer_a.software_manager.software.get("Terminal") + computer_b: Computer = network.get_node_by_hostname("node_b") + + payload: SSHPacket = SSHPacket( + payload=["software_manager", "application", "install", "RansomwareScript"], + transport_message=SSHTransportMessage.SSH_MSG_SERVICE_REQUEST, + connection_message=SSHConnectionMessage.SSH_MSG_CHANNEL_OPEN, + ) + + term_a_on_node_b: RemoteTerminalConnection = terminal_a.login( + username="admin", password="admin", ip_address="192.168.0.11" + ) + + term_a_on_node_b.execute(["software_manager", "application", "install", "RansomwareScript"]) + + assert computer_b.software_manager.software.get("RansomwareScript") + + +def test_terminal_fail_when_closed(basic_network): + """Ensure Terminal won't attempt to send/receive when off""" + network: Network = basic_network + computer: Computer = network.get_node_by_hostname("node_a") + terminal: Terminal = computer.software_manager.software.get("Terminal") + computer_b: Computer = network.get_node_by_hostname("node_b") + + terminal.operating_state = ServiceOperatingState.STOPPED + + assert not terminal.login(username="admin", password="admin", ip_address=computer_b.network_interface[1].ip_address) + + +def test_terminal_disconnect(basic_network): + """Test Terminal disconnects""" + network: Network = basic_network + computer_a: Computer = network.get_node_by_hostname("node_a") + terminal_a: Terminal = computer_a.software_manager.software.get("Terminal") + computer_b: Computer = network.get_node_by_hostname("node_b") + terminal_b: Terminal = computer_b.software_manager.software.get("Terminal") + + assert len(terminal_b._connections) == 0 + + term_a_on_term_b = terminal_a.login( + username="admin", password="admin", ip_address=computer_b.network_interface[1].ip_address + ) + + assert len(terminal_b._connections) == 1 + + term_a_on_term_b.disconnect() + + assert len(terminal_b._connections) == 0 + + assert term_a_on_term_b.is_active is False + + +def test_terminal_ignores_when_off(basic_network): + """Terminal should ignore commands when not running""" + network: Network = basic_network + computer_a: Computer = network.get_node_by_hostname("node_a") + terminal_a: Terminal = computer_a.software_manager.software.get("Terminal") + + computer_b: Computer = network.get_node_by_hostname("node_b") + + term_a_on_term_b: RemoteTerminalConnection = terminal_a.login( + username="admin", password="admin", ip_address="192.168.0.11" + ) # login to computer_b + + terminal_a.operating_state = ServiceOperatingState.STOPPED + + assert not term_a_on_term_b.execute(["software_manager", "application", "install", "RansomwareScript"]) + + +def test_computer_remote_login_to_router(wireless_wan_network): + """Test to confirm that a computer can SSH into a router.""" + pc_a, _, router_1, _ = wireless_wan_network + + pc_a_terminal: Terminal = pc_a.software_manager.software.get("Terminal") + + assert len(pc_a_terminal._connections) == 0 + + pc_a_on_router_1 = pc_a_terminal.login(username="admin", password="admin", ip_address="192.168.1.1") + + assert len(pc_a_terminal._connections) == 1 + + payload = ["software_manager", "application", "install", "RansomwareScript"] + + pc_a_on_router_1.execute(payload) + + assert router_1.software_manager.software.get("RansomwareScript") + + +def test_router_remote_login_to_computer(wireless_wan_network): + """Test to confirm that a router can ssh into a computer.""" + pc_a, _, router_1, _ = wireless_wan_network + + router_1_terminal: Terminal = router_1.software_manager.software.get("Terminal") + + assert len(router_1_terminal._connections) == 0 + + router_1_on_pc_a = router_1_terminal.login(username="admin", password="admin", ip_address="192.168.0.2") + + assert len(router_1_terminal._connections) == 1 + + payload = ["software_manager", "application", "install", "RansomwareScript"] + + router_1_on_pc_a.execute(payload) + + assert pc_a.software_manager.software.get("RansomwareScript") + + +def test_router_blocks_SSH_traffic(wireless_wan_network): + """Test to check that router will block SSH traffic if no ACL rule.""" + pc_a, _, router_1, _ = wireless_wan_network + + # Remove rule that allows SSH traffic. + router_1.acl.remove_rule(position=21) + + pc_a_terminal: Terminal = pc_a.software_manager.software.get("Terminal") + + assert len(pc_a_terminal._connections) == 0 + + pc_a_terminal.login(username="admin", password="admin", ip_address="192.168.0.2") + + assert len(pc_a_terminal._connections) == 0 + + +def test_SSH_across_network(wireless_wan_network): + """Test to show ability to SSH across a network.""" + pc_a, pc_b, router_1, router_2 = wireless_wan_network + + terminal_a: Terminal = pc_a.software_manager.software.get("Terminal") + terminal_b: Terminal = pc_b.software_manager.software.get("Terminal") + + router_2.acl.add_rule(action=ACLAction.PERMIT, src_port=Port.SSH, dst_port=Port.SSH, position=21) + + assert len(terminal_a._connections) == 0 + + terminal_b_on_terminal_a = terminal_b.login(username="admin", password="admin", ip_address="192.168.0.2") + + assert len(terminal_a._connections) == 1 + + +def test_multiple_remote_terminals_same_node(basic_network): + """Test to check that multiple remote terminals can be spawned by one node.""" + network: Network = basic_network + computer_a: Computer = network.get_node_by_hostname("node_a") + terminal_a: Terminal = computer_a.software_manager.software.get("Terminal") + computer_b: Computer = network.get_node_by_hostname("node_b") + + assert len(terminal_a._connections) == 0 + + # Spam login requests to node. + for attempt in range(3): + remote_connection = terminal_a.login(username="admin", password="admin", ip_address="192.168.0.11") + + terminal_a.show() + + assert len(terminal_a._connections) == 3 + + +def test_terminal_rejects_commands_if_disconnect(basic_network): + """Test to check terminal will ignore commands from disconnected connections""" + network: Network = basic_network + computer_a: Computer = network.get_node_by_hostname("node_a") + terminal_a: Terminal = computer_a.software_manager.software.get("Terminal") + computer_b: Computer = network.get_node_by_hostname("node_b") + + terminal_b: Terminal = computer_b.software_manager.software.get("Terminal") + + remote_connection = terminal_a.login(username="admin", password="admin", ip_address="192.168.0.11") + + assert len(terminal_a._connections) == 1 + assert len(terminal_b._connections) == 1 + + remote_connection.disconnect() + + assert len(terminal_a._connections) == 0 + assert len(terminal_b._connections) == 0 + + assert remote_connection.execute(["software_manager", "application", "install", "RansomwareScript"]) is False + + assert not computer_b.software_manager.software.get("RansomwareScript") + + assert remote_connection.is_active is False + + +def test_terminal_connection_timeout(basic_network): + """Test that terminal_connections are affected by UserSession timeout.""" + network: Network = basic_network + computer_a: Computer = network.get_node_by_hostname("node_a") + terminal_a: Terminal = computer_a.software_manager.software.get("Terminal") + computer_b: Computer = network.get_node_by_hostname("node_b") + terminal_b: Terminal = computer_b.software_manager.software.get("Terminal") + + remote_connection = terminal_a.login(username="admin", password="admin", ip_address="192.168.0.11") + + assert len(terminal_a._connections) == 1 + assert len(terminal_b._connections) == 1 + assert len(computer_b.user_session_manager.remote_sessions) == 1 + + remote_session = computer_b.user_session_manager.remote_sessions[remote_connection.connection_uuid] + computer_b.user_session_manager._timeout_session(remote_session) + + assert len(terminal_a._connections) == 0 + assert len(terminal_b._connections) == 0 + assert len(computer_b.user_session_manager.remote_sessions) == 0 + + assert not remote_connection.is_active