Skip to content

Commit bbcacda

Browse files
committed
Merge branch 'main' into enable_acs_deploy
2 parents d5b08d4 + cb6e45b commit bbcacda

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

60 files changed

+2551
-2178
lines changed

.github/workflows/testEndToEnd.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ jobs:
1212
fail-fast: false
1313
matrix:
1414
python_version: ["3.11", "3.12"]
15-
operating_system: ["ubuntu-20.04", "ubuntu-22.04", "macos-latest", "macos-14"]
15+
operating_system: ["ubuntu-20.04", "ubuntu-22.04", "macos-latest", "macos-14", "windows-2022"]
1616
#operating_system: ["ubuntu-20.04", "ubuntu-22.04", "macos-latest"]
1717

1818

.github/workflows/test_against_escu.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,10 @@ jobs:
1818
fail-fast: false
1919
matrix:
2020
python_version: ["3.11", "3.12"]
21+
2122
operating_system: ["ubuntu-20.04", "ubuntu-22.04", "macos-latest", "macos-14"]
22-
#operating_system: ["ubuntu-20.04", "ubuntu-22.04", "macos-latest"]
23+
# Do not test against ESCU until known character encoding issue is resolved
24+
# operating_system: ["ubuntu-20.04", "ubuntu-22.04", "macos-latest", "macos-14", "windows-2022"]
2325

2426

2527
runs-on: ${{ matrix.operating_system }}

