contentctl 5.0.0a0__py3-none-any.whl → 5.0.0a3__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.
Files changed (107) hide show
  1. contentctl/__init__.py +1 -1
  2. contentctl/actions/build.py +88 -55
  3. contentctl/actions/deploy_acs.py +29 -24
  4. contentctl/actions/detection_testing/DetectionTestingManager.py +66 -41
  5. contentctl/actions/detection_testing/GitService.py +134 -76
  6. contentctl/actions/detection_testing/generate_detection_coverage_badge.py +48 -30
  7. contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructure.py +163 -124
  8. contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructureContainer.py +45 -32
  9. contentctl/actions/detection_testing/progress_bar.py +3 -0
  10. contentctl/actions/detection_testing/views/DetectionTestingView.py +15 -18
  11. contentctl/actions/detection_testing/views/DetectionTestingViewCLI.py +1 -5
  12. contentctl/actions/detection_testing/views/DetectionTestingViewFile.py +2 -2
  13. contentctl/actions/detection_testing/views/DetectionTestingViewWeb.py +1 -4
  14. contentctl/actions/doc_gen.py +9 -5
  15. contentctl/actions/initialize.py +45 -33
  16. contentctl/actions/inspect.py +118 -61
  17. contentctl/actions/new_content.py +78 -50
  18. contentctl/actions/release_notes.py +276 -146
  19. contentctl/actions/reporting.py +23 -19
  20. contentctl/actions/test.py +31 -25
  21. contentctl/actions/validate.py +54 -34
  22. contentctl/api.py +54 -45
  23. contentctl/contentctl.py +12 -13
  24. contentctl/enrichments/attack_enrichment.py +112 -72
  25. contentctl/enrichments/cve_enrichment.py +34 -28
  26. contentctl/enrichments/splunk_app_enrichment.py +38 -36
  27. contentctl/helper/link_validator.py +101 -78
  28. contentctl/helper/splunk_app.py +69 -41
  29. contentctl/helper/utils.py +58 -39
  30. contentctl/input/director.py +69 -37
  31. contentctl/input/new_content_questions.py +26 -34
  32. contentctl/input/yml_reader.py +22 -17
  33. contentctl/objects/abstract_security_content_objects/detection_abstract.py +250 -314
  34. contentctl/objects/abstract_security_content_objects/security_content_object_abstract.py +58 -36
  35. contentctl/objects/alert_action.py +8 -8
  36. contentctl/objects/annotated_types.py +1 -1
  37. contentctl/objects/atomic.py +64 -54
  38. contentctl/objects/base_test.py +2 -1
  39. contentctl/objects/base_test_result.py +16 -8
  40. contentctl/objects/baseline.py +41 -30
  41. contentctl/objects/baseline_tags.py +29 -22
  42. contentctl/objects/config.py +772 -560
  43. contentctl/objects/constants.py +29 -58
  44. contentctl/objects/correlation_search.py +75 -55
  45. contentctl/objects/dashboard.py +55 -41
  46. contentctl/objects/data_source.py +13 -13
  47. contentctl/objects/deployment.py +44 -37
  48. contentctl/objects/deployment_email.py +1 -1
  49. contentctl/objects/deployment_notable.py +2 -1
  50. contentctl/objects/deployment_phantom.py +5 -5
  51. contentctl/objects/deployment_rba.py +1 -1
  52. contentctl/objects/deployment_scheduling.py +1 -1
  53. contentctl/objects/deployment_slack.py +1 -1
  54. contentctl/objects/detection.py +5 -2
  55. contentctl/objects/detection_metadata.py +1 -0
  56. contentctl/objects/detection_stanza.py +7 -2
  57. contentctl/objects/detection_tags.py +54 -64
  58. contentctl/objects/drilldown.py +66 -35
  59. contentctl/objects/enums.py +61 -43
  60. contentctl/objects/errors.py +16 -24
  61. contentctl/objects/integration_test.py +3 -3
  62. contentctl/objects/integration_test_result.py +1 -0
  63. contentctl/objects/investigation.py +41 -26
  64. contentctl/objects/investigation_tags.py +29 -17
  65. contentctl/objects/lookup.py +234 -113
  66. contentctl/objects/macro.py +55 -38
  67. contentctl/objects/manual_test.py +3 -3
  68. contentctl/objects/manual_test_result.py +1 -0
  69. contentctl/objects/mitre_attack_enrichment.py +17 -16
  70. contentctl/objects/notable_action.py +2 -1
  71. contentctl/objects/notable_event.py +1 -3
  72. contentctl/objects/playbook.py +37 -35
  73. contentctl/objects/playbook_tags.py +22 -16
  74. contentctl/objects/rba.py +14 -8
  75. contentctl/objects/risk_analysis_action.py +15 -11
  76. contentctl/objects/risk_event.py +27 -20
  77. contentctl/objects/risk_object.py +1 -0
  78. contentctl/objects/savedsearches_conf.py +9 -7
  79. contentctl/objects/security_content_object.py +5 -2
  80. contentctl/objects/story.py +45 -44
  81. contentctl/objects/story_tags.py +56 -44
  82. contentctl/objects/test_group.py +5 -2
  83. contentctl/objects/threat_object.py +1 -0
  84. contentctl/objects/throttling.py +27 -18
  85. contentctl/objects/unit_test.py +3 -4
  86. contentctl/objects/unit_test_baseline.py +4 -5
  87. contentctl/objects/unit_test_result.py +6 -6
  88. contentctl/output/api_json_output.py +22 -22
  89. contentctl/output/attack_nav_output.py +21 -21
  90. contentctl/output/attack_nav_writer.py +29 -37
  91. contentctl/output/conf_output.py +230 -174
  92. contentctl/output/data_source_writer.py +38 -25
  93. contentctl/output/doc_md_output.py +53 -27
  94. contentctl/output/jinja_writer.py +19 -15
  95. contentctl/output/json_writer.py +20 -8
  96. contentctl/output/svg_output.py +56 -38
  97. contentctl/output/templates/savedsearches_detections.j2 +1 -1
  98. contentctl/output/templates/transforms.j2 +2 -2
  99. contentctl/output/yml_writer.py +18 -24
  100. {contentctl-5.0.0a0.dist-info → contentctl-5.0.0a3.dist-info}/METADATA +1 -1
  101. contentctl-5.0.0a3.dist-info/RECORD +168 -0
  102. contentctl/actions/initialize_old.py +0 -245
  103. contentctl/objects/observable.py +0 -39
  104. contentctl-5.0.0a0.dist-info/RECORD +0 -170
  105. {contentctl-5.0.0a0.dist-info → contentctl-5.0.0a3.dist-info}/LICENSE.md +0 -0
  106. {contentctl-5.0.0a0.dist-info → contentctl-5.0.0a3.dist-info}/WHEEL +0 -0
  107. {contentctl-5.0.0a0.dist-info → contentctl-5.0.0a3.dist-info}/entry_points.txt +0 -0
