contentctl 4.1.4__py3-none-any.whl → 4.1.5__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.
@@ -111,11 +111,19 @@ class GitService(BaseModel):
111
111
  raise Exception(f"More than 1 Lookup reference the modified CSV file '{decoded_path}': {[l.file_path for l in matched ]}")
112
112
  else:
113
113
  updatedLookup = matched[0]
114
+ elif decoded_path.suffix == ".mlmodel":
115
+ # Detected a changed .mlmodel file. However, since we do not have testing for these detections at
116
+ # this time, we will ignore this change.
117
+ updatedLookup = None
118
+
119
+
114
120
  else:
115
- raise Exception(f"Error getting lookup object for file {str(decoded_path)}")
121
+ raise Exception(f"Detected a changed file in the lookups/ directory '{str(decoded_path)}'.\n"
122
+ "Only files ending in .csv, .yml, or .mlmodel are supported in this "
123
+ "directory. This file must be removed from the lookups/ directory.")
116
124
 
117
- if updatedLookup not in updated_lookups:
118
- # It is possible that both th CSV and YML have been modified for the same lookup,
125
+ if updatedLookup is not None and updatedLookup not in updated_lookups:
126
+ # It is possible that both the CSV and YML have been modified for the same lookup,
119
127
  # and we do not want to add it twice.
120
128
  updated_lookups.append(updatedLookup)
121
129
 
@@ -1,20 +1,11 @@
1
- import sys
2
1
 
3
- from dataclasses import dataclass
4
-
5
- from pydantic import ValidationError
6
- from typing import Union
7
-
8
- from contentctl.objects.enums import SecurityContentProduct
9
- from contentctl.objects.abstract_security_content_objects.security_content_object_abstract import (
10
- SecurityContentObject_Abstract,
11
- )
2
+ import pathlib
12
3
  from contentctl.input.director import Director, DirectorOutputDto
13
-
14
4
  from contentctl.objects.config import validate
15
5
  from contentctl.enrichments.attack_enrichment import AttackEnrichment
16
6
  from contentctl.enrichments.cve_enrichment import CveEnrichment
17
7
  from contentctl.objects.atomic import AtomicTest
8
+ from contentctl.helper.utils import Utils
18
9
 
19
10
 
20
11
  class Validate:
@@ -42,38 +33,44 @@ class Validate:
42
33
 
43
34
  director = Director(director_output_dto)
44
35
  director.execute(input_dto)
36
+ self.ensure_no_orphaned_files_in_lookups(input_dto.path, director_output_dto)
45
37
  return director_output_dto
46
38
 
47
- def validate_duplicate_uuids(
48
- self, security_content_objects: list[SecurityContentObject_Abstract]
49
- ):
50
- all_uuids = set()
51
- duplicate_uuids = set()
52
- for elem in security_content_objects:
53
- if elem.id in all_uuids:
54
- # The uuid has been found more than once
55
- duplicate_uuids.add(elem.id)
56
- else:
57
- # This is the first time the uuid has been found
58
- all_uuids.add(elem.id)
39
+
40
+ def ensure_no_orphaned_files_in_lookups(self, repo_path:pathlib.Path, director_output_dto:DirectorOutputDto):
41
+ """
42
+ This function ensures that only files which are relevant to lookups are included in the lookups folder.
43
+ This means that a file must be either:
44
+ 1. A lookup YML (.yml)
45
+ 2. A lookup CSV (.csv) which is referenced by a YML
46
+ 3. A lookup MLMODEL (.mlmodel) which is referenced by a YML.
47
+
48
+ All other files, includes CSV and MLMODEL files which are NOT
49
+ referenced by a YML, will generate an exception from this function.
50
+
51
+ Args:
52
+ repo_path (pathlib.Path): path to the root of the app
53
+ director_output_dto (DirectorOutputDto): director object with all constructed content
59
54
 
60
- if len(duplicate_uuids) == 0:
61
- return
55
+ Raises:
56
+ Exception: An Exception will be raised if there are any non .yml, .csv, or .mlmodel
57
+ files in this directory. Additionally, an exception will be raised if there
58
+ exists one or more .csv or .mlmodel files that are not referenced by at least 1
59
+ detection .yml file in this directory.
60
+ This avoids having additional, unused files in this directory that may be copied into
61
+ the app when it is built (which can cause appinspect errors or larger app size.)
62
+ """
63
+ lookupsDirectory = repo_path/"lookups"
64
+
65
+ # Get all of the files referneced by Lookups
66
+ usedLookupFiles:list[pathlib.Path] = [lookup.filename for lookup in director_output_dto.lookups if lookup.filename is not None] + [lookup.file_path for lookup in director_output_dto.lookups if lookup.file_path is not None]
62
67
 