.vscode/launch.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
{
22
"configurations": [
3+
{
4+
"name":"contentctl (pick args)",
5+
"type":"debugpy",
6+
"request":"launch",
7+
"program":"${workspaceFolder}/.venv/bin/contentctl",
8+
"console":"integratedTerminal",
9+
"cwd":"${env:SECURITY_CONTENT_PATH}",
10+
"args":"${command:pickArgs}"},
311
{
412
"name": "contentctl init",
513
"type": "debugpy",

.vscode/settings.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33
"python.envFile": "${workspaceFolder}/.env",
44
"python.testing.cwd": "${workspaceFolder}",
55
"python.languageServer": "Pylance",
6-
"python.analysis.typeCheckingMode": "strict"
6+
"python.analysis.typeCheckingMode": "strict",
7+
"editor.defaultFormatter": "ms-python.black-formatter"
78

89

910
}

README.md

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,20 @@
33
<p align="center">
44
<img src="docs/contentctl_logo_white.png" title="In case you're wondering, it's a capybara" alt="contentctl logo" width="250" height="250"></p>
55

6-
7-
6+
# contentctl Quick Start Guide
7+
If you are already familiar with contentctl, the following common commands may be very useful for basic operations
8+
9+
| Operation | Command |
10+
|-----------|---------|
11+
| Create a repository | `contentctl init` |
12+
| Validate Your Content | `contentctl validate` |
13+
| Validate Your Content, performing MITRE Enrichments | `contentctl validate –-enrichments`|
14+
| Build Your App | `contentctl build` |
15+
| Test All the content in your app, pausing so that you can debug a search if it fails | `contentctl test –-post-test-behavior pause_on_failure mode:all` |
16+
| Test All the content in your app, pausing after every detection to allow debugging | `contentctl test –-post-test-behavior always_pause mode:all` |
17+
| Test 1 or more specified detections. If you are testing more than one detection, the paths are space-separated. You may also use shell-expanded regexes | `contentctl test –-post-test-behavior always_pause mode:selected --mode.files detections/endpoint/7zip_commandline_to_smb_share_path.yml detections/cloud/aws_multi_factor_authentication_disabled.yml detections/application/okta*` |
18+
| Diff your current branch with a target_branch and test detections that have been updated. Your current branch **must be DIFFERENT** than the target_branch | `contentctl test –-post-test-behavior always_pause mode:changes –-mode.target_branch develop` |
19+
| Perform Integration Testing of all content. Note that Enterprise Security MUST be listed as an app in your contentctl.yml folder, otherwise all tests will subsequently fail | `contentctl test –-enable-integration-testing --post-test-behavior never_pause mode:all` |
820

921
# Introduction
1022
#### Security Is Hard
@@ -65,10 +77,7 @@ Testing is run using [GitHub Hosted Runners](https://docs.github.com/en/actions/
6577

6678
| Requirement | Supported | Description | Passing Integration Tests |
6779
| --------------------- | ----- | ---- | ------ |
68-
| Python <3.9 | No | No support planned. contentctl tool uses modern language constructs not supported ion Python3.8 and below | N/A |
69-
| Python 3.9 | Yes | contentctl tool is written in Python | Yes (locally + GitHub Actions) |
70-
| Python 3.10 | Yes | contentctl tool is written in Python | Yes (locally + GitHub Actions) |
71-
| Python 3.11 | Yes | contentctl tool is written in Python | Yes (locally + GitHub Actions) |
80+
| Python 3.11+ | Yes | contentctl tool is written in Python | Yes (locally + GitHub Actions) |
7281
| Docker (local) | Yes | A running Splunk Server is required for Dynamic Testing. contentctl can automatically create, configure, and destroy this server as a Splunk container during the lifetime of a test. | (locally + GitHub Actions) |
7382
| Docker (remote) | Planned | A running Splunk Server is required for Dynamic Testing. contentctl can automatically create, configure, and destroy this server as a Splunk container during the lifetime of a test. | No |
7483

@@ -80,7 +89,7 @@ It is typically recommended to install poetry to the Global Python Environment.*
8089

8190
#### Install via pip (recommended):
8291
```
83-
python3.9 -m venv .venv
92+
python3.11 -m venv .venv
8493
source .venv/bin/activate
8594
pip install contentctl
8695
```
@@ -89,7 +98,7 @@ pip install contentctl
8998
```
9099
git clone https://github.com/splunk/contentctl
91100
cd contentctl
92-
python3.9 -m pip install poetry
101+
python3.11 -m pip install poetry
93102
poetry install
94103
poetry shell
95104
contentctl --help

contentctl/actions/build.py

Lines changed: 14 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,9 @@
88
from contentctl.input.director import Director, DirectorOutputDto
99
from contentctl.output.conf_output import ConfOutput
1010
from contentctl.output.conf_writer import ConfWriter
11-
from contentctl.output.ba_yml_output import BAYmlOutput
1211
from contentctl.output.api_json_output import ApiJsonOutput
12+
from contentctl.output.data_source_writer import DataSourceWriter
13+
from contentctl.objects.lookup import Lookup
1314
import pathlib
1415
import json
1516
import datetime
@@ -28,9 +29,20 @@ class Build:
2829

2930

3031
def execute(self, input_dto: BuildInputDto) -> DirectorOutputDto:
31-
if input_dto.config.build_app:
32+
if input_dto.config.build_app:
33+
3234
updated_conf_files:set[pathlib.Path] = set()
3335
conf_output = ConfOutput(input_dto.config)
36+
37+
# Construct a special lookup whose CSV is created at runtime and
38+
# written directly into the output folder. It is created with model_construct,
39+
# not model_validate, because the CSV does not exist yet.
40+
data_sources_lookup_csv_path = input_dto.config.getPackageDirectoryPath() / "lookups" / "data_sources.csv"
41+
DataSourceWriter.writeDataSourceCsv(input_dto.director_output_dto.data_sources, data_sources_lookup_csv_path)
42+
input_dto.director_output_dto.addContentToDictMappings(Lookup.model_construct(description= "A lookup file that will contain the data source objects for detections.",
43+
filename=data_sources_lookup_csv_path,
44+
name="data_sources"))
45+
3446
updated_conf_files.update(conf_output.writeHeaders())
3547
updated_conf_files.update(conf_output.writeObjects(input_dto.director_output_dto.detections, SecurityContentType.detections))
3648
updated_conf_files.update(conf_output.writeObjects(input_dto.director_output_dto.stories, SecurityContentType.stories))
@@ -73,17 +85,4 @@ def execute(self, input_dto: BuildInputDto) -> DirectorOutputDto:
7385

7486
print(f"Build of '{input_dto.config.app.title}' API successful to {input_dto.config.getAPIPath()}")
7587

76-
if input_dto.config.build_ssa:
77-
78-
srs_path = input_dto.config.getSSAPath() / 'srs'
79-
complex_path = input_dto.config.getSSAPath() / 'complex'
80-
shutil.rmtree(srs_path, ignore_errors=True)
81-
shutil.rmtree(complex_path, ignore_errors=True)
82-
srs_path.mkdir(parents=True)
83-
complex_path.mkdir(parents=True)
84-
ba_yml_output = BAYmlOutput()
85-
ba_yml_output.writeObjects(input_dto.director_output_dto.ssa_detections, str(input_dto.config.getSSAPath()))
86-
87-
print(f"Build of 'SSA' successful to {input_dto.config.getSSAPath()}")
88-
8988
return input_dto.director_output_dto

contentctl/actions/convert.py

Lines changed: 0 additions & 25 deletions
This file was deleted.

contentctl/actions/detection_testing/GitService.py

Lines changed: 28 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -76,33 +76,28 @@ def getChanges(self, target_branch:str)->List[Detection]:
7676
if diff.delta.status in (DeltaStatus.ADDED, DeltaStatus.MODIFIED, DeltaStatus.RENAMED):
7777
#print(f"{DeltaStatus(diff.delta.status).name:<8}:{diff.delta.new_file.raw_path}")
7878
decoded_path = pathlib.Path(diff.delta.new_file.raw_path.decode('utf-8'))
79-
if 'app_template/' in str(decoded_path) or 'ssa_detections' in str(decoded_path) or str(self.config.getBuildDir()) in str(decoded_path):
80-
#Ignore anything that is embedded in the app template.
81-
#Also ignore ssa detections
82-
pass
83-
elif 'detections/' in str(decoded_path) and decoded_path.suffix == ".yml":
79+
# Note that we only handle updates to detections, lookups, and macros at this time. All other changes are ignored.
80+
if decoded_path.is_relative_to(self.config.path/"detections") and decoded_path.suffix == ".yml":
8481
detectionObject = filepath_to_content_map.get(decoded_path, None)
8582
if isinstance(detectionObject, Detection):
8683
updated_detections.append(detectionObject)
8784
else:
8885
raise Exception(f"Error getting detection object for file {str(decoded_path)}")
8986

90-
elif 'macros/' in str(decoded_path) and decoded_path.suffix == ".yml":
87+
elif decoded_path.is_relative_to(self.config.path/"macros") and decoded_path.suffix == ".yml":
9188
macroObject = filepath_to_content_map.get(decoded_path, None)
9289
if isinstance(macroObject, Macro):
9390
updated_macros.append(macroObject)
9491
else:
9592
raise Exception(f"Error getting macro object for file {str(decoded_path)}")
9693

97-
elif 'lookups/' in str(decoded_path):
94+
elif decoded_path.is_relative_to(self.config.path/"lookups"):
9895
# We need to convert this to a yml. This means we will catch
9996
# both changes to a csv AND changes to the YML that uses it
100-
101-
10297
if decoded_path.suffix == ".yml":
10398
updatedLookup = filepath_to_content_map.get(decoded_path, None)
10499
if not isinstance(updatedLookup,Lookup):
105-
raise Exception(f"Expected {decoded_path} to be type {type(Lookup)}, but instead if was {(type(lookupObject))}")
100+
raise Exception(f"Expected {decoded_path} to be type {type(Lookup)}, but instead if was {(type(updatedLookup))}")
106101
updated_lookups.append(updatedLookup)
107102

108103
elif decoded_path.suffix == ".csv":
@@ -116,23 +111,27 @@ def getChanges(self, target_branch:str)->List[Detection]:
116111
raise Exception(f"More than 1 Lookup reference the modified CSV file '{decoded_path}': {[l.file_path for l in matched ]}")
117112
else:
118113
updatedLookup = matched[0]
114+
elif decoded_path.suffix == ".mlmodel":
115+
# Detected a changed .mlmodel file. However, since we do not have testing for these detections at
116+
# this time, we will ignore this change.
117+
updatedLookup = None
118+
119+
119120
else:
120-
raise Exception(f"Error getting lookup object for file {str(decoded_path)}")
121+
raise Exception(f"Detected a changed file in the lookups/ directory '{str(decoded_path)}'.\n"
122+
"Only files ending in .csv, .yml, or .mlmodel are supported in this "
123+
"directory. This file must be removed from the lookups/ directory.")
121124

122-
if updatedLookup not in updated_lookups:
123-
# It is possible that both th CSV and YML have been modified for the same lookup,
125+
if updatedLookup is not None and updatedLookup not in updated_lookups:
126+
# It is possible that both the CSV and YML have been modified for the same lookup,
124127
# and we do not want to add it twice.
125128
updated_lookups.append(updatedLookup)
126129

127130
else:
128131
pass
129132
#print(f"Ignore changes to file {decoded_path} since it is not a detection, macro, or lookup.")
130-
131-
# else:
132-
# print(f"{diff.delta.new_file.raw_path}:{DeltaStatus(diff.delta.status).name} (IGNORED)")
133-
# pass
134133
else:
135-
raise Exception(f"Unrecognized type {type(diff)}")
134+
raise Exception(f"Unrecognized diff type {type(diff)}")
136135

137136

138137
# If a detection has at least one dependency on changed content,
@@ -153,24 +152,25 @@ def getChanges(self, target_branch:str)->List[Detection]:
153152
#Print out the names of all modified/new content
154153
modifiedAndNewContentString = "\n - ".join(sorted([d.name for d in updated_detections]))
155154

156-
print(f"[{len(updated_detections)}] Pieces of modifed and new content to test:\n - {modifiedAndNewContentString}")
155+
print(f"[{len(updated_detections)}] Pieces of modifed and new content (this may include experimental/deprecated/manual_test content):\n - {modifiedAndNewContentString}")
157156
return updated_detections
158157

159-
def getSelected(self, detectionFilenames:List[FilePath])->List[Detection]:
160-
filepath_to_content_map:dict[FilePath, SecurityContentObject] = { obj.file_path:obj for (_,obj) in self.director.name_to_content_map.items() if obj.file_path is not None}
158+
def getSelected(self, detectionFilenames: List[FilePath]) -> List[Detection]:
159+
filepath_to_content_map: dict[FilePath, SecurityContentObject] = {
160+
obj.file_path: obj for (_, obj) in self.director.name_to_content_map.items() if obj.file_path is not None
161+
}
161162
errors = []
162-
detections:List[Detection] = []
163+
detections: List[Detection] = []
163164
for name in detectionFilenames:
164-
obj = filepath_to_content_map.get(name,None)
165-
if obj == None:
165+
obj = filepath_to_content_map.get(name, None)
166+
if obj is None:
166167
errors.append(f"There is no detection file or security_content_object at '{name}'")
167168
elif not isinstance(obj, Detection):
168169
errors.append(f"The security_content_object at '{name}' is of type '{type(obj).__name__}', NOT '{Detection.__name__}'")
169170
else:
170171
detections.append(obj)
171172

172-
if len(errors) > 0:
173+
if errors:
173174
errorsString = "\n - ".join(errors)
174-
raise Exception(f"There following errors were encountered while getting selected detections to test:\n - {errorsString}")
175-
return detections
176-
175+
raise Exception(f"The following errors were encountered while getting selected detections to test:\n - {errorsString}")
176+
return detections

0 commit comments

Comments
 (0)