mcp-souschef 2.1.2__py3-none-any.whl → 2.2.0__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.1.2.dist-info → mcp_souschef-2.2.0.dist-info}/METADATA +36 -8
- mcp_souschef-2.2.0.dist-info/RECORD +31 -0
- souschef/assessment.py +448 -180
- souschef/cli.py +90 -0
- souschef/converters/playbook.py +43 -5
- souschef/converters/resource.py +146 -49
- souschef/core/__init__.py +22 -0
- souschef/core/errors.py +275 -0
- souschef/deployment.py +412 -100
- souschef/parsers/habitat.py +35 -6
- souschef/parsers/inspec.py +72 -34
- souschef/parsers/metadata.py +59 -23
- souschef/profiling.py +568 -0
- souschef/server.py +589 -149
- mcp_souschef-2.1.2.dist-info/RECORD +0 -29
- {mcp_souschef-2.1.2.dist-info → mcp_souschef-2.2.0.dist-info}/WHEEL +0 -0
- {mcp_souschef-2.1.2.dist-info → mcp_souschef-2.2.0.dist-info}/entry_points.txt +0 -0
- {mcp_souschef-2.1.2.dist-info → mcp_souschef-2.2.0.dist-info}/licenses/LICENSE +0 -0
souschef/parsers/habitat.py
CHANGED
|
@@ -205,6 +205,32 @@ def _extract_plan_exports(content: str, var_name: str) -> list[dict[str, str]]:
|
|
|
205
205
|
return exports
|
|
206
206
|
|
|
207
207
|
|
|
208
|
+
def _is_quote_blocked(
|
|
209
|
+
ch: str, in_single_quote: bool, in_double_quote: bool, in_backtick: bool
|
|
210
|
+
) -> bool:
|
|
211
|
+
"""Check if a quote character is blocked by other active quotes."""
|
|
212
|
+
if ch == "'":
|
|
213
|
+
return in_double_quote or in_backtick
|
|
214
|
+
if ch == '"':
|
|
215
|
+
return in_single_quote or in_backtick
|
|
216
|
+
if ch == "`":
|
|
217
|
+
return in_single_quote or in_double_quote
|
|
218
|
+
return False
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def _toggle_quote(
|
|
222
|
+
ch: str, in_single_quote: bool, in_double_quote: bool, in_backtick: bool
|
|
223
|
+
) -> tuple[bool, bool, bool]:
|
|
224
|
+
"""Toggle the appropriate quote state based on character."""
|
|
225
|
+
if ch == "'":
|
|
226
|
+
return not in_single_quote, in_double_quote, in_backtick
|
|
227
|
+
if ch == '"':
|
|
228
|
+
return in_single_quote, not in_double_quote, in_backtick
|
|
229
|
+
if ch == "`":
|
|
230
|
+
return in_single_quote, in_double_quote, not in_backtick
|
|
231
|
+
return in_single_quote, in_double_quote, in_backtick
|
|
232
|
+
|
|
233
|
+
|
|
208
234
|
def _update_quote_state(
|
|
209
235
|
ch: str,
|
|
210
236
|
in_single_quote: bool,
|
|
@@ -213,18 +239,21 @@ def _update_quote_state(
|
|
|
213
239
|
escape_next: bool,
|
|
214
240
|
) -> tuple[bool, bool, bool, bool]:
|
|
215
241
|
"""Update quote tracking state for shell script parsing."""
|
|
242
|
+
# Handle escape sequences
|
|
216
243
|
if escape_next:
|
|
217
244
|
return in_single_quote, in_double_quote, in_backtick, False
|
|
218
245
|
|
|
219
246
|
if ch == "\\":
|
|
220
247
|
return in_single_quote, in_double_quote, in_backtick, True
|
|
221
248
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
249
|
+
# Handle quote characters
|
|
250
|
+
if ch in ("'", '"', "`") and not _is_quote_blocked(
|
|
251
|
+
ch, in_single_quote, in_double_quote, in_backtick
|
|
252
|
+
):
|
|
253
|
+
single, double, backtick = _toggle_quote(
|
|
254
|
+
ch, in_single_quote, in_double_quote, in_backtick
|
|
255
|
+
)
|
|
256
|
+
return single, double, backtick, False
|
|
228
257
|
|
|
229
258
|
return in_single_quote, in_double_quote, in_backtick, False
|
|
230
259
|
|
souschef/parsers/inspec.py
CHANGED
|
@@ -616,6 +616,50 @@ def _generate_inspec_package_checks(
|
|
|
616
616
|
return lines
|
|
617
617
|
|
|
618
618
|
|
|
619
|
+
def _generate_inspec_resource_checks(
|
|
620
|
+
resource_type: str,
|
|
621
|
+
resource_name: str,
|
|
622
|
+
properties: dict[str, Any] | None = None,
|
|
623
|
+
custom_checks: list[str] | None = None,
|
|
624
|
+
) -> list[str]:
|
|
625
|
+
"""
|
|
626
|
+
Generate InSpec checks for a resource using a generic pattern.
|
|
627
|
+
|
|
628
|
+
Args:
|
|
629
|
+
resource_type: InSpec resource type (e.g., 'file', 'user', 'service').
|
|
630
|
+
resource_name: Name/path of the resource.
|
|
631
|
+
properties: Optional resource properties to check.
|
|
632
|
+
custom_checks: Optional list of custom check lines.
|
|
633
|
+
|
|
634
|
+
Returns:
|
|
635
|
+
List of InSpec check lines.
|
|
636
|
+
|
|
637
|
+
"""
|
|
638
|
+
lines = [f" describe {resource_type}('{resource_name}') do"]
|
|
639
|
+
|
|
640
|
+
# Add custom checks if provided
|
|
641
|
+
if custom_checks:
|
|
642
|
+
lines.extend(custom_checks)
|
|
643
|
+
else:
|
|
644
|
+
# Default: should exist
|
|
645
|
+
lines.append(INSPEC_SHOULD_EXIST)
|
|
646
|
+
|
|
647
|
+
# Add property checks
|
|
648
|
+
if properties:
|
|
649
|
+
property_map = {
|
|
650
|
+
"mode": lambda v: f" its('mode') {{ should cmp '{v}' }}",
|
|
651
|
+
"owner": lambda v: f" its('owner') {{ should eq '{v}' }}",
|
|
652
|
+
"group": lambda v: f" its('group') {{ should eq '{v}' }}",
|
|
653
|
+
"shell": lambda v: f" its('shell') {{ should eq '{v}' }}",
|
|
654
|
+
}
|
|
655
|
+
for prop, value in properties.items():
|
|
656
|
+
if prop in property_map:
|
|
657
|
+
lines.append(property_map[prop](value))
|
|
658
|
+
|
|
659
|
+
lines.append(INSPEC_END_INDENT)
|
|
660
|
+
return lines
|
|
661
|
+
|
|
662
|
+
|
|
619
663
|
def _generate_inspec_service_checks(resource_name: str) -> list[str]:
|
|
620
664
|
"""
|
|
621
665
|
Generate InSpec checks for service resource.
|
|
@@ -627,12 +671,14 @@ def _generate_inspec_service_checks(resource_name: str) -> list[str]:
|
|
|
627
671
|
List of InSpec check lines.
|
|
628
672
|
|
|
629
673
|
"""
|
|
630
|
-
return
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
674
|
+
return _generate_inspec_resource_checks(
|
|
675
|
+
"service",
|
|
676
|
+
resource_name,
|
|
677
|
+
custom_checks=[
|
|
678
|
+
" it { should be_running }",
|
|
679
|
+
" it { should be_enabled }",
|
|
680
|
+
],
|
|
681
|
+
)
|
|
636
682
|
|
|
637
683
|
|
|
638
684
|
def _generate_inspec_file_checks(
|
|
@@ -649,15 +695,11 @@ def _generate_inspec_file_checks(
|
|
|
649
695
|
List of InSpec check lines.
|
|
650
696
|
|
|
651
697
|
"""
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
if "group" in properties:
|
|
658
|
-
lines.append(f" its('group') {{ should eq '{properties['group']}' }}")
|
|
659
|
-
lines.append(INSPEC_END_INDENT)
|
|
660
|
-
return lines
|
|
698
|
+
return _generate_inspec_resource_checks(
|
|
699
|
+
"file",
|
|
700
|
+
resource_name,
|
|
701
|
+
properties=properties,
|
|
702
|
+
)
|
|
661
703
|
|
|
662
704
|
|
|
663
705
|
def _generate_inspec_directory_checks(
|
|
@@ -674,15 +716,15 @@ def _generate_inspec_directory_checks(
|
|
|
674
716
|
List of InSpec check lines.
|
|
675
717
|
|
|
676
718
|
"""
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
719
|
+
return _generate_inspec_resource_checks(
|
|
720
|
+
"file",
|
|
721
|
+
resource_name,
|
|
722
|
+
properties=properties,
|
|
723
|
+
custom_checks=[
|
|
724
|
+
INSPEC_SHOULD_EXIST,
|
|
725
|
+
" it { should be_directory }",
|
|
726
|
+
],
|
|
727
|
+
)
|
|
686
728
|
|
|
687
729
|
|
|
688
730
|
def _generate_inspec_user_checks(
|
|
@@ -699,11 +741,11 @@ def _generate_inspec_user_checks(
|
|
|
699
741
|
List of InSpec check lines.
|
|
700
742
|
|
|
701
743
|
"""
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
744
|
+
return _generate_inspec_resource_checks(
|
|
745
|
+
"user",
|
|
746
|
+
resource_name,
|
|
747
|
+
properties=properties,
|
|
748
|
+
)
|
|
707
749
|
|
|
708
750
|
|
|
709
751
|
def _generate_inspec_group_checks(resource_name: str) -> list[str]:
|
|
@@ -717,11 +759,7 @@ def _generate_inspec_group_checks(resource_name: str) -> list[str]:
|
|
|
717
759
|
List of InSpec check lines.
|
|
718
760
|
|
|
719
761
|
"""
|
|
720
|
-
return
|
|
721
|
-
f" describe group('{resource_name}') do",
|
|
722
|
-
INSPEC_SHOULD_EXIST,
|
|
723
|
-
INSPEC_END_INDENT,
|
|
724
|
-
]
|
|
762
|
+
return _generate_inspec_resource_checks("group", resource_name)
|
|
725
763
|
|
|
726
764
|
|
|
727
765
|
def _generate_inspec_from_resource(
|
souschef/parsers/metadata.py
CHANGED
|
@@ -45,6 +45,64 @@ def read_cookbook_metadata(path: str) -> str:
|
|
|
45
45
|
return f"An error occurred: {e}"
|
|
46
46
|
|
|
47
47
|
|
|
48
|
+
def _scan_cookbook_directory(
|
|
49
|
+
cookbook_path, dir_name: str
|
|
50
|
+
) -> tuple[str, list[str]] | None:
|
|
51
|
+
"""
|
|
52
|
+
Scan a single cookbook directory for files.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
cookbook_path: Path to the cookbook root.
|
|
56
|
+
dir_name: Name of the subdirectory to scan.
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
Tuple of (dir_name, files) if directory exists and has files, None otherwise.
|
|
60
|
+
|
|
61
|
+
"""
|
|
62
|
+
dir_path = _safe_join(cookbook_path, dir_name)
|
|
63
|
+
if not dir_path.exists() or not dir_path.is_dir():
|
|
64
|
+
return None
|
|
65
|
+
|
|
66
|
+
files = [f.name for f in dir_path.iterdir() if f.is_file()]
|
|
67
|
+
return (dir_name, files) if files else None
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _collect_cookbook_structure(cookbook_path) -> dict[str, list[str]]:
|
|
71
|
+
"""
|
|
72
|
+
Collect all standard cookbook directories and their files.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
cookbook_path: Path to the cookbook root.
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
Dictionary mapping directory names to file lists.
|
|
79
|
+
|
|
80
|
+
"""
|
|
81
|
+
structure = {}
|
|
82
|
+
common_dirs = [
|
|
83
|
+
"recipes",
|
|
84
|
+
"attributes",
|
|
85
|
+
"templates",
|
|
86
|
+
"files",
|
|
87
|
+
"resources",
|
|
88
|
+
"providers",
|
|
89
|
+
"libraries",
|
|
90
|
+
"definitions",
|
|
91
|
+
]
|
|
92
|
+
|
|
93
|
+
for dir_name in common_dirs:
|
|
94
|
+
result = _scan_cookbook_directory(cookbook_path, dir_name)
|
|
95
|
+
if result:
|
|
96
|
+
structure[result[0]] = result[1]
|
|
97
|
+
|
|
98
|
+
# Check for metadata.rb
|
|
99
|
+
metadata_path = _safe_join(cookbook_path, METADATA_FILENAME)
|
|
100
|
+
if metadata_path.exists():
|
|
101
|
+
structure["metadata"] = [METADATA_FILENAME]
|
|
102
|
+
|
|
103
|
+
return structure
|
|
104
|
+
|
|
105
|
+
|
|
48
106
|
def list_cookbook_structure(path: str) -> str:
|
|
49
107
|
"""
|
|
50
108
|
List the structure of a Chef cookbook directory.
|
|
@@ -62,29 +120,7 @@ def list_cookbook_structure(path: str) -> str:
|
|
|
62
120
|
if not cookbook_path.is_dir():
|
|
63
121
|
return f"Error: {path} is not a directory"
|
|
64
122
|
|
|
65
|
-
structure =
|
|
66
|
-
common_dirs = [
|
|
67
|
-
"recipes",
|
|
68
|
-
"attributes",
|
|
69
|
-
"templates",
|
|
70
|
-
"files",
|
|
71
|
-
"resources",
|
|
72
|
-
"providers",
|
|
73
|
-
"libraries",
|
|
74
|
-
"definitions",
|
|
75
|
-
]
|
|
76
|
-
|
|
77
|
-
for dir_name in common_dirs:
|
|
78
|
-
dir_path = _safe_join(cookbook_path, dir_name)
|
|
79
|
-
if dir_path.exists() and dir_path.is_dir():
|
|
80
|
-
files = [f.name for f in dir_path.iterdir() if f.is_file()]
|
|
81
|
-
if files:
|
|
82
|
-
structure[dir_name] = files
|
|
83
|
-
|
|
84
|
-
# Check for metadata.rb
|
|
85
|
-
metadata_path = _safe_join(cookbook_path, METADATA_FILENAME)
|
|
86
|
-
if metadata_path.exists():
|
|
87
|
-
structure["metadata"] = [METADATA_FILENAME]
|
|
123
|
+
structure = _collect_cookbook_structure(cookbook_path)
|
|
88
124
|
|
|
89
125
|
if not structure:
|
|
90
126
|
return f"Warning: No standard cookbook structure found in {path}"
|