63
- # At least once duplicate uuid has been found. Enumerate all
64
- # the pieces of content that use duplicate uuids
65
- duplicate_messages = []
66
- for uuid in duplicate_uuids:
67
- duplicate_uuid_content = [
68
- str(content.file_path)
69
- for content in security_content_objects
70
- if content.id in duplicate_uuids
71
- ]
72
- duplicate_messages.append(
73
- f"Duplicate UUID [{uuid}] in {duplicate_uuid_content}"
74
- )
75
-
76
- raise ValueError(
77
- "ERROR: Duplicate ID(s) found in objects:\n"
78
- + "\n - ".join(duplicate_messages)
79
- )
68
+ # Get all of the mlmodel and csv files in the lookups directory
69
+ csvAndMlmodelFiles = Utils.get_security_content_files_from_directory(lookupsDirectory, allowedFileExtensions=[".yml",".csv",".mlmodel"], fileExtensionsToReturn=[".csv",".mlmodel"])
70
+
71
+ # Generate an exception of any csv or mlmodel files exist but are not used
72
+ unusedLookupFiles:list[pathlib.Path] = [testFile for testFile in csvAndMlmodelFiles if testFile not in usedLookupFiles]
73
+ if len(unusedLookupFiles) > 0:
74
+ raise Exception(f"The following .csv or .mlmodel files exist in '{lookupsDirectory}', but are not referenced by a lookup file: {[str(path) for path in unusedLookupFiles]}")
75
+ return
76
+
@@ -34,6 +34,49 @@ class Utils:
34
34
  listOfFiles.append(pathlib.Path(os.path.join(dirpath, file)))
35
35
 
36
36
  return sorted(listOfFiles)
37
+
38
+ @staticmethod
39
+ def get_security_content_files_from_directory(path: pathlib.Path, allowedFileExtensions:list[str]=[".yml"], fileExtensionsToReturn:list[str]=[".yml"]) -> list[pathlib.Path]:
40
+
41
+ """
42
+ Get all of the Security Content Object Files rooted in a given directory. These will almost
43
+ certain be YML files, but could be other file types as specified by the user
44
+
45
+ Args:
46
+ path (pathlib.Path): The root path at which to enumerate all Security Content Files. All directories will be traversed.
47
+ allowedFileExtensions (set[str], optional): File extensions which are allowed to be present in this directory. In most cases, we do not want to allow the presence of non-YML files. Defaults to [".yml"].
48
+ fileExtensionsToReturn (set[str], optional): Filenames with extensions that should be returned from this function. For example, the lookups/ directory contains YML, CSV, and MLMODEL directories, but only the YMLs are Security Content Objects for constructing Lookyps. Defaults to[".yml"].
49
+
50
+ Raises:
51
+ Exception: Will raise an exception if allowedFileExtensions is not a subset of fileExtensionsToReturn.
52
+ Exception: Will raise an exception if the path passed to the function does not exist or is not a directory
53
+ Exception: Will raise an exception if there are any files rooted in the directory which are not in allowedFileExtensions
54
+
55
+ Returns:
56
+ list[pathlib.Path]: list of files with an extension in fileExtensionsToReturn found in path
57
+ """
58
+ if not set(fileExtensionsToReturn).issubset(set(allowedFileExtensions)):
59
+ raise Exception(f"allowedFileExtensions {allowedFileExtensions} MUST be a subset of fileExtensionsToReturn {fileExtensionsToReturn}, but it is not")
60
+
61
+ if not path.exists() or not path.is_dir():
62
+ raise Exception(f"Unable to get security_content files, required directory '{str(path)}' does not exist or is not a directory")
63
+
64
+ allowedFiles:list[pathlib.Path] = []
65
+ erroneousFiles:list[pathlib.Path] = []
66
+ #Get every single file extension
67
+ for filePath in path.glob("**/*.*"):
68
+ if filePath.suffix in allowedFileExtensions:
69
+ # Yes these are allowed
70
+ allowedFiles.append(filePath)
71
+ else:
72
+ # No these have not been allowed
73
+ erroneousFiles.append(filePath)
74
+
75
+ if len(erroneousFiles):
76
+ raise Exception(f"The following files are not allowed in the directory '{path}'. Only files with the extensions {allowedFileExtensions} are allowed:{[str(filePath) for filePath in erroneousFiles]}")
77
+
78
+ # There were no errorneous files, so return the requested files
79
+ return sorted([filePath for filePath in allowedFiles if filePath.suffix in fileExtensionsToReturn])
37
80
 
