mcp-souschef 2.1.2__py3-none-any.whl → 2.5.3__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.
@@ -1,7 +1,5 @@
1
1
  """Filesystem operations for Chef cookbook exploration."""
2
2
 
3
- from mcp.server.fastmcp import FastMCP
4
-
5
3
  from souschef.core.constants import (
6
4
  ERROR_FILE_NOT_FOUND,
7
5
  ERROR_IS_DIRECTORY,
@@ -9,11 +7,7 @@ from souschef.core.constants import (
9
7
  )
10
8
  from souschef.core.path_utils import _normalize_path
11
9
 
12
- # Get MCP instance to register tools
13
- mcp = FastMCP("souschef")
14
-
15
10
 
16
- @mcp.tool()
17
11
  def list_directory(path: str) -> list[str] | str:
18
12
  """
19
13
  List the contents of a directory.
@@ -40,7 +34,6 @@ def list_directory(path: str) -> list[str] | str:
40
34
  return f"An error occurred: {e}"
41
35
 
42
36
 
43
- @mcp.tool()
44
37
  def read_file(path: str) -> str:
45
38
  """
46
39
  Read the contents of a file.
@@ -13,7 +13,11 @@ from souschef.parsers.inspec import (
13
13
  generate_inspec_from_chef,
14
14
  parse_inspec_profile,
15
15
  )
16
- from souschef.parsers.metadata import list_cookbook_structure, read_cookbook_metadata
16
+ from souschef.parsers.metadata import (
17
+ list_cookbook_structure,
18
+ parse_cookbook_metadata,
19
+ read_cookbook_metadata,
20
+ )
17
21
  from souschef.parsers.recipe import parse_recipe
18
22
  from souschef.parsers.resource import parse_custom_resource
19
23
  from souschef.parsers.template import parse_template
@@ -24,6 +28,7 @@ __all__ = [
24
28
  "parse_attributes",
25
29
  "parse_custom_resource",
26
30
  "read_cookbook_metadata",
31
+ "parse_cookbook_metadata",
27
32
  "list_cookbook_structure",
28
33
  "parse_inspec_profile",
29
34
  "convert_inspec_to_test",
@@ -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
- if ch == "'" and not in_double_quote and not in_backtick:
223
- return not in_single_quote, in_double_quote, in_backtick, False
224
- if ch == '"' and not in_single_quote and not in_backtick:
225
- return in_single_quote, not in_double_quote, in_backtick, False
226
- if ch == "`" and not in_single_quote and not in_double_quote:
227
- return in_single_quote, in_double_quote, not in_backtick, False
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
 
@@ -8,6 +8,14 @@ from typing import Any
8
8
  from souschef.core.constants import ERROR_PREFIX, INSPEC_END_INDENT, INSPEC_SHOULD_EXIST
9
9
  from souschef.core.path_utils import _normalize_path, _safe_join
10
10
 
11
+ # Regex patterns used across converters
12
+ _VERSION_PATTERN = r"match\s+/([^/]+)/"
13
+ _MODE_PATTERN = r"cmp\s+'([^']+)'"
14
+ _OWNER_PATTERN = r"eq\s+['\"]([^'\"]+)['\"]"
15
+
16
+ # ServerSpec formatting constants
17
+ _SERVERSPEC_END = " end"
18
+
11
19
 
12
20
  def parse_inspec_profile(path: str) -> str:
13
21
  """
@@ -21,17 +29,32 @@ def parse_inspec_profile(path: str) -> str:
21
29
 
22
30
  """
23
31
  try:
32
+ # Validate input
33
+ if not path or not path.strip():
34
+ return (
35
+ "Error: Path cannot be empty\n\n"
36
+ "Suggestion: Provide a path to an InSpec profile directory "
37
+ "or control file"
38
+ )
39
+
24
40
  profile_path = _normalize_path(path)
25
41
 
26
42
  if not profile_path.exists():
27
- return f"Error: Path does not exist: {path}"
43
+ return (
44
+ f"Error: Path does not exist: {path}\n\n"
45
+ "Suggestion: Check that the path is correct and the InSpec "
46
+ "profile exists"
47
+ )
28
48
 
29
49
  if profile_path.is_dir():
30
50
  controls = _parse_controls_from_directory(profile_path)
31
51
  elif profile_path.is_file():
32
52
  controls = _parse_controls_from_file(profile_path)
33
53
  else:
34
- return f"Error: Invalid path type: {path}"
54
+ return (
55
+ f"Error: Invalid path type: {path}\n\n"
56
+ "Suggestion: Provide a directory or file path, not a special file type"
57
+ )
35
58
 
36
59
  return json.dumps(
37
60
  {
@@ -43,18 +66,19 @@ def parse_inspec_profile(path: str) -> str:
43
66
  )
44
67
 
45
68
  except (FileNotFoundError, RuntimeError) as e:
46
- return f"Error: {e}"
69
+ return f"Error: {e}\n\nSuggestion: Verify the path exists and is accessible"
47
70
  except Exception as e:
48
71
  return f"An error occurred while parsing InSpec profile: {e}"
49
72
 
50
73
 
51
- def convert_inspec_to_test(inspec_path: str, output_format: str = "testinfra") -> str:
74
+ def convert_inspec_to_test(inspec_path: str, output_format: str = "testinfra") -> str: # noqa: C901
52
75
  """
53
- Convert InSpec controls to Ansible test format.
76
+ Convert InSpec controls to test framework format.
54
77
 
55
78
  Args:
56
79
  inspec_path: Path to InSpec profile or control file.
57
- output_format: Output format ('testinfra' or 'ansible_assert').
80
+ output_format: Output format ('testinfra', 'ansible_assert',
81
+ 'serverspec', or 'goss').
58
82
 
59
83
  Returns:
60
84
  Converted test code or error message.
@@ -73,19 +97,51 @@ def convert_inspec_to_test(inspec_path: str, output_format: str = "testinfra") -
73
97
  controls = profile_data["controls"]
74
98
 
75
99
  if not controls:
76
- return "Warning: No controls found to convert"
100
+ return "Error: No controls found in InSpec profile"
77
101
 
78
102
  # Convert each control
79
- converted = []
80
- for control in controls:
81
- if output_format == "testinfra":
82
- converted.append(_convert_inspec_to_testinfra(control))
83
- elif output_format == "ansible_assert":
84
- converted.append(_convert_inspec_to_ansible_assert(control))
85
- else:
86
- return f"Error: Unsupported output format: {output_format}"
103
+ converted_tests = []
104
+
105
+ if output_format == "testinfra":
106
+ converted_tests.append("import pytest")
107
+ converted_tests.append("")
108
+ converted_tests.append("")
109
+ for control in controls:
110
+ test_code = _convert_inspec_to_testinfra(control)
111
+ converted_tests.append(test_code)
112
+
113
+ elif output_format == "ansible_assert":
114
+ converted_tests.append("---")
115
+ converted_tests.append("# Validation tasks converted from InSpec")
116
+ converted_tests.append("")
117
+ for control in controls:
118
+ assert_code = _convert_inspec_to_ansible_assert(control)
119
+ converted_tests.append(assert_code)
120
+ converted_tests.append("")
121
+
122
+ elif output_format == "serverspec":
123
+ converted_tests.append("# frozen_string_literal: true")
124
+ converted_tests.append("")
125
+ converted_tests.append("require 'serverspec'")
126
+ converted_tests.append("")
127
+ converted_tests.append("set :backend, :exec")
128
+ converted_tests.append("")
129
+ for control in controls:
130
+ serverspec_code = _convert_inspec_to_serverspec(control)
131
+ converted_tests.append(serverspec_code)
132
+
133
+ elif output_format == "goss":
134
+ # Goss uses a single YAML spec for all tests
135
+ return _convert_inspec_to_goss(controls)
87
136
 
88
- return "\n".join(converted)
137
+ else:
138
+ error_msg = (
139
+ f"Error: Unsupported format '{output_format}'. "
140
+ "Use 'testinfra', 'ansible_assert', 'serverspec', or 'goss'"
141
+ )
142
+ return error_msg
143
+
144
+ return "\n".join(converted_tests)
89
145
 
90
146
  except json.JSONDecodeError as e:
91
147
  return f"Error parsing InSpec result: {e}"
@@ -388,7 +444,7 @@ def _convert_package_to_testinfra(
388
444
  if "be_installed" in exp["matcher"]:
389
445
  lines.append(" assert pkg.is_installed")
390
446
  elif exp["type"] == "its" and exp["property"] == "version":
391
- version_match = re.search(r"match\s+/([^/]+)/", exp["matcher"])
447
+ version_match = re.search(_VERSION_PATTERN, exp["matcher"])
392
448
  if version_match:
393
449
  version = version_match.group(1)
394
450
  lines.append(f' assert pkg.version.startswith("{version}")')
@@ -500,6 +556,275 @@ def _convert_inspec_to_testinfra(control: dict[str, Any]) -> str:
500
556
  return "\n".join(lines)
501
557
 
502
558
 
559
+ def _convert_package_to_serverspec(
560
+ lines: list[str], resource_name: str, expectations: list[dict[str, Any]]
561
+ ) -> None:
562
+ """
563
+ Convert package resource to ServerSpec expectations.
564
+
565
+ Args:
566
+ lines: List to append test lines to.
567
+ resource_name: Name of the package.
568
+ expectations: List of InSpec expectations.
569
+
570
+ """
571
+ lines.append(f" describe package('{resource_name}') do")
572
+ for exp in expectations:
573
+ if "be_installed" in exp["matcher"]:
574
+ lines.append(" it { should be_installed }")
575
+ elif exp["type"] == "its" and exp["property"] == "version":
576
+ version_match = re.search(_VERSION_PATTERN, exp["matcher"])
577
+ if version_match:
578
+ version = version_match.group(1)
579
+ lines.append(f" its('version') {{ should match /{version}/ }}")
580
+ lines.append(_SERVERSPEC_END)
581
+
582
+
583
+ def _convert_service_to_serverspec(
584
+ lines: list[str], resource_name: str, expectations: list[dict[str, Any]]
585
+ ) -> None:
586
+ """
587
+ Convert service resource to ServerSpec expectations.
588
+
589
+ Args:
590
+ lines: List to append test lines to.
591
+ resource_name: Name of the service.
592
+ expectations: List of InSpec expectations.
593
+
594
+ """
595
+ lines.append(f" describe service('{resource_name}') do")
596
+ for exp in expectations:
597
+ if "be_running" in exp["matcher"]:
598
+ lines.append(" it { should be_running }")
599
+ elif "be_enabled" in exp["matcher"]:
600
+ lines.append(" it { should be_enabled }")
601
+ lines.append(_SERVERSPEC_END)
602
+
603
+
604
+ def _convert_file_to_serverspec(
605
+ lines: list[str], resource_name: str, expectations: list[dict[str, Any]]
606
+ ) -> None:
607
+ """
608
+ Convert file resource to ServerSpec expectations.
609
+
610
+ Args:
611
+ lines: List to append test lines to.
612
+ resource_name: Path to the file.
613
+ expectations: List of InSpec expectations.
614
+
615
+ """
616
+ lines.append(f" describe file('{resource_name}') do")
617
+ for exp in expectations:
618
+ if "exist" in exp["matcher"]:
619
+ lines.append(" it { should exist }")
620
+ elif exp["type"] == "its" and exp["property"] == "mode":
621
+ mode_match = re.search(_MODE_PATTERN, exp["matcher"])
622
+ if mode_match:
623
+ mode = mode_match.group(1)
624
+ lines.append(f" its('mode') {{ should cmp '{mode}' }}")
625
+ elif exp["type"] == "its" and exp["property"] == "owner":
626
+ owner_match = re.search(_OWNER_PATTERN, exp["matcher"])
627
+ if owner_match:
628
+ owner = owner_match.group(1)
629
+ lines.append(f" its('owner') {{ should eq '{owner}' }}")
630
+ lines.append(_SERVERSPEC_END)
631
+
632
+
633
+ def _convert_port_to_serverspec(
634
+ lines: list[str], resource_name: str, expectations: list[dict[str, Any]]
635
+ ) -> None:
636
+ """
637
+ Convert port resource to ServerSpec expectations.
638
+
639
+ Args:
640
+ lines: List to append test lines to.
641
+ resource_name: Port number or address.
642
+ expectations: List of InSpec expectations.
643
+
644
+ """
645
+ lines.append(f" describe port({resource_name}) do")
646
+ for exp in expectations:
647
+ if "be_listening" in exp["matcher"]:
648
+ lines.append(" it { should be_listening }")
649
+ lines.append(_SERVERSPEC_END)
650
+
651
+
652
+ def _convert_inspec_to_serverspec(control: dict[str, Any]) -> str:
653
+ """
654
+ Convert InSpec control to ServerSpec test.
655
+
656
+ Args:
657
+ control: Parsed InSpec control dictionary.
658
+
659
+ Returns:
660
+ ServerSpec test code as string.
661
+
662
+ """
663
+ lines = []
664
+
665
+ # Add describe block for the control
666
+ lines.append(f"describe '{control.get('title') or control['id']}' do")
667
+
668
+ # Convert each test within the control
669
+ for test in control["tests"]:
670
+ resource_type = test["resource_type"]
671
+ resource_name = test["resource_name"]
672
+ expectations = test["expectations"]
673
+
674
+ # Map InSpec resources to ServerSpec using dedicated converters
675
+ if resource_type == "package":
676
+ _convert_package_to_serverspec(lines, resource_name, expectations)
677
+ elif resource_type == "service":
678
+ _convert_service_to_serverspec(lines, resource_name, expectations)
679
+ elif resource_type == "file":
680
+ _convert_file_to_serverspec(lines, resource_name, expectations)
681
+ elif resource_type == "port":
682
+ _convert_port_to_serverspec(lines, resource_name, expectations)
683
+
684
+ lines.append("end")
685
+ lines.append("")
686
+ return "\n".join(lines)
687
+
688
+
689
+ def _convert_package_to_goss(expectations: list[dict[str, Any]]) -> dict[str, Any]:
690
+ """
691
+ Convert package resource to Goss specification.
692
+
693
+ Args:
694
+ expectations: List of InSpec expectations.
695
+
696
+ Returns:
697
+ Goss package specification dictionary.
698
+
699
+ """
700
+ spec: dict[str, Any] = {}
701
+ for exp in expectations:
702
+ if "be_installed" in exp["matcher"]:
703
+ spec["installed"] = True
704
+ elif exp["type"] == "its" and exp["property"] == "version":
705
+ version_match = re.search(_VERSION_PATTERN, exp["matcher"])
706
+ if version_match:
707
+ spec["versions"] = [version_match.group(1)]
708
+ return spec
709
+
710
+
711
+ def _convert_service_to_goss(expectations: list[dict[str, Any]]) -> dict[str, Any]:
712
+ """
713
+ Convert service resource to Goss specification.
714
+
715
+ Args:
716
+ expectations: List of InSpec expectations.
717
+
718
+ Returns:
719
+ Goss service specification dictionary.
720
+
721
+ """
722
+ spec: dict[str, Any] = {}
723
+ for exp in expectations:
724
+ if "be_running" in exp["matcher"]:
725
+ spec["running"] = True
726
+ elif "be_enabled" in exp["matcher"]:
727
+ spec["enabled"] = True
728
+ return spec
729
+
730
+
731
+ def _convert_file_to_goss(expectations: list[dict[str, Any]]) -> dict[str, Any]:
732
+ """
733
+ Convert file resource to Goss specification.
734
+
735
+ Args:
736
+ expectations: List of InSpec expectations.
737
+
738
+ Returns:
739
+ Goss file specification dictionary.
740
+
741
+ """
742
+ spec: dict[str, Any] = {}
743
+ for exp in expectations:
744
+ if "exist" in exp["matcher"]:
745
+ spec["exists"] = True
746
+ elif exp["type"] == "its" and exp["property"] == "mode":
747
+ mode_match = re.search(_MODE_PATTERN, exp["matcher"])
748
+ if mode_match:
749
+ spec["mode"] = mode_match.group(1)
750
+ elif exp["type"] == "its" and exp["property"] == "owner":
751
+ owner_match = re.search(_OWNER_PATTERN, exp["matcher"])
752
+ if owner_match:
753
+ spec["owner"] = owner_match.group(1)
754
+ return spec
755
+
756
+
757
+ def _convert_port_to_goss(expectations: list[dict[str, Any]]) -> dict[str, Any]:
758
+ """
759
+ Convert port resource to Goss specification.
760
+
761
+ Args:
762
+ expectations: List of InSpec expectations.
763
+
764
+ Returns:
765
+ Goss port specification dictionary.
766
+
767
+ """
768
+ spec: dict[str, Any] = {}
769
+ for exp in expectations:
770
+ if "be_listening" in exp["matcher"]:
771
+ spec["listening"] = True
772
+ return spec
773
+
774
+
775
+ def _convert_inspec_to_goss(controls: list[dict[str, Any]]) -> str:
776
+ """
777
+ Convert InSpec controls to Goss YAML specification.
778
+
779
+ Args:
780
+ controls: List of parsed InSpec control dictionaries.
781
+
782
+ Returns:
783
+ Goss specification as YAML string.
784
+
785
+ """
786
+ goss_spec: dict[str, dict[str, Any]] = {
787
+ "package": {},
788
+ "service": {},
789
+ "file": {},
790
+ "port": {},
791
+ }
792
+
793
+ # Process all controls and group by resource type
794
+ for control in controls:
795
+ for test in control["tests"]:
796
+ resource_type = test["resource_type"]
797
+ resource_name = test["resource_name"]
798
+ expectations = test["expectations"]
799
+
800
+ if resource_type == "package":
801
+ spec = _convert_package_to_goss(expectations)
802
+ goss_spec["package"][resource_name] = spec
803
+ elif resource_type == "service":
804
+ spec = _convert_service_to_goss(expectations)
805
+ goss_spec["service"][resource_name] = spec
806
+ elif resource_type == "file":
807
+ spec = _convert_file_to_goss(expectations)
808
+ goss_spec["file"][resource_name] = spec
809
+ elif resource_type == "port":
810
+ # Goss uses string format for ports
811
+ port_key = f"tcp://{resource_name}"
812
+ spec = _convert_port_to_goss(expectations)
813
+ goss_spec["port"][port_key] = spec
814
+
815
+ # Remove empty sections
816
+ goss_spec = {k: v for k, v in goss_spec.items() if v}
817
+
818
+ # Convert to YAML (using JSON for now, will need PyYAML for proper YAML)
819
+ try:
820
+ import yaml
821
+
822
+ return yaml.dump(goss_spec, default_flow_style=False, sort_keys=False)
823
+ except ImportError:
824
+ # Fallback to JSON if PyYAML not available
825
+ return json.dumps(goss_spec, indent=2)
826
+
827
+
503
828
  def _convert_package_to_ansible_assert(
504
829
  lines: list[str], resource_name: str, expectations: list[dict[str, Any]]
505
830
  ) -> None:
@@ -566,7 +891,7 @@ def _convert_inspec_to_ansible_assert(control: dict[str, Any]) -> str:
566
891
 
567
892
  """
568
893
  lines = [
569
- f"- name: Verify {control['title'] or control['id']}",
894
+ f"- name: Verify {control.get('title') or control['id']}",
570
895
  " ansible.builtin.assert:",
571
896
  " that:",
572
897
  ]
@@ -616,6 +941,50 @@ def _generate_inspec_package_checks(
616
941
  return lines
617
942
 
618
943
 
944
+ def _generate_inspec_resource_checks(
945
+ resource_type: str,
946
+ resource_name: str,
947
+ properties: dict[str, Any] | None = None,
948
+ custom_checks: list[str] | None = None,
949
+ ) -> list[str]:
950
+ """
951
+ Generate InSpec checks for a resource using a generic pattern.
952
+
953
+ Args:
954
+ resource_type: InSpec resource type (e.g., 'file', 'user', 'service').
955
+ resource_name: Name/path of the resource.
956
+ properties: Optional resource properties to check.
957
+ custom_checks: Optional list of custom check lines.
958
+
959
+ Returns:
960
+ List of InSpec check lines.
961
+
962
+ """
963
+ lines = [f" describe {resource_type}('{resource_name}') do"]
964
+
965
+ # Add custom checks if provided
966
+ if custom_checks:
967
+ lines.extend(custom_checks)
968
+ else:
969
+ # Default: should exist
970
+ lines.append(INSPEC_SHOULD_EXIST)
971
+
972
+ # Add property checks
973
+ if properties:
974
+ property_map = {
975
+ "mode": lambda v: f" its('mode') {{ should cmp '{v}' }}",
976
+ "owner": lambda v: f" its('owner') {{ should eq '{v}' }}",
977
+ "group": lambda v: f" its('group') {{ should eq '{v}' }}",
978
+ "shell": lambda v: f" its('shell') {{ should eq '{v}' }}",
979
+ }
980
+ for prop, value in properties.items():
981
+ if prop in property_map:
982
+ lines.append(property_map[prop](value))
983
+
984
+ lines.append(INSPEC_END_INDENT)
985
+ return lines
986
+
987
+
619
988
  def _generate_inspec_service_checks(resource_name: str) -> list[str]:
620
989
  """
621
990
  Generate InSpec checks for service resource.
@@ -627,12 +996,14 @@ def _generate_inspec_service_checks(resource_name: str) -> list[str]:
627
996
  List of InSpec check lines.
628
997
 
629
998
  """
630
- return [
631
- f" describe service('{resource_name}') do",
632
- " it { should be_running }",
633
- " it { should be_enabled }",
634
- INSPEC_END_INDENT,
635
- ]
999
+ return _generate_inspec_resource_checks(
1000
+ "service",
1001
+ resource_name,
1002
+ custom_checks=[
1003
+ " it { should be_running }",
1004
+ " it { should be_enabled }",
1005
+ ],
1006
+ )
636
1007
 
637
1008
 
638
1009
  def _generate_inspec_file_checks(
@@ -649,15 +1020,11 @@ def _generate_inspec_file_checks(
649
1020
  List of InSpec check lines.
650
1021
 
651
1022
  """
652
- lines = [f" describe file('{resource_name}') do", INSPEC_SHOULD_EXIST]
653
- if "mode" in properties:
654
- lines.append(f" its('mode') {{ should cmp '{properties['mode']}' }}")
655
- if "owner" in properties:
656
- lines.append(f" its('owner') {{ should eq '{properties['owner']}' }}")
657
- if "group" in properties:
658
- lines.append(f" its('group') {{ should eq '{properties['group']}' }}")
659
- lines.append(INSPEC_END_INDENT)
660
- return lines
1023
+ return _generate_inspec_resource_checks(
1024
+ "file",
1025
+ resource_name,
1026
+ properties=properties,
1027
+ )
661
1028
 
662
1029
 
663
1030
  def _generate_inspec_directory_checks(
@@ -674,15 +1041,15 @@ def _generate_inspec_directory_checks(
674
1041
  List of InSpec check lines.
675
1042
 
676
1043
  """
677
- lines = [
678
- f" describe file('{resource_name}') do",
679
- INSPEC_SHOULD_EXIST,
680
- " it { should be_directory }",
681
- ]
682
- if "mode" in properties:
683
- lines.append(f" its('mode') {{ should cmp '{properties['mode']}' }}")
684
- lines.append(INSPEC_END_INDENT)
685
- return lines
1044
+ return _generate_inspec_resource_checks(
1045
+ "file",
1046
+ resource_name,
1047
+ properties=properties,
1048
+ custom_checks=[
1049
+ INSPEC_SHOULD_EXIST,
1050
+ " it { should be_directory }",
1051
+ ],
1052
+ )
686
1053
 
687
1054
 
688
1055
  def _generate_inspec_user_checks(
@@ -699,11 +1066,11 @@ def _generate_inspec_user_checks(
699
1066
  List of InSpec check lines.
700
1067
 
701
1068
  """
702
- lines = [f" describe user('{resource_name}') do", INSPEC_SHOULD_EXIST]
703
- if "shell" in properties:
704
- lines.append(f" its('shell') {{ should eq '{properties['shell']}' }}")
705
- lines.append(INSPEC_END_INDENT)
706
- return lines
1069
+ return _generate_inspec_resource_checks(
1070
+ "user",
1071
+ resource_name,
1072
+ properties=properties,
1073
+ )
707
1074
 
708
1075
 
709
1076
  def _generate_inspec_group_checks(resource_name: str) -> list[str]:
@@ -717,11 +1084,7 @@ def _generate_inspec_group_checks(resource_name: str) -> list[str]:
717
1084
  List of InSpec check lines.
718
1085
 
719
1086
  """
720
- return [
721
- f" describe group('{resource_name}') do",
722
- INSPEC_SHOULD_EXIST,
723
- INSPEC_END_INDENT,
724
- ]
1087
+ return _generate_inspec_resource_checks("group", resource_name)
725
1088
 
726
1089
 
727
1090
  def _generate_inspec_from_resource(