@@ -17,10 +17,12 @@ class DetectionTestingInfrastructureContainer(DetectionTestingInfrastructure):
17
17
  # If we are configured to use the persistent container, then check and see if it's already
18
18
  # running. If so, just use it without additional configuration.
19
19
  try:
20
- self.container = self.get_docker_client().containers.get(self.get_name())
20
+ self.container = self.get_docker_client().containers.get(
21
+ self.get_name()
22
+ )
21
23
  return
22
24
  except Exception:
23
- #We did not find the container running, we will set it up
25
+ # We did not find the container running, we will set it up
24
26
  pass
25
27
 
26
28
  self.container = self.make_container()
@@ -47,9 +49,10 @@ class DetectionTestingInfrastructureContainer(DetectionTestingInfrastructure):
47
49
  raise (Exception(f"Failed to get docker client: {str(e)}"))
48
50
 
49
51
  def check_for_teardown(self):
50
-
51
52
  try:
52
- container: docker.models.containers.Container = self.get_docker_client().containers.get(self.get_name())
53
+ container: docker.models.containers.Container = (
54
+ self.get_docker_client().containers.get(self.get_name())
55
+ )
53
56
  except Exception as e:
54
57
  if self.sync_obj.terminate is not True:
55
58
  self.pbar.write(
@@ -57,7 +60,7 @@ class DetectionTestingInfrastructureContainer(DetectionTestingInfrastructure):
57
60
  )