38
81
  @staticmethod
39
82
  def get_all_yml_files_from_directory_one_layer_deep(path: str) -> list[pathlib.Path]:
@@ -2,6 +2,7 @@ from __future__ import annotations
2
2
  from pydantic import field_validator, ValidationInfo, model_validator, FilePath, model_serializer
3
3
  from typing import TYPE_CHECKING, Optional, Any, Union
4
4
  import re
5
+ import csv
5
6
  if TYPE_CHECKING:
6
7
  from contentctl.input.director import DirectorOutputDto
7
8
  from contentctl.objects.config import validate
@@ -61,15 +62,53 @@ class Lookup(SecurityContentObject):
61
62
  raise ValueError("config required for constructing lookup filename, but it was not")
62
63
  return data
63
64
 
64
- @field_validator('filename')
65
- @classmethod
66
- def lookup_file_valid(cls, v: Union[FilePath,None], info: ValidationInfo):
67
- if not v:
68
- return v
69
- if not (v.name.endswith(".csv") or v.name.endswith(".mlmodel")):
70
- raise ValueError(f"All Lookup files must be CSV files and end in .csv. The following file does not: '{v}'")
71
65
 
72
- return v
66
+ def model_post_init(self, ctx:dict[str,Any]):
67
+ if not self.filename:
68
+ return
69
+ import pathlib
70
+ filenamePath = pathlib.Path(self.filename)
71
+
72
+ if filenamePath.suffix not in [".csv", ".mlmodel"]:
73
+ raise ValueError(f"All Lookup files must be CSV files and end in .csv. The following file does not: '{filenamePath}'")
74
+
75
+
76
+
77
+ if filenamePath.suffix == ".mlmodel":
78
+ # Do not need any additional checks for an mlmodel file
79
+ return
80
+
81
+ # https://docs.python.org/3/library/csv.html#csv.DictReader
82
+ # Column Names (fieldnames) determine by the number of columns in the first row.
83
+ # If a row has MORE fields than fieldnames, they will be dumped in a list under the key 'restkey' - this should throw an Exception
84
+ # If a row has LESS fields than fieldnames, then the field should contain None by default. This should also throw an exception.
85
+ csv_errors:list[str] = []
86
+ with open(filenamePath, "r") as csv_fp:
87
+ RESTKEY = "extra_fields_in_a_row"
88
+ csv_dict = csv.DictReader(csv_fp, restkey=RESTKEY)
89
+ if csv_dict.fieldnames is None:
90
+ raise ValueError(f"Error validating the CSV referenced by the lookup: {filenamePath}:\n\t"
91
+ "Unable to read fieldnames from CSV. Is the CSV empty?\n"
92
+ " Please try opening the file with a CSV Editor to ensure that it is correct.")
93
+ # Remember that row 1 has the headers and we do not iterate over it in the loop below
94
+ # CSVs are typically indexed starting a row 1 for the header.
95
+ for row_index, data_row in enumerate(csv_dict):
96
+ row_index+=2
97
+ if len(data_row.get(RESTKEY,[])) > 0:
98
+ csv_errors.append(f"row [{row_index}] should have [{len(csv_dict.fieldnames)}] columns,"
99
+ f" but instead had [{len(csv_dict.fieldnames) + len(data_row.get(RESTKEY,[]))}].")
100
+
101
+ for column_index, column_name in enumerate(data_row):
102
+ if data_row[column_name] is None:
103
+ csv_errors.append(f"row [{row_index}] should have [{len(csv_dict.fieldnames)}] columns, "
104
+ f"but instead had [{column_index}].")
105
+ if len(csv_errors) > 0:
106
+ err_string = '\n\t'.join(csv_errors)
107
+ raise ValueError(f"Error validating the CSV referenced by the lookup: {filenamePath}:\n\t{err_string}\n"
108
+ f" Please try opening the file with a CSV Editor to ensure that it is correct.")
109
+
110
+ return
111
+
73
112
 
