Skip to content

Commit 1ab9b74

Browse files
authored
Merge branch 'main' into feature/risk-model-validation
2 parents 6c1bc7d + dd20cf7 commit 1ab9b74

31 files changed

+1467
-395
lines changed

contentctl/actions/build.py

Lines changed: 5 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,14 @@
1+
import datetime
2+
import json
3+
import pathlib
14
import shutil
2-
35
from dataclasses import dataclass
46

57
from contentctl.input.director import DirectorOutputDto
8+
from contentctl.objects.config import build
9+
from contentctl.output.api_json_output import ApiJsonOutput
610
from contentctl.output.conf_output import ConfOutput
711
from contentctl.output.conf_writer import ConfWriter
8-
from contentctl.output.api_json_output import ApiJsonOutput
9-
from contentctl.output.data_source_writer import DataSourceWriter
10-
from contentctl.objects.lookup import CSVLookup, Lookup_Type
11-
import pathlib
12-
import json
13-
import datetime
14-
import uuid
15-
16-
from contentctl.objects.config import build
1712

1813

1914
@dataclass(frozen=True)
@@ -28,39 +23,6 @@ def execute(self, input_dto: BuildInputDto) -> DirectorOutputDto:
2823
updated_conf_files: set[pathlib.Path] = set()
2924
conf_output = ConfOutput(input_dto.config)
3025