58
61
  self.sync_obj.terminate = True
59
62
  else:
60
- if container.status != 'running':
63
+ if container.status != "running":
61
64
  self.sync_obj.terminate = True
62
65
  self.container = None
63
66
 
@@ -90,28 +93,33 @@ class DetectionTestingInfrastructureContainer(DetectionTestingInfrastructure):
90
93
  environment["SPLUNK_PASSWORD"] = self.infrastructure.splunk_app_password
91
94
  # Files have already been staged by the time that we call this. Files must only be staged
92
95
  # once, not staged by every container
93
- environment["SPLUNK_APPS_URL"] = self.global_config.getContainerEnvironmentString(stage_file=False)
96
+ environment["SPLUNK_APPS_URL"] = (
97
+ self.global_config.getContainerEnvironmentString(stage_file=False)
98
+ )
94
99
  if (
95
100
  self.global_config.splunk_api_username is not None
96
101
  and self.global_config.splunk_api_password is not None
97
102
  ):
98
103
  environment["SPLUNKBASE_USERNAME"] = self.global_config.splunk_api_username
99
104
  environment["SPLUNKBASE_PASSWORD"] = self.global_config.splunk_api_password
100
-
101
-
102
105
 
103
106
  def emit_docker_run_equivalent():
104
- environment_string = " ".join([f'-e "{k}={environment.get(k)}"' for k in environment.keys()])
105
- print(f"\n\ndocker run -d "\
106
- f"-p {self.infrastructure.web_ui_port}:8000 "
107
- f"-p {self.infrastructure.hec_port}:8088 "
108
- f"-p {self.infrastructure.api_port}:8089 "
109
- f"{environment_string} "
110
- f" --name {self.get_name()} "
111
- f"--platform linux/amd64 "
112
- f"{self.global_config.container_settings.full_image_path}\n\n")
113
- #emit_docker_run_equivalent()
114
-
107
+ environment_string = " ".join(
108
+ [f'-e "{k}={environment.get(k)}"' for k in environment.keys()]
109
+ )
110
+ print(
111
+ f"\n\ndocker run -d "
112
+ f"-p {self.infrastructure.web_ui_port}:8000 "
113
+ f"-p {self.infrastructure.hec_port}:8088 "
114
+ f"-p {self.infrastructure.api_port}:8089 "
115
+ f"{environment_string} "
116
+ f" --name {self.get_name()} "
117
+ f"--platform linux/amd64 "
118
+ f"{self.global_config.container_settings.full_image_path}\n\n"
119
+ )
120
+
121
+ # emit_docker_run_equivalent()
122
+
115
123
  container = self.get_docker_client().containers.create(
116
124
  self.global_config.container_settings.full_image_path,
117
125
  ports=ports_dict,
@@ -119,20 +127,21 @@ class DetectionTestingInfrastructureContainer(DetectionTestingInfrastructure):
119
127
  name=self.get_name(),
120
128
  mounts=mounts,
121
129
  detach=True,
122
- platform="linux/amd64"
130
+ platform="linux/amd64",
123
131
  )
124
-
132
+
125
133
  if self.global_config.enterpriseSecurityInApps():
126
- #ES sets up https, so make sure it is included in the link
134
+ # ES sets up https, so make sure it is included in the link
127
135
  address = f"https://{self.infrastructure.instance_address}:{self.infrastructure.web_ui_port}"
128
136
  else:
129
137
  address = f"http://{self.infrastructure.instance_address}:{self.infrastructure.web_ui_port}"