74
113
  @field_validator('match_type')
75
114
  @classmethod
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: contentctl
3
- Version: 4.1.4
3
+ Version: 4.1.5
4
4
  Summary: Splunk Content Control Tool
5
5
  License: Apache 2.0
6
6
  Author: STRT
@@ -3,7 +3,7 @@ contentctl/actions/build.py,sha256=BVc-1E63zeUQ9wWAHTC_fLNvfEK5YT3Z6_QLiE72TQs,4
3
3
  contentctl/actions/convert.py,sha256=0KBWLxvP1hSPXpExePqpOQPRvlQLamvPLyQqeTIWNbk,704
4
4
  contentctl/actions/deploy_acs.py,sha256=mf3uk495H1EU_LNN-TiOsYCo18HMGoEBMb6ojeTr0zw,1418
5
5
  contentctl/actions/detection_testing/DetectionTestingManager.py,sha256=zg8JasDjCpSC-yhseEyUwO8qbDJIUJbhlus9Li9ZAnA,8818
6
- contentctl/actions/detection_testing/GitService.py,sha256=FY_JtMi3qL-uC31Yyf7CTWZ5kLQhAMAvcj-8QXtkfPM,8386
6
+ contentctl/actions/detection_testing/GitService.py,sha256=xNhuvK8oUoTxFlC0XBhlew9V0DO7l2hqaBMffEk5ohM,9000
7
7
  contentctl/actions/detection_testing/generate_detection_coverage_badge.py,sha256=N5mznaeErVak3mOBwsd0RDBFJO3bku0EZvpayCyU-uk,2259
8
8
  contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructure.py,sha256=VFhSHdw_0N6ol668hDkaj7yFjPsZqBoFNC8FKzWKICc,53141
9
9
  contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructureContainer.py,sha256=HVGWCXy0GQeBqu2cVJn5H-I8GY8rwgkkc53ilO1TfZA,6846
@@ -21,7 +21,7 @@ contentctl/actions/new_content.py,sha256=o5ZYBQ216RN6TnW_wRxVGJybx2SsJ7ht4PAi1dw
21
21
  contentctl/actions/release_notes.py,sha256=akkFfLhsJuaPUyjsb6dLlKt9cUM-JApAjTFQMbYoXeM,13115
22
22
  contentctl/actions/reporting.py,sha256=MJEmvmoA1WnSFZEU9QM6daL_W94oOX0WXAcX1qAM2As,1583
23
23
  contentctl/actions/test.py,sha256=dx7f750_MrlvysxOmOdIro1bH0iVKF4K54TSwhvU2MU,5146
24
- contentctl/actions/validate.py,sha256=HnmB_qsluYr0BFHQzg0HvXGLHM4M1taBtsWt774esf8,2537
24
+ contentctl/actions/validate.py,sha256=E6bQ0lZkFiFyC4hyRuypKiMybZSE4EXvzd94B0fQUFo,3590
25
25
  contentctl/api.py,sha256=FBOpRhbBCBdjORmwe_8MPQ3PRZ6T0KrrFcfKovVFkug,6343
26
26
  contentctl/contentctl.py,sha256=Vr2cuvaPjpJpYvD9kVoYq7iD6rhLQEpTKmcGoq4emhA,10470
27
27
  contentctl/enrichments/attack_enrichment.py,sha256=EkEloG3hMmPTloPyYiVkhq3iT_BieXaJmprJ5stfyRw,6732
@@ -29,7 +29,7 @@ contentctl/enrichments/cve_enrichment.py,sha256=IzkKSdnQi3JrAUUyLpcGA_Y2g_B7latq
29
29
  contentctl/enrichments/splunk_app_enrichment.py,sha256=zDNHFLZTi2dJ1gdnh0sHkD6F1VtkblqFnhacFcCMBfc,3418
30
30
  contentctl/helper/link_validator.py,sha256=-XorhxfGtjLynEL1X4hcpRMiyemogf2JEnvLwhHq80c,7139
31
31
  contentctl/helper/logger.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
32
- contentctl/helper/utils.py,sha256=THV4ZuaeEHG6PK5JeZUkTLBmvN4fUV0Ar6opH1zXxKs,16545
32
+ contentctl/helper/utils.py,sha256=8ICRvE7DUiNL9BK4Hw71hCLFbd3R2u86OwKeDOdaBTY,19454
33
33
  contentctl/input/backend_splunk_ba.py,sha256=Y70tJqgaUM0nzfm2SiGMof4HkhY84feqf-xnRx1xPb4,5861
34
34
  contentctl/input/director.py,sha256=BR1RvBD0U_JtHtrM3jM_FpcvaaNlME7nc-gNO4RJLM8,13323
35
35
  contentctl/input/new_content_questions.py,sha256=o4prlBoUhEMxqpZukquI9WKbzfFJfYhEF7a8m2q_BEE,5565
@@ -63,7 +63,7 @@ contentctl/objects/integration_test.py,sha256=W_VksBN_cRo7DTXdr1aLujjS9mgkEp0uvo
63
63
  contentctl/objects/integration_test_result.py,sha256=DrIZRRlILSHGcsK_Rlm3KJLnbKPtIen8uEPFi4ZdJ8s,370
64
64
  contentctl/objects/investigation.py,sha256=JRoZxc_qi1fu_VFTRaxOc3B7zzSzCfEURsNzWPUCrtY,2620
65
65
  contentctl/objects/investigation_tags.py,sha256=nFpMRKBVBsW21YW_vy2G1lXaSARX-kfFyrPoCyE77Q8,1280
66
- contentctl/objects/lookup.py,sha256=P8YbzdDAj_MsTBJTEsym35zhQjiN9Eq0MlfON-qvuTM,4556
66
+ contentctl/objects/lookup.py,sha256=TwNQqeMPeE8sfAjChxS2yDnejI2Xf3ils3_Xdgthr5c,6924
67
67
  contentctl/objects/macro.py,sha256=9nE-bxkFhtaltHOUCr0luU8jCCthmglHjhKs6Q2YzLU,2684
68
68
  contentctl/objects/mitre_attack_enrichment.py,sha256=bWrMG-Xj3knmULR5q2YZk7mloJBdQUzU1moZfEw9lQM,1073
69
69
  contentctl/objects/notable_action.py,sha256=ValkblBaG-60TF19y_vSnNzoNZ3eg48wIfr0qZxyKTA,1605
@@ -163,8 +163,8 @@ contentctl/templates/detections/web/.gitkeep,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRk
163
163
  contentctl/templates/macros/security_content_ctime.yml,sha256=Gg1YNllHVsX_YB716H1SJLWzxXZEfuJlnsgB2fuyoHU,159
164
164
  contentctl/templates/macros/security_content_summariesonly.yml,sha256=9BYUxAl2E4Nwh8K19F3AJS8Ka7ceO6ZDBjFiO3l3LY0,162
165
165
  contentctl/templates/stories/cobalt_strike.yml,sha256=rlaXxMN-5k8LnKBLPafBoksyMtlmsPMHPJOjTiMiZ-M,3063
166
- contentctl-4.1.4.dist-info/LICENSE.md,sha256=hQWUayRk-pAiOZbZnuy8djmoZkjKBx8MrCFpW-JiOgo,11344
167
- contentctl-4.1.4.dist-info/METADATA,sha256=gyBEu2sSknwen-oTm0WdQtCvaPgbWuJBd4jvPuel6iQ,19706
168
- contentctl-4.1.4.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
169
- contentctl-4.1.4.dist-info/entry_points.txt,sha256=5bjZ2NkbQfSwK47uOnA77yCtjgXhvgxnmCQiynRF_-U,57
170
- contentctl-4.1.4.dist-info/RECORD,,
166
+ contentctl-4.1.5.dist-info/LICENSE.md,sha256=hQWUayRk-pAiOZbZnuy8djmoZkjKBx8MrCFpW-JiOgo,11344
167
+ contentctl-4.1.5.dist-info/METADATA,sha256=dzhmOQMe0mFoHlZ12p7FsQzHGF-jSKvzbAosP5fTsC8,19706
168
+ contentctl-4.1.5.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
169
+ contentctl-4.1.5.dist-info/entry_points.txt,sha256=5bjZ2NkbQfSwK47uOnA77yCtjgXhvgxnmCQiynRF_-U,57
170
+ contentctl-4.1.5.dist-info/RECORD,,