31-
# Construct a path to a YML that does not actually exist.
32-
# We mock this "fake" path since the YML does not exist.
33-
# This ensures the checking for the existence of the CSV is correct
34-
data_sources_fake_yml_path = (
35-
input_dto.config.getPackageDirectoryPath()
36-
/ "lookups"
37-
/ "data_sources.yml"
38-
)
39-
40-
# Construct a special lookup whose CSV is created at runtime and
41-
# written directly into the lookups folder. We will delete this after a build,
42-
# assuming that it is successful.
43-
data_sources_lookup_csv_path = (
44-
input_dto.config.getPackageDirectoryPath()
45-
/ "lookups"
46-
/ "data_sources.csv"
47-
)
48-
49-
DataSourceWriter.writeDataSourceCsv(
50-
input_dto.director_output_dto.data_sources, data_sources_lookup_csv_path
51-
)
52-
input_dto.director_output_dto.addContentToDictMappings(
53-
CSVLookup.model_construct(
54-
name="data_sources",
55-
id=uuid.UUID("b45c1403-6e09-47b0-824f-cf6e44f15ac8"),
56-
version=1,
57-
author=input_dto.config.app.author_name,
58-
date=datetime.date.today(),
59-
description="A lookup file that will contain the data source objects for detections.",
60-
lookup_type=Lookup_Type.csv,
61-
file_path=data_sources_fake_yml_path,
62-
)
63-
)
6426
updated_conf_files.update(conf_output.writeHeaders())
6527
updated_conf_files.update(
6628
conf_output.writeLookups(input_dto.director_output_dto.lookups)

contentctl/actions/detection_testing/GitService.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
from contentctl.objects.config import All, Changes, Selected, test_common
1515
from contentctl.objects.data_source import DataSource
1616
from contentctl.objects.detection import Detection
17-
from contentctl.objects.lookup import CSVLookup, Lookup
17+
from contentctl.objects.lookup import CSVLookup, Lookup, RuntimeCSV
1818
from contentctl.objects.macro import Macro
1919
from contentctl.objects.security_content_object import SecurityContentObject
2020

@@ -148,6 +148,9 @@ def getChanges(self, target_branch: str) -> List[Detection]:
148148
matched = list(
149149
filter(
150150
lambda x: isinstance(x, CSVLookup)
151+
and not isinstance(
152+
x, RuntimeCSV
153+
) # RuntimeCSV is not used directly by any content
151154
and x.filename == decoded_path,
152155
self.director.lookups,
153156
)

contentctl/actions/detection_testing/views/DetectionTestingView.py

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,13 @@
44

55
from pydantic import BaseModel
66

7-
from contentctl.objects.config import test_common
8-
97
from contentctl.actions.detection_testing.infrastructures.DetectionTestingInfrastructure import (
108
DetectionTestingManagerOutputDto,
119
)
1210
from contentctl.helper.utils import Utils
13-
from contentctl.objects.enums import DetectionStatus
1411
from contentctl.objects.base_test_result import TestResultStatus
12+
from contentctl.objects.config import test_common
13+
from contentctl.objects.enums import ContentStatus
1514

1615

1716
class DetectionTestingView(BaseModel, abc.ABC):
@@ -117,11 +116,11 @@ def getSummaryObject(
117116
total_skipped += 1
118117

119118
# Aggregate production status metrics
120-
if detection.status == DetectionStatus.production:
119+
if detection.status == ContentStatus.production:
121120
total_production += 1
122-
elif detection.status == DetectionStatus.experimental:
121+
elif detection.status == ContentStatus.experimental:
123122
total_experimental += 1
124-
elif detection.status == DetectionStatus.deprecated:
123+
elif detection.status == ContentStatus.deprecated:
125124
total_deprecated += 1
126125

127126
# Check if the detection is manual_test

contentctl/actions/initialize.py

Lines changed: 35 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,21 @@
1-
import shutil
21
import os
32
import pathlib
3+
import shutil
4+
5+
from contentctl.objects.baseline import Baseline
46
from contentctl.objects.config import test
7+
from contentctl.objects.dashboard import Dashboard
8+
from contentctl.objects.data_source import DataSource
9+
from contentctl.objects.deployment import Deployment
10+
from contentctl.objects.detection import Detection
11+
from contentctl.objects.investigation import Investigation
12+
from contentctl.objects.lookup import Lookup
13+
from contentctl.objects.macro import Macro
14+
from contentctl.objects.playbook import Playbook
15+
from contentctl.objects.removed_security_content_object import (
16+
RemovedSecurityContentObject,
17+
)
18+
from contentctl.objects.story import Story
519
from contentctl.output.yml_writer import YmlWriter
620

721

@@ -13,21 +27,33 @@ def execute(self, config: test) -> None:
1327

1428
YmlWriter.writeYmlFile(str(config.path / "contentctl.yml"), config.model_dump())
1529

16-
# Create the following empty directories:
30+
# Create the following empty directories. Each type of content,
31+
# even if you don't have any of that type of content, need its own directory to exist.
32+
for contentType in [
33+
Detection,
34+
Playbook,
35+
Story,
36+
DataSource,
37+
Investigation,
38+
Macro,
39+
Lookup,
40+
Dashboard,
41+
Baseline,
42+
Deployment,
43+
RemovedSecurityContentObject,
44+
]:
45+
contentType.containing_folder().mkdir(exist_ok=False, parents=True)
46+
47+
# Some other directories that do not map directly to a piece of content also must exist
48+
1749
for emptyDir in [
18-
"lookups",
19-
"baselines",
20-
"data_sources",
2150
"docs",
2251
"reporting",
23-
"investigations",
2452
"detections/application",
2553
"detections/cloud",
2654
"detections/endpoint",
2755
"detections/network",
2856
"detections/web",
29-
"macros",
30-
"stories",
3157
]:
3258
# Throw an error if this directory already exists
3359
(config.path / emptyDir).mkdir(exist_ok=False, parents=True)
@@ -60,7 +86,7 @@ def execute(self, config: test) -> None:
6086
source_directory = pathlib.Path(os.path.dirname(__file__)) / templateDir
6187
target_directory = config.path / targetDir
6288
# Throw an exception if the target exists
63-
shutil.copytree(source_directory, target_directory, dirs_exist_ok=False)
89+
shutil.copytree(source_directory, target_directory, dirs_exist_ok=True)
6490

6591
# Create a README.md file. Note that this is the README.md for the repository, not the
6692
# one which will actually be packaged into the app. That is located in the app_template folder.

contentctl/actions/inspect.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
DetectionMissingError,
1717
MetadataValidationError,
1818
VersionBumpingError,
19+
VersionBumpingTooFarError,
1920
VersionDecrementedError,
2021
)
2122
from contentctl.objects.savedsearches_conf import SavedsearchesConf
@@ -101,7 +102,7 @@ def inspectAppAPI(self, config: inspect) -> str:
101102
-F "app_package=@<PATH/APP-PACKAGE>" \
102103
-F "included_tags=cloud" \
103104
--url "https://appinspect.splunk.com/v1/app/validate"
104-
105+
105106
This is confirmed by the great resource:
106107
https://curlconverter.com/
107108
"""
@@ -429,6 +430,19 @@ def check_detection_metadata(self, config: inspect) -> None:
429430
)
430431
)
431432

433+
# Versions should never increase more than one version between releases
434+
if (
435+
current_stanza.metadata.detection_version
436+
> previous_stanza.metadata.detection_version + 1
437+
):
438+
validation_errors[rule_name].append(
439+
VersionBumpingTooFarError(
440+
rule_name=rule_name,
441+
current_version=current_stanza.metadata.detection_version,
442+
previous_version=previous_stanza.metadata.detection_version,
443+
)
444+
)
445+
432446
# Convert our dict mapping to a flat list of errors for use in reporting
433447
validation_error_list = [
434448
x for inner_list in validation_errors.values() for x in inner_list

contentctl/actions/validate.py

Lines changed: 27 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,38 @@
11
import pathlib
22

3-
from contentctl.input.director import Director, DirectorOutputDto
4-
from contentctl.objects.config import validate
53
from contentctl.enrichments.attack_enrichment import AttackEnrichment
64
from contentctl.enrichments.cve_enrichment import CveEnrichment
7-
from contentctl.objects.atomic import AtomicEnrichment
8-
from contentctl.objects.lookup import FileBackedLookup
5+
from contentctl.helper.splunk_app import SplunkApp
96
from contentctl.helper.utils import Utils
7+
from contentctl.input.director import Director, DirectorOutputDto, ValidationFailedError
8+
from contentctl.objects.atomic import AtomicEnrichment
9+
from contentctl.objects.config import validate
1010
from contentctl.objects.data_source import DataSource
11-
from contentctl.helper.splunk_app import SplunkApp
11+
from contentctl.objects.lookup import FileBackedLookup, RuntimeCSV
1212

1313

1414
class Validate:
1515
def execute(self, input_dto: validate) -> DirectorOutputDto:
16-
director_output_dto = DirectorOutputDto(
17-
AtomicEnrichment.getAtomicEnrichment(input_dto),
18-
AttackEnrichment.getAttackEnrichment(input_dto),
19-
CveEnrichment.getCveEnrichment(input_dto),
20-
[],
21-
[],
22-
[],
23-
[],
24-
[],
25-
[],
26-
[],
27-
[],
28-
[],
29-
[],
30-
)
16+
try:
17+
director_output_dto = DirectorOutputDto(
18+
AtomicEnrichment.getAtomicEnrichment(input_dto),
19+
AttackEnrichment.getAttackEnrichment(input_dto),
20+
CveEnrichment.getCveEnrichment(input_dto),
21+
)
22+
23+
director = Director(director_output_dto)
24+
director.execute(input_dto)
25+
self.ensure_no_orphaned_files_in_lookups(
26+
input_dto.path, director_output_dto
27+
)
28+
if input_dto.data_source_TA_validation:
29+
self.validate_latest_TA_information(director_output_dto.data_sources)
3130

32-
director = Director(director_output_dto)
33-
director.execute(input_dto)
34-
self.ensure_no_orphaned_files_in_lookups(input_dto.path, director_output_dto)
35-
if input_dto.data_source_TA_validation:
36-
self.validate_latest_TA_information(director_output_dto.data_sources)
31+
return director_output_dto
3732

38-
return director_output_dto
33+
except ValidationFailedError:
34+
# Just re-raise without additional output since we already formatted everything
35+
raise SystemExit(1)
3936

4037
def ensure_no_orphaned_files_in_lookups(
4138
self, repo_path: pathlib.Path, director_output_dto: DirectorOutputDto
@@ -64,11 +61,14 @@ def ensure_no_orphaned_files_in_lookups(
6461
"""
6562
lookupsDirectory = repo_path / "lookups"
6663

67-
# Get all of the files referneced by Lookups
64+
# Get all of the files referenced by Lookups
6865
usedLookupFiles: list[pathlib.Path] = [
6966
lookup.filename
7067
for lookup in director_output_dto.lookups
68+
# Of course Runtime CSVs do not have underlying CSV files, so make
69+
# sure that we do not check for that existence.
7170
if isinstance(lookup, FileBackedLookup)
71+
and not isinstance(lookup, RuntimeCSV)
7272
] + [
7373
lookup.file_path
7474
for lookup in director_output_dto.lookups

contentctl/helper/utils.py

Lines changed: 14 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,34 @@
1-
import os
2-
import git
3-
import shutil
4-
import requests
1+
import logging
2+
import pathlib
53
import random
4+
import shutil
65
import string
6+
from math import ceil
77
from timeit import default_timer
8-
import pathlib
9-
import logging
8+
from typing import TYPE_CHECKING, Tuple, Union
109

11-
from typing import Union, Tuple
10+
import git
11+
import requests
1212
import tqdm
13-
from math import ceil
14-
15-
from typing import TYPE_CHECKING
1613

1714
if TYPE_CHECKING:
1815
from contentctl.objects.security_content_object import SecurityContentObject
1916
from contentctl.objects.security_content_object import SecurityContentObject
2017

21-
2218
TOTAL_BYTES = 0
2319
ALWAYS_PULL = True
2420

2521

2622
class Utils:
2723
@staticmethod
28-
def get_all_yml_files_from_directory(path: str) -> list[pathlib.Path]:
29-
listOfFiles: list[pathlib.Path] = []
30-
base_path = pathlib.Path(path)
31-
if not base_path.exists():
32-
return listOfFiles
33-
for dirpath, dirnames, filenames in os.walk(path):
34-
for file in filenames:
35-
if file.endswith(".yml"):
36-
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+
)
3730

38-
return sorted(listOfFiles)
31+
return sorted(pathlib.Path(yml_path) for yml_path in path.glob("**/*.yml"))
3932

4033
@staticmethod
4134
def get_security_content_files_from_directory(

0 commit comments

Comments
 (0)