130
- print(f"\nStarted container with the following information:\n"
131
- f"\tname : [{self.get_name()}]\n"
132
- f"\taddress : [{address}]\n"
133
- f"\tusername: [{self.infrastructure.splunk_app_username}]\n"
134
- f"\tpassword: [{self.infrastructure.splunk_app_password}]\n"
135
- )
138
+ print(
139
+ f"\nStarted container with the following information:\n"
140
+ f"\tname : [{self.get_name()}]\n"
141
+ f"\taddress : [{address}]\n"
142
+ f"\tusername: [{self.infrastructure.splunk_app_username}]\n"
143
+ f"\tpassword: [{self.infrastructure.splunk_app_password}]\n"
144
+ )
136
145
 
137
146
  return container
138
147
 
@@ -141,15 +150,19 @@ class DetectionTestingInfrastructureContainer(DetectionTestingInfrastructure):
141
150
  container: docker.models.containers.Container = (
142
151
  self.get_docker_client().containers.get(self.get_name())
143
152
  )
144
- except Exception as e:
153
+ except Exception:
145
154
  # Container does not exist, no need to try and remove it
146
155
  return
147
156
  try:
148
157
  # If the user wants to persist the container (or use a previously configured container), then DO NOT remove it.
149
- # Emit the following message, which they will see on initial setup and teardown at the end of the test.
158
+ # Emit the following message, which they will see on initial setup and teardown at the end of the test.
150
159
  if self.global_config.container_settings.leave_running:
151
- print(f"\nContainer [{self.get_name()}] has NOT been terminated because 'contentctl_test.yml ---> infrastructure_config ---> persist_and_reuse_container = True'")
152
- print(f"To remove it, please manually run the following at the command line: `docker container rm -fv {self.get_name()}`\n")
160
+ print(
161
+ f"\nContainer [{self.get_name()}] has NOT been terminated because 'contentctl_test.yml ---> infrastructure_config ---> persist_and_reuse_container = True'"
162
+ )
163
+ print(
164
+ f"To remove it, please manually run the following at the command line: `docker container rm -fv {self.get_name()}`\n"
165
+ )
153
166
  return
154
167
  # container was found, so now we try to remove it
155
168
  # v also removes volumes linked to the container
@@ -8,6 +8,7 @@ class TestReportingType(StrEnum):
8
8
  """
9
9
  5-char identifiers for the type of testing being reported on
10
10
  """
11
+
11
12
  # Reporting around general testing setup (e.g. infra, role configuration)
12
13
  SETUP = "SETUP"
13
14
 
@@ -25,6 +26,7 @@ class TestingStates(StrEnum):
25
26
  """
26
27
  Defined testing states
27
28
  """
29
+
28
30
  BEGINNING_GROUP = "Beginning Test Group"
29
31
  BEGINNING_TEST = "Beginning Test"
30
32
  DOWNLOADING = "Downloading Data"
@@ -47,6 +49,7 @@ class FinalTestingStates(StrEnum):
47
49
  """
48
50
  The possible final states for a test (for pbar reporting)
49
51
  """
52
+
50
53
  FAIL = "\x1b[0;30;41m" + "FAIL ".ljust(LONGEST_STATE) + "\x1b[0m"
51
54
  ERROR = "\x1b[0;30;41m" + "ERROR".ljust(LONGEST_STATE) + "\x1b[0m"
52
55
  PASS = "\x1b[0;30;42m" + "PASS ".ljust(LONGEST_STATE) + "\x1b[0m"
@@ -64,7 +64,7 @@ class DetectionTestingView(BaseModel, abc.ABC):
64
64
  try:
65
65
  runtime = self.getRuntime()
66
66
  time_per_detection = runtime / num_tested
67
- remaining_time = (num_untested+.5) * time_per_detection
67
+ remaining_time = (num_untested + 0.5) * time_per_detection
68
68
  remaining_time -= datetime.timedelta(
69
69
  microseconds=remaining_time.microseconds
70
70
  )
@@ -74,7 +74,14 @@ class DetectionTestingView(BaseModel, abc.ABC):
74
74
 
75
75
  def getSummaryObject(
76
76
  self,
77
- test_result_fields: list[str] = ["success", "message", "exception", "status", "duration", "wait_duration"],
77
+ test_result_fields: list[str] = [
78
+ "success",
79
+ "message",
80
+ "exception",
81
+ "status",
82
+ "duration",
83
+ "wait_duration",
84
+ ],
78
85
  test_job_fields: list[str] = ["resultCount", "runDuration"],
79
86
  ) -> dict[str, dict[str, Any] | list[dict[str, Any]] | str]:
80
87
  """
@@ -110,11 +117,11 @@ class DetectionTestingView(BaseModel, abc.ABC):
110
117
  total_skipped += 1
111
118
 
112
119
  # Aggregate production status metrics
113
- if detection.status == DetectionStatus.production:
120
+ if detection.status == DetectionStatus.production:
114
121
  total_production += 1
115
- elif detection.status == DetectionStatus.experimental:
122
+ elif detection.status == DetectionStatus.experimental:
116
123
  total_experimental += 1
117
- elif detection.status == DetectionStatus.deprecated:
124
+ elif detection.status == DetectionStatus.deprecated:
118
125
  total_deprecated += 1
119
126
 
120
127
  # Check if the detection is manual_test
@@ -128,19 +135,11 @@ class DetectionTestingView(BaseModel, abc.ABC):
128
135
  tested_detections.append(summary)
129
136
 
130
137
  # Sort tested detections s.t. all failures appear first, then by name
131
- tested_detections.sort(
132
- key=lambda x: (
133
- x["success"],
134
- x["name"]
135
- )
136
- )
138
+ tested_detections.sort(key=lambda x: (x["success"], x["name"]))
137
139
 
138
140
  # Sort skipped detections s.t. detections w/ tests appear before those w/o, then by name
139
141
  skipped_detections.sort(
140
- key=lambda x: (
141
- 0 if len(x["tests"]) > 0 else 1,
142
- x["name"]
143
- )
142
+ key=lambda x: (0 if len(x["tests"]) > 0 else 1, x["name"])
144
143
  )
145
144
 
146
145
  # TODO (#267): Align test reporting more closely w/ status enums (as it relates to
@@ -170,9 +169,7 @@ class DetectionTestingView(BaseModel, abc.ABC):
170
169
  percent_complete = Utils.getPercent(
171
170
  len(tested_detections), len(untested_detections), 1
172
171
  )
173
- success_rate = Utils.getPercent(
174
- total_pass, total_tested_detections, 1
175
- )
172
+ success_rate = Utils.getPercent(total_pass, total_tested_detections, 1)
176
173
 
177
174
  # TODO (#230): expand testing metrics reported (and make nested)
178
175
  # Construct and return the larger results dict
@@ -3,7 +3,6 @@ from contentctl.actions.detection_testing.views.DetectionTestingView import (
3
3
  )
4
4
 
5
5
  import time
6
- import datetime
7
6
  import tqdm
8
7
 
9
8
 
@@ -39,15 +38,12 @@ class DetectionTestingViewCLI(DetectionTestingView, arbitrary_types_allowed=True
39
38
  miniters=0,
40
39
  mininterval=0,
41
40
  )
42
- fmt = self.format_pbar(
43
- len(self.sync_obj.outputQueue), len(self.sync_obj.inputQueue)
44
- )
41
+ self.format_pbar(len(self.sync_obj.outputQueue), len(self.sync_obj.inputQueue))
45
42
 
46
43
  self.showStatus()
47
44
 
48
45
  # TODO (#267): Align test reporting more closely w/ status enums (as it relates to "untested")
49
46
  def showStatus(self, interval: int = 1):
50
-
51
47
  while True:
52
48
  summary = self.getSummaryObject()
53
49
 
@@ -13,7 +13,7 @@ class DetectionTestingViewFile(DetectionTestingView):
13
13
  output_filename: str = OUTPUT_FILENAME
14
14
 
15
15
  def getOutputFilePath(self) -> pathlib.Path:
16
- folder_path = pathlib.Path('.') / self.output_folder
16
+ folder_path = pathlib.Path(".") / self.output_folder
17
17
  output_file = folder_path / self.output_filename
18
18
 
19
19
  return output_file
@@ -22,7 +22,7 @@ class DetectionTestingViewFile(DetectionTestingView):
22
22
  pass
23
23
 
24
24
  def stop(self):
25
- folder_path = pathlib.Path('.') / self.output_folder
25
+ folder_path = pathlib.Path(".") / self.output_folder
26
26
  output_file = self.getOutputFilePath()
27
27
 
28
28
  folder_path.mkdir(parents=True, exist_ok=True)
@@ -102,9 +102,7 @@ class SimpleWebServer(ServerAdapter):
102
102
  class DetectionTestingViewWeb(DetectionTestingView):
103
103
  bottleApp: Bottle = Bottle()
104
104
  server: SimpleWebServer = SimpleWebServer(host="0.0.0.0", port=DEFAULT_WEB_UI_PORT)
105
- model_config = ConfigDict(
106
- arbitrary_types_allowed=True
107
- )
105
+ model_config = ConfigDict(arbitrary_types_allowed=True)
108
106
 
109
107
  def setup(self):
110
108
  self.bottleApp.route("/", callback=self.showStatus)
@@ -123,7 +121,6 @@ class DetectionTestingViewWeb(DetectionTestingView):
123
121
  print(f"Could not open webbrowser for status page: {str(e)}")
124
122
 
125
123
  def stop(self):
126
-
127
124
  if self.server.server is None:
128
125
  print("Web Server is not running anyway - nothing to shut down")
129
126
  return
@@ -10,17 +10,21 @@ from contentctl.output.doc_md_output import DocMdOutput
10
10
  class DocGenInputDto:
11
11
  director_input_dto: DirectorInputDto
12
12
 
13
- class DocGen:
14
13
 
14
+ class DocGen:
15
15
  def execute(self, input_dto: DocGenInputDto) -> None:
16
- director_output_dto = DirectorOutputDto([],[],[],[],[],[],[],[],[],[])
16
+ director_output_dto = DirectorOutputDto([], [], [], [], [], [], [], [], [], [])
17
17
  director = Director(director_output_dto)
18
18
  director.execute(input_dto.director_input_dto)
19
19
 
20
20
  doc_md_output = DocMdOutput()
21
21
  doc_md_output.writeObjects(
22
- [director_output_dto.stories, director_output_dto.detections, director_output_dto.playbooks],
23
- os.path.join(input_dto.director_input_dto.input_path, "docs")
22
+ [
23
+ director_output_dto.stories,
24
+ director_output_dto.detections,
25
+ director_output_dto.playbooks,
26
+ ],
27
+ os.path.join(input_dto.director_input_dto.input_path, "docs"),
24
28
  )
25
29
 
26
- print('Generating Docs of security content successful.')
30
+ print("Generating Docs of security content successful.")
@@ -1,4 +1,3 @@
1
-
2
1
  import shutil
3
2
  import os
4
3
  import pathlib
@@ -7,57 +6,70 @@ from contentctl.output.yml_writer import YmlWriter
7
6
 
8
7
 
9
8
  class Initialize:
10
-
11
9
  def execute(self, config: test) -> None:
12
10
  # construct a test object from the init object
13
11
  # This way we can easily populate a yml with ALL the important
14
- # fields for validating, building, and testing your app.
15
-
16
- YmlWriter.writeYmlFile(str(config.path/'contentctl.yml'), config.model_dump())
17
-
18
-
19
- #Create the following empty directories:
20
- for emptyDir in ['lookups', 'baselines', 'data_sources', 'docs', 'reporting', 'investigations',
21
- 'detections/application', 'detections/cloud', 'detections/endpoint',
22
- 'detections/network', 'detections/web', 'macros', 'stories']:
23
- #Throw an error if this directory already exists
24
- (config.path/emptyDir).mkdir(exist_ok=False, parents=True)
12
+ # fields for validating, building, and testing your app.
13
+
14
+ YmlWriter.writeYmlFile(str(config.path / "contentctl.yml"), config.model_dump())
15
+
16
+ # Create the following empty directories:
17
+ for emptyDir in [
18
+ "lookups",
19
+ "baselines",
20
+ "data_sources",
21
+ "docs",
22
+ "reporting",
23
+ "investigations",
24
+ "detections/application",
25
+ "detections/cloud",
26
+ "detections/endpoint",
27
+ "detections/network",
28
+ "detections/web",
29
+ "macros",
30
+ "stories",
31
+ ]:
32
+ # Throw an error if this directory already exists
33
+ (config.path / emptyDir).mkdir(exist_ok=False, parents=True)
25
34
 
26
35
  # If this is not a bare config, then populate
27
36
  # a small amount of content into the directories
28
37
  if not config.bare:
29
- #copy the contents of all template directories
38
+ # copy the contents of all template directories
30
39
  for templateDir, targetDir in [
31
- ('../templates/detections/', 'detections'),
32
- ('../templates/data_sources/', 'data_sources'),
33
- ('../templates/macros/', 'macros'),
34
- ('../templates/stories/', 'stories'),
40
+ ("../templates/detections/", "detections"),
41
+ ("../templates/data_sources/", "data_sources"),
42
+ ("../templates/macros/", "macros"),
43
+ ("../templates/stories/", "stories"),
35
44
  ]:
36
- source_directory = pathlib.Path(os.path.dirname(__file__))/templateDir
37
- target_directory = config.path/targetDir
38
-
39
- # Do not throw an exception if the directory exists. In fact, it was
45
+ source_directory = pathlib.Path(os.path.dirname(__file__)) / templateDir
46
+ target_directory = config.path / targetDir
47
+
48
+ # Do not throw an exception if the directory exists. In fact, it was
40
49
  # created above when the structure of the app was created.
41
50
  shutil.copytree(source_directory, target_directory, dirs_exist_ok=True)
42
-
51
+
43
52
  # The contents of app_template must ALWAYS be copied because it contains
44
53
  # several special files.
45
54
  # For now, we also copy the deployments because the ability to create custom
46
55
  # deployment files is limited with built-in functionality.
47
56
  for templateDir, targetDir in [
48
- ('../templates/app_template/', 'app_template'),
49
- ('../templates/deployments/', 'deployments')
57
+ ("../templates/app_template/", "app_template"),
58
+ ("../templates/deployments/", "deployments"),
50
59
  ]:
51
- source_directory = pathlib.Path(os.path.dirname(__file__))/templateDir
52
- target_directory = config.path/targetDir
53
- #Throw an exception if the target exists
60
+ source_directory = pathlib.Path(os.path.dirname(__file__)) / templateDir
61
+ target_directory = config.path / targetDir
62
+ # Throw an exception if the target exists
54
63
  shutil.copytree(source_directory, target_directory, dirs_exist_ok=False)
55
64
 
56
65
  # Create a README.md file. Note that this is the README.md for the repository, not the
57
66
  # one which will actually be packaged into the app. That is located in the app_template folder.
58
- shutil.copyfile(pathlib.Path(os.path.dirname(__file__))/'../templates/README.md','README.md')
59
-
60
-
61
- print(f"The app '{config.app.title}' has been initialized. "
62
- "Please run 'contentctl new --type {detection,story}' to create new content")
67
+ shutil.copyfile(
68
+ pathlib.Path(os.path.dirname(__file__)) / "../templates/README.md",
69
+ "README.md",
70
+ )
63
71
 
72
+ print(
73
+ f"The app '{config.app.title}' has been initialized. "
74
+ "Please run 'contentctl new --type {detection,story}' to create new content"
75
+ )