contentctl 3.6.0__py3-none-any.whl → 4.0.2__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 +89 -0
- contentctl/actions/detection_testing/DetectionTestingManager.py +48 -49
- contentctl/actions/detection_testing/GitService.py +148 -230
- contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructure.py +14 -24
- contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructureContainer.py +43 -17
- contentctl/actions/detection_testing/views/DetectionTestingView.py +3 -2
- contentctl/actions/detection_testing/views/DetectionTestingViewFile.py +0 -8
- contentctl/actions/doc_gen.py +1 -1
- contentctl/actions/initialize.py +28 -65
- contentctl/actions/inspect.py +260 -0
- contentctl/actions/new_content.py +106 -13
- contentctl/actions/release_notes.py +168 -144
- contentctl/actions/reporting.py +24 -13
- contentctl/actions/test.py +39 -20
- contentctl/actions/validate.py +25 -48
- contentctl/contentctl.py +196 -754
- contentctl/enrichments/attack_enrichment.py +69 -19
- contentctl/enrichments/cve_enrichment.py +28 -13
- contentctl/helper/link_validator.py +24 -26
- contentctl/helper/utils.py +7 -3
- contentctl/input/director.py +139 -201
- contentctl/input/new_content_questions.py +63 -61
- contentctl/input/sigma_converter.py +1 -2
- contentctl/input/ssa_detection_builder.py +16 -7
- contentctl/input/yml_reader.py +4 -3
- contentctl/objects/abstract_security_content_objects/detection_abstract.py +487 -154
- contentctl/objects/abstract_security_content_objects/security_content_object_abstract.py +155 -51
- contentctl/objects/alert_action.py +40 -0
- contentctl/objects/atomic.py +212 -0
- contentctl/objects/baseline.py +44 -43
- contentctl/objects/baseline_tags.py +69 -20
- contentctl/objects/config.py +857 -125
- contentctl/objects/constants.py +0 -1
- contentctl/objects/correlation_search.py +1 -1
- contentctl/objects/data_source.py +2 -4
- contentctl/objects/deployment.py +61 -21
- contentctl/objects/deployment_email.py +2 -2
- contentctl/objects/deployment_notable.py +4 -4
- contentctl/objects/deployment_phantom.py +2 -2
- contentctl/objects/deployment_rba.py +3 -4
- contentctl/objects/deployment_scheduling.py +2 -3
- contentctl/objects/deployment_slack.py +2 -2
- contentctl/objects/detection.py +1 -5
- contentctl/objects/detection_tags.py +210 -119
- contentctl/objects/enums.py +312 -24
- contentctl/objects/integration_test.py +1 -1
- contentctl/objects/integration_test_result.py +0 -2
- contentctl/objects/investigation.py +62 -53
- contentctl/objects/investigation_tags.py +30 -6
- contentctl/objects/lookup.py +80 -31
- contentctl/objects/macro.py +29 -45
- contentctl/objects/mitre_attack_enrichment.py +29 -5
- contentctl/objects/observable.py +3 -7
- contentctl/objects/playbook.py +60 -30
- contentctl/objects/playbook_tags.py +45 -8
- contentctl/objects/security_content_object.py +1 -5
- contentctl/objects/ssa_detection.py +8 -4
- contentctl/objects/ssa_detection_tags.py +19 -26
- contentctl/objects/story.py +142 -44
- contentctl/objects/story_tags.py +46 -33
- contentctl/objects/unit_test.py +7 -2
- contentctl/objects/unit_test_attack_data.py +10 -19
- contentctl/objects/unit_test_baseline.py +1 -1
- contentctl/objects/unit_test_old.py +4 -3
- contentctl/objects/unit_test_result.py +5 -3
- contentctl/objects/unit_test_ssa.py +31 -0
- contentctl/output/api_json_output.py +202 -130
- contentctl/output/attack_nav_output.py +20 -9
- contentctl/output/attack_nav_writer.py +3 -3
- contentctl/output/ba_yml_output.py +3 -3
- contentctl/output/conf_output.py +125 -391
- contentctl/output/conf_writer.py +169 -31
- contentctl/output/jinja_writer.py +2 -2
- contentctl/output/json_writer.py +17 -5
- contentctl/output/new_content_yml_output.py +8 -7
- contentctl/output/svg_output.py +17 -27
- contentctl/output/templates/analyticstories_detections.j2 +8 -4
- contentctl/output/templates/analyticstories_investigations.j2 +1 -1
- contentctl/output/templates/analyticstories_stories.j2 +6 -6
- contentctl/output/templates/app.conf.j2 +2 -2
- contentctl/output/templates/app.manifest.j2 +2 -2
- contentctl/output/templates/detection_coverage.j2 +6 -8
- contentctl/output/templates/doc_detection_page.j2 +2 -2
- contentctl/output/templates/doc_detections.j2 +2 -2
- contentctl/output/templates/doc_stories.j2 +1 -1
- contentctl/output/templates/es_investigations_investigations.j2 +1 -1
- contentctl/output/templates/es_investigations_stories.j2 +1 -1
- contentctl/output/templates/header.j2 +2 -1
- contentctl/output/templates/macros.j2 +6 -10
- contentctl/output/templates/savedsearches_baselines.j2 +5 -5
- contentctl/output/templates/savedsearches_detections.j2 +36 -33
- contentctl/output/templates/savedsearches_investigations.j2 +4 -4
- contentctl/output/templates/transforms.j2 +4 -4
- contentctl/output/yml_writer.py +2 -2
- contentctl/templates/app_template/README.md +7 -0
- contentctl/{output/templates/splunk_app → templates/app_template}/default/data/ui/nav/default.xml +1 -0
- contentctl/templates/app_template/lookups/mitre_enrichment.csv +638 -0
- contentctl/templates/deployments/{00_default_anomaly.yml → escu_default_configuration_anomaly.yml} +1 -2
- contentctl/templates/deployments/{00_default_baseline.yml → escu_default_configuration_baseline.yml} +1 -2
- contentctl/templates/deployments/{00_default_correlation.yml → escu_default_configuration_correlation.yml} +2 -2
- contentctl/templates/deployments/{00_default_hunting.yml → escu_default_configuration_hunting.yml} +2 -2
- contentctl/templates/deployments/{00_default_ttp.yml → escu_default_configuration_ttp.yml} +1 -2
- contentctl/templates/detections/anomalous_usage_of_7zip.yml +0 -1
- contentctl/templates/stories/cobalt_strike.yml +0 -1
- {contentctl-3.6.0.dist-info → contentctl-4.0.2.dist-info}/METADATA +36 -15
- contentctl-4.0.2.dist-info/RECORD +168 -0
- contentctl/actions/detection_testing/DataManipulation.py +0 -149
- contentctl/actions/generate.py +0 -91
- contentctl/helper/config_handler.py +0 -75
- contentctl/input/baseline_builder.py +0 -66
- contentctl/input/basic_builder.py +0 -58
- contentctl/input/detection_builder.py +0 -370
- contentctl/input/investigation_builder.py +0 -42
- contentctl/input/new_content_generator.py +0 -95
- contentctl/input/playbook_builder.py +0 -68
- contentctl/input/story_builder.py +0 -106
- contentctl/objects/app.py +0 -214
- contentctl/objects/repo_config.py +0 -163
- contentctl/objects/test_config.py +0 -630
- contentctl/output/templates/macros_detections.j2 +0 -7
- contentctl/output/templates/splunk_app/README.md +0 -7
- contentctl-3.6.0.dist-info/RECORD +0 -176
- /contentctl/{output/templates/splunk_app → templates/app_template}/README/essoc_story_detail.txt +0 -0
- /contentctl/{output/templates/splunk_app → templates/app_template}/README/essoc_summary.txt +0 -0
- /contentctl/{output/templates/splunk_app → templates/app_template}/README/essoc_usage_dashboard.txt +0 -0
- /contentctl/{output/templates/splunk_app → templates/app_template}/default/analytic_stories.conf +0 -0
- /contentctl/{output/templates/splunk_app → templates/app_template}/default/app.conf +0 -0
- /contentctl/{output/templates/splunk_app → templates/app_template}/default/commands.conf +0 -0
- /contentctl/{output/templates/splunk_app → templates/app_template}/default/content-version.conf +0 -0
- /contentctl/{output/templates/splunk_app → templates/app_template}/default/data/ui/views/escu_summary.xml +0 -0
- /contentctl/{output/templates/splunk_app → templates/app_template}/default/data/ui/views/feedback.xml +0 -0
- /contentctl/{output/templates/splunk_app → templates/app_template}/default/distsearch.conf +0 -0
- /contentctl/{output/templates/splunk_app → templates/app_template}/default/usage_searches.conf +0 -0
- /contentctl/{output/templates/splunk_app → templates/app_template}/default/use_case_library.conf +0 -0
- /contentctl/{output/templates/splunk_app → templates/app_template}/metadata/default.meta +0 -0
- /contentctl/{output/templates/splunk_app → templates/app_template}/static/appIcon.png +0 -0
- /contentctl/{output/templates/splunk_app → templates/app_template}/static/appIconAlt.png +0 -0
- /contentctl/{output/templates/splunk_app → templates/app_template}/static/appIconAlt_2x.png +0 -0
- /contentctl/{output/templates/splunk_app → templates/app_template}/static/appIcon_2x.png +0 -0
- {contentctl-3.6.0.dist-info → contentctl-4.0.2.dist-info}/LICENSE.md +0 -0
- {contentctl-3.6.0.dist-info → contentctl-4.0.2.dist-info}/WHEEL +0 -0
- {contentctl-3.6.0.dist-info → contentctl-4.0.2.dist-info}/entry_points.txt +0 -0
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
import os
|
|
2
|
-
import collections
|
|
3
|
-
import sys
|
|
4
|
-
import pathlib
|
|
5
|
-
|
|
6
|
-
from contentctl.input.yml_reader import YmlReader
|
|
7
|
-
from contentctl.objects.config import Config, TestConfig, ConfigEnrichments
|
|
8
|
-
from contentctl.objects.test_config import InfrastructureConfig, Infrastructure
|
|
9
|
-
from contentctl.objects.enums import DetectionTestingMode
|
|
10
|
-
from typing import Union
|
|
11
|
-
import argparse
|
|
12
|
-
|
|
13
|
-
from contentctl.objects.enums import (
|
|
14
|
-
DetectionTestingTargetInfrastructure,
|
|
15
|
-
)
|
|
16
|
-
|
|
17
|
-
class ConfigHandler:
|
|
18
|
-
|
|
19
|
-
@classmethod
|
|
20
|
-
def read_config(cls, args:argparse.Namespace) -> Config:
|
|
21
|
-
config_path = pathlib.Path(args.path)/"contentctl.yml"
|
|
22
|
-
try:
|
|
23
|
-
yml_dict = YmlReader.load_file(config_path, add_fields=False)
|
|
24
|
-
|
|
25
|
-
except:
|
|
26
|
-
print("ERROR: no contentctl.yml found in given path")
|
|
27
|
-
sys.exit(1)
|
|
28
|
-
|
|
29
|
-
try:
|
|
30
|
-
config = Config.parse_obj(yml_dict)
|
|
31
|
-
if args.enable_enrichment:
|
|
32
|
-
config.enrichments.attack_enrichment = True
|
|
33
|
-
else:
|
|
34
|
-
# Use whatever setting is in contentctl.yml
|
|
35
|
-
pass
|
|
36
|
-
except Exception as e:
|
|
37
|
-
raise Exception(f"Error reading config file: {str(e)}")
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
return config
|
|
41
|
-
|
|
42
|
-
@classmethod
|
|
43
|
-
def read_test_config(cls, args:argparse.Namespace) -> TestConfig:
|
|
44
|
-
test_config_path = pathlib.Path(args.path)/"contentctl_test.yml"
|
|
45
|
-
try:
|
|
46
|
-
yml_dict = YmlReader.load_file(test_config_path, add_fields=False)
|
|
47
|
-
except:
|
|
48
|
-
print("ERROR: no contentctl_test.yml found in given path")
|
|
49
|
-
sys.exit(1)
|
|
50
|
-
|
|
51
|
-
try:
|
|
52
|
-
if args.dry_run:
|
|
53
|
-
yml_dict['apps'] = []
|
|
54
|
-
yml_dict['infrastructure_config'] = InfrastructureConfig(infrastructure_type=DetectionTestingTargetInfrastructure.server, ).__dict__
|
|
55
|
-
if args.server_info is None:
|
|
56
|
-
yml_dict['infrastructure_config']['infrastructures'] = [Infrastructure().__dict__]
|
|
57
|
-
if args.mode != DetectionTestingMode.changes:
|
|
58
|
-
yml_dict['version_control_config'] = None
|
|
59
|
-
if yml_dict.get("version_control_config", None) is not None:
|
|
60
|
-
#If they have been passed, override the target and test branch. If not, keep the defaults
|
|
61
|
-
yml_dict.get("version_control_config", None)['target_branch'] = args.target_branch or yml_dict.get("version_control_config", None)['target_branch']
|
|
62
|
-
yml_dict.get("version_control_config", None)['test_branch'] = args.test_branch or yml_dict.get("version_control_config", None)['test_branch']
|
|
63
|
-
if yml_dict.get("infrastructure_config", None) is not None:
|
|
64
|
-
yml_dict.get("infrastructure_config", None)['infrastructure_type'] = args.infrastructure or yml_dict.get("infrastructure_config", None)['infrastructure_type']
|
|
65
|
-
test_config = TestConfig.parse_obj(yml_dict)
|
|
66
|
-
except Exception as e:
|
|
67
|
-
raise Exception(f"Error reading test config file: {str(e)}")
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
return test_config
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
import sys
|
|
2
|
-
import pathlib
|
|
3
|
-
from pydantic import ValidationError
|
|
4
|
-
|
|
5
|
-
from contentctl.input.yml_reader import YmlReader
|
|
6
|
-
from contentctl.objects.baseline import Baseline
|
|
7
|
-
from contentctl.objects.enums import SecurityContentType
|
|
8
|
-
from contentctl.objects.enums import SecurityContentProduct
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
class BaselineBuilder():
|
|
12
|
-
baseline : Baseline
|
|
13
|
-
|
|
14
|
-
def setObject(self, path: pathlib.Path) -> None:
|
|
15
|
-
yml_dict = YmlReader.load_file(path)
|
|
16
|
-
yml_dict["tags"]["name"] = yml_dict["name"]
|
|
17
|
-
|
|
18
|
-
try:
|
|
19
|
-
self.baseline = Baseline.parse_obj(yml_dict)
|
|
20
|
-
|
|
21
|
-
except ValidationError as e:
|
|
22
|
-
print('Validation Error for file ' + str(path))
|
|
23
|
-
print(e)
|
|
24
|
-
sys.exit(1)
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
def addDeployment(self, deployments: list) -> None:
|
|
28
|
-
if not self.baseline.deployment:
|
|
29
|
-
|
|
30
|
-
matched_deployments = []
|
|
31
|
-
|
|
32
|
-
for d in deployments:
|
|
33
|
-
d_tags = dict(d.tags)
|
|
34
|
-
baseline_dict = self.baseline.dict()
|
|
35
|
-
baseline_tags_dict = self.baseline.tags.dict()
|
|
36
|
-
for d_tag in d_tags.keys():
|
|
37
|
-
for attr in baseline_dict.keys():
|
|
38
|
-
if attr == d_tag:
|
|
39
|
-
if isinstance(baseline_dict[attr], str):
|
|
40
|
-
if baseline_dict[attr] == d_tags[d_tag]:
|
|
41
|
-
matched_deployments.append(d)
|
|
42
|
-
elif isinstance(baseline_dict[attr], list):
|
|
43
|
-
if d_tags[d_tag] in baseline_dict[attr]:
|
|
44
|
-
matched_deployments.append(d)
|
|
45
|
-
|
|
46
|
-
for attr in baseline_tags_dict.keys():
|
|
47
|
-
if attr == d_tag:
|
|
48
|
-
if isinstance(baseline_tags_dict[attr], str):
|
|
49
|
-
if baseline_tags_dict[attr] == d_tags[d_tag]:
|
|
50
|
-
matched_deployments.append(d)
|
|
51
|
-
elif isinstance(baseline_tags_dict[attr], list):
|
|
52
|
-
if d_tags[d_tag] in baseline_tags_dict[attr]:
|
|
53
|
-
matched_deployments.append(d)
|
|
54
|
-
|
|
55
|
-
if len(matched_deployments) == 0:
|
|
56
|
-
raise ValueError('No deployment found for baseline: ' + self.baseline.name)
|
|
57
|
-
|
|
58
|
-
self.baseline.deployment = matched_deployments[-1]
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
def reset(self) -> None:
|
|
62
|
-
self.baseline = None
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
def getObject(self) -> Baseline:
|
|
66
|
-
return self.baseline
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
import sys
|
|
2
|
-
import pathlib
|
|
3
|
-
from pydantic import ValidationError
|
|
4
|
-
|
|
5
|
-
from contentctl.objects.security_content_object import SecurityContentObject
|
|
6
|
-
from contentctl.input.yml_reader import YmlReader
|
|
7
|
-
from contentctl.objects.enums import SecurityContentType
|
|
8
|
-
from contentctl.objects.deployment import Deployment
|
|
9
|
-
from contentctl.objects.macro import Macro
|
|
10
|
-
from contentctl.objects.lookup import Lookup
|
|
11
|
-
from contentctl.objects.playbook import Playbook
|
|
12
|
-
from contentctl.objects.unit_test import UnitTest
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
class BasicBuilder():
|
|
16
|
-
security_content_obj : SecurityContentObject
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
def setObject(self, path: pathlib.Path, type: SecurityContentType) -> None:
|
|
20
|
-
yml_dict = YmlReader.load_file(path)
|
|
21
|
-
if type == SecurityContentType.deployments:
|
|
22
|
-
if "alert_action" in yml_dict:
|
|
23
|
-
alert_action_dict = yml_dict["alert_action"]
|
|
24
|
-
for key in alert_action_dict.keys():
|
|
25
|
-
yml_dict[key] = yml_dict["alert_action"][key]
|
|
26
|
-
try:
|
|
27
|
-
self.security_content_obj = Deployment.parse_obj(yml_dict)
|
|
28
|
-
except ValidationError as e:
|
|
29
|
-
print('Validation Error for file ' + str(path))
|
|
30
|
-
print(e)
|
|
31
|
-
sys.exit(1)
|
|
32
|
-
elif type == SecurityContentType.macros:
|
|
33
|
-
try:
|
|
34
|
-
self.security_content_obj = Macro.parse_obj(yml_dict)
|
|
35
|
-
except ValidationError as e:
|
|
36
|
-
print('Validation Error for file ' + str(path))
|
|
37
|
-
print(e)
|
|
38
|
-
sys.exit(1)
|
|
39
|
-
elif type == SecurityContentType.lookups:
|
|
40
|
-
try:
|
|
41
|
-
self.security_content_obj = Lookup.parse_obj(yml_dict)
|
|
42
|
-
except ValidationError as e:
|
|
43
|
-
print('Validation Error for file ' + str(path))
|
|
44
|
-
print(e)
|
|
45
|
-
sys.exit(1)
|
|
46
|
-
elif type == SecurityContentType.unit_tests:
|
|
47
|
-
try:
|
|
48
|
-
self.security_content_obj = UnitTest.parse_obj(yml_dict)
|
|
49
|
-
except ValidationError as e:
|
|
50
|
-
print('Validation Error for file ' + str(path))
|
|
51
|
-
print(e)
|
|
52
|
-
sys.exit(1)
|
|
53
|
-
|
|
54
|
-
def reset(self) -> None:
|
|
55
|
-
self.security_content_obj = None
|
|
56
|
-
|
|
57
|
-
def getObject(self) -> SecurityContentObject:
|
|
58
|
-
return self.security_content_obj
|
|
@@ -1,370 +0,0 @@
|
|
|
1
|
-
import sys
|
|
2
|
-
import re
|
|
3
|
-
import os
|
|
4
|
-
|
|
5
|
-
from pydantic import ValidationError
|
|
6
|
-
|
|
7
|
-
from contentctl.input.yml_reader import YmlReader
|
|
8
|
-
from contentctl.objects.detection import Detection
|
|
9
|
-
from contentctl.objects.security_content_object import SecurityContentObject
|
|
10
|
-
from contentctl.objects.macro import Macro
|
|
11
|
-
from contentctl.objects.lookup import Lookup
|
|
12
|
-
from contentctl.objects.mitre_attack_enrichment import MitreAttackEnrichment
|
|
13
|
-
from contentctl.objects.integration_test import IntegrationTest
|
|
14
|
-
from contentctl.enrichments.cve_enrichment import CveEnrichment
|
|
15
|
-
from contentctl.enrichments.splunk_app_enrichment import SplunkAppEnrichment
|
|
16
|
-
from contentctl.objects.config import ConfigDetectionConfiguration
|
|
17
|
-
from contentctl.objects.constants import ATTACK_TACTICS_KILLCHAIN_MAPPING
|
|
18
|
-
|
|
19
|
-
class DetectionBuilder():
|
|
20
|
-
security_content_obj : SecurityContentObject
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
def setObject(self, path: str) -> None:
|
|
24
|
-
yml_dict = YmlReader.load_file(path)
|
|
25
|
-
yml_dict["tags"]["name"] = yml_dict["name"]
|
|
26
|
-
self.security_content_obj = Detection.parse_obj(yml_dict)
|
|
27
|
-
self.security_content_obj.source = os.path.split(os.path.dirname(self.security_content_obj.file_path))[-1]
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
def addDeployment(self, deployments: list) -> None:
|
|
31
|
-
if self.security_content_obj:
|
|
32
|
-
if not self.security_content_obj.deployment:
|
|
33
|
-
matched_deployments = []
|
|
34
|
-
for d in deployments:
|
|
35
|
-
d_tags = dict(d.tags)
|
|
36
|
-
for d_tag in d_tags.keys():
|
|
37
|
-
for attr in dir(self.security_content_obj):
|
|
38
|
-
if not (attr.startswith('__') or attr.startswith('_')):
|
|
39
|
-
if attr == d_tag:
|
|
40
|
-
if type(self.security_content_obj.__getattribute__(attr)) is str:
|
|
41
|
-
attr_values = [self.security_content_obj.__getattribute__(attr)]
|
|
42
|
-
else:
|
|
43
|
-
attr_values = self.security_content_obj.__getattribute__(attr)
|
|
44
|
-
|
|
45
|
-
for attr_value in attr_values:
|
|
46
|
-
if attr_value == d_tags[d_tag]:
|
|
47
|
-
matched_deployments.append(d)
|
|
48
|
-
|
|
49
|
-
if len(matched_deployments) == 0:
|
|
50
|
-
self.security_content_obj.deployment = None
|
|
51
|
-
else:
|
|
52
|
-
self.security_content_obj.deployment = matched_deployments[-1]
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
def addRBA(self) -> None:
|
|
56
|
-
if self.security_content_obj:
|
|
57
|
-
|
|
58
|
-
risk_objects = []
|
|
59
|
-
risk_object_user_types = {'user', 'username', 'email address'}
|
|
60
|
-
risk_object_system_types = {'device', 'endpoint', 'hostname', 'ip address'}
|
|
61
|
-
process_threat_object_types = {'process name','process'}
|
|
62
|
-
file_threat_object_types = {'file name','file', 'file hash'}
|
|
63
|
-
url_threat_object_types = {'url string','url'}
|
|
64
|
-
ip_threat_object_types = {'ip address'}
|
|
65
|
-
|
|
66
|
-
if hasattr(self.security_content_obj.tags, 'observable') and hasattr(self.security_content_obj.tags, 'risk_score'):
|
|
67
|
-
for entity in self.security_content_obj.tags.observable:
|
|
68
|
-
|
|
69
|
-
risk_object = dict()
|
|
70
|
-
if 'Victim' in entity.role and entity.type.lower() in risk_object_user_types:
|
|
71
|
-
risk_object['risk_object_type'] = 'user'
|
|
72
|
-
risk_object['risk_object_field'] = entity.name
|
|
73
|
-
risk_object['risk_score'] = self.security_content_obj.tags.risk_score
|
|
74
|
-
risk_objects.append(risk_object)
|
|
75
|
-
|
|
76
|
-
elif 'Victim' in entity.role and entity.type.lower() in risk_object_system_types:
|
|
77
|
-
risk_object['risk_object_type'] = 'system'
|
|
78
|
-
risk_object['risk_object_field'] = entity.name
|
|
79
|
-
risk_object['risk_score'] = self.security_content_obj.tags.risk_score
|
|
80
|
-
risk_objects.append(risk_object)
|
|
81
|
-
|
|
82
|
-
elif 'Attacker' in entity.role and entity.type.lower() in process_threat_object_types:
|
|
83
|
-
risk_object['threat_object_field'] = entity.name
|
|
84
|
-
risk_object['threat_object_type'] = "process"
|
|
85
|
-
risk_objects.append(risk_object)
|
|
86
|
-
|
|
87
|
-
elif 'Attacker' in entity.role and entity.type.lower() in file_threat_object_types:
|
|
88
|
-
risk_object['threat_object_field'] = entity.name
|
|
89
|
-
risk_object['threat_object_type'] = "file_name"
|
|
90
|
-
risk_objects.append(risk_object)
|
|
91
|
-
|
|
92
|
-
elif 'Attacker' in entity.role and entity.type.lower() in ip_threat_object_types:
|
|
93
|
-
risk_object['threat_object_field'] = entity.name
|
|
94
|
-
risk_object['threat_object_type'] = "ip_address"
|
|
95
|
-
risk_objects.append(risk_object)
|
|
96
|
-
|
|
97
|
-
elif 'Attacker' in entity.role and entity.type.lower() in url_threat_object_types:
|
|
98
|
-
risk_object['threat_object_field'] = entity.name
|
|
99
|
-
risk_object['threat_object_type'] = "url"
|
|
100
|
-
risk_objects.append(risk_object)
|
|
101
|
-
|
|
102
|
-
else:
|
|
103
|
-
risk_object['risk_object_type'] = 'other'
|
|
104
|
-
risk_object['risk_object_field'] = entity.name
|
|
105
|
-
risk_object['risk_score'] = self.security_content_obj.tags.risk_score
|
|
106
|
-
risk_objects.append(risk_object)
|
|
107
|
-
continue
|
|
108
|
-
|
|
109
|
-
if self.security_content_obj.tags.risk_score >= 80:
|
|
110
|
-
self.security_content_obj.tags.risk_severity = 'high'
|
|
111
|
-
elif (self.security_content_obj.tags.risk_score >= 50 and self.security_content_obj.tags.risk_score <= 79):
|
|
112
|
-
self.security_content_obj.tags.risk_severity = 'medium'
|
|
113
|
-
else:
|
|
114
|
-
self.security_content_obj.tags.risk_severity = 'low'
|
|
115
|
-
|
|
116
|
-
self.security_content_obj.risk = risk_objects
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
def addProvidingTechnologies(self) -> None:
|
|
120
|
-
if self.security_content_obj:
|
|
121
|
-
if 'Endpoint' in str(self.security_content_obj.search):
|
|
122
|
-
self.security_content_obj.providing_technologies = ["Sysmon", "Microsoft Windows","Carbon Black Response","CrowdStrike Falcon", "Symantec Endpoint Protection"]
|
|
123
|
-
|
|
124
|
-
if "`sysmon`" in str(self.security_content_obj.search):
|
|
125
|
-
self.security_content_obj.providing_technologies = ["Microsoft Sysmon"]
|
|
126
|
-
|
|
127
|
-
if "`cloudtrail`" in str(self.security_content_obj.search):
|
|
128
|
-
self.security_content_obj.providing_technologies = ["Amazon Web Services - Cloudtrail"]
|
|
129
|
-
|
|
130
|
-
if '`wineventlog_security`' in self.security_content_obj.search or '`powershell`' in self.security_content_obj.search:
|
|
131
|
-
self.security_content_obj.providing_technologies = ["Microsoft Windows"]
|
|
132
|
-
|
|
133
|
-
if '`ms_defender`' in self.security_content_obj.search:
|
|
134
|
-
self.security_content_obj.providing_technologies = ["Microsoft Defender"]
|
|
135
|
-
if '`pingid`' in self.security_content_obj.search:
|
|
136
|
-
self.security_content_obj.providing_technologies = ["Ping ID"]
|
|
137
|
-
if '`okta' in self.security_content_obj.search:
|
|
138
|
-
self.security_content_obj.providing_technologies = ["Okta"]
|
|
139
|
-
if '`zeek_' in self.security_content_obj.search:
|
|
140
|
-
self.security_content_obj.providing_technologies = ["Zeek"]
|
|
141
|
-
if '`amazon_security_lake`' in self.security_content_obj.search:
|
|
142
|
-
self.security_content_obj.providing_technologies = ["Amazon Security Lake"]
|
|
143
|
-
|
|
144
|
-
if '`azure_monitor_aad`' in self.security_content_obj.search :
|
|
145
|
-
self.security_content_obj.providing_technologies = ["Azure AD", "Entra ID"]
|
|
146
|
-
|
|
147
|
-
if '`o365_' in self.security_content_obj.search:
|
|
148
|
-
self.security_content_obj.providing_technologies = ["Microsoft Office 365"]
|
|
149
|
-
|
|
150
|
-
if '`gsuite' in self.security_content_obj.search or '`google_' in self.security_content_obj.search or '`gws_' in self.security_content_obj.search:
|
|
151
|
-
self.security_content_obj.providing_technologies = ["Google Workspace","Google Cloud Platform"]
|
|
152
|
-
|
|
153
|
-
if '`splunkd_' in self.security_content_obj.search or 'audit_searches' in self.security_content_obj.search:
|
|
154
|
-
self.security_content_obj.providing_technologies = ["Splunk Internal Logs"]
|
|
155
|
-
|
|
156
|
-
if '`kube' in self.security_content_obj.search:
|
|
157
|
-
self.security_content_obj.providing_technologies = ["Kubernetes"]
|
|
158
|
-
|
|
159
|
-
def addNesFields(self) -> None:
|
|
160
|
-
if self.security_content_obj:
|
|
161
|
-
if self.security_content_obj.deployment:
|
|
162
|
-
if self.security_content_obj.deployment.notable:
|
|
163
|
-
nes_fields = ",".join(list(self.security_content_obj.deployment.notable.nes_fields))
|
|
164
|
-
self.security_content_obj.nes_fields = nes_fields
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
def addMappings(self) -> None:
|
|
168
|
-
if self.security_content_obj:
|
|
169
|
-
keys = ['mitre_attack', 'kill_chain_phases', 'cis20', 'nist']
|
|
170
|
-
mappings = {}
|
|
171
|
-
for key in keys:
|
|
172
|
-
if key == 'mitre_attack':
|
|
173
|
-
if getattr(self.security_content_obj.tags, 'mitre_attack_id'):
|
|
174
|
-
mappings[key] = getattr(self.security_content_obj.tags, 'mitre_attack_id')
|
|
175
|
-
elif getattr(self.security_content_obj.tags, key):
|
|
176
|
-
mappings[key] = getattr(self.security_content_obj.tags, key)
|
|
177
|
-
self.security_content_obj.mappings = mappings
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
def addAnnotations(self) -> None:
|
|
181
|
-
if self.security_content_obj:
|
|
182
|
-
annotations = {}
|
|
183
|
-
annotation_keys = ['mitre_attack', 'kill_chain_phases', 'cis20', 'nist',
|
|
184
|
-
'analytic_story', 'context', 'impact', 'confidence', 'cve']
|
|
185
|
-
for key in annotation_keys:
|
|
186
|
-
if key == 'mitre_attack':
|
|
187
|
-
if getattr(self.security_content_obj.tags, 'mitre_attack_id'):
|
|
188
|
-
annotations[key] = getattr(self.security_content_obj.tags, 'mitre_attack_id')
|
|
189
|
-
try:
|
|
190
|
-
if getattr(self.security_content_obj.tags, key):
|
|
191
|
-
annotations[key] = getattr(self.security_content_obj.tags, key)
|
|
192
|
-
except AttributeError as e:
|
|
193
|
-
continue
|
|
194
|
-
self.security_content_obj.annotations = annotations
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
def addPlaybook(self, playbooks: list) -> None:
|
|
198
|
-
if self.security_content_obj:
|
|
199
|
-
matched_playbooks = []
|
|
200
|
-
for playbook in playbooks:
|
|
201
|
-
if playbook.tags.detections:
|
|
202
|
-
for detection in playbook.tags.detections:
|
|
203
|
-
if detection == self.security_content_obj.name:
|
|
204
|
-
matched_playbooks.append(playbook)
|
|
205
|
-
|
|
206
|
-
self.security_content_obj.playbooks = matched_playbooks
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
def addBaseline(self, baselines: list) -> None:
|
|
210
|
-
if self.security_content_obj:
|
|
211
|
-
matched_baselines = []
|
|
212
|
-
for baseline in baselines:
|
|
213
|
-
for detection in baseline.tags.detections:
|
|
214
|
-
if detection == self.security_content_obj.name:
|
|
215
|
-
matched_baselines.append(baseline)
|
|
216
|
-
|
|
217
|
-
self.security_content_obj.baselines = matched_baselines
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
def addUnitTest(self) -> None:
|
|
221
|
-
if self.security_content_obj:
|
|
222
|
-
if self.security_content_obj.tests:
|
|
223
|
-
self.security_content_obj.test = self.security_content_obj.tests[0]
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
def addMitreAttackEnrichment(self, attack_enrichment: dict) -> None:
|
|
227
|
-
if self.security_content_obj:
|
|
228
|
-
if attack_enrichment:
|
|
229
|
-
if self.security_content_obj.tags.mitre_attack_id:
|
|
230
|
-
self.security_content_obj.tags.mitre_attack_enrichments = []
|
|
231
|
-
|
|
232
|
-
for mitre_attack_id in self.security_content_obj.tags.mitre_attack_id:
|
|
233
|
-
if mitre_attack_id in attack_enrichment:
|
|
234
|
-
mitre_attack_enrichment = MitreAttackEnrichment(
|
|
235
|
-
mitre_attack_id = mitre_attack_id,
|
|
236
|
-
mitre_attack_technique = attack_enrichment[mitre_attack_id]["technique"],
|
|
237
|
-
mitre_attack_tactics = sorted(attack_enrichment[mitre_attack_id]["tactics"]),
|
|
238
|
-
mitre_attack_groups = sorted(attack_enrichment[mitre_attack_id]["groups"])
|
|
239
|
-
)
|
|
240
|
-
self.security_content_obj.tags.mitre_attack_enrichments.append(mitre_attack_enrichment)
|
|
241
|
-
else:
|
|
242
|
-
#print("mitre_attack_id " + mitre_attack_id + " doesn't exist for detecction " + self.security_content_obj.name)
|
|
243
|
-
raise ValueError("mitre_attack_id " + mitre_attack_id + " doesn't exist for detection " + self.security_content_obj.name)
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
def addMacros(self, macros: list) -> None:
|
|
247
|
-
if self.security_content_obj:
|
|
248
|
-
found_macros, missing_macros = Macro.get_macros(self.security_content_obj.search, macros)
|
|
249
|
-
name = self.security_content_obj.name.replace(' ', '_').replace('-', '_').replace('.', '_').replace('/', '_').lower() + '_filter'
|
|
250
|
-
macro = Macro(name=name, definition='search *', description='Update this macro to limit the output results to filter out false positives.')
|
|
251
|
-
found_macros.append(macro)
|
|
252
|
-
self.security_content_obj.macros = found_macros
|
|
253
|
-
if len(missing_macros) > 0:
|
|
254
|
-
raise Exception(f"{self.security_content_obj.name} is missing the following macros: {missing_macros}")
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
def addLookups(self, lookups: list) -> None:
|
|
259
|
-
if self.security_content_obj:
|
|
260
|
-
found_lookups, missing_lookups = Lookup.get_lookups(self.security_content_obj.search, lookups)
|
|
261
|
-
self.security_content_obj.lookups = found_lookups
|
|
262
|
-
if len(missing_lookups) > 0:
|
|
263
|
-
raise Exception(f"{self.security_content_obj.name} is missing the following lookups: {missing_lookups}")
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
def addCve(self) -> None:
|
|
268
|
-
if self.security_content_obj:
|
|
269
|
-
self.security_content_obj.cve_enrichment = []
|
|
270
|
-
if self.security_content_obj.tags.cve:
|
|
271
|
-
for cve in self.security_content_obj.tags.cve:
|
|
272
|
-
self.security_content_obj.cve_enrichment.append(CveEnrichment.enrich_cve(cve))
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
def addSplunkApp(self) -> None:
|
|
276
|
-
if self.security_content_obj:
|
|
277
|
-
self.security_content_obj.splunk_app_enrichment = []
|
|
278
|
-
if self.security_content_obj.tags.supported_tas:
|
|
279
|
-
for splunk_app in self.security_content_obj.tags.supported_tas:
|
|
280
|
-
self.security_content_obj.splunk_app_enrichment.append(SplunkAppEnrichment.enrich_splunk_app(splunk_app))
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
def addCIS(self) -> None:
|
|
284
|
-
if self.security_content_obj:
|
|
285
|
-
if self.security_content_obj.tags.security_domain == "network":
|
|
286
|
-
self.security_content_obj.tags.cis20 = ["CIS 13"]
|
|
287
|
-
else:
|
|
288
|
-
self.security_content_obj.tags.cis20 = ["CIS 10"]
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
def addKillChainPhase(self) -> None:
|
|
292
|
-
if self.security_content_obj:
|
|
293
|
-
if not self.security_content_obj.tags.kill_chain_phases:
|
|
294
|
-
kill_chain_phases = list()
|
|
295
|
-
if self.security_content_obj.tags.mitre_attack_enrichments:
|
|
296
|
-
for mitre_attack_enrichment in self.security_content_obj.tags.mitre_attack_enrichments:
|
|
297
|
-
for mitre_attack_tactic in mitre_attack_enrichment.mitre_attack_tactics:
|
|
298
|
-
kill_chain_phases.append(ATTACK_TACTICS_KILLCHAIN_MAPPING[mitre_attack_tactic])
|
|
299
|
-
self.security_content_obj.tags.kill_chain_phases = list(dict.fromkeys(kill_chain_phases))
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
def addNist(self) -> None:
|
|
303
|
-
if self.security_content_obj:
|
|
304
|
-
if self.security_content_obj.type == "TTP":
|
|
305
|
-
self.security_content_obj.tags.nist = ["DE.CM"]
|
|
306
|
-
else:
|
|
307
|
-
self.security_content_obj.tags.nist = ["DE.AE"]
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
def addDatamodel(self) -> None:
|
|
311
|
-
if self.security_content_obj:
|
|
312
|
-
self.security_content_obj.datamodel = []
|
|
313
|
-
data_models = [
|
|
314
|
-
"Authentication",
|
|
315
|
-
"Change",
|
|
316
|
-
"Change_Analysis",
|
|
317
|
-
"Email",
|
|
318
|
-
"Endpoint",
|
|
319
|
-
"Network_Resolution",
|
|
320
|
-
"Network_Sessions",
|
|
321
|
-
"Network_Traffic",
|
|
322
|
-
"Risk",
|
|
323
|
-
"Splunk_Audit",
|
|
324
|
-
"UEBA",
|
|
325
|
-
"Updates",
|
|
326
|
-
"Vulnerabilities",
|
|
327
|
-
"Web"
|
|
328
|
-
]
|
|
329
|
-
for data_model in data_models:
|
|
330
|
-
if data_model in self.security_content_obj.search:
|
|
331
|
-
self.security_content_obj.datamodel.append(data_model)
|
|
332
|
-
|
|
333
|
-
def skipIntegrationTests(self) -> None:
|
|
334
|
-
"""
|
|
335
|
-
Skip all integration tests
|
|
336
|
-
"""
|
|
337
|
-
# Sanity check for typing and in setObject wasn't called yet
|
|
338
|
-
if self.security_content_obj is not None and isinstance(self.security_content_obj, Detection):
|
|
339
|
-
for test in self.security_content_obj.tests:
|
|
340
|
-
if isinstance(test, IntegrationTest):
|
|
341
|
-
test.skip("TEST SKIPPED: Skipping all integration tests")
|
|
342
|
-
else:
|
|
343
|
-
raise ValueError(
|
|
344
|
-
"security_content_obj must be an instance of Detection to skip integration tests, "
|
|
345
|
-
f"not {type(self.security_content_obj)}"
|
|
346
|
-
)
|
|
347
|
-
|
|
348
|
-
def skipAllTests(self, manual_test_explanation:str) -> None:
|
|
349
|
-
"""
|
|
350
|
-
Skip all unit and integration tests if the manual_test flag is defined in the yml
|
|
351
|
-
"""
|
|
352
|
-
# Sanity check for typing and in setObject wasn't called yet
|
|
353
|
-
if self.security_content_obj is not None and isinstance(self.security_content_obj, Detection):
|
|
354
|
-
for test in self.security_content_obj.tests:
|
|
355
|
-
#This should skip both unit and integration tests as appropriate
|
|
356
|
-
test.skip(f"TEST SKIPPED: Detection marked as 'manual_test' with explanation: {manual_test_explanation}")
|
|
357
|
-
|
|
358
|
-
else:
|
|
359
|
-
raise ValueError(
|
|
360
|
-
"security_content_obj must be an instance of Detection to skip unit and integration tests due "
|
|
361
|
-
f"to the presence of the manual_test field, not {type(self.security_content_obj)}"
|
|
362
|
-
)
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
def reset(self) -> None:
|
|
366
|
-
self.security_content_obj = None
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
def getObject(self) -> SecurityContentObject:
|
|
370
|
-
return self.security_content_obj
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
import re
|
|
2
|
-
import sys
|
|
3
|
-
|
|
4
|
-
from pydantic import ValidationError
|
|
5
|
-
|
|
6
|
-
from contentctl.objects.investigation import Investigation
|
|
7
|
-
from contentctl.input.yml_reader import YmlReader
|
|
8
|
-
from contentctl.objects.enums import SecurityContentType
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
class InvestigationBuilder():
|
|
12
|
-
investigation: Investigation
|
|
13
|
-
|
|
14
|
-
def setObject(self, path: str) -> None:
|
|
15
|
-
yml_dict = YmlReader.load_file(path)
|
|
16
|
-
try:
|
|
17
|
-
self.investigation = Investigation.parse_obj(yml_dict)
|
|
18
|
-
except ValidationError as e:
|
|
19
|
-
print('Validation Error for file ' + path)
|
|
20
|
-
print(e)
|
|
21
|
-
sys.exit(1)
|
|
22
|
-
|
|
23
|
-
def reset(self) -> None:
|
|
24
|
-
self.investigation = None
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
def getObject(self) -> Investigation:
|
|
28
|
-
return self.investigation
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
def addInputs(self) -> None:
|
|
32
|
-
pattern = r"\$([^\s.]*)\$"
|
|
33
|
-
inputs = []
|
|
34
|
-
|
|
35
|
-
for input in re.findall(pattern, self.investigation.search):
|
|
36
|
-
inputs.append(input)
|
|
37
|
-
|
|
38
|
-
self.investigation.inputs = inputs
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
def addLowercaseName(self) -> None:
|
|
42
|
-
self.investigation.lowercase_name = self.investigation.name.replace(' ', '_').replace('-','_').replace('.','_').replace('/','_').lower().replace(' ', '_').replace('-','_').replace('.','_').replace('/','_').lower()
|