mcp-souschef 2.0.1__py3-none-any.whl → 2.1.2__py3-none-any.whl
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.
- {mcp_souschef-2.0.1.dist-info → mcp_souschef-2.1.2.dist-info}/METADATA +427 -79
- mcp_souschef-2.1.2.dist-info/RECORD +29 -0
- souschef/__init__.py +17 -0
- souschef/assessment.py +1230 -0
- souschef/converters/__init__.py +23 -0
- souschef/converters/habitat.py +674 -0
- souschef/converters/playbook.py +1698 -0
- souschef/converters/resource.py +228 -0
- souschef/core/__init__.py +58 -0
- souschef/core/constants.py +145 -0
- souschef/core/path_utils.py +58 -0
- souschef/core/ruby_utils.py +39 -0
- souschef/core/validation.py +555 -0
- souschef/deployment.py +1594 -0
- souschef/filesystem/__init__.py +5 -0
- souschef/filesystem/operations.py +67 -0
- souschef/parsers/__init__.py +36 -0
- souschef/parsers/attributes.py +257 -0
- souschef/parsers/habitat.py +288 -0
- souschef/parsers/inspec.py +771 -0
- souschef/parsers/metadata.py +175 -0
- souschef/parsers/recipe.py +200 -0
- souschef/parsers/resource.py +170 -0
- souschef/parsers/template.py +342 -0
- souschef/server.py +1532 -7599
- mcp_souschef-2.0.1.dist-info/RECORD +0 -8
- {mcp_souschef-2.0.1.dist-info → mcp_souschef-2.1.2.dist-info}/WHEEL +0 -0
- {mcp_souschef-2.0.1.dist-info → mcp_souschef-2.1.2.dist-info}/entry_points.txt +0 -0
- {mcp_souschef-2.0.1.dist-info → mcp_souschef-2.1.2.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
"""Chef resource to Ansible task converter."""
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
import json
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from souschef.core.constants import ACTION_TO_STATE, RESOURCE_MAPPINGS
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _parse_properties(properties_str: str) -> dict[str, Any]:
|
|
11
|
+
"""
|
|
12
|
+
Parse properties string into a dictionary.
|
|
13
|
+
|
|
14
|
+
Args:
|
|
15
|
+
properties_str: String representation of properties dict.
|
|
16
|
+
|
|
17
|
+
Returns:
|
|
18
|
+
Dictionary of properties.
|
|
19
|
+
|
|
20
|
+
"""
|
|
21
|
+
if not properties_str:
|
|
22
|
+
return {}
|
|
23
|
+
try:
|
|
24
|
+
# Try ast.literal_eval first for safety
|
|
25
|
+
result = ast.literal_eval(properties_str)
|
|
26
|
+
if isinstance(result, dict):
|
|
27
|
+
return result
|
|
28
|
+
return {}
|
|
29
|
+
except (ValueError, SyntaxError):
|
|
30
|
+
# Fallback to eval if needed, but this is less safe
|
|
31
|
+
try:
|
|
32
|
+
result = eval(properties_str) # noqa: S307
|
|
33
|
+
if isinstance(result, dict):
|
|
34
|
+
return result
|
|
35
|
+
return {}
|
|
36
|
+
except Exception:
|
|
37
|
+
return {}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def convert_resource_to_task(
|
|
41
|
+
resource_type: str, resource_name: str, action: str = "create", properties: str = ""
|
|
42
|
+
) -> str:
|
|
43
|
+
"""
|
|
44
|
+
Convert a Chef resource to an Ansible task.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
resource_type: The Chef resource type (e.g., 'package', 'service').
|
|
48
|
+
resource_name: The name of the resource.
|
|
49
|
+
action: The Chef action (e.g., 'install', 'start', 'create').
|
|
50
|
+
Defaults to 'create'.
|
|
51
|
+
properties: Additional resource properties as a string representation.
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
YAML representation of the equivalent Ansible task.
|
|
55
|
+
|
|
56
|
+
"""
|
|
57
|
+
try:
|
|
58
|
+
task = _convert_chef_resource_to_ansible(
|
|
59
|
+
resource_type, resource_name, action, properties
|
|
60
|
+
)
|
|
61
|
+
return _format_ansible_task(task)
|
|
62
|
+
except Exception as e:
|
|
63
|
+
return f"An error occurred during conversion: {e}"
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _get_service_params(resource_name: str, action: str) -> dict[str, Any]:
|
|
67
|
+
"""
|
|
68
|
+
Get Ansible service module parameters.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
resource_name: Service name.
|
|
72
|
+
action: Chef action.
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
Dictionary of Ansible service parameters.
|
|
76
|
+
|
|
77
|
+
"""
|
|
78
|
+
params: dict[str, Any] = {"name": resource_name}
|
|
79
|
+
if action in ["enable", "start"]:
|
|
80
|
+
params["enabled"] = True
|
|
81
|
+
params["state"] = "started"
|
|
82
|
+
elif action in ["disable", "stop"]:
|
|
83
|
+
params["enabled"] = False
|
|
84
|
+
params["state"] = "stopped"
|
|
85
|
+
else:
|
|
86
|
+
params["state"] = ACTION_TO_STATE.get(action, action)
|
|
87
|
+
return params
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _get_file_params(
|
|
91
|
+
resource_name: str, action: str, resource_type: str
|
|
92
|
+
) -> dict[str, Any]:
|
|
93
|
+
"""
|
|
94
|
+
Get Ansible file module parameters.
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
resource_name: File/directory path.
|
|
98
|
+
action: Chef action.
|
|
99
|
+
resource_type: Type of file resource (file/directory/template).
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
Dictionary of Ansible file parameters.
|
|
103
|
+
|
|
104
|
+
"""
|
|
105
|
+
params: dict[str, Any] = {}
|
|
106
|
+
|
|
107
|
+
if resource_type == "template":
|
|
108
|
+
params["src"] = resource_name
|
|
109
|
+
params["dest"] = resource_name.replace(".erb", "")
|
|
110
|
+
if action == "create":
|
|
111
|
+
params["mode"] = "0644"
|
|
112
|
+
elif resource_type == "file":
|
|
113
|
+
params["path"] = resource_name
|
|
114
|
+
if action == "create":
|
|
115
|
+
params["state"] = "file"
|
|
116
|
+
params["mode"] = "0644"
|
|
117
|
+
else:
|
|
118
|
+
params["state"] = ACTION_TO_STATE.get(action, action)
|
|
119
|
+
elif resource_type == "directory":
|
|
120
|
+
params["path"] = resource_name
|
|
121
|
+
params["state"] = "directory"
|
|
122
|
+
if action == "create":
|
|
123
|
+
params["mode"] = "0755"
|
|
124
|
+
|
|
125
|
+
return params
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _convert_chef_resource_to_ansible(
|
|
129
|
+
resource_type: str, resource_name: str, action: str, properties: str
|
|
130
|
+
) -> dict[str, Any]:
|
|
131
|
+
"""
|
|
132
|
+
Convert Chef resource to Ansible task dictionary.
|
|
133
|
+
|
|
134
|
+
Args:
|
|
135
|
+
resource_type: The Chef resource type.
|
|
136
|
+
resource_name: The name of the resource.
|
|
137
|
+
action: The Chef action.
|
|
138
|
+
properties: Additional properties string.
|
|
139
|
+
|
|
140
|
+
Returns:
|
|
141
|
+
Dictionary representing an Ansible task.
|
|
142
|
+
|
|
143
|
+
"""
|
|
144
|
+
# Get Ansible module name
|
|
145
|
+
ansible_module = RESOURCE_MAPPINGS.get(resource_type, f"# Unknown: {resource_type}")
|
|
146
|
+
|
|
147
|
+
# Start building the task
|
|
148
|
+
task: dict[str, Any] = {
|
|
149
|
+
"name": f"{action.capitalize()} {resource_type} {resource_name}",
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
# Build module parameters based on resource type
|
|
153
|
+
module_params: dict[str, Any] = {}
|
|
154
|
+
|
|
155
|
+
# Parse properties if provided
|
|
156
|
+
props = _parse_properties(properties)
|
|
157
|
+
|
|
158
|
+
if resource_type == "package":
|
|
159
|
+
module_params["name"] = resource_name
|
|
160
|
+
module_params["state"] = ACTION_TO_STATE.get(action, action)
|
|
161
|
+
elif resource_type in ["service", "systemd_unit"]:
|
|
162
|
+
module_params = _get_service_params(resource_name, action)
|
|
163
|
+
elif resource_type in ["template", "file", "directory"]:
|
|
164
|
+
module_params = _get_file_params(resource_name, action, resource_type)
|
|
165
|
+
elif resource_type in ["execute", "bash"]:
|
|
166
|
+
module_params["cmd"] = resource_name
|
|
167
|
+
task["changed_when"] = "false"
|
|
168
|
+
elif resource_type in ["user", "group"]:
|
|
169
|
+
module_params["name"] = resource_name
|
|
170
|
+
module_params["state"] = ACTION_TO_STATE.get(action, "present")
|
|
171
|
+
elif resource_type == "remote_file":
|
|
172
|
+
module_params["dest"] = resource_name
|
|
173
|
+
if "source" in props:
|
|
174
|
+
module_params["url"] = props["source"]
|
|
175
|
+
if "mode" in props:
|
|
176
|
+
module_params["mode"] = props["mode"]
|
|
177
|
+
if "owner" in props:
|
|
178
|
+
module_params["owner"] = props["owner"]
|
|
179
|
+
if "group" in props:
|
|
180
|
+
module_params["group"] = props["group"]
|
|
181
|
+
if "checksum" in props:
|
|
182
|
+
module_params["checksum"] = props["checksum"]
|
|
183
|
+
else:
|
|
184
|
+
module_params["name"] = resource_name
|
|
185
|
+
if action in ACTION_TO_STATE:
|
|
186
|
+
module_params["state"] = ACTION_TO_STATE[action]
|
|
187
|
+
|
|
188
|
+
task[ansible_module] = module_params
|
|
189
|
+
return task
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _format_yaml_value(value: Any) -> str:
|
|
193
|
+
"""Format a value for YAML output."""
|
|
194
|
+
if isinstance(value, str):
|
|
195
|
+
return f'"{value}"'
|
|
196
|
+
return json.dumps(value)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def _format_dict_value(key: str, value: dict[str, Any]) -> list[str]:
|
|
200
|
+
"""Format a dictionary value for YAML output."""
|
|
201
|
+
lines = [f" {key}:"]
|
|
202
|
+
for param_key, param_value in value.items():
|
|
203
|
+
lines.append(f" {param_key}: {_format_yaml_value(param_value)}")
|
|
204
|
+
return lines
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def _format_ansible_task(task: dict[str, Any]) -> str:
|
|
208
|
+
"""
|
|
209
|
+
Format an Ansible task dictionary as YAML.
|
|
210
|
+
|
|
211
|
+
Args:
|
|
212
|
+
task: Dictionary representing an Ansible task.
|
|
213
|
+
|
|
214
|
+
Returns:
|
|
215
|
+
YAML-formatted string.
|
|
216
|
+
|
|
217
|
+
"""
|
|
218
|
+
result = ["- name: " + task["name"]]
|
|
219
|
+
|
|
220
|
+
for key, value in task.items():
|
|
221
|
+
if key == "name":
|
|
222
|
+
continue
|
|
223
|
+
if isinstance(value, dict):
|
|
224
|
+
result.extend(_format_dict_value(key, value))
|
|
225
|
+
else:
|
|
226
|
+
result.append(f" {key}: {_format_yaml_value(value)}")
|
|
227
|
+
|
|
228
|
+
return "\n".join(result)
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""Core utilities for SousChef."""
|
|
2
|
+
|
|
3
|
+
from souschef.core.constants import (
|
|
4
|
+
ACTION_TO_STATE,
|
|
5
|
+
ANSIBLE_SERVICE_MODULE,
|
|
6
|
+
CHEF_RECIPE_PREFIX,
|
|
7
|
+
CHEF_ROLE_PREFIX,
|
|
8
|
+
ERB_PATTERNS,
|
|
9
|
+
ERROR_FILE_NOT_FOUND,
|
|
10
|
+
ERROR_IS_DIRECTORY,
|
|
11
|
+
ERROR_PERMISSION_DENIED,
|
|
12
|
+
ERROR_PREFIX,
|
|
13
|
+
INSPEC_END_INDENT,
|
|
14
|
+
INSPEC_SHOULD_EXIST,
|
|
15
|
+
JINJA2_ELIF,
|
|
16
|
+
JINJA2_ELSE,
|
|
17
|
+
JINJA2_ENDIF,
|
|
18
|
+
JINJA2_FOR,
|
|
19
|
+
JINJA2_IF_NOT,
|
|
20
|
+
JINJA2_IF_START,
|
|
21
|
+
JINJA2_NODE_ATTR_REPLACEMENT,
|
|
22
|
+
JINJA2_VAR_REPLACEMENT,
|
|
23
|
+
METADATA_FILENAME,
|
|
24
|
+
NODE_PREFIX,
|
|
25
|
+
REGEX_ERB_CONDITION,
|
|
26
|
+
REGEX_ERB_EACH,
|
|
27
|
+
REGEX_ERB_ELSE,
|
|
28
|
+
REGEX_ERB_ELSIF,
|
|
29
|
+
REGEX_ERB_END,
|
|
30
|
+
REGEX_ERB_IF_START,
|
|
31
|
+
REGEX_ERB_NODE_ATTR,
|
|
32
|
+
REGEX_ERB_OUTPUT,
|
|
33
|
+
REGEX_ERB_UNLESS,
|
|
34
|
+
REGEX_QUOTE_DO_END,
|
|
35
|
+
REGEX_RESOURCE_BRACKET,
|
|
36
|
+
REGEX_RUBY_INTERPOLATION,
|
|
37
|
+
REGEX_WHITESPACE_QUOTE,
|
|
38
|
+
REGEX_WORD_SYMBOLS,
|
|
39
|
+
RESOURCE_MAPPINGS,
|
|
40
|
+
)
|
|
41
|
+
from souschef.core.path_utils import _normalize_path, _safe_join
|
|
42
|
+
from souschef.core.ruby_utils import _normalize_ruby_value
|
|
43
|
+
from souschef.core.validation import (
|
|
44
|
+
ValidationCategory,
|
|
45
|
+
ValidationEngine,
|
|
46
|
+
ValidationLevel,
|
|
47
|
+
ValidationResult,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
__all__ = [
|
|
51
|
+
"_normalize_path",
|
|
52
|
+
"_normalize_ruby_value",
|
|
53
|
+
"_safe_join",
|
|
54
|
+
"ValidationCategory",
|
|
55
|
+
"ValidationEngine",
|
|
56
|
+
"ValidationLevel",
|
|
57
|
+
"ValidationResult",
|
|
58
|
+
]
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"""Constants used throughout SousChef."""
|
|
2
|
+
|
|
3
|
+
__all__ = [
|
|
4
|
+
"ANSIBLE_SERVICE_MODULE",
|
|
5
|
+
"METADATA_FILENAME",
|
|
6
|
+
"ERROR_PREFIX",
|
|
7
|
+
"NODE_PREFIX",
|
|
8
|
+
"CHEF_RECIPE_PREFIX",
|
|
9
|
+
"CHEF_ROLE_PREFIX",
|
|
10
|
+
"REGEX_WHITESPACE_QUOTE",
|
|
11
|
+
"REGEX_QUOTE_DO_END",
|
|
12
|
+
"REGEX_RESOURCE_BRACKET",
|
|
13
|
+
"REGEX_ERB_OUTPUT",
|
|
14
|
+
"REGEX_ERB_CONDITION",
|
|
15
|
+
"REGEX_ERB_NODE_ATTR",
|
|
16
|
+
"REGEX_ERB_IF_START",
|
|
17
|
+
"REGEX_ERB_UNLESS",
|
|
18
|
+
"REGEX_ERB_ELSE",
|
|
19
|
+
"REGEX_ERB_ELSIF",
|
|
20
|
+
"REGEX_ERB_END",
|
|
21
|
+
"REGEX_ERB_EACH",
|
|
22
|
+
"REGEX_WORD_SYMBOLS",
|
|
23
|
+
"REGEX_RUBY_INTERPOLATION",
|
|
24
|
+
"JINJA2_VAR_REPLACEMENT",
|
|
25
|
+
"JINJA2_NODE_ATTR_REPLACEMENT",
|
|
26
|
+
"JINJA2_IF_START",
|
|
27
|
+
"JINJA2_IF_NOT",
|
|
28
|
+
"JINJA2_ELSE",
|
|
29
|
+
"JINJA2_ELIF",
|
|
30
|
+
"JINJA2_ENDIF",
|
|
31
|
+
"JINJA2_FOR",
|
|
32
|
+
"ERB_PATTERNS",
|
|
33
|
+
"INSPEC_END_INDENT",
|
|
34
|
+
"INSPEC_SHOULD_EXIST",
|
|
35
|
+
"ERROR_FILE_NOT_FOUND",
|
|
36
|
+
"ERROR_IS_DIRECTORY",
|
|
37
|
+
"ERROR_PERMISSION_DENIED",
|
|
38
|
+
"RESOURCE_MAPPINGS",
|
|
39
|
+
"ACTION_TO_STATE",
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
# Ansible module names
|
|
43
|
+
ANSIBLE_SERVICE_MODULE = "ansible.builtin.service"
|
|
44
|
+
|
|
45
|
+
# File names
|
|
46
|
+
METADATA_FILENAME = "metadata.rb"
|
|
47
|
+
|
|
48
|
+
# Common prefixes
|
|
49
|
+
ERROR_PREFIX = "Error:"
|
|
50
|
+
NODE_PREFIX = "node["
|
|
51
|
+
CHEF_RECIPE_PREFIX = "recipe["
|
|
52
|
+
CHEF_ROLE_PREFIX = "role["
|
|
53
|
+
|
|
54
|
+
# Regular expression patterns for Ruby/ERB parsing
|
|
55
|
+
REGEX_WHITESPACE_QUOTE = r"\s+['\"]?"
|
|
56
|
+
REGEX_QUOTE_DO_END = r"['\"]?\s+do\s*([^\n]{0,15000})\nend"
|
|
57
|
+
REGEX_RESOURCE_BRACKET = r"(\w+)\[([^\]]+)\]"
|
|
58
|
+
REGEX_ERB_OUTPUT = r"<%=\s*([^%]{1,200}?)\s*%>"
|
|
59
|
+
REGEX_ERB_CONDITION = r"[^%]{1,200}?"
|
|
60
|
+
REGEX_ERB_NODE_ATTR = rf"<%=\s*node\[(['\"])({REGEX_ERB_CONDITION})\1\]\s*%>"
|
|
61
|
+
REGEX_ERB_IF_START = rf"<%\s*if\s+({REGEX_ERB_CONDITION})\s*%>"
|
|
62
|
+
REGEX_ERB_UNLESS = rf"<%\s*unless\s+({REGEX_ERB_CONDITION})\s*%>"
|
|
63
|
+
REGEX_ERB_ELSE = r"<%\s*else\s*%>"
|
|
64
|
+
REGEX_ERB_ELSIF = rf"<%\s*elsif\s+({REGEX_ERB_CONDITION})\s*%>"
|
|
65
|
+
REGEX_ERB_END = r"<%\s*end\s*%>"
|
|
66
|
+
REGEX_ERB_EACH = rf"<%\s*({REGEX_ERB_CONDITION})\.each\s+do\s+\|(\w+)\|\s*%>"
|
|
67
|
+
REGEX_WORD_SYMBOLS = r"[\w.\[\]'\"]+"
|
|
68
|
+
REGEX_RUBY_INTERPOLATION = r"#\{([^}]+)\}"
|
|
69
|
+
|
|
70
|
+
# Jinja2 template replacements
|
|
71
|
+
JINJA2_VAR_REPLACEMENT = r"{{ \1 }}"
|
|
72
|
+
JINJA2_NODE_ATTR_REPLACEMENT = r"{{ \2 }}"
|
|
73
|
+
JINJA2_IF_START = r"{% if \1 %}"
|
|
74
|
+
JINJA2_IF_NOT = r"{% if not \1 %}"
|
|
75
|
+
JINJA2_ELSE = r"{% else %}"
|
|
76
|
+
JINJA2_ELIF = r"{% elif \1 %}"
|
|
77
|
+
JINJA2_ENDIF = r"{% endif %}"
|
|
78
|
+
JINJA2_FOR = r"{% for \2 in \1 %}"
|
|
79
|
+
|
|
80
|
+
# ERB to Jinja2 pattern mappings
|
|
81
|
+
ERB_PATTERNS = {
|
|
82
|
+
# Variable output: <%= var %> -> {{ var }}
|
|
83
|
+
"output": (REGEX_ERB_OUTPUT, JINJA2_VAR_REPLACEMENT),
|
|
84
|
+
# Variable with node prefix: <%= node['attr'] %> -> {{ attr }}
|
|
85
|
+
"node_attr": (REGEX_ERB_NODE_ATTR, JINJA2_NODE_ATTR_REPLACEMENT),
|
|
86
|
+
# If statements: <% if condition %> -> {% if condition %}
|
|
87
|
+
"if_start": (REGEX_ERB_IF_START, JINJA2_IF_START),
|
|
88
|
+
# Unless (negated if): <% unless condition %> -> {% if not condition %}
|
|
89
|
+
"unless": (REGEX_ERB_UNLESS, JINJA2_IF_NOT),
|
|
90
|
+
# Else: <% else %> -> {% else %}
|
|
91
|
+
"else": (REGEX_ERB_ELSE, JINJA2_ELSE),
|
|
92
|
+
# Elsif: <% elsif condition %> -> {% elif condition %}
|
|
93
|
+
"elsif": (REGEX_ERB_ELSIF, JINJA2_ELIF),
|
|
94
|
+
# End: <% end %> -> {% endif %}
|
|
95
|
+
"end": (REGEX_ERB_END, JINJA2_ENDIF),
|
|
96
|
+
# Each loop: <% array.each do |item| %> -> {% for item in array %}
|
|
97
|
+
# Use [^%]{1,200} to prevent matching across %> boundaries and ReDoS
|
|
98
|
+
"each": (REGEX_ERB_EACH, JINJA2_FOR),
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
# InSpec output formatting
|
|
102
|
+
INSPEC_END_INDENT = " end"
|
|
103
|
+
INSPEC_SHOULD_EXIST = " it { should exist }"
|
|
104
|
+
|
|
105
|
+
# Error message templates
|
|
106
|
+
ERROR_FILE_NOT_FOUND = "Error: File not found at {path}"
|
|
107
|
+
ERROR_IS_DIRECTORY = "Error: {path} is a directory, not a file"
|
|
108
|
+
ERROR_PERMISSION_DENIED = "Error: Permission denied for {path}"
|
|
109
|
+
|
|
110
|
+
# Chef resource to Ansible module mappings
|
|
111
|
+
RESOURCE_MAPPINGS = {
|
|
112
|
+
"package": "ansible.builtin.package",
|
|
113
|
+
"apt_package": "ansible.builtin.apt",
|
|
114
|
+
"yum_package": "ansible.builtin.yum",
|
|
115
|
+
"service": ANSIBLE_SERVICE_MODULE,
|
|
116
|
+
"systemd_unit": "ansible.builtin.systemd",
|
|
117
|
+
"template": "ansible.builtin.template",
|
|
118
|
+
"file": "ansible.builtin.file",
|
|
119
|
+
"directory": "ansible.builtin.file",
|
|
120
|
+
"execute": "ansible.builtin.command",
|
|
121
|
+
"bash": "ansible.builtin.shell",
|
|
122
|
+
"script": "ansible.builtin.script",
|
|
123
|
+
"user": "ansible.builtin.user",
|
|
124
|
+
"group": "ansible.builtin.group",
|
|
125
|
+
"cron": "ansible.builtin.cron",
|
|
126
|
+
"mount": "ansible.builtin.mount",
|
|
127
|
+
"git": "ansible.builtin.git",
|
|
128
|
+
"remote_file": "ansible.builtin.get_url",
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
# Chef action to Ansible state mappings
|
|
132
|
+
ACTION_TO_STATE = {
|
|
133
|
+
"create": "present",
|
|
134
|
+
"delete": "absent",
|
|
135
|
+
"remove": "absent",
|
|
136
|
+
"install": "present",
|
|
137
|
+
"upgrade": "latest",
|
|
138
|
+
"purge": "absent",
|
|
139
|
+
"enable": "started",
|
|
140
|
+
"disable": "stopped",
|
|
141
|
+
"start": "started",
|
|
142
|
+
"stop": "stopped",
|
|
143
|
+
"restart": "restarted",
|
|
144
|
+
"reload": "reloaded",
|
|
145
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""Path utility functions for safe filesystem operations."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def _normalize_path(path_str: str) -> Path:
|
|
7
|
+
"""
|
|
8
|
+
Normalize a file path for safe filesystem operations.
|
|
9
|
+
|
|
10
|
+
This function resolves relative paths and symlinks to absolute paths,
|
|
11
|
+
preventing path traversal attacks (CWE-23). Note: This MCP server
|
|
12
|
+
intentionally allows full filesystem access as it runs in the user's
|
|
13
|
+
local environment with their permissions.
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
path_str: Path string to normalize.
|
|
17
|
+
|
|
18
|
+
Returns:
|
|
19
|
+
Resolved absolute Path object.
|
|
20
|
+
|
|
21
|
+
Raises:
|
|
22
|
+
ValueError: If the path contains null bytes or is invalid.
|
|
23
|
+
|
|
24
|
+
"""
|
|
25
|
+
if "\x00" in path_str:
|
|
26
|
+
raise ValueError(f"Path contains null bytes: {path_str!r}")
|
|
27
|
+
|
|
28
|
+
try:
|
|
29
|
+
# Resolve to absolute path, removing .., ., and resolving symlinks
|
|
30
|
+
# This is the path normalization function itself that validates input
|
|
31
|
+
# lgtm[py/path-injection]
|
|
32
|
+
# codeql[py/path-injection]
|
|
33
|
+
return Path(path_str).resolve()
|
|
34
|
+
except (OSError, RuntimeError) as e:
|
|
35
|
+
raise ValueError(f"Invalid path {path_str}: {e}") from e
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _safe_join(base_path: Path, *parts: str) -> Path:
|
|
39
|
+
"""
|
|
40
|
+
Safely join path components ensuring result stays within base directory.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
base_path: Normalized base path.
|
|
44
|
+
*parts: Path components to join.
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
Joined path within base_path.
|
|
48
|
+
|
|
49
|
+
Raises:
|
|
50
|
+
ValueError: If result would escape base_path.
|
|
51
|
+
|
|
52
|
+
"""
|
|
53
|
+
result = base_path.joinpath(*parts).resolve()
|
|
54
|
+
try:
|
|
55
|
+
result.relative_to(base_path)
|
|
56
|
+
return result
|
|
57
|
+
except ValueError as e:
|
|
58
|
+
raise ValueError(f"Path traversal attempt: {parts} escapes {base_path}") from e
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Ruby value normalization utilities for Chef-to-Ansible conversion.
|
|
3
|
+
|
|
4
|
+
This module provides utilities for normalizing Ruby values and syntax
|
|
5
|
+
during the conversion process from Chef to Ansible.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import re
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _normalize_ruby_value(value: str) -> str:
|
|
12
|
+
"""
|
|
13
|
+
Normalize Ruby value representation.
|
|
14
|
+
|
|
15
|
+
Converts Ruby-specific syntax to a normalized string representation
|
|
16
|
+
suitable for Ansible playbooks.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
value: Raw Ruby value string.
|
|
20
|
+
|
|
21
|
+
Returns:
|
|
22
|
+
Normalized value string.
|
|
23
|
+
|
|
24
|
+
Examples:
|
|
25
|
+
>>> _normalize_ruby_value(":symbol")
|
|
26
|
+
'"symbol"'
|
|
27
|
+
>>> _normalize_ruby_value("[:a, :b]")
|
|
28
|
+
'["a", "b"]'
|
|
29
|
+
|
|
30
|
+
"""
|
|
31
|
+
value = value.strip()
|
|
32
|
+
# Handle symbols: :symbol -> "symbol"
|
|
33
|
+
if value.startswith(":") and value[1:].replace("_", "").isalnum():
|
|
34
|
+
return f'"{value[1:]}"'
|
|
35
|
+
# Handle arrays: [:a, :b] -> ["a", "b"]
|
|
36
|
+
if value.startswith("[") and value.endswith("]"):
|
|
37
|
+
# Simple symbol array conversion
|
|
38
|
+
value = re.sub(r":(\w+)", r'"\1"', value)
|
|
39
|
+
return value
|