contentctl 5.1.0__py3-none-any.whl → 5.3.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.
- contentctl/actions/build.py +5 -43
- contentctl/actions/detection_testing/DetectionTestingManager.py +64 -24
- contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructure.py +147 -43
- contentctl/actions/detection_testing/views/DetectionTestingView.py +5 -6
- contentctl/actions/detection_testing/views/DetectionTestingViewCLI.py +2 -0
- contentctl/actions/initialize.py +35 -9
- contentctl/actions/release_notes.py +14 -12
- contentctl/actions/test.py +16 -20
- contentctl/actions/validate.py +8 -15
- contentctl/helper/utils.py +69 -20
- contentctl/input/director.py +147 -119
- contentctl/input/yml_reader.py +39 -27
- contentctl/objects/abstract_security_content_objects/detection_abstract.py +121 -20
- contentctl/objects/abstract_security_content_objects/security_content_object_abstract.py +548 -8
- contentctl/objects/baseline.py +24 -6
- contentctl/objects/config.py +32 -8
- contentctl/objects/content_versioning_service.py +508 -0
- contentctl/objects/correlation_search.py +53 -63
- contentctl/objects/dashboard.py +15 -1
- contentctl/objects/data_source.py +15 -1
- contentctl/objects/deployment.py +23 -9
- contentctl/objects/detection.py +2 -0
- contentctl/objects/enums.py +28 -18
- contentctl/objects/investigation.py +40 -20
- contentctl/objects/lookup.py +77 -8
- contentctl/objects/macro.py +19 -4
- contentctl/objects/playbook.py +16 -2
- contentctl/objects/rba.py +1 -33
- contentctl/objects/removed_security_content_object.py +50 -0
- contentctl/objects/security_content_object.py +1 -0
- contentctl/objects/story.py +37 -5
- contentctl/output/api_json_output.py +5 -3
- contentctl/output/attack_nav_output.py +11 -4
- contentctl/output/attack_nav_writer.py +53 -37
- contentctl/output/conf_output.py +9 -1
- contentctl/output/runtime_csv_writer.py +111 -0
- contentctl/output/svg_output.py +4 -5
- contentctl/output/templates/savedsearches_detections.j2 +2 -6
- contentctl/output/templates/transforms.j2 +2 -2
- {contentctl-5.1.0.dist-info → contentctl-5.3.0.dist-info}/METADATA +4 -3
- {contentctl-5.1.0.dist-info → contentctl-5.3.0.dist-info}/RECORD +44 -42
- {contentctl-5.1.0.dist-info → contentctl-5.3.0.dist-info}/WHEEL +1 -1
- contentctl/output/data_source_writer.py +0 -52
- {contentctl-5.1.0.dist-info → contentctl-5.3.0.dist-info}/LICENSE.md +0 -0
- {contentctl-5.1.0.dist-info → contentctl-5.3.0.dist-info}/entry_points.txt +0 -0
contentctl/actions/test.py
CHANGED
|
@@ -1,44 +1,36 @@
|
|
|
1
|
+
import pathlib
|
|
1
2
|
from dataclasses import dataclass
|
|
2
3
|
from typing import List
|
|
3
4
|
|
|
4
|
-
from contentctl.objects.config import test_common, Selected, Changes
|
|
5
|
-
from contentctl.objects.detection import Detection
|
|
6
|
-
|
|
7
|
-
|
|
8
5
|
from contentctl.actions.detection_testing.DetectionTestingManager import (
|
|
9
6
|
DetectionTestingManager,
|
|
10
7
|
DetectionTestingManagerInputDto,
|
|
11
8
|
)
|
|
12
|
-
|
|
13
|
-
|
|
14
9
|
from contentctl.actions.detection_testing.infrastructures.DetectionTestingInfrastructure import (
|
|
15
10
|
DetectionTestingManagerOutputDto,
|
|
16
11
|
)
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
from contentctl.actions.detection_testing.views.DetectionTestingViewWeb import (
|
|
20
|
-
DetectionTestingViewWeb,
|
|
21
|
-
)
|
|
22
|
-
|
|
23
12
|
from contentctl.actions.detection_testing.views.DetectionTestingViewCLI import (
|
|
24
13
|
DetectionTestingViewCLI,
|
|
25
14
|
)
|
|
26
|
-
|
|
27
15
|
from contentctl.actions.detection_testing.views.DetectionTestingViewFile import (
|
|
28
16
|
DetectionTestingViewFile,
|
|
29
17
|
)
|
|
30
|
-
|
|
18
|
+
from contentctl.actions.detection_testing.views.DetectionTestingViewWeb import (
|
|
19
|
+
DetectionTestingViewWeb,
|
|
20
|
+
)
|
|
21
|
+
from contentctl.objects.config import Changes, Selected
|
|
22
|
+
from contentctl.objects.config import test as test_
|
|
23
|
+
from contentctl.objects.config import test_servers
|
|
24
|
+
from contentctl.objects.detection import Detection
|
|
31
25
|
from contentctl.objects.integration_test import IntegrationTest
|
|
32
26
|
|
|
33
|
-
import pathlib
|
|
34
|
-
|
|
35
27
|
MAXIMUM_CONFIGURATION_TIME_SECONDS = 600
|
|
36
28
|
|
|
37
29
|
|
|
38
30
|
@dataclass(frozen=True)
|
|
39
31
|
class TestInputDto:
|
|
40
32
|
detections: List[Detection]
|
|
41
|
-
config:
|
|
33
|
+
config: test_ | test_servers
|
|
42
34
|
|
|
43
35
|
|
|
44
36
|
class Test:
|
|
@@ -77,8 +69,8 @@ class Test:
|
|
|
77
69
|
|
|
78
70
|
if len(input_dto.detections) == 0:
|
|
79
71
|
print(
|
|
80
|
-
f"With Detection Testing Mode '{input_dto.config.mode.mode_name}', there were
|
|
81
|
-
"
|
|
72
|
+
f"With Detection Testing Mode '{input_dto.config.mode.mode_name}', there were "
|
|
73
|
+
"[0] detections found to test.\nAs such, we will quit immediately."
|
|
82
74
|
)
|
|
83
75
|
# Directly call stop so that the summary.yml will be generated. Of course it will not
|
|
84
76
|
# have any test results, but we still want it to contain a summary showing that now
|
|
@@ -109,6 +101,10 @@ class Test:
|
|
|
109
101
|
try:
|
|
110
102
|
summary_results = file.getSummaryObject()
|
|
111
103
|
summary = summary_results.get("summary", {})
|
|
104
|
+
if not isinstance(summary, dict):
|
|
105
|
+
raise ValueError(
|
|
106
|
+
f"Summary in results was an unexpected type ({type(summary)}): {summary}"
|
|
107
|
+
)
|
|
112
108
|
|
|
113
109
|
print(f"Test Summary (mode: {summary.get('mode', 'Error')})")
|
|
114
110
|
print(f"\tSuccess : {summary.get('success', False)}")
|
|
@@ -152,7 +148,7 @@ class Test:
|
|
|
152
148
|
"detection types (e.g. Correlation), but there may be overlap between these\n"
|
|
153
149
|
"categories."
|
|
154
150
|
)
|
|
155
|
-
return
|
|
151
|
+
return summary.get("success", False)
|
|
156
152
|
|
|
157
153
|
except Exception as e:
|
|
158
154
|
print(f"Error determining if whole test was successful: {str(e)}")
|
contentctl/actions/validate.py
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import pathlib
|
|
2
2
|
|
|
3
|
-
from contentctl.input.director import Director, DirectorOutputDto
|
|
4
|
-
from contentctl.objects.config import validate
|
|
5
3
|
from contentctl.enrichments.attack_enrichment import AttackEnrichment
|
|
6
4
|
from contentctl.enrichments.cve_enrichment import CveEnrichment
|
|
7
|
-
from contentctl.
|
|
8
|
-
from contentctl.objects.lookup import FileBackedLookup
|
|
5
|
+
from contentctl.helper.splunk_app import SplunkApp
|
|
9
6
|
from contentctl.helper.utils import Utils
|
|
7
|
+
from contentctl.input.director import Director, DirectorOutputDto
|
|
8
|
+
from contentctl.objects.atomic import AtomicEnrichment
|
|
9
|
+
from contentctl.objects.config import validate
|
|
10
10
|
from contentctl.objects.data_source import DataSource
|
|
11
|
-
from contentctl.
|
|
11
|
+
from contentctl.objects.lookup import FileBackedLookup, RuntimeCSV
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
class Validate:
|
|
@@ -17,16 +17,6 @@ class Validate:
|
|
|
17
17
|
AtomicEnrichment.getAtomicEnrichment(input_dto),
|
|
18
18
|
AttackEnrichment.getAttackEnrichment(input_dto),
|
|
19
19
|
CveEnrichment.getCveEnrichment(input_dto),
|
|
20
|
-
[],
|
|
21
|
-
[],
|
|
22
|
-
[],
|
|
23
|
-
[],
|
|
24
|
-
[],
|
|
25
|
-
[],
|
|
26
|
-
[],
|
|
27
|
-
[],
|
|
28
|
-
[],
|
|
29
|
-
[],
|
|
30
20
|
)
|
|
31
21
|
|
|
32
22
|
director = Director(director_output_dto)
|
|
@@ -68,7 +58,10 @@ class Validate:
|
|
|
68
58
|
usedLookupFiles: list[pathlib.Path] = [
|
|
69
59
|
lookup.filename
|
|
70
60
|
for lookup in director_output_dto.lookups
|
|
61
|
+
# Of course Runtime CSVs do not have underlying CSV files, so make
|
|
62
|
+
# sure that we do not check for that existence.
|
|
71
63
|
if isinstance(lookup, FileBackedLookup)
|
|
64
|
+
and not isinstance(lookup, RuntimeCSV)
|
|
72
65
|
] + [
|
|
73
66
|
lookup.file_path
|
|
74
67
|
for lookup in director_output_dto.lookups
|
contentctl/helper/utils.py
CHANGED
|
@@ -1,40 +1,34 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
import shutil
|
|
4
|
-
import requests
|
|
1
|
+
import logging
|
|
2
|
+
import pathlib
|
|
5
3
|
import random
|
|
4
|
+
import shutil
|
|
6
5
|
import string
|
|
6
|
+
from math import ceil
|
|
7
7
|
from timeit import default_timer
|
|
8
|
-
import
|
|
8
|
+
from typing import TYPE_CHECKING, Tuple, Union
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
import git
|
|
11
|
+
import requests
|
|
11
12
|
import tqdm
|
|
12
|
-
from math import ceil
|
|
13
|
-
|
|
14
|
-
from typing import TYPE_CHECKING
|
|
15
13
|
|
|
16
14
|
if TYPE_CHECKING:
|
|
17
15
|
from contentctl.objects.security_content_object import SecurityContentObject
|
|
18
16
|
from contentctl.objects.security_content_object import SecurityContentObject
|
|
19
17
|
|
|
20
|
-
|
|
21
18
|
TOTAL_BYTES = 0
|
|
22
19
|
ALWAYS_PULL = True
|
|
23
20
|
|
|
24
21
|
|
|
25
22
|
class Utils:
|
|
26
23
|
@staticmethod
|
|
27
|
-
def get_all_yml_files_from_directory(path:
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
for file in filenames:
|
|
34
|
-
if file.endswith(".yml"):
|
|
35
|
-
listOfFiles.append(pathlib.Path(os.path.join(dirpath, file)))
|
|
24
|
+
def get_all_yml_files_from_directory(path: pathlib.Path) -> list[pathlib.Path]:
|
|
25
|
+
if not path.exists():
|
|
26
|
+
raise FileNotFoundError(
|
|
27
|
+
f"Trying to find files in the directory '{path.absolute()}', but it does not exist.\n"
|
|
28
|
+
"It is not mandatory to have content/YMLs in this directory, but it must exist. Please create it."
|
|
29
|
+
)
|
|
36
30
|
|
|
37
|
-
return sorted(
|
|
31
|
+
return sorted(pathlib.Path(yml_path) for yml_path in path.glob("**/*.yml"))
|
|
38
32
|
|
|
39
33
|
@staticmethod
|
|
40
34
|
def get_security_content_files_from_directory(
|
|
@@ -490,3 +484,58 @@ class Utils:
|
|
|
490
484
|
ratio = numerator / denominator
|
|
491
485
|
percent = ratio * 100
|
|
492
486
|
return Utils.getFixedWidth(percent, decimal_places) + "%"
|
|
487
|
+
|
|
488
|
+
@staticmethod
|
|
489
|
+
def get_logger(
|
|
490
|
+
name: str, log_level: int, log_path: str, enable_logging: bool
|
|
491
|
+
) -> logging.Logger:
|
|
492
|
+
"""
|
|
493
|
+
Gets a logger instance for the given name; logger is configured if not already configured.
|
|
494
|
+
The NullHandler is used to suppress loggging when running in production so as not to
|
|
495
|
+
conflict w/ contentctl's larger pbar-based logging. The StreamHandler is enabled by setting
|
|
496
|
+
enable_logging to True (useful for debugging/testing locally)
|
|
497
|
+
|
|
498
|
+
:param name: the logger name
|
|
499
|
+
:type name: str
|
|
500
|
+
:param log_level: the logging level (e.g. `logging.Debug`)
|
|
501
|
+
:type log_level: int
|
|
502
|
+
:param log_path: the path for the log file
|
|
503
|
+
:type log_path: str
|
|
504
|
+
:param enable_logging: a flag indicating whether logging should be redirected from null to
|
|
505
|
+
the stream handler
|
|
506
|
+
:type enable_logging: bool
|
|
507
|
+
|
|
508
|
+
:return: a logger
|
|
509
|
+
:rtype: :class:`logging.Logger`
|
|
510
|
+
"""
|
|
511
|
+
# get logger for module
|
|
512
|
+
logger = logging.getLogger(name)
|
|
513
|
+
|
|
514
|
+
# set propagate to False if not already set as such (needed to that we do not flow up to any
|
|
515
|
+
# root loggers)
|
|
516
|
+
if logger.propagate:
|
|
517
|
+
logger.propagate = False
|
|
518
|
+
|
|
519
|
+
# if logger has no handlers, it needs to be configured for the first time
|
|
520
|
+
if not logger.hasHandlers():
|
|
521
|
+
# set level
|
|
522
|
+
logger.setLevel(log_level)
|
|
523
|
+
|
|
524
|
+
# if logging enabled, use a StreamHandler; else, use the NullHandler to suppress logging
|
|
525
|
+
handler: logging.Handler
|
|
526
|
+
if enable_logging:
|
|
527
|
+
handler = logging.FileHandler(log_path)
|
|
528
|
+
else:
|
|
529
|
+
handler = logging.NullHandler()
|
|
530
|
+
|
|
531
|
+
# Format our output
|
|
532
|
+
formatter = logging.Formatter(
|
|
533
|
+
"%(asctime)s - %(levelname)s:%(name)s - %(message)s"
|
|
534
|
+
)
|
|
535
|
+
handler.setFormatter(formatter)
|
|
536
|
+
|
|
537
|
+
# Set handler level and add to logger
|
|
538
|
+
handler.setLevel(log_level)
|
|
539
|
+
logger.addHandler(handler)
|
|
540
|
+
|
|
541
|
+
return logger
|
contentctl/input/director.py
CHANGED
|
@@ -1,29 +1,42 @@
|
|
|
1
|
-
import os
|
|
2
1
|
import sys
|
|
3
2
|
from dataclasses import dataclass, field
|
|
4
3
|
from pathlib import Path
|
|
5
4
|
from uuid import UUID
|
|
6
5
|
|
|
7
|
-
from pydantic import ValidationError
|
|
6
|
+
from pydantic import TypeAdapter, ValidationError
|
|
8
7
|
|
|
9
8
|
from contentctl.enrichments.attack_enrichment import AttackEnrichment
|
|
10
9
|
from contentctl.enrichments.cve_enrichment import CveEnrichment
|
|
11
10
|
from contentctl.helper.utils import Utils
|
|
12
11
|
from contentctl.input.yml_reader import YmlReader
|
|
12
|
+
from contentctl.objects.abstract_security_content_objects.security_content_object_abstract import (
|
|
13
|
+
DeprecationDocumentationFile,
|
|
14
|
+
)
|
|
13
15
|
from contentctl.objects.atomic import AtomicEnrichment
|
|
14
16
|
from contentctl.objects.baseline import Baseline
|
|
15
|
-
from contentctl.objects.config import validate
|
|
17
|
+
from contentctl.objects.config import CustomApp, validate
|
|
16
18
|
from contentctl.objects.dashboard import Dashboard
|
|
17
19
|
from contentctl.objects.data_source import DataSource
|
|
18
20
|
from contentctl.objects.deployment import Deployment
|
|
19
21
|
from contentctl.objects.detection import Detection
|
|
20
|
-
from contentctl.objects.enums import SecurityContentType
|
|
21
22
|
from contentctl.objects.investigation import Investigation
|
|
22
|
-
from contentctl.objects.lookup import
|
|
23
|
+
from contentctl.objects.lookup import (
|
|
24
|
+
CSVLookup,
|
|
25
|
+
KVStoreLookup,
|
|
26
|
+
Lookup,
|
|
27
|
+
Lookup_Type,
|
|
28
|
+
LookupAdapter,
|
|
29
|
+
MlModel,
|
|
30
|
+
RuntimeCSV,
|
|
31
|
+
)
|
|
23
32
|
from contentctl.objects.macro import Macro
|
|
24
33
|
from contentctl.objects.playbook import Playbook
|
|
34
|
+
from contentctl.objects.removed_security_content_object import (
|
|
35
|
+
RemovedSecurityContentObject,
|
|
36
|
+
)
|
|
25
37
|
from contentctl.objects.security_content_object import SecurityContentObject
|
|
26
38
|
from contentctl.objects.story import Story
|
|
39
|
+
from contentctl.output.runtime_csv_writer import RuntimeCsvWriter
|
|
27
40
|
|
|
28
41
|
|
|
29
42
|
@dataclass
|
|
@@ -33,17 +46,20 @@ class DirectorOutputDto:
|
|
|
33
46
|
atomic_enrichment: AtomicEnrichment
|
|
34
47
|
attack_enrichment: AttackEnrichment
|
|
35
48
|
cve_enrichment: CveEnrichment
|
|
36
|
-
detections: list[Detection]
|
|
37
|
-
stories: list[Story]
|
|
38
|
-
baselines: list[Baseline]
|
|
39
|
-
investigations: list[Investigation]
|
|
40
|
-
playbooks: list[Playbook]
|
|
41
|
-
macros: list[Macro]
|
|
42
|
-
lookups: list[Lookup]
|
|
43
|
-
deployments: list[Deployment]
|
|
44
|
-
dashboards: list[Dashboard]
|
|
45
|
-
|
|
46
|
-
data_sources: list[DataSource]
|
|
49
|
+
detections: list[Detection] = field(default_factory=list)
|
|
50
|
+
stories: list[Story] = field(default_factory=list)
|
|
51
|
+
baselines: list[Baseline] = field(default_factory=list)
|
|
52
|
+
investigations: list[Investigation] = field(default_factory=list)
|
|
53
|
+
playbooks: list[Playbook] = field(default_factory=list)
|
|
54
|
+
macros: list[Macro] = field(default_factory=list)
|
|
55
|
+
lookups: list[Lookup] = field(default_factory=list)
|
|
56
|
+
deployments: list[Deployment] = field(default_factory=list)
|
|
57
|
+
dashboards: list[Dashboard] = field(default_factory=list)
|
|
58
|
+
deprecated: list[RemovedSecurityContentObject] = field(default_factory=list)
|
|
59
|
+
data_sources: list[DataSource] = field(default_factory=list)
|
|
60
|
+
deprecation_documentation: DeprecationDocumentationFile = field(
|
|
61
|
+
default_factory=DeprecationDocumentationFile
|
|
62
|
+
)
|
|
47
63
|
name_to_content_map: dict[str, SecurityContentObject] = field(default_factory=dict)
|
|
48
64
|
uuid_to_content_map: dict[UUID, SecurityContentObject] = field(default_factory=dict)
|
|
49
65
|
|
|
@@ -82,9 +98,10 @@ class DirectorOutputDto:
|
|
|
82
98
|
self.detections.append(content)
|
|
83
99
|
elif isinstance(content, Dashboard):
|
|
84
100
|
self.dashboards.append(content)
|
|
85
|
-
|
|
86
101
|
elif isinstance(content, DataSource):
|
|
87
102
|
self.data_sources.append(content)
|
|
103
|
+
elif isinstance(content, RemovedSecurityContentObject):
|
|
104
|
+
self.deprecated.append(content)
|
|
88
105
|
else:
|
|
89
106
|
raise Exception(f"Unknown security content type: {type(content)}")
|
|
90
107
|
|
|
@@ -101,123 +118,134 @@ class Director:
|
|
|
101
118
|
|
|
102
119
|
def execute(self, input_dto: validate) -> None:
|
|
103
120
|
self.input_dto = input_dto
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
if contentType in [
|
|
117
|
-
SecurityContentType.deployments,
|
|
118
|
-
SecurityContentType.lookups,
|
|
119
|
-
SecurityContentType.macros,
|
|
120
|
-
SecurityContentType.stories,
|
|
121
|
-
SecurityContentType.baselines,
|
|
122
|
-
SecurityContentType.investigations,
|
|
123
|
-
SecurityContentType.playbooks,
|
|
124
|
-
SecurityContentType.detections,
|
|
125
|
-
SecurityContentType.data_sources,
|
|
126
|
-
SecurityContentType.dashboards,
|
|
121
|
+
|
|
122
|
+
for content in [
|
|
123
|
+
Deployment,
|
|
124
|
+
LookupAdapter,
|
|
125
|
+
Macro,
|
|
126
|
+
Story,
|
|
127
|
+
Baseline,
|
|
128
|
+
DataSource,
|
|
129
|
+
Playbook,
|
|
130
|
+
Detection,
|
|
131
|
+
Dashboard,
|
|
132
|
+
RemovedSecurityContentObject,
|
|
127
133
|
]:
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
134
|
+
self.createSecurityContent(content)
|
|
135
|
+
|
|
136
|
+
self.loadDeprecationInfo(input_dto.app)
|
|
137
|
+
self.buildRuntimeCsvs()
|
|
138
|
+
|
|
139
|
+
def buildRuntimeCsvs(self):
|
|
140
|
+
self.buildDataSourceCsv()
|
|
141
|
+
self.buildDeprecationRemovalCsv()
|
|
142
|
+
|
|
143
|
+
def buildDeprecationRemovalCsv(self):
|
|
144
|
+
if self.input_dto.enforce_deprecation_mapping_requirement is False:
|
|
145
|
+
# Do not build the CSV, it would be wasteful to include it if it
|
|
146
|
+
# is not even used
|
|
147
|
+
return
|
|
148
|
+
deprecation_lookup = RuntimeCSV(
|
|
149
|
+
name="deprecation_info",
|
|
150
|
+
id=UUID("99262bf2-9606-4b52-b377-c96713527b35"),
|
|
151
|
+
version=1,
|
|
152
|
+
author=self.input_dto.app.author_name,
|
|
153
|
+
description="A lookup file that contains information about content that has been deprecated or removed from the app.",
|
|
154
|
+
lookup_type=Lookup_Type.csv,
|
|
155
|
+
contents=RuntimeCsvWriter.generateDeprecationCSVContent(
|
|
156
|
+
self.output_dto, self.input_dto.app
|
|
157
|
+
),
|
|
158
|
+
)
|
|
159
|
+
self.output_dto.addContentToDictMappings(deprecation_lookup)
|
|
160
|
+
|
|
161
|
+
def buildDataSourceCsv(self):
|
|
162
|
+
datasource_lookup = RuntimeCSV(
|
|
163
|
+
name="data_sources",
|
|
164
|
+
id=UUID("b45c1403-6e09-47b0-824f-cf6e44f15ac8"),
|
|
165
|
+
version=1,
|
|
166
|
+
author=self.input_dto.app.author_name,
|
|
167
|
+
description="A lookup file that contains the data source objects for detections.",
|
|
168
|
+
lookup_type=Lookup_Type.csv,
|
|
169
|
+
contents=RuntimeCsvWriter.generateDatasourceCSVContent(
|
|
170
|
+
self.output_dto.data_sources
|
|
171
|
+
),
|
|
172
|
+
)
|
|
173
|
+
self.output_dto.addContentToDictMappings(datasource_lookup)
|
|
174
|
+
|
|
175
|
+
def loadDeprecationInfo(self, app: CustomApp):
|
|
176
|
+
mapping_file_paths = list(
|
|
177
|
+
(self.input_dto.path / "removed").glob("deprecation_mapping*.YML")
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
if self.input_dto.enforce_deprecation_mapping_requirement is False:
|
|
181
|
+
# If we are not required to enforce deprecation mapping, then do nothing at all (even if the files exist)
|
|
182
|
+
if len(mapping_file_paths) > 0:
|
|
183
|
+
file_paths = "\n - " + "\n - ".join(
|
|
184
|
+
str(name) for name in mapping_file_paths
|
|
136
185
|
)
|
|
137
|
-
|
|
186
|
+
print(
|
|
187
|
+
"The following deprecation_mapping*.YML files were found, but will not be parsed because "
|
|
188
|
+
f"[enforce_deprecation_mapping_requirement = {self.input_dto.enforce_deprecation_mapping_requirement}]:",
|
|
189
|
+
file_paths,
|
|
190
|
+
)
|
|
191
|
+
# Otherwise, no need to output extra information
|
|
192
|
+
return
|
|
193
|
+
|
|
194
|
+
# If there are no mapping files, that's okay. We will other throw exceptions later on if
|
|
195
|
+
# there are 1 or more detections marked as deprecated or removed.
|
|
196
|
+
for mapping_file_path in mapping_file_paths:
|
|
197
|
+
print(f"Parsing mapping file {mapping_file_path.name}")
|
|
198
|
+
data = YmlReader.load_file(mapping_file_path)
|
|
199
|
+
mapping = DeprecationDocumentationFile.model_validate(data)
|
|
200
|
+
self.output_dto.deprecation_documentation += mapping
|
|
201
|
+
|
|
202
|
+
self.output_dto.deprecation_documentation.mapAllContent(self.output_dto, app)
|
|
203
|
+
|
|
204
|
+
def createSecurityContent(
|
|
205
|
+
self,
|
|
206
|
+
contentType: type[SecurityContentObject]
|
|
207
|
+
| TypeAdapter[CSVLookup | KVStoreLookup | MlModel],
|
|
208
|
+
) -> None:
|
|
209
|
+
files = Utils.get_all_yml_files_from_directory(
|
|
210
|
+
self.input_dto.path / contentType.containing_folder() # type: ignore
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
# convert this generator to a list so that we can
|
|
214
|
+
# calculate progress as we iterate over the files
|
|
215
|
+
security_content_files = [f for f in files]
|
|
138
216
|
|
|
139
217
|
validation_errors: list[tuple[Path, ValueError]] = []
|
|
140
218
|
|
|
141
219
|
already_ran = False
|
|
142
220
|
progress_percent = 0
|
|
221
|
+
context: dict[str, validate | DirectorOutputDto] = {
|
|
222
|
+
"output_dto": self.output_dto,
|
|
223
|
+
"config": self.input_dto,
|
|
224
|
+
}
|
|
225
|
+
contentCartegoryName: str = contentType.__name__.upper() # type: ignore
|
|
143
226
|
|
|
144
227
|
for index, file in enumerate(security_content_files):
|
|
145
228
|
progress_percent = ((index + 1) / len(security_content_files)) * 100
|
|
146
229
|
try:
|
|
147
|
-
type_string = contentType.
|
|
230
|
+
type_string = contentType.__name__.upper() # type: ignore
|
|
148
231
|
modelDict = YmlReader.load_file(file)
|
|
149
232
|
|
|
150
|
-
if contentType
|
|
151
|
-
|
|
152
|
-
modelDict,
|
|
153
|
-
context={
|
|
154
|
-
"output_dto": self.output_dto,
|
|
155
|
-
"config": self.input_dto,
|
|
156
|
-
},
|
|
157
|
-
)
|
|
158
|
-
# lookup = Lookup.model_validate(modelDict, context={"output_dto":self.output_dto, "config":self.input_dto})
|
|
159
|
-
self.output_dto.addContentToDictMappings(lookup)
|
|
160
|
-
|
|
161
|
-
elif contentType == SecurityContentType.macros:
|
|
162
|
-
macro = Macro.model_validate(
|
|
163
|
-
modelDict, context={"output_dto": self.output_dto}
|
|
233
|
+
if isinstance(contentType, type(SecurityContentObject)):
|
|
234
|
+
content: SecurityContentObject = contentType.model_validate(
|
|
235
|
+
modelDict, context=context
|
|
164
236
|
)
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
deployment = Deployment.model_validate(
|
|
169
|
-
modelDict, context={"output_dto": self.output_dto}
|
|
237
|
+
elif contentType == LookupAdapter:
|
|
238
|
+
content: SecurityContentObject = ( # type: ignore
|
|
239
|
+
contentType.validate_python(modelDict, context=context) # type:ignore
|
|
170
240
|
)
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
modelDict, context={"output_dto": self.output_dto}
|
|
176
|
-
)
|
|
177
|
-
self.output_dto.addContentToDictMappings(playbook)
|
|
178
|
-
|
|
179
|
-
elif contentType == SecurityContentType.baselines:
|
|
180
|
-
baseline = Baseline.model_validate(
|
|
181
|
-
modelDict, context={"output_dto": self.output_dto}
|
|
182
|
-
)
|
|
183
|
-
self.output_dto.addContentToDictMappings(baseline)
|
|
184
|
-
|
|
185
|
-
elif contentType == SecurityContentType.investigations:
|
|
186
|
-
investigation = Investigation.model_validate(
|
|
187
|
-
modelDict, context={"output_dto": self.output_dto}
|
|
188
|
-
)
|
|
189
|
-
self.output_dto.addContentToDictMappings(investigation)
|
|
190
|
-
|
|
191
|
-
elif contentType == SecurityContentType.stories:
|
|
192
|
-
story = Story.model_validate(
|
|
193
|
-
modelDict, context={"output_dto": self.output_dto}
|
|
194
|
-
)
|
|
195
|
-
self.output_dto.addContentToDictMappings(story)
|
|
196
|
-
|
|
197
|
-
elif contentType == SecurityContentType.detections:
|
|
198
|
-
detection = Detection.model_validate(
|
|
199
|
-
modelDict,
|
|
200
|
-
context={
|
|
201
|
-
"output_dto": self.output_dto,
|
|
202
|
-
"app": self.input_dto.app,
|
|
203
|
-
},
|
|
204
|
-
)
|
|
205
|
-
self.output_dto.addContentToDictMappings(detection)
|
|
206
|
-
|
|
207
|
-
elif contentType == SecurityContentType.dashboards:
|
|
208
|
-
dashboard = Dashboard.model_validate(
|
|
209
|
-
modelDict, context={"output_dto": self.output_dto}
|
|
210
|
-
)
|
|
211
|
-
self.output_dto.addContentToDictMappings(dashboard)
|
|
212
|
-
|
|
213
|
-
elif contentType == SecurityContentType.data_sources:
|
|
214
|
-
data_source = DataSource.model_validate(
|
|
215
|
-
modelDict, context={"output_dto": self.output_dto}
|
|
216
|
-
)
|
|
217
|
-
self.output_dto.addContentToDictMappings(data_source)
|
|
218
|
-
|
|
241
|
+
if not isinstance(content, SecurityContentObject):
|
|
242
|
+
raise Exception(
|
|
243
|
+
f"Expected lookup to be a SecurityContentObject (CSVLookup, KVStoreLookup, or MLModel), but it was actually: {type(content)}" # type: ignore
|
|
244
|
+
)
|
|
219
245
|
else:
|
|
220
|
-
raise Exception(f"
|
|
246
|
+
raise Exception(f"Unknown contentType in Director: {contentType}")
|
|
247
|
+
|
|
248
|
+
self.output_dto.addContentToDictMappings(content)
|
|
221
249
|
|
|
222
250
|
if (
|
|
223
251
|
sys.stdout.isatty() and sys.stdin.isatty() and sys.stderr.isatty()
|
|
@@ -236,7 +264,7 @@ class Director:
|
|
|
236
264
|
validation_errors.append((relative_path, e))
|
|
237
265
|
|
|
238
266
|
print(
|
|
239
|
-
f"\r{f'{
|
|
267
|
+
f"\r{f'{contentCartegoryName} Progress'.rjust(23)}: [{progress_percent:3.0f}%]...",
|
|
240
268
|
end="",
|
|
241
269
|
flush=True,
|
|
242
270
|
)
|
contentctl/input/yml_reader.py
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
from typing import Dict, Any
|
|
2
|
-
import yaml
|
|
3
|
-
import sys
|
|
4
1
|
import pathlib
|
|
2
|
+
import sys
|
|
3
|
+
from typing import Any, Dict
|
|
4
|
+
|
|
5
|
+
import yaml
|
|
5
6
|
|
|
6
7
|
|
|
7
8
|
class YmlReader:
|
|
@@ -13,40 +14,51 @@ class YmlReader:
|
|
|
13
14
|
) -> Dict[str, Any]:
|
|
14
15
|
try:
|
|
15
16
|
file_handler = open(file_path, "r", encoding="utf-8")
|
|
17
|
+
except OSError as exc:
|
|
18
|
+
print(
|
|
19
|
+
f"\nThere was an unrecoverable error when opening the file '{file_path}' - we will exit immediately:\n{str(exc)}"
|
|
20
|
+
)
|
|
21
|
+
sys.exit(1)
|
|
16
22
|
|
|
17
23
|
# The following code can help diagnose issues with duplicate keys or
|
|
18
24
|
# poorly-formatted but still "compliant" YML. This code should be
|
|
19
25
|
# enabled manually for debugging purposes. As such, strictyaml
|
|
20
26
|
# library is intentionally excluded from the contentctl requirements
|
|
21
27
|
|
|
28
|
+
try:
|
|
22
29
|
if STRICT_YML_CHECKING:
|
|
30
|
+
# This is an extra level of verbose parsing that can be
|
|
31
|
+
# enabled for debugging purpose. It is intentionally done in
|
|
32
|
+
# addition to the regular yml parsing
|
|
23
33
|
import strictyaml
|
|
24
34
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
file_handler.seek(0)
|
|
28
|
-
except Exception as e:
|
|
29
|
-
print(f"Error loading YML file {file_path}: {str(e)}")
|
|
30
|
-
sys.exit(1)
|
|
31
|
-
try:
|
|
32
|
-
# Ideally we should use
|
|
33
|
-
# from contentctl.actions.new_content import NewContent
|
|
34
|
-
# and use NewContent.UPDATE_PREFIX,
|
|
35
|
-
# but there is a circular dependency right now which makes that difficult.
|
|
36
|
-
# We have instead hardcoded UPDATE_PREFIX
|
|
37
|
-
UPDATE_PREFIX = "__UPDATE__"
|
|
38
|
-
data = file_handler.read()
|
|
39
|
-
if UPDATE_PREFIX in data:
|
|
40
|
-
raise Exception(
|
|
41
|
-
f"The file {file_path} contains the value '{UPDATE_PREFIX}'. Please fill out any unpopulated fields as required."
|
|
42
|
-
)
|
|
43
|
-
yml_obj = yaml.load(data, Loader=yaml.CSafeLoader)
|
|
44
|
-
except yaml.YAMLError as exc:
|
|
45
|
-
print(exc)
|
|
46
|
-
sys.exit(1)
|
|
35
|
+
strictyaml.dirty_load(file_handler.read(), allow_flow_style=True)
|
|
36
|
+
file_handler.seek(0)
|
|
47
37
|
|
|
48
|
-
|
|
49
|
-
|
|
38
|
+
# Ideally we should use
|
|
39
|
+
# from contentctl.actions.new_content import NewContent
|
|
40
|
+
# and use NewContent.UPDATE_PREFIX,
|
|
41
|
+
# but there is a circular dependency right now which makes that difficult.
|
|
42
|
+
# We have instead hardcoded UPDATE_PREFIX
|
|
43
|
+
UPDATE_PREFIX = "__UPDATE__"
|
|
44
|
+
data = file_handler.read()
|
|
45
|
+
if UPDATE_PREFIX in data:
|
|
46
|
+
raise Exception(
|
|
47
|
+
f"\nThe file {file_path} contains the value '{UPDATE_PREFIX}'. Please fill out any unpopulated fields as required."
|
|
48
|
+
)
|
|
49
|
+
yml_obj = yaml.load(data, Loader=yaml.CSafeLoader)
|
|
50
|
+
if yml_obj is None:
|
|
51
|
+
raise yaml.YAMLError(
|
|
52
|
+
f"The YML file's value was parsed as [{None}]. "
|
|
53
|
+
"This probably means that the file was entirely "
|
|
54
|
+
"empty or contains only comments, which is not "
|
|
55
|
+
"supported. Please ensure this file is NOT empty "
|
|
56
|
+
"or remove the file."
|
|
57
|
+
)
|
|
58
|
+
except yaml.YAMLError as exc:
|
|
59
|
+
print(
|
|
60
|
+
f"\nThere was an unrecoverable YML Parsing error when reading or parsing the file '{file_path}' - we will exit immediately:\n{str(exc)}"
|
|
61
|
+
)
|
|
50
62
|
sys.exit(1)
|
|
51
63
|
|
|
52
64
|
if add_fields is False:
|