contentctl 4.1.5__tar.gz → 4.2.0__tar.gz
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-4.1.5 → contentctl-4.2.0}/PKG-INFO +1 -1
- {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/actions/build.py +14 -1
- {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/actions/initialize.py +1 -0
- {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/actions/validate.py +0 -1
- {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/input/director.py +39 -56
- {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/input/yml_reader.py +2 -0
- {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/objects/abstract_security_content_objects/detection_abstract.py +45 -23
- {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/objects/abstract_security_content_objects/security_content_object_abstract.py +29 -2
- contentctl-4.2.0/contentctl/objects/data_source.py +42 -0
- {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/objects/enums.py +0 -1
- contentctl-4.2.0/contentctl/objects/event_source.py +11 -0
- {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/objects/story.py +12 -5
- contentctl-4.2.0/contentctl/output/data_source_writer.py +40 -0
- contentctl-4.2.0/contentctl/templates/data_sources/sysmon_eventid_1.yml +171 -0
- {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/templates/detections/endpoint/anomalous_usage_of_7zip.yml +1 -1
- {contentctl-4.1.5 → contentctl-4.2.0}/pyproject.toml +1 -1
- contentctl-4.1.5/contentctl/objects/data_source.py +0 -28
- contentctl-4.1.5/contentctl/objects/event_source.py +0 -10
- {contentctl-4.1.5 → contentctl-4.2.0}/LICENSE.md +0 -0
- {contentctl-4.1.5 → contentctl-4.2.0}/README.md +0 -0
- {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/__init__.py +0 -0
- {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/actions/convert.py +0 -0
- {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/actions/deploy_acs.py +0 -0
- {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/actions/detection_testing/DetectionTestingManager.py +0 -0
- {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/actions/detection_testing/GitService.py +0 -0
- {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/actions/detection_testing/generate_detection_coverage_badge.py +0 -0
- {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructure.py +0 -0
- {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructureContainer.py +0 -0
- {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructureServer.py +0 -0
- {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/actions/detection_testing/progress_bar.py +0 -0
- {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/actions/detection_testing/views/DetectionTestingView.py +0 -0
- {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/actions/detection_testing/views/DetectionTestingViewCLI.py +0 -0
- {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/actions/detection_testing/views/DetectionTestingViewFile.py +0 -0
- {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/actions/detection_testing/views/DetectionTestingViewWeb.py +0 -0
- {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/actions/doc_gen.py +0 -0
- {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/actions/initialize_old.py +0 -0
- {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/actions/inspect.py +0 -0
- {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/actions/new_content.py +0 -0
- {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/actions/release_notes.py +0 -0
- {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/actions/reporting.py +0 -0
- {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/actions/test.py +0 -0
- {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/api.py +0 -0
- {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/contentctl.py +0 -0
- {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/enrichments/attack_enrichment.py +0 -0
- {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/enrichments/cve_enrichment.py +0 -0
- {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/enrichments/splunk_app_enrichment.py +0 -0
- {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/helper/link_validator.py +0 -0
- {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/helper/logger.py +0 -0
- {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/helper/utils.py +0 -0
- {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/input/backend_splunk_ba.py +0 -0
- {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/input/new_content_questions.py +0 -0
- {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/input/sigma_converter.py +0 -0
- {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/input/ssa_detection_builder.py +0 -0
- {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/objects/alert_action.py +0 -0
- {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/objects/atomic.py +0 -0
- {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/objects/base_test.py +0 -0
- {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/objects/base_test_result.py +0 -0
- {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/objects/baseline.py +0 -0
- {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/objects/baseline_tags.py +0 -0
- {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/objects/config.py +0 -0
- {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/objects/constants.py +0 -0
- {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/objects/correlation_search.py +0 -0
- {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/objects/deployment.py +0 -0
- {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/objects/deployment_email.py +0 -0
- {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/objects/deployment_notable.py +0 -0
- {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/objects/deployment_phantom.py +0 -0
- {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/objects/deployment_rba.py +0 -0
- {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/objects/deployment_scheduling.py +0 -0
- {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/objects/deployment_slack.py +0 -0
- {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/objects/detection.py +0 -0
- {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/objects/detection_tags.py +0 -0
- {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/objects/integration_test.py +0 -0
- {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/objects/integration_test_result.py +0 -0
- {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/objects/investigation.py +0 -0
- {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/objects/investigation_tags.py +0 -0
- {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/objects/lookup.py +0 -0
- {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/objects/macro.py +0 -0
- {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/objects/mitre_attack_enrichment.py +0 -0
- {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/objects/notable_action.py +0 -0
- {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/objects/observable.py +0 -0
- {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/objects/playbook.py +0 -0
- {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/objects/playbook_tags.py +0 -0
- {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/objects/risk_analysis_action.py +0 -0
- {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/objects/risk_object.py +0 -0
- {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/objects/security_content_object.py +0 -0
- {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/objects/ssa_detection.py +0 -0
- {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/objects/ssa_detection_tags.py +0 -0
- {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/objects/story_tags.py +0 -0
- {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/objects/test_group.py +0 -0
- {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/objects/threat_object.py +0 -0
- {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/objects/unit_test.py +0 -0
- {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/objects/unit_test_attack_data.py +0 -0
- {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/objects/unit_test_baseline.py +0 -0
- {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/objects/unit_test_old.py +0 -0
- {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/objects/unit_test_result.py +0 -0
- {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/objects/unit_test_ssa.py +0 -0
- {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/output/api_json_output.py +0 -0
- {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/output/attack_nav_output.py +0 -0
- {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/output/attack_nav_writer.py +0 -0
- {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/output/ba_yml_output.py +0 -0
- {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/output/conf_output.py +0 -0
- {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/output/conf_writer.py +0 -0
- {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/output/detection_writer.py +0 -0
- {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/output/doc_md_output.py +0 -0
- {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/output/finding_report_writer.py +0 -0
- {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/output/jinja_writer.py +0 -0
- {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/output/json_writer.py +0 -0
- {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/output/new_content_yml_output.py +0 -0
- {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/output/svg_output.py +0 -0
- {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/output/templates/analyticstories_detections.j2 +0 -0
- {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/output/templates/analyticstories_investigations.j2 +0 -0
- {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/output/templates/analyticstories_stories.j2 +0 -0
- {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/output/templates/app.conf.j2 +0 -0
- {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/output/templates/app.manifest.j2 +0 -0
- {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/output/templates/collections.j2 +0 -0
- {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/output/templates/content-version.j2 +0 -0
- {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/output/templates/detection_count.j2 +0 -0
- {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/output/templates/detection_coverage.j2 +0 -0
- {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/output/templates/doc_detection_page.j2 +0 -0
- {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/output/templates/doc_detections.j2 +0 -0
- {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/output/templates/doc_navigation.j2 +0 -0
- {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/output/templates/doc_navigation_pages.j2 +0 -0
- {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/output/templates/doc_playbooks.j2 +0 -0
- {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/output/templates/doc_playbooks_page.j2 +0 -0
- {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/output/templates/doc_stories.j2 +0 -0
- {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/output/templates/doc_story_page.j2 +0 -0
- {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/output/templates/es_investigations_investigations.j2 +0 -0
- {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/output/templates/es_investigations_stories.j2 +0 -0
- {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/output/templates/finding_report.j2 +0 -0
- {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/output/templates/header.j2 +0 -0
- {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/output/templates/macros.j2 +0 -0
- {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/output/templates/panel.j2 +0 -0
- {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/output/templates/savedsearches_baselines.j2 +0 -0
- {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/output/templates/savedsearches_detections.j2 +0 -0
- {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/output/templates/savedsearches_investigations.j2 +0 -0
- {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/output/templates/transforms.j2 +0 -0
- {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/output/templates/workflow_actions.j2 +0 -0
- {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/output/yml_output.py +0 -0
- {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/output/yml_writer.py +0 -0
- {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/templates/README +0 -0
- {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/templates/app_default.yml +0 -0
- {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/templates/app_template/README/essoc_story_detail.txt +0 -0
- {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/templates/app_template/README/essoc_summary.txt +0 -0
- {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/templates/app_template/README/essoc_usage_dashboard.txt +0 -0
- {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/templates/app_template/README.md +0 -0
- {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/templates/app_template/default/analytic_stories.conf +0 -0
- {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/templates/app_template/default/app.conf +0 -0
- {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/templates/app_template/default/commands.conf +0 -0
- {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/templates/app_template/default/content-version.conf +0 -0
- {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/templates/app_template/default/data/ui/nav/default.xml +0 -0
- {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/templates/app_template/default/data/ui/views/escu_summary.xml +0 -0
- {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/templates/app_template/default/data/ui/views/feedback.xml +0 -0
- {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/templates/app_template/default/use_case_library.conf +0 -0
- {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/templates/app_template/lookups/mitre_enrichment.csv +0 -0
- {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/templates/app_template/metadata/default.meta +0 -0
- {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/templates/app_template/static/appIcon.png +0 -0
- {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/templates/app_template/static/appIconAlt.png +0 -0
- {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/templates/app_template/static/appIconAlt_2x.png +0 -0
- {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/templates/app_template/static/appIcon_2x.png +0 -0
- {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/templates/datamodels_cim.conf +0 -0
- {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/templates/datamodels_custom.conf +0 -0
- {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/templates/deployments/escu_default_configuration_anomaly.yml +0 -0
- {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/templates/deployments/escu_default_configuration_baseline.yml +0 -0
- {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/templates/deployments/escu_default_configuration_correlation.yml +0 -0
- {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/templates/deployments/escu_default_configuration_hunting.yml +0 -0
- {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/templates/deployments/escu_default_configuration_ttp.yml +0 -0
- {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/templates/detections/application/.gitkeep +0 -0
- {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/templates/detections/cloud/.gitkeep +0 -0
- {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/templates/detections/network/.gitkeep +0 -0
- {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/templates/detections/web/.gitkeep +0 -0
- {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/templates/macros/security_content_ctime.yml +0 -0
- {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/templates/macros/security_content_summariesonly.yml +0 -0
- {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/templates/stories/cobalt_strike.yml +0 -0
|
@@ -10,6 +10,8 @@ from contentctl.output.conf_output import ConfOutput
|
|
|
10
10
|
from contentctl.output.conf_writer import ConfWriter
|
|
11
11
|
from contentctl.output.ba_yml_output import BAYmlOutput
|
|
12
12
|
from contentctl.output.api_json_output import ApiJsonOutput
|
|
13
|
+
from contentctl.output.data_source_writer import DataSourceWriter
|
|
14
|
+
from contentctl.objects.lookup import Lookup
|
|
13
15
|
import pathlib
|
|
14
16
|
import json
|
|
15
17
|
import datetime
|
|
@@ -28,9 +30,20 @@ class Build:
|
|
|
28
30
|
|
|
29
31
|
|
|
30
32
|
def execute(self, input_dto: BuildInputDto) -> DirectorOutputDto:
|
|
31
|
-
if input_dto.config.build_app:
|
|
33
|
+
if input_dto.config.build_app:
|
|
34
|
+
|
|
32
35
|
updated_conf_files:set[pathlib.Path] = set()
|
|
33
36
|
conf_output = ConfOutput(input_dto.config)
|
|
37
|
+
|
|
38
|
+
# Construct a special lookup whose CSV is created at runtime and
|
|
39
|
+
# written directly into the output folder. It is created with model_construct,
|
|
40
|
+
# not model_validate, because the CSV does not exist yet.
|
|
41
|
+
data_sources_lookup_csv_path = input_dto.config.getPackageDirectoryPath() / "lookups" / "data_sources.csv"
|
|
42
|
+
DataSourceWriter.writeDataSourceCsv(input_dto.director_output_dto.data_sources, data_sources_lookup_csv_path)
|
|
43
|
+
input_dto.director_output_dto.addContentToDictMappings(Lookup.model_construct(description= "A lookup file that will contain the data source objects for detections.",
|
|
44
|
+
filename=data_sources_lookup_csv_path,
|
|
45
|
+
name="data_sources"))
|
|
46
|
+
|
|
34
47
|
updated_conf_files.update(conf_output.writeHeaders())
|
|
35
48
|
updated_conf_files.update(conf_output.writeObjects(input_dto.director_output_dto.detections, SecurityContentType.detections))
|
|
36
49
|
updated_conf_files.update(conf_output.writeObjects(input_dto.director_output_dto.stories, SecurityContentType.stories))
|
|
@@ -28,6 +28,7 @@ class Initialize:
|
|
|
28
28
|
('../templates/app_template/', 'app_template'),
|
|
29
29
|
('../templates/deployments/', 'deployments'),
|
|
30
30
|
('../templates/detections/', 'detections'),
|
|
31
|
+
('../templates/data_sources/', 'data_sources'),
|
|
31
32
|
('../templates/macros/','macros'),
|
|
32
33
|
('../templates/stories/', 'stories'),
|
|
33
34
|
]:
|
|
@@ -58,7 +58,6 @@ class DirectorOutputDto:
|
|
|
58
58
|
deployments: list[Deployment]
|
|
59
59
|
ssa_detections: list[SSADetection]
|
|
60
60
|
data_sources: list[DataSource]
|
|
61
|
-
event_sources: list[EventSource]
|
|
62
61
|
name_to_content_map: dict[str, SecurityContentObject] = field(default_factory=dict)
|
|
63
62
|
uuid_to_content_map: dict[UUID, SecurityContentObject] = field(default_factory=dict)
|
|
64
63
|
|
|
@@ -68,17 +67,19 @@ class DirectorOutputDto:
|
|
|
68
67
|
# Since SSA detections may have the same name as ESCU detection,
|
|
69
68
|
# for this function we prepend 'SSA ' to the name.
|
|
70
69
|
content_name = f"SSA {content_name}"
|
|
70
|
+
|
|
71
71
|
if content_name in self.name_to_content_map:
|
|
72
72
|
raise ValueError(
|
|
73
73
|
f"Duplicate name '{content_name}' with paths:\n"
|
|
74
74
|
f" - {content.file_path}\n"
|
|
75
75
|
f" - {self.name_to_content_map[content_name].file_path}"
|
|
76
76
|
)
|
|
77
|
-
|
|
77
|
+
|
|
78
|
+
if content.id in self.uuid_to_content_map:
|
|
78
79
|
raise ValueError(
|
|
79
80
|
f"Duplicate id '{content.id}' with paths:\n"
|
|
80
81
|
f" - {content.file_path}\n"
|
|
81
|
-
f" - {self.
|
|
82
|
+
f" - {self.uuid_to_content_map[content.id].file_path}"
|
|
82
83
|
)
|
|
83
84
|
|
|
84
85
|
if isinstance(content, Lookup):
|
|
@@ -99,9 +100,10 @@ class DirectorOutputDto:
|
|
|
99
100
|
self.detections.append(content)
|
|
100
101
|
elif isinstance(content, SSADetection):
|
|
101
102
|
self.ssa_detections.append(content)
|
|
103
|
+
elif isinstance(content, DataSource):
|
|
104
|
+
self.data_sources.append(content)
|
|
102
105
|
else:
|
|
103
|
-
|
|
104
|
-
|
|
106
|
+
raise Exception(f"Unknown security content type: {type(content)}")
|
|
105
107
|
|
|
106
108
|
self.name_to_content_map[content_name] = content
|
|
107
109
|
self.uuid_to_content_map[content.id] = content
|
|
@@ -124,41 +126,27 @@ class Director():
|
|
|
124
126
|
self.createSecurityContent(SecurityContentType.stories)
|
|
125
127
|
self.createSecurityContent(SecurityContentType.baselines)
|
|
126
128
|
self.createSecurityContent(SecurityContentType.investigations)
|
|
127
|
-
self.createSecurityContent(SecurityContentType.event_sources)
|
|
128
129
|
self.createSecurityContent(SecurityContentType.data_sources)
|
|
129
130
|
self.createSecurityContent(SecurityContentType.playbooks)
|
|
130
131
|
self.createSecurityContent(SecurityContentType.detections)
|
|
131
132
|
self.createSecurityContent(SecurityContentType.ssa_detections)
|
|
132
133
|
|
|
134
|
+
|
|
135
|
+
from contentctl.objects.abstract_security_content_objects.detection_abstract import MISSING_SOURCES
|
|
136
|
+
if len(MISSING_SOURCES) > 0:
|
|
137
|
+
missing_sources_string = "\n 🟡 ".join(sorted(list(MISSING_SOURCES)))
|
|
138
|
+
print("WARNING: The following data_sources have been used in detections, but are not yet defined.\n"
|
|
139
|
+
"This is not yet an error since not all data_sources have been defined, but will be convered to an error soon:\n 🟡 "
|
|
140
|
+
f"{missing_sources_string}")
|
|
141
|
+
else:
|
|
142
|
+
print("No missing data_sources!")
|
|
143
|
+
|
|
133
144
|
def createSecurityContent(self, contentType: SecurityContentType) -> None:
|
|
134
145
|
if contentType == SecurityContentType.ssa_detections:
|
|
135
146
|
files = Utils.get_all_yml_files_from_directory(
|
|
136
147
|
os.path.join(self.input_dto.path, "ssa_detections")
|
|
137
148
|
)
|
|
138
149
|
security_content_files = [f for f in files if f.name.startswith("ssa___")]
|
|
139
|
-
|
|
140
|
-
elif contentType == SecurityContentType.data_sources:
|
|
141
|
-
security_content_files = (
|
|
142
|
-
Utils.get_all_yml_files_from_directory_one_layer_deep(
|
|
143
|
-
os.path.join(self.input_dto.path, "data_sources")
|
|
144
|
-
)
|
|
145
|
-
)
|
|
146
|
-
|
|
147
|
-
elif contentType == SecurityContentType.event_sources:
|
|
148
|
-
security_content_files = Utils.get_all_yml_files_from_directory(
|
|
149
|
-
os.path.join(self.input_dto.path, "data_sources", "cloud", "event_sources")
|
|
150
|
-
)
|
|
151
|
-
security_content_files.extend(
|
|
152
|
-
Utils.get_all_yml_files_from_directory(
|
|
153
|
-
os.path.join(self.input_dto.path, "data_sources", "endpoint", "event_sources")
|
|
154
|
-
)
|
|
155
|
-
)
|
|
156
|
-
security_content_files.extend(
|
|
157
|
-
Utils.get_all_yml_files_from_directory(
|
|
158
|
-
os.path.join(self.input_dto.path, "data_sources", "network", "event_sources")
|
|
159
|
-
)
|
|
160
|
-
)
|
|
161
|
-
|
|
162
150
|
elif contentType in [
|
|
163
151
|
SecurityContentType.deployments,
|
|
164
152
|
SecurityContentType.lookups,
|
|
@@ -168,6 +156,7 @@ class Director():
|
|
|
168
156
|
SecurityContentType.investigations,
|
|
169
157
|
SecurityContentType.playbooks,
|
|
170
158
|
SecurityContentType.detections,
|
|
159
|
+
SecurityContentType.data_sources,
|
|
171
160
|
]:
|
|
172
161
|
files = Utils.get_all_yml_files_from_directory(
|
|
173
162
|
os.path.join(self.input_dto.path, str(contentType.name))
|
|
@@ -190,54 +179,48 @@ class Director():
|
|
|
190
179
|
modelDict = YmlReader.load_file(file)
|
|
191
180
|
|
|
192
181
|
if contentType == SecurityContentType.lookups:
|
|
193
|
-
|
|
194
|
-
|
|
182
|
+
lookup = Lookup.model_validate(modelDict,context={"output_dto":self.output_dto, "config":self.input_dto})
|
|
183
|
+
self.output_dto.addContentToDictMappings(lookup)
|
|
195
184
|
|
|
196
185
|
elif contentType == SecurityContentType.macros:
|
|
197
|
-
|
|
198
|
-
|
|
186
|
+
macro = Macro.model_validate(modelDict,context={"output_dto":self.output_dto})
|
|
187
|
+
self.output_dto.addContentToDictMappings(macro)
|
|
199
188
|
|
|
200
189
|
elif contentType == SecurityContentType.deployments:
|
|
201
|
-
|
|
202
|
-
|
|
190
|
+
deployment = Deployment.model_validate(modelDict,context={"output_dto":self.output_dto})
|
|
191
|
+
self.output_dto.addContentToDictMappings(deployment)
|
|
203
192
|
|
|
204
193
|
elif contentType == SecurityContentType.playbooks:
|
|
205
|
-
|
|
206
|
-
|
|
194
|
+
playbook = Playbook.model_validate(modelDict,context={"output_dto":self.output_dto})
|
|
195
|
+
self.output_dto.addContentToDictMappings(playbook)
|
|
207
196
|
|
|
208
197
|
elif contentType == SecurityContentType.baselines:
|
|
209
|
-
|
|
210
|
-
|
|
198
|
+
baseline = Baseline.model_validate(modelDict,context={"output_dto":self.output_dto})
|
|
199
|
+
self.output_dto.addContentToDictMappings(baseline)
|
|
211
200
|
|
|
212
201
|
elif contentType == SecurityContentType.investigations:
|
|
213
|
-
|
|
214
|
-
|
|
202
|
+
investigation = Investigation.model_validate(modelDict,context={"output_dto":self.output_dto})
|
|
203
|
+
self.output_dto.addContentToDictMappings(investigation)
|
|
215
204
|
|
|
216
205
|
elif contentType == SecurityContentType.stories:
|
|
217
|
-
|
|
218
|
-
|
|
206
|
+
story = Story.model_validate(modelDict,context={"output_dto":self.output_dto})
|
|
207
|
+
self.output_dto.addContentToDictMappings(story)
|
|
219
208
|
|
|
220
209
|
elif contentType == SecurityContentType.detections:
|
|
221
|
-
|
|
222
|
-
|
|
210
|
+
detection = Detection.model_validate(modelDict,context={"output_dto":self.output_dto, "app":self.input_dto.app})
|
|
211
|
+
self.output_dto.addContentToDictMappings(detection)
|
|
223
212
|
|
|
224
213
|
elif contentType == SecurityContentType.ssa_detections:
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
214
|
+
self.constructSSADetection(self.ssa_detection_builder, self.output_dto,str(file))
|
|
215
|
+
ssa_detection = self.ssa_detection_builder.getObject()
|
|
216
|
+
if ssa_detection.status in [DetectionStatus.production.value, DetectionStatus.validation.value]:
|
|
217
|
+
self.output_dto.addContentToDictMappings(ssa_detection)
|
|
229
218
|
|
|
230
219
|
elif contentType == SecurityContentType.data_sources:
|
|
231
220
|
data_source = DataSource.model_validate(
|
|
232
221
|
modelDict, context={"output_dto": self.output_dto}
|
|
233
222
|
)
|
|
234
|
-
self.output_dto.
|
|
235
|
-
|
|
236
|
-
elif contentType == SecurityContentType.event_sources:
|
|
237
|
-
event_source = EventSource.model_validate(
|
|
238
|
-
modelDict, context={"output_dto": self.output_dto}
|
|
239
|
-
)
|
|
240
|
-
self.output_dto.event_sources.append(event_source)
|
|
223
|
+
self.output_dto.addContentToDictMappings(data_source)
|
|
241
224
|
|
|
242
225
|
else:
|
|
243
226
|
raise Exception(f"Unsupported type: [{contentType}]")
|
|
@@ -22,12 +22,14 @@ from contentctl.objects.deployment import Deployment
|
|
|
22
22
|
from contentctl.objects.unit_test import UnitTest
|
|
23
23
|
from contentctl.objects.test_group import TestGroup
|
|
24
24
|
from contentctl.objects.integration_test import IntegrationTest
|
|
25
|
-
|
|
25
|
+
from contentctl.objects.event_source import EventSource
|
|
26
|
+
from contentctl.objects.data_source import DataSource
|
|
26
27
|
|
|
27
28
|
#from contentctl.objects.playbook import Playbook
|
|
28
|
-
from contentctl.objects.enums import
|
|
29
|
+
from contentctl.objects.enums import ProvidingTechnology
|
|
29
30
|
from contentctl.enrichments.cve_enrichment import CveEnrichmentObj
|
|
30
31
|
|
|
32
|
+
MISSING_SOURCES:set[str] = set()
|
|
31
33
|
|
|
32
34
|
class Detection_Abstract(SecurityContentObject):
|
|
33
35
|
model_config = ConfigDict(use_enum_values=True)
|
|
@@ -35,12 +37,11 @@ class Detection_Abstract(SecurityContentObject):
|
|
|
35
37
|
#contentType: SecurityContentType = SecurityContentType.detections
|
|
36
38
|
type: AnalyticsType = Field(...)
|
|
37
39
|
status: DetectionStatus = Field(...)
|
|
38
|
-
data_source:
|
|
40
|
+
data_source: list[str] = []
|
|
39
41
|
tags: DetectionTags = Field(...)
|
|
40
42
|
search: Union[str, dict[str,Any]] = Field(...)
|
|
41
43
|
how_to_implement: str = Field(..., min_length=4)
|
|
42
44
|
known_false_positives: str = Field(..., min_length=4)
|
|
43
|
-
data_source_objects: Optional[List[DataSource]] = None
|
|
44
45
|
|
|
45
46
|
enabled_by_default: bool = False
|
|
46
47
|
file_path: FilePath = Field(...)
|
|
@@ -53,6 +54,8 @@ class Detection_Abstract(SecurityContentObject):
|
|
|
53
54
|
# A list of groups of tests, relying on the same data
|
|
54
55
|
test_groups: Union[list[TestGroup], None] = Field(None,validate_default=True)
|
|
55
56
|
|
|
57
|
+
data_source_objects: list[DataSource] = []
|
|
58
|
+
|
|
56
59
|
|
|
57
60
|
@field_validator("search", mode="before")
|
|
58
61
|
@classmethod
|
|
@@ -138,6 +141,7 @@ class Detection_Abstract(SecurityContentObject):
|
|
|
138
141
|
else:
|
|
139
142
|
return []
|
|
140
143
|
|
|
144
|
+
|
|
141
145
|
@computed_field
|
|
142
146
|
@property
|
|
143
147
|
def source(self)->str:
|
|
@@ -161,10 +165,12 @@ class Detection_Abstract(SecurityContentObject):
|
|
|
161
165
|
annotations_dict["type"] = self.type
|
|
162
166
|
#annotations_dict["version"] = self.version
|
|
163
167
|
|
|
168
|
+
annotations_dict["data_source"] = self.data_source
|
|
169
|
+
|
|
164
170
|
#The annotations object is a superset of the mappings object.
|
|
165
171
|
# So start with the mapping object.
|
|
166
172
|
annotations_dict.update(self.mappings)
|
|
167
|
-
|
|
173
|
+
|
|
168
174
|
#Make sure that the results are sorted for readability/easier diffs
|
|
169
175
|
return dict(sorted(annotations_dict.items(), key=lambda item: item[0]))
|
|
170
176
|
|
|
@@ -384,23 +390,37 @@ class Detection_Abstract(SecurityContentObject):
|
|
|
384
390
|
raise ValueError(f"Error, failed to replace detection reference in Baseline '{baseline.name}' to detection '{self.name}'")
|
|
385
391
|
baseline.tags.detections = new_detections
|
|
386
392
|
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
393
|
+
# Data source may be defined 1 on each line, OR they may be defined as
|
|
394
|
+
# SOUCE_1 AND ANOTHERSOURCE AND A_THIRD_SOURCE
|
|
395
|
+
# if more than 1 data source is required for a detection (for example, because it includes a join)
|
|
396
|
+
# Parse and update the list to resolve individual names and remove potential duplicates
|
|
397
|
+
updated_data_source_names:set[str] = set()
|
|
398
|
+
|
|
399
|
+
for ds in self.data_source:
|
|
400
|
+
split_data_sources = {d.strip() for d in ds.split('AND')}
|
|
401
|
+
updated_data_source_names.update(split_data_sources)
|
|
402
|
+
|
|
403
|
+
sources = sorted(list(updated_data_source_names))
|
|
404
|
+
|
|
405
|
+
matched_data_sources:list[DataSource] = []
|
|
406
|
+
missing_sources:list[str] = []
|
|
407
|
+
for source in sources:
|
|
408
|
+
try:
|
|
409
|
+
matched_data_sources += DataSource.mapNamesToSecurityContentObjects([source], director)
|
|
410
|
+
except Exception as data_source_mapping_exception:
|
|
411
|
+
# We gobble this up and add it to a global set so that we
|
|
412
|
+
# can print it ONCE at the end of the build of datasources.
|
|
413
|
+
# This will be removed later as per the note below
|
|
414
|
+
MISSING_SOURCES.add(source)
|
|
415
|
+
|
|
416
|
+
if len(missing_sources) > 0:
|
|
417
|
+
# This will be changed to ValueError when we have a complete list of data sources
|
|
418
|
+
print(f"WARNING: The following exception occurred when mapping the data_source field to DataSource objects:{missing_sources}")
|
|
419
|
+
|
|
420
|
+
self.data_source_objects = matched_data_sources
|
|
399
421
|
|
|
400
422
|
for story in self.tags.analytic_story:
|
|
401
|
-
story.detections.append(self)
|
|
402
|
-
story.data_sources.extend(self.data_source_objects)
|
|
403
|
-
|
|
423
|
+
story.detections.append(self)
|
|
404
424
|
return self
|
|
405
425
|
|
|
406
426
|
|
|
@@ -424,14 +444,16 @@ class Detection_Abstract(SecurityContentObject):
|
|
|
424
444
|
raise ValueError("Error, baselines are constructed automatically at runtime. Please do not include this field.")
|
|
425
445
|
|
|
426
446
|
|
|
427
|
-
name:Union[str,
|
|
447
|
+
name:Union[str,None] = info.data.get("name",None)
|
|
428
448
|
if name is None:
|
|
429
449
|
raise ValueError("Error, cannot get Baselines because the Detection does not have a 'name' defined.")
|
|
430
|
-
|
|
450
|
+
|
|
431
451
|
director:DirectorOutputDto = info.context.get("output_dto",None)
|
|
432
452
|
baselines:List[Baseline] = []
|
|
433
453
|
for baseline in director.baselines:
|
|
434
|
-
|
|
454
|
+
# This matching is a bit strange, because baseline.tags.detections starts as a list of strings, but
|
|
455
|
+
# is eventually updated to a list of Detections as we construct all of the detection objects.
|
|
456
|
+
if name in [detection_name for detection_name in baseline.tags.detections if isinstance(detection_name,str)]:
|
|
435
457
|
baselines.append(baseline)
|
|
436
458
|
|
|
437
459
|
return baselines
|
|
@@ -125,9 +125,9 @@ class SecurityContentObject_Abstract(BaseModel, abc.ABC):
|
|
|
125
125
|
errors:list[str] = []
|
|
126
126
|
if len(missing_objects) > 0:
|
|
127
127
|
errors.append(f"Failed to find the following '{cls.__name__}': {missing_objects}")
|
|
128
|
-
if len(
|
|
128
|
+
if len(mistyped_objects) > 0:
|
|
129
129
|
for mistyped_object in mistyped_objects:
|
|
130
|
-
errors.append(f"'{mistyped_object.name}' expected to have type '{
|
|
130
|
+
errors.append(f"'{mistyped_object.name}' expected to have type '{cls}', but actually had type '{type(mistyped_object)}'")
|
|
131
131
|
|
|
132
132
|
if len(errors) > 0:
|
|
133
133
|
error_string = "\n - ".join(errors)
|
|
@@ -194,6 +194,33 @@ class SecurityContentObject_Abstract(BaseModel, abc.ABC):
|
|
|
194
194
|
|
|
195
195
|
def __str__(self)->str:
|
|
196
196
|
return(self.__repr__())
|
|
197
|
+
|
|
198
|
+
def __lt__(self, other:object)->bool:
|
|
199
|
+
if not isinstance(other,SecurityContentObject_Abstract):
|
|
200
|
+
raise Exception(f"SecurityContentObject can only be compared to each other, not to {type(other)}")
|
|
201
|
+
return self.name < other.name
|
|
202
|
+
|
|
203
|
+
def __eq__(self, other:object)->bool:
|
|
204
|
+
if not isinstance(other,SecurityContentObject_Abstract):
|
|
205
|
+
raise Exception(f"SecurityContentObject can only be compared to each other, not to {type(other)}")
|
|
206
|
+
|
|
207
|
+
if id(self) == id(other) and self.name == other.name and self.id == other.id:
|
|
208
|
+
# Yes, this is the same object
|
|
209
|
+
return True
|
|
210
|
+
|
|
211
|
+
elif id(self) == id(other) or self.name == other.name or self.id == other.id:
|
|
212
|
+
raise Exception("Attempted to compare two SecurityContentObjects, but their fields indicate they were not globally unique:"
|
|
213
|
+
f"\n\tid(obj1) : {id(self)}"
|
|
214
|
+
f"\n\tid(obj2) : {id(other)}"
|
|
215
|
+
f"\n\tobj1.name : {self.name}"
|
|
216
|
+
f"\n\tobj2.name : {other.name}"
|
|
217
|
+
f"\n\tobj1.id : {self.id}"
|
|
218
|
+
f"\n\tobj2.id : {other.id}")
|
|
219
|
+
else:
|
|
220
|
+
return False
|
|
221
|
+
|
|
222
|
+
def __hash__(self) -> NonNegativeInt:
|
|
223
|
+
return id(self)
|
|
197
224
|
|
|
198
225
|
|
|
199
226
|
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from typing import Optional, Any
|
|
3
|
+
from pydantic import Field, FilePath, model_serializer
|
|
4
|
+
from contentctl.objects.security_content_object import SecurityContentObject
|
|
5
|
+
from contentctl.objects.event_source import EventSource
|
|
6
|
+
|
|
7
|
+
class DataSource(SecurityContentObject):
|
|
8
|
+
source: str = Field(...)
|
|
9
|
+
sourcetype: str = Field(...)
|
|
10
|
+
separator: Optional[str] = None
|
|
11
|
+
configuration: Optional[str] = None
|
|
12
|
+
supported_TA: Optional[list] = None
|
|
13
|
+
fields: Optional[list] = None
|
|
14
|
+
field_mappings: Optional[list] = None
|
|
15
|
+
convert_to_log_source: Optional[list] = None
|
|
16
|
+
example_log: Optional[str] = None
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@model_serializer
|
|
20
|
+
def serialize_model(self):
|
|
21
|
+
#Call serializer for parent
|
|
22
|
+
super_fields = super().serialize_model()
|
|
23
|
+
|
|
24
|
+
#All fields custom to this model
|
|
25
|
+
model:dict[str,Any] = {
|
|
26
|
+
"source": self.source,
|
|
27
|
+
"sourcetype": self.sourcetype,
|
|
28
|
+
"separator": self.separator,
|
|
29
|
+
"configuration": self.configuration,
|
|
30
|
+
"supported_TA": self.supported_TA,
|
|
31
|
+
"fields": self.fields,
|
|
32
|
+
"field_mappings": self.field_mappings,
|
|
33
|
+
"convert_to_log_source": self.convert_to_log_source,
|
|
34
|
+
"example_log":self.example_log
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
#Combine fields from this model with fields from parent
|
|
39
|
+
super_fields.update(model)
|
|
40
|
+
|
|
41
|
+
#return the model
|
|
42
|
+
return super_fields
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from typing import Union, Optional, List
|
|
3
|
+
from pydantic import BaseModel, Field
|
|
4
|
+
|
|
5
|
+
from contentctl.objects.security_content_object import SecurityContentObject
|
|
6
|
+
|
|
7
|
+
class EventSource(SecurityContentObject):
|
|
8
|
+
fields: Optional[list[str]] = None
|
|
9
|
+
field_mappings: Optional[list[dict]] = None
|
|
10
|
+
convert_to_log_source: Optional[list[dict]] = None
|
|
11
|
+
example_log: Optional[str] = None
|
|
@@ -33,7 +33,18 @@ class Story(SecurityContentObject):
|
|
|
33
33
|
detections:List[Detection] = []
|
|
34
34
|
investigations: List[Investigation] = []
|
|
35
35
|
baselines: List[Baseline] = []
|
|
36
|
-
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@computed_field
|
|
39
|
+
@property
|
|
40
|
+
def data_sources(self)-> list[DataSource]:
|
|
41
|
+
# Only add a data_source if it does not already exist in the story
|
|
42
|
+
data_source_objects:set[DataSource] = set()
|
|
43
|
+
for detection in self.detections:
|
|
44
|
+
data_source_objects.update(set(detection.data_source_objects))
|
|
45
|
+
|
|
46
|
+
return sorted(list(data_source_objects))
|
|
47
|
+
|
|
37
48
|
|
|
38
49
|
def storyAndInvestigationNamesWithApp(self, app_name:str)->List[str]:
|
|
39
50
|
return [f"{app_name} - {name} - Rule" for name in self.detection_names] + \
|
|
@@ -141,7 +152,3 @@ class Story(SecurityContentObject):
|
|
|
141
152
|
def baseline_names(self)->List[str]:
|
|
142
153
|
return [baseline.name for baseline in self.baselines]
|
|
143
154
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import csv
|
|
2
|
+
from contentctl.objects.data_source import DataSource
|
|
3
|
+
from contentctl.objects.event_source import EventSource
|
|
4
|
+
from typing import List
|
|
5
|
+
import pathlib
|
|
6
|
+
|
|
7
|
+
class DataSourceWriter:
|
|
8
|
+
|
|
9
|
+
@staticmethod
|
|
10
|
+
def writeDataSourceCsv(data_source_objects: List[DataSource], file_path: pathlib.Path):
|
|
11
|
+
with open(file_path, mode='w', newline='') as file:
|
|
12
|
+
writer = csv.writer(file)
|
|
13
|
+
# Write the header
|
|
14
|
+
writer.writerow([
|
|
15
|
+
"name", "id", "author", "source", "sourcetype", "separator",
|
|
16
|
+
"supported_TA_name", "supported_TA_version", "supported_TA_url",
|
|
17
|
+
"description"
|
|
18
|
+
])
|
|
19
|
+
# Write the data
|
|
20
|
+
for data_source in data_source_objects:
|
|
21
|
+
if data_source.supported_TA and isinstance(data_source.supported_TA, list) and len(data_source.supported_TA) > 0:
|
|
22
|
+
supported_TA_name = data_source.supported_TA[0].get('name', '')
|
|
23
|
+
supported_TA_version = data_source.supported_TA[0].get('version', '')
|
|
24
|
+
supported_TA_url = data_source.supported_TA[0].get('url', '')
|
|
25
|
+
else:
|
|
26
|
+
supported_TA_name = ''
|
|
27
|
+
supported_TA_version = ''
|
|
28
|
+
supported_TA_url = ''
|
|
29
|
+
writer.writerow([
|
|
30
|
+
data_source.name,
|
|
31
|
+
data_source.id,
|
|
32
|
+
data_source.author,
|
|
33
|
+
data_source.source,
|
|
34
|
+
data_source.sourcetype,
|
|
35
|
+
data_source.separator,
|
|
36
|
+
supported_TA_name,
|
|
37
|
+
supported_TA_version,
|
|
38
|
+
supported_TA_url,
|
|
39
|
+
data_source.description,
|
|
40
|
+
])
|