mongo-x-ray-gmd 2.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. mongo_x_ray_gmd-2.0.0/PKG-INFO +79 -0
  2. mongo_x_ray_gmd-2.0.0/README.md +68 -0
  3. mongo_x_ray_gmd-2.0.0/pyproject.toml +63 -0
  4. mongo_x_ray_gmd-2.0.0/setup.cfg +4 -0
  5. mongo_x_ray_gmd-2.0.0/src/mongo_x_ray_gmd/__init__.py +11 -0
  6. mongo_x_ray_gmd-2.0.0/src/mongo_x_ray_gmd/framework.py +137 -0
  7. mongo_x_ray_gmd-2.0.0/src/mongo_x_ray_gmd/gmd_items/__init__.py +11 -0
  8. mongo_x_ray_gmd-2.0.0/src/mongo_x_ray_gmd/gmd_items/base_item.py +218 -0
  9. mongo_x_ray_gmd-2.0.0/src/mongo_x_ray_gmd/gmd_items/build_info_item.py +43 -0
  10. mongo_x_ray_gmd-2.0.0/src/mongo_x_ray_gmd/gmd_items/coll_info_item.py +107 -0
  11. mongo_x_ray_gmd-2.0.0/src/mongo_x_ray_gmd/gmd_items/db_item.py +78 -0
  12. mongo_x_ray_gmd-2.0.0/src/mongo_x_ray_gmd/gmd_items/host_info_item.py +64 -0
  13. mongo_x_ray_gmd-2.0.0/src/mongo_x_ray_gmd/gmd_items/index_info_item.py +97 -0
  14. mongo_x_ray_gmd-2.0.0/src/mongo_x_ray_gmd/gmd_items/rs_info_item.py +131 -0
  15. mongo_x_ray_gmd-2.0.0/src/mongo_x_ray_gmd/gmd_items/security_item.py +64 -0
  16. mongo_x_ray_gmd-2.0.0/src/mongo_x_ray_gmd/gmd_items/server_status_item.py +129 -0
  17. mongo_x_ray_gmd-2.0.0/src/mongo_x_ray_gmd/gmd_items/sh_info_item.py +88 -0
  18. mongo_x_ray_gmd-2.0.0/src/mongo_x_ray_gmd/gmd_items/summary_item.py +93 -0
  19. mongo_x_ray_gmd-2.0.0/src/mongo_x_ray_gmd/parsers/__init__.py +9 -0
  20. mongo_x_ray_gmd-2.0.0/src/mongo_x_ray_gmd/parsers/base_parser.py +22 -0
  21. mongo_x_ray_gmd-2.0.0/src/mongo_x_ray_gmd/parsers/coll_stats_parser.py +139 -0
  22. mongo_x_ray_gmd-2.0.0/src/mongo_x_ray_gmd/parsers/db_parser.py +138 -0
  23. mongo_x_ray_gmd-2.0.0/src/mongo_x_ray_gmd/parsers/sh_details_parser.py +42 -0
  24. mongo_x_ray_gmd-2.0.0/src/mongo_x_ray_gmd/plugin.py +79 -0
  25. mongo_x_ray_gmd-2.0.0/src/mongo_x_ray_gmd/shared.py +91 -0
  26. mongo_x_ray_gmd-2.0.0/src/mongo_x_ray_gmd/templates/gmd/full.html +1 -0
  27. mongo_x_ray_gmd-2.0.0/src/mongo_x_ray_gmd/templates/gmd/full.raw.html +30 -0
  28. mongo_x_ray_gmd-2.0.0/src/mongo_x_ray_gmd/templates/gmd/script.js +1 -0
  29. mongo_x_ray_gmd-2.0.0/src/mongo_x_ray_gmd/templates/gmd/script.raw.js +108 -0
  30. mongo_x_ray_gmd-2.0.0/src/mongo_x_ray_gmd/templates/gmd/snippets/CollStatsParser_1.js +1 -0
  31. mongo_x_ray_gmd-2.0.0/src/mongo_x_ray_gmd/templates/gmd/snippets/CollStatsParser_1.raw.js +89 -0
  32. mongo_x_ray_gmd-2.0.0/src/mongo_x_ray_gmd/templates/gmd/snippets/DBParser_1.js +1 -0
  33. mongo_x_ray_gmd-2.0.0/src/mongo_x_ray_gmd/templates/gmd/snippets/DBParser_1.raw.js +47 -0
  34. mongo_x_ray_gmd-2.0.0/src/mongo_x_ray_gmd/templates/minify.sh +51 -0
  35. mongo_x_ray_gmd-2.0.0/src/mongo_x_ray_gmd.egg-info/PKG-INFO +79 -0
  36. mongo_x_ray_gmd-2.0.0/src/mongo_x_ray_gmd.egg-info/SOURCES.txt +41 -0
  37. mongo_x_ray_gmd-2.0.0/src/mongo_x_ray_gmd.egg-info/dependency_links.txt +1 -0
  38. mongo_x_ray_gmd-2.0.0/src/mongo_x_ray_gmd.egg-info/entry_points.txt +2 -0
  39. mongo_x_ray_gmd-2.0.0/src/mongo_x_ray_gmd.egg-info/requires.txt +4 -0
  40. mongo_x_ray_gmd-2.0.0/src/mongo_x_ray_gmd.egg-info/top_level.txt +1 -0
  41. mongo_x_ray_gmd-2.0.0/tests/test_pdf_output.py +44 -0
  42. mongo_x_ray_gmd-2.0.0/tests/test_report_ui.py +214 -0
  43. mongo_x_ray_gmd-2.0.0/tests/test_shared.py +66 -0
@@ -0,0 +1,79 @@
1
+ Metadata-Version: 2.4
2
+ Name: mongo-x-ray-gmd
3
+ Version: 2.0.0
4
+ Summary: getMongoData analysis plugin for x-ray
5
+ Requires-Python: <4,>=3.10
6
+ Description-Content-Type: text/markdown
7
+ Requires-Dist: mongo-x-ray>=2.0.0
8
+ Requires-Dist: mongo-x-ray-hc>=2.0.0
9
+ Requires-Dist: pymongo>=4.0
10
+ Requires-Dist: python-dateutil>=2.9.0
11
+
12
+ # mongo-x-ray-gmd
13
+
14
+ [![CI](https://github.com/zhangyaoxing/mongo-x-ray-gmd/actions/workflows/ci.yml/badge.svg)](https://github.com/zhangyaoxing/mongo-x-ray-gmd/actions/workflows/ci.yml)
15
+ [![PyPI](https://img.shields.io/pypi/v/mongo-x-ray-gmd.svg)](https://pypi.org/project/mongo-x-ray-gmd/)
16
+
17
+ getMongoData analysis plugin for [x-ray](https://github.com/mongodb-ps/ce-mongo-x-ray).
18
+
19
+ ## Install
20
+
21
+ ```bash
22
+ pip install mongo-x-ray mongo-x-ray-hc mongo-x-ray-gmd
23
+ ```
24
+
25
+ ## Usage
26
+
27
+ ```bash
28
+ x-ray gmd /path/to/getMongoData-output.json
29
+ x-ray gmd /path/to/getMongoData-output.json -f html -o /path/to/output/
30
+ ```
31
+
32
+ ## Compatibility
33
+
34
+ Supports MongoDB 5.0 and above on all topologies:
35
+
36
+ | Replica Set | Sharded Cluster | Standalone |
37
+ | :---------: | :-------------: | :--------: |
38
+ | ✅ | ✅ | ✅ |
39
+
40
+ ## Parameters
41
+
42
+ ```bash
43
+ x-ray gmd [-h] [-s CHECKSET] [-o OUTPUT] [-f {markdown,html,pdf}] [--no-browser]
44
+ gmd_file
45
+ ```
46
+
47
+ | Argument | Description | Default |
48
+ | --- | --- | --- |
49
+ | `gmd_file` | Path to the getMongoData output JSON file. | required |
50
+ | `-s, --checkset` | Checkset to run. | `default` |
51
+ | `-o, --output` | Output folder path. | `output/` |
52
+ | `-f, --format` | Output format: `markdown`, `html` or `pdf` (PDF also keeps Markdown and HTML). | `html` |
53
+ | `--no-browser` | Do not open the generated report in the browser. | `false` |
54
+
55
+ ## Analysis Items
56
+
57
+ | Item | Purpose |
58
+ | --- | --- |
59
+ | `SummaryItem` | Overall summary of the getMongoData output. |
60
+ | `BuildInfoItem` | Build information (reuses the healthcheck build info parser and version EOL rule). |
61
+ | `CollInfoItem` | Collection statistics: sizes and fragmentation. |
62
+ | `DBItem` | Database-level information. |
63
+ | `HostInfoItem` | Host filesystem type, NUMA settings and host properties. |
64
+ | `IndexInfoItem` | Index information. |
65
+ | `RSInfoItem` | Replica set topology, oplog window and status. |
66
+ | `SecurityItem` | Security posture. |
67
+ | `ServerStatusItem` | Cache, connections and query targeting status. |
68
+ | `SHInfoItem` | Sharding architecture and shard details. |
69
+
70
+ ## Development
71
+
72
+ Requires Python 3.10+, MongoDB 5.0 or later, the [mongo-x-ray](https://github.com/mongodb-ps/ce-mongo-x-ray) core and the
73
+ [mongo-x-ray-hc](https://github.com/zhangyaoxing/mongo-x-ray-hc) plugin (gmd reuses its parsers and rules).
74
+
75
+ ```bash
76
+ make unit-test # run the unit tests
77
+ make lint # ruff check + ruff format --check
78
+ make minify # minify templates
79
+ ```
@@ -0,0 +1,68 @@
1
+ # mongo-x-ray-gmd
2
+
3
+ [![CI](https://github.com/zhangyaoxing/mongo-x-ray-gmd/actions/workflows/ci.yml/badge.svg)](https://github.com/zhangyaoxing/mongo-x-ray-gmd/actions/workflows/ci.yml)
4
+ [![PyPI](https://img.shields.io/pypi/v/mongo-x-ray-gmd.svg)](https://pypi.org/project/mongo-x-ray-gmd/)
5
+
6
+ getMongoData analysis plugin for [x-ray](https://github.com/mongodb-ps/ce-mongo-x-ray).
7
+
8
+ ## Install
9
+
10
+ ```bash
11
+ pip install mongo-x-ray mongo-x-ray-hc mongo-x-ray-gmd
12
+ ```
13
+
14
+ ## Usage
15
+
16
+ ```bash
17
+ x-ray gmd /path/to/getMongoData-output.json
18
+ x-ray gmd /path/to/getMongoData-output.json -f html -o /path/to/output/
19
+ ```
20
+
21
+ ## Compatibility
22
+
23
+ Supports MongoDB 5.0 and above on all topologies:
24
+
25
+ | Replica Set | Sharded Cluster | Standalone |
26
+ | :---------: | :-------------: | :--------: |
27
+ | ✅ | ✅ | ✅ |
28
+
29
+ ## Parameters
30
+
31
+ ```bash
32
+ x-ray gmd [-h] [-s CHECKSET] [-o OUTPUT] [-f {markdown,html,pdf}] [--no-browser]
33
+ gmd_file
34
+ ```
35
+
36
+ | Argument | Description | Default |
37
+ | --- | --- | --- |
38
+ | `gmd_file` | Path to the getMongoData output JSON file. | required |
39
+ | `-s, --checkset` | Checkset to run. | `default` |
40
+ | `-o, --output` | Output folder path. | `output/` |
41
+ | `-f, --format` | Output format: `markdown`, `html` or `pdf` (PDF also keeps Markdown and HTML). | `html` |
42
+ | `--no-browser` | Do not open the generated report in the browser. | `false` |
43
+
44
+ ## Analysis Items
45
+
46
+ | Item | Purpose |
47
+ | --- | --- |
48
+ | `SummaryItem` | Overall summary of the getMongoData output. |
49
+ | `BuildInfoItem` | Build information (reuses the healthcheck build info parser and version EOL rule). |
50
+ | `CollInfoItem` | Collection statistics: sizes and fragmentation. |
51
+ | `DBItem` | Database-level information. |
52
+ | `HostInfoItem` | Host filesystem type, NUMA settings and host properties. |
53
+ | `IndexInfoItem` | Index information. |
54
+ | `RSInfoItem` | Replica set topology, oplog window and status. |
55
+ | `SecurityItem` | Security posture. |
56
+ | `ServerStatusItem` | Cache, connections and query targeting status. |
57
+ | `SHInfoItem` | Sharding architecture and shard details. |
58
+
59
+ ## Development
60
+
61
+ Requires Python 3.10+, MongoDB 5.0 or later, the [mongo-x-ray](https://github.com/mongodb-ps/ce-mongo-x-ray) core and the
62
+ [mongo-x-ray-hc](https://github.com/zhangyaoxing/mongo-x-ray-hc) plugin (gmd reuses its parsers and rules).
63
+
64
+ ```bash
65
+ make unit-test # run the unit tests
66
+ make lint # ruff check + ruff format --check
67
+ make minify # minify templates
68
+ ```
@@ -0,0 +1,63 @@
1
+ [build-system]
2
+ requires = ["setuptools==83.0.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "mongo-x-ray-gmd"
7
+ version = "2.0.0"
8
+ description = "getMongoData analysis plugin for x-ray"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10,<4"
11
+ dependencies = [
12
+ "mongo-x-ray>=2.0.0",
13
+ "mongo-x-ray-hc>=2.0.0",
14
+ "pymongo>=4.0",
15
+ "python-dateutil>=2.9.0",
16
+ ]
17
+
18
+ [project.entry-points."mongo_x_ray.plugins"]
19
+ gmd = "mongo_x_ray_gmd.plugin:GmdPlugin"
20
+
21
+ [tool.setuptools.packages.find]
22
+ where = ["src"]
23
+ include = ["mongo_x_ray_gmd*"]
24
+
25
+ [tool.setuptools.package-data]
26
+ "mongo_x_ray_gmd" = ["templates/**/*"]
27
+
28
+ [tool.pytest.ini_options]
29
+ testpaths = ["tests"]
30
+ pythonpath = ["src"]
31
+ python_files = ["test_*.py"]
32
+ python_classes = ["Test*"]
33
+ python_functions = ["test_*"]
34
+ markers = [
35
+ "slow: marks tests as slow",
36
+ "integration: marks tests as integration tests",
37
+ "unit: marks tests as unit tests",
38
+ ]
39
+
40
+
41
+ [tool.ruff]
42
+ line-length = 120
43
+ target-version = "py310"
44
+ exclude = [".venv", "build", "dist"]
45
+
46
+ [tool.ruff.lint]
47
+ select = ["E", "F", "I"]
48
+ ignore = ["E501"]
49
+
50
+ [tool.ruff.lint.isort]
51
+ known-first-party = ["mongo_x_ray", "mongo_x_ray_ftdc", "mongo_x_ray_gmd", "mongo_x_ray_hc", "mongo_x_ray_log", "mongo_x_ray_risk"]
52
+
53
+
54
+ [tool.pyright]
55
+ pythonVersion = "3.10"
56
+ venvPath = "../ce-mongo-x-ray"
57
+ venv = ".venv"
58
+ typeCheckingMode = "basic"
59
+ extraPaths = [
60
+ "src",
61
+ "../ce-mongo-x-ray/src",
62
+ "../mongo-x-ray-hc/src",
63
+ ]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,11 @@
1
+ """
2
+ Copyright (c) 2026 MongoDB Inc.
3
+
4
+ DISCLAIMER: THESE CODE SAMPLES ARE PROVIDED FOR EDUCATIONAL AND ILLUSTRATIVE PURPOSES ONLY,
5
+ TO DEMONSTRATE THE FUNCTIONALITY OF SPECIFIC MONGODB FEATURES.
6
+ THEY ARE NOT PRODUCTION-READY AND MAY LACK THE SECURITY HARDENING, ERROR HANDLING, AND TESTING REQUIRED FOR A LIVE ENVIRONMENT.
7
+ YOU ARE RESPONSIBLE FOR TESTING, VALIDATING, AND SECURING THIS CODE WITHIN YOUR OWN ENVIRONMENT BEFORE IMPLEMENTATION.
8
+ THIS MATERIAL IS PROVIDED "AS IS" WITHOUT WARRANTY OR LIABILITY.
9
+
10
+ MongoDB getMongoData analysis package.
11
+ """
@@ -0,0 +1,137 @@
1
+ """
2
+ Copyright (c) 2026 MongoDB Inc.
3
+
4
+ DISCLAIMER: THESE CODE SAMPLES ARE PROVIDED FOR EDUCATIONAL AND ILLUSTRATIVE PURPOSES ONLY,
5
+ TO DEMONSTRATE THE FUNCTIONALITY OF SPECIFIC MONGODB FEATURES.
6
+ THEY ARE NOT PRODUCTION-READY AND MAY LACK THE SECURITY HARDENING, ERROR HANDLING, AND TESTING REQUIRED FOR A LIVE ENVIRONMENT.
7
+ YOU ARE RESPONSIBLE FOR TESTING, VALIDATING, AND SECURING THIS CODE WITHIN YOUR OWN ENVIRONMENT BEFORE IMPLEMENTATION.
8
+ THIS MATERIAL IS PROVIDED "AS IS" WITHOUT WARRANTY OR LIABILITY.
9
+ """
10
+
11
+ from datetime import datetime, timezone
12
+ from typing import TextIO
13
+
14
+ from mongo_x_ray.framework import BaseFramework
15
+ from mongo_x_ray.shared import str_to_md_id, to_json
16
+ from mongo_x_ray.utils import bold, cyan, env, green, load_classes, red, yellow
17
+ from mongo_x_ray_gmd.gmd_items.summary_item import SummaryItem
18
+ from mongo_x_ray_gmd.shared import load_json
19
+
20
+ GMD_CLASSES = load_classes("mongo_x_ray_gmd.gmd_items")
21
+
22
+
23
+ class Framework(BaseFramework):
24
+ template_module = "gmd"
25
+ template_package = "mongo_x_ray_gmd"
26
+
27
+ def __init__(self, file_path: str, config: dict):
28
+ super().__init__(config)
29
+ self._file_path = file_path
30
+ self._logger.debug(to_json(self._config))
31
+ self._log_start = None
32
+ self._log_end = None
33
+ if env == "development":
34
+ self._logger.info(yellow("Running in development mode."))
35
+
36
+ def run_gmd_analysis(self, gmd_set_name: str, *_args, **kwargs):
37
+ self._set_name = gmd_set_name
38
+ # Create output folder if it doesn't exist
39
+ output_folder = kwargs.get("output_folder", "output/")
40
+ batch_folder = self._get_output_folder(output_folder)
41
+ # Dynamically load the gmd checkset based on the name
42
+ gmdsets = self._config.get("gmdsets", {})
43
+ if gmd_set_name not in gmdsets:
44
+ self._logger.warning(yellow(f"GMD checkset '{gmd_set_name}' not found in configuration. Using default."))
45
+ gmd_set_name = "default"
46
+ gmdset = gmdsets[gmd_set_name]
47
+ self._logger.info("Running GMD checkset: %s", bold(cyan(gmd_set_name)))
48
+
49
+ self._items = []
50
+ for item_name in gmdset.get("items", []):
51
+ item_cls = GMD_CLASSES.get(item_name)
52
+ if not item_cls:
53
+ self._logger.warning(yellow(f"GMD item '{item_name}' not found. Skipping."))
54
+ continue
55
+ # The config for the item can be specified in the `item_config` section, under the item class name.
56
+ item_config = self._config.get("item_config", {}).get(item_name, {})
57
+ item = item_cls(str(batch_folder), item_config)
58
+ self._items.append(item)
59
+ self._logger.info("GMD analyze item loaded: %s", bold(cyan(item_name)))
60
+ gmd_output = self._file_path
61
+
62
+ # Read the getMongoData output and parse the whole content.
63
+ with open(gmd_output, "r", encoding="utf-8", errors="ignore") as f:
64
+ content = f.read()
65
+ try:
66
+ objects = load_json(content)
67
+ except Exception as ex:
68
+ self._logger.error(red(f"Failed to parse the getMongoData output as JSON: {ex}"))
69
+ return
70
+
71
+ self._logger.info("Ingesting %s objects from getMongoData output...", green(str(len(objects))))
72
+
73
+ for i, obj in enumerate(objects):
74
+ if (i + 1) % 10000 == 0:
75
+ self._logger.info("%s objects ingested...", green(str(i + 1)))
76
+ for item in self._items:
77
+ try:
78
+ item.test(obj)
79
+ except Exception as e:
80
+ self._logger.warning(yellow(f"GMD analysis item '{item.name}' failed: {e}"))
81
+ continue
82
+
83
+ for item in self._items:
84
+ try:
85
+ item.finalize_analysis()
86
+ except Exception as e:
87
+ self._logger.warning(yellow(f"GMD analysis item '{item.name}' finalization failed: {e}"))
88
+ continue
89
+
90
+ def _render_markdown(self, output: TextIO) -> None:
91
+ output.write("# getMongoData Analysis Report\n")
92
+ output.write(f"Generated at: `{str(datetime.now(tz=timezone.utc))} UTC`\n\n")
93
+ output.write(f"File path: `{self._file_path}`\n\n")
94
+ output.write("## 1 Review Test Results\n\n")
95
+ output.write("### Overview\n\n")
96
+ # Enrich all test results with matched risks before building summary
97
+ risk_available = False
98
+ try:
99
+ from mongo_x_ray_risk import enrich_test_results, has_risks
100
+
101
+ risk_available = has_risks()
102
+ for item in self._items:
103
+ enrich_test_results(item._test_result)
104
+ except Exception:
105
+ self._logger.debug("Risk register matching not available", exc_info=True)
106
+ summary_item = SummaryItem(risk_available=risk_available)
107
+ summary_item.summarize(self._items)
108
+ summary_item.overview(output)
109
+ for i, item in enumerate(self._items):
110
+ if item._in_complete_flag:
111
+ self._logger.warning(
112
+ yellow(f"GMD item '{item.name}' is incomplete because of too many databases/collections.")
113
+ )
114
+ try:
115
+ title = f"1.{i + 1} {item.name}"
116
+ review_title = f"2.{i + 1} Review {item.name}"
117
+ review_title_id = str_to_md_id(review_title)
118
+ output.write(f"### {title}\n\n")
119
+ output.write(f"{item.description}\n\n")
120
+ output.write(f"[Review Raw Results &rarr;](#{review_title_id})\n\n")
121
+ item.test_result_markdown(output)
122
+ except Exception as e:
123
+ self._logger.warning(yellow(f"Failed to generate markdown for GMD item '{item.name}': {e}"))
124
+ continue
125
+
126
+ output.write("## 2 Review Raw Results\n\n")
127
+ for i, item in enumerate(self._items):
128
+ try:
129
+ title = f"1.{i + 1} {item.name}"
130
+ title_id = str_to_md_id(title)
131
+ review_title = f"2.{i + 1} Review {item.name}"
132
+ output.write(f"### {review_title}\n\n")
133
+ output.write(f"[&larr; Review Test Results](#{title_id})\n\n")
134
+ item.review_results_markdown(output)
135
+ except Exception as e:
136
+ self._logger.warning(yellow(f"Failed to generate review markdown for GMD item '{item.name}': {e}"))
137
+ continue
@@ -0,0 +1,11 @@
1
+ """
2
+ Copyright (c) 2026 MongoDB Inc.
3
+
4
+ DISCLAIMER: THESE CODE SAMPLES ARE PROVIDED FOR EDUCATIONAL AND ILLUSTRATIVE PURPOSES ONLY,
5
+ TO DEMONSTRATE THE FUNCTIONALITY OF SPECIFIC MONGODB FEATURES.
6
+ THEY ARE NOT PRODUCTION-READY AND MAY LACK THE SECURITY HARDENING, ERROR HANDLING, AND TESTING REQUIRED FOR A LIVE ENVIRONMENT.
7
+ YOU ARE RESPONSIBLE FOR TESTING, VALIDATING, AND SECURING THIS CODE WITHIN YOUR OWN ENVIRONMENT BEFORE IMPLEMENTATION.
8
+ THIS MATERIAL IS PROVIDED "AS IS" WITHOUT WARRANTY OR LIABILITY.
9
+
10
+ Package for GMD analysis items.
11
+ """
@@ -0,0 +1,218 @@
1
+ """
2
+ Copyright (c) 2026 MongoDB Inc.
3
+
4
+ DISCLAIMER: THESE CODE SAMPLES ARE PROVIDED FOR EDUCATIONAL AND ILLUSTRATIVE PURPOSES ONLY,
5
+ TO DEMONSTRATE THE FUNCTIONALITY OF SPECIFIC MONGODB FEATURES.
6
+ THEY ARE NOT PRODUCTION-READY AND MAY LACK THE SECURITY HARDENING, ERROR HANDLING, AND TESTING REQUIRED FOR A LIVE ENVIRONMENT.
7
+ YOU ARE RESPONSIBLE FOR TESTING, VALIDATING, AND SECURING THIS CODE WITHIN YOUR OWN ENVIRONMENT BEFORE IMPLEMENTATION.
8
+ THIS MATERIAL IS PROVIDED "AS IS" WITHOUT WARRANTY OR LIABILITY.
9
+ """
10
+
11
+ import html as html_mod
12
+ import logging
13
+ import os
14
+ from abc import abstractmethod
15
+ from typing import Callable, Optional
16
+
17
+ from bson import json_util
18
+
19
+ from mongo_x_ray.shared import SEVERITY
20
+ from mongo_x_ray.utils import bold, to_ejson, yellow
21
+ from mongo_x_ray.version import Version
22
+ from mongo_x_ray_gmd.shared import GmdEvents
23
+ from mongo_x_ray_hc.check_items.base_item import colorize_severity
24
+ from mongo_x_ray_hc.rules.base_rule import BaseRule
25
+
26
+
27
+ class BaseItem:
28
+ def __init__(self, output_folder: str, config, **_kwargs) -> None:
29
+ self.config: dict = config
30
+ self._output_file = os.path.join(output_folder, f"{self.__class__.__name__}.json")
31
+ self._logger = logging.getLogger(__name__)
32
+ self._server_version: Optional[Version] = None
33
+ self._hostname: Optional[str] = None
34
+ self._set_name: Optional[str] = None
35
+ self._cluster_type: Optional[str] = None
36
+ self._test_result: list = []
37
+ self._rules: dict[str, BaseRule] = {}
38
+ if os.path.isfile(self._output_file):
39
+ os.remove(self._output_file)
40
+
41
+ self._in_complete_flag = False
42
+ # Subscribe some common events that most items care about
43
+ self._cache = None
44
+ self._watched_events: dict[GmdEvents, list[Callable]] = {}
45
+ self._watched_all_events: list[tuple[set[GmdEvents], Callable]] = []
46
+ self._fired_events: set[GmdEvents] = set()
47
+
48
+ def get_version(block):
49
+ self._server_version = Version.parse(block.get("output", {}).get("version", ""))
50
+
51
+ def get_host(block):
52
+ if self._hostname is None:
53
+ self._hostname = block.get("output", {}).get("system", {}).get("hostname", "unknown")
54
+
55
+ def get_cluster_type(block):
56
+ output = block.get("output", {})
57
+ set_name = output.get("setName", None)
58
+ msg = output.get("msg", "")
59
+ if set_name is not None:
60
+ self._cluster_type = "RS"
61
+ self._set_name = set_name
62
+ elif msg == "isdbgrid":
63
+ self._cluster_type = "SH"
64
+ self._set_name = "mongos"
65
+ else:
66
+ self._cluster_type = "STANDALONE"
67
+
68
+ # Use hostname in ismaster whenever possible
69
+ if "me" in output:
70
+ self._hostname = output["me"]
71
+
72
+ self.watch_one(GmdEvents.SERVER_BUILD_INFO, get_version)
73
+ self.watch_one(GmdEvents.HOST_INFO, get_host)
74
+ self.watch_one(GmdEvents.ISMASTER, get_cluster_type)
75
+
76
+ def test(self, block) -> None:
77
+ sub_sec = block.get("subsection", "")
78
+ sub_sec = sub_sec.replace("INCOMPLETE_", "")
79
+ try:
80
+ current_event = GmdEvents(sub_sec)
81
+ except ValueError:
82
+ current_event = GmdEvents.UNKNOWN
83
+ # Fire subscribed single events
84
+ for event, funcs in self._watched_events.items():
85
+ if current_event == event:
86
+ if block.get("subsection", "").startswith("INCOMPLETE_"):
87
+ self._in_complete_flag = True
88
+ for func in funcs:
89
+ try:
90
+ func(block)
91
+ except Exception as e:
92
+ self._logger.warning(yellow("Error in subscribed function for event %s: %s"), event.value, e)
93
+ self._fired_events.add(event)
94
+
95
+ # Fire subscribed all events
96
+ for events, func in self._watched_all_events:
97
+ if events.issubset(self._fired_events) and current_event in events:
98
+ try:
99
+ func()
100
+ except Exception as e:
101
+ self._logger.warning("Error in subscribed all-events function for events %s: %s", events, e)
102
+
103
+ @property
104
+ def name(self) -> str:
105
+ return self._name
106
+
107
+ @name.setter
108
+ def name(self, value: str) -> None:
109
+ self._name = value
110
+
111
+ @property
112
+ def description(self) -> str:
113
+ desc: str = ""
114
+ for rule in self._rules.values():
115
+ desc += rule.description_md + "\n"
116
+ return desc
117
+
118
+ @property
119
+ def test_result(self) -> list:
120
+ return self._test_result
121
+
122
+ @property
123
+ def captured_sample(self) -> Optional[dict]:
124
+ try:
125
+ with open(self._output_file, "r", encoding="utf-8") as f:
126
+ return json_util.loads(f.read())
127
+ except FileNotFoundError:
128
+ self._logger.warning(
129
+ "Captured sample file not found: %s. This is probably because the getMongoData output is incomplete.",
130
+ bold(self._output_file),
131
+ )
132
+ return None
133
+
134
+ @captured_sample.setter
135
+ def captured_sample(self, data: Optional[dict]) -> None:
136
+ with open(self._output_file, "w", encoding="utf-8") as f:
137
+ f.write(to_ejson(data, indent=None))
138
+
139
+ def test_result_markdown(self, output) -> None:
140
+ if len(self._test_result) == 0:
141
+ output.write("<b style='color: green;'>Pass.</b>\n\n")
142
+ return
143
+
144
+ output.write(
145
+ '| <span data-sortable="false">\\#</span>{60px}'
146
+ ' | <span data-sortable="true">Host</span>{180px}'
147
+ ' | <span data-sortable="true">Severity</span>{120px}'
148
+ ' | <span data-sortable="true">Category</span>{200px}'
149
+ ' | <span data-sortable="false">Message</span>{*} |\n'
150
+ )
151
+ output.write("|:----------:|:----------:|:----------:|---------|---------|\n")
152
+ for idx, item in enumerate(self._test_result):
153
+ severity = item["severity"]
154
+ severity_cell = (
155
+ f'<span data-sort-value="{severity.value}">'
156
+ f"<b style='color: {colorize_severity(severity)}'>"
157
+ f" {severity.name} </b></span>"
158
+ )
159
+ category_cell = item["title"]
160
+ risk = item.get("matched_risk")
161
+ if risk:
162
+ risk_id = html_mod.escape(str(risk.get("id", "")))
163
+ risk_name = html_mod.escape(str(risk.get("name", ""))).replace("\r\n", "<br>").replace("\n", "<br>")
164
+ risk_desc = (
165
+ html_mod.escape(str(risk.get("description", ""))).replace("\r\n", "<br>").replace("\n", "<br>")
166
+ )
167
+ category_cell += (
168
+ f' <span class="risk-badge">RISK-{risk_id}'
169
+ f'<span class="risk-tooltip">'
170
+ f'<span class="risk-name">{risk_name}</span>'
171
+ f"{risk_desc}"
172
+ f"</span></span>"
173
+ )
174
+ output.write(
175
+ f"| **{idx + 1}** | `{item['host']}` | {severity_cell} | {category_cell} | {item['message']} |\n"
176
+ )
177
+ output.write("\n")
178
+
179
+ def finalize_analysis(self) -> None:
180
+ """
181
+ This function will be called after all GMD data is ingested and before generating the report.
182
+ You can do some final analysis here that requires all data to be available.
183
+ """
184
+
185
+ @abstractmethod
186
+ def review_results_markdown(self, output) -> None:
187
+ raise NotImplementedError("Subclasses should implement this method.")
188
+
189
+ def append_test_result(self, host: str, severity: SEVERITY, title: str, message: str) -> None:
190
+ self._test_result.append({"host": host, "severity": severity, "title": title, "message": message})
191
+
192
+ def append_test_results(self, items: list) -> None:
193
+ for item in items:
194
+ self.append_test_result(item["host"], item["severity"], item["title"], item["description"])
195
+
196
+ def watch_one(self, event: GmdEvents, func) -> None:
197
+ """
198
+ Fires when the specified event occurs.
199
+ The order of `watch_one` depends on the order of events in the GMD log, not the order of `watch_one` calls.
200
+ """
201
+ if event not in self._watched_events:
202
+ self._watched_events[event] = []
203
+ self._watched_events[event].append(func)
204
+
205
+ def watch_all(self, events: set[GmdEvents], func) -> None:
206
+ """
207
+ Fires when all the specified events occured.
208
+ `watch_all` fires after the last `watch_one` fires.
209
+ """
210
+ self._watched_all_events.append((events, func))
211
+
212
+ @property
213
+ def all_events_fired(self) -> bool:
214
+ """
215
+ Check if all watched events have been fired.
216
+ If not all events are fired, some data may be missing in the GMD.
217
+ """
218
+ return all(event in self._fired_events for event in self._watched_events)
@@ -0,0 +1,43 @@
1
+ """
2
+ Copyright (c) 2026 MongoDB Inc.
3
+
4
+ DISCLAIMER: THESE CODE SAMPLES ARE PROVIDED FOR EDUCATIONAL AND ILLUSTRATIVE PURPOSES ONLY,
5
+ TO DEMONSTRATE THE FUNCTIONALITY OF SPECIFIC MONGODB FEATURES.
6
+ THEY ARE NOT PRODUCTION-READY AND MAY LACK THE SECURITY HARDENING, ERROR HANDLING, AND TESTING REQUIRED FOR A LIVE ENVIRONMENT.
7
+ YOU ARE RESPONSIBLE FOR TESTING, VALIDATING, AND SECURING THIS CODE WITHIN YOUR OWN ENVIRONMENT BEFORE IMPLEMENTATION.
8
+ THIS MATERIAL IS PROVIDED "AS IS" WITHOUT WARRANTY OR LIABILITY.
9
+ """
10
+
11
+ from typing import Any
12
+
13
+ from mongo_x_ray.parsers.base_parser import BaseParser
14
+ from mongo_x_ray_gmd.gmd_items.base_item import BaseItem
15
+ from mongo_x_ray_gmd.shared import GmdEvents
16
+ from mongo_x_ray_hc.parsers.build_info_parser import BuildInfoParser
17
+ from mongo_x_ray_hc.rules.version_eol_rule import VersionEOLRule
18
+
19
+
20
+ class BuildInfoItem(BaseItem):
21
+ def __init__(self, output_folder: str, config, **kwargs):
22
+ super().__init__(output_folder, config, **kwargs)
23
+ self.name: str = "Build Information"
24
+ self._build_info: dict[str, Any] = {}
25
+ self._rules["version_eol"] = VersionEOLRule(config)
26
+
27
+ def get_build_info(block):
28
+ self._build_info = block.get("output", {})
29
+
30
+ def process_build_info():
31
+ test_result, _ = self._rules["version_eol"].apply(self._build_info, extra_info={"host": self._hostname})
32
+ self.append_test_results(test_result)
33
+ # self.captured_sample = self._build_info
34
+
35
+ self.watch_one(GmdEvents.SERVER_BUILD_INFO, get_build_info)
36
+ self.watch_all({GmdEvents.SERVER_BUILD_INFO, GmdEvents.HOST_INFO}, process_build_info)
37
+
38
+ def review_results_markdown(self, output) -> None:
39
+ data = self._build_info
40
+ assert data is not None, f"GMD subsection {GmdEvents.SERVER_BUILD_INFO.value} should be available for review."
41
+ parser: BaseParser = BuildInfoParser()
42
+ parsed_output = parser.markdown([(self._set_name, self._hostname, data)], caller=self.__class__.__name__)
43
+ output.write(parsed_output)