contentctl 5.4.0__py3-none-any.whl → 5.5.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructure.py +2 -9
- contentctl/contentctl.py +1 -0
- contentctl/input/director.py +7 -2
- contentctl/objects/abstract_security_content_objects/detection_abstract.py +2 -2
- contentctl/objects/config.py +173 -0
- contentctl/objects/test_attack_data.py +36 -1
- contentctl/output/templates/savedsearches_detections.j2 +3 -27
- {contentctl-5.4.0.dist-info → contentctl-5.5.0.dist-info}/METADATA +2 -2
- {contentctl-5.4.0.dist-info → contentctl-5.5.0.dist-info}/RECORD +12 -12
- {contentctl-5.4.0.dist-info → contentctl-5.5.0.dist-info}/LICENSE.md +0 -0
- {contentctl-5.4.0.dist-info → contentctl-5.5.0.dist-info}/WHEEL +0 -0
- {contentctl-5.4.0.dist-info → contentctl-5.5.0.dist-info}/entry_points.txt +0 -0
|
@@ -7,7 +7,6 @@ import pathlib
|
|
|
7
7
|
import time
|
|
8
8
|
import urllib.parse
|
|
9
9
|
import uuid
|
|
10
|
-
from shutil import copyfile
|
|
11
10
|
from ssl import SSLEOFError, SSLZeroReturnError
|
|
12
11
|
from sys import stdout
|
|
13
12
|
from tempfile import TemporaryDirectory, mktemp
|
|
@@ -1402,7 +1401,6 @@ class DetectionTestingInfrastructure(BaseModel, abc.ABC):
|
|
|
1402
1401
|
f"The only valid indexes on the server are {self.all_indexes_on_server}"
|
|
1403
1402
|
)
|
|
1404
1403
|
|
|
1405
|
-
tempfile = mktemp(dir=tmp_dir)
|
|
1406
1404
|
if not (
|
|
1407
1405
|
str(attack_data_file.data).startswith("http://")
|
|
1408
1406
|
or str(attack_data_file.data).startswith("https://")
|
|
@@ -1415,13 +1413,7 @@ class DetectionTestingInfrastructure(BaseModel, abc.ABC):
|
|
|
1415
1413
|
test_group_start_time,
|
|
1416
1414
|
)
|
|
1417
1415
|
|
|
1418
|
-
|
|
1419
|
-
copyfile(str(attack_data_file.data), tempfile)
|
|
1420
|
-
except Exception as e:
|
|
1421
|
-
raise Exception(
|
|
1422
|
-
f"Error copying local Attack Data File for [{test_group.name}] - [{attack_data_file.data}]: "
|
|
1423
|
-
f"{str(e)}"
|
|
1424
|
-
)
|
|
1416
|
+
tempfile = str(attack_data_file.data)
|
|
1425
1417
|
else:
|
|
1426
1418
|
raise Exception(
|
|
1427
1419
|
f"Attack Data File for [{test_group.name}] is local [{attack_data_file.data}], but does not exist."
|
|
@@ -1432,6 +1424,7 @@ class DetectionTestingInfrastructure(BaseModel, abc.ABC):
|
|
|
1432
1424
|
# We need to overwrite the file - mkstemp will create an empty file with the
|
|
1433
1425
|
# given name
|
|
1434
1426
|
try:
|
|
1427
|
+
tempfile = mktemp(dir=tmp_dir)
|
|
1435
1428
|
# In case the path is a local file, try to get it
|
|
1436
1429
|
|
|
1437
1430
|
self.format_pbar_string(
|
contentctl/contentctl.py
CHANGED
contentctl/input/director.py
CHANGED
|
@@ -338,10 +338,15 @@ class Director:
|
|
|
338
338
|
for err in error.errors():
|
|
339
339
|
error_msg = err.get("msg", "")
|
|
340
340
|
if "https://errors.pydantic.dev" in error_msg:
|
|
341
|
-
|
|
341
|
+
# Unfortunately, this is a catch-all for untyped errors. We will still need to emit this
|
|
342
|
+
# This is harder to read, but the other option is suppressing it which we cannot do as
|
|
343
|
+
# it makes troubleshooting extremelt difficult
|
|
344
|
+
print(
|
|
345
|
+
f" {Colors.RED}{Colors.ERROR} {error_msg}{Colors.END}"
|
|
346
|
+
)
|
|
342
347
|
|
|
343
348
|
# Clean error categorization
|
|
344
|
-
|
|
349
|
+
elif "Field required" in error_msg:
|
|
345
350
|
print(
|
|
346
351
|
f" {Colors.YELLOW}{Colors.WARNING} Field Required: {err.get('loc', [''])[0]}{Colors.END}"
|
|
347
352
|
)
|
|
@@ -913,7 +913,7 @@ class Detection_Abstract(SecurityContentObject):
|
|
|
913
913
|
return self
|
|
914
914
|
|
|
915
915
|
@field_validator("tests", mode="before")
|
|
916
|
-
def ensure_yml_test_is_unittest(cls, v: list[dict]):
|
|
916
|
+
def ensure_yml_test_is_unittest(cls, v: list[dict], info: ValidationInfo):
|
|
917
917
|
"""The typing for the tests field allows it to be one of
|
|
918
918
|
a number of different types of tests. However, ONLY
|
|
919
919
|
UnitTest should be allowed to be defined in the YML
|
|
@@ -941,7 +941,7 @@ class Detection_Abstract(SecurityContentObject):
|
|
|
941
941
|
for unitTest in v:
|
|
942
942
|
# This raises a ValueError on a failed UnitTest.
|
|
943
943
|
try:
|
|
944
|
-
UnitTest.model_validate(unitTest)
|
|
944
|
+
UnitTest.model_validate(unitTest, context=info.context)
|
|
945
945
|
except ValueError as e:
|
|
946
946
|
valueErrors.append(e)
|
|
947
947
|
if len(valueErrors):
|
contentctl/objects/config.py
CHANGED
|
@@ -26,6 +26,7 @@ from pydantic import (
|
|
|
26
26
|
field_validator,
|
|
27
27
|
model_validator,
|
|
28
28
|
)
|
|
29
|
+
from requests import RequestException, head
|
|
29
30
|
|
|
30
31
|
from contentctl.helper.splunk_app import SplunkApp
|
|
31
32
|
from contentctl.helper.utils import Utils
|
|
@@ -261,6 +262,37 @@ class init(Config_Base):
|
|
|
261
262
|
)
|
|
262
263
|
|
|
263
264
|
|
|
265
|
+
# There can be a number of attack data file warning mapping exceptions, or errors,
|
|
266
|
+
# that can occur when using attack data caches. In order to avoid very complex
|
|
267
|
+
# output, we will only emit the verbose versions of these message once per file.
|
|
268
|
+
# This is a non-intuitive place to put this, but it is good enough for now.
|
|
269
|
+
ATTACK_DATA_CACHE_MAPPING_EXCEPTIONS: set[str] = set()
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
class AttackDataCache(BaseModel):
|
|
273
|
+
base_url: str = Field(
|
|
274
|
+
"This is the beginning of a URL that the data must begin with to map to this cache object."
|
|
275
|
+
)
|
|
276
|
+
base_directory_name: str = Field(
|
|
277
|
+
"This is the root folder name where the attack data should be downloaded to. Note that this path MUST be in the external_repos/ folder",
|
|
278
|
+
pattern=r"^external_repos/.+",
|
|
279
|
+
)
|
|
280
|
+
# suggested checkout information for our attack_data repo
|
|
281
|
+
# curl https://attack-range-attack-data.s3.us-west-2.amazonaws.com/attack_data.tar.zstd | zstd --decompress | tar -x -C attack_data/
|
|
282
|
+
# suggested YML values for this:
|
|
283
|
+
helptext: str | None = Field(
|
|
284
|
+
default="This repo is set up to use test_data_caches. This can be extremely helpful in validating correct links for test attack_data and speeding up testing.\n"
|
|
285
|
+
"Include the following in your contentctl.yml file to use this cache:\n\n"
|
|
286
|
+
"test_data_caches:\n"
|
|
287
|
+
"- base_url: https://media.githubusercontent.com/media/splunk/attack_data/master/\n"
|
|
288
|
+
" base_directory_name: external_repos/attack_data\n\n"
|
|
289
|
+
"In order to check out STRT Attack Data, you can use the following command:\n"
|
|
290
|
+
"mkdir -p external_repos; curl https://attack-range-attack-data.s3.us-west-2.amazonaws.com/attack_data.tar.zstd | zstd --decompress | tar -x -C external_repos/\n"
|
|
291
|
+
"or\n"
|
|
292
|
+
"""echo "First ensure git-lfs is enabled"; git clone https://github.com/splunk/attack_data external_repos/attack_data"""
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
|
|
264
296
|
class validate(Config_Base):
|
|
265
297
|
model_config = ConfigDict(validate_default=True, arbitrary_types_allowed=True)
|
|
266
298
|
enforce_deprecation_mapping_requirement: bool = Field(
|
|
@@ -291,10 +323,151 @@ class validate(Config_Base):
|
|
|
291
323
|
default=False, description="Validate latest TA information from Splunkbase"
|
|
292
324
|
)
|
|
293
325
|
|
|
326
|
+
test_data_caches: list[AttackDataCache] = Field(
|
|
327
|
+
default=[],
|
|
328
|
+
description="A list of attack data that can "
|
|
329
|
+
"be used in lieu of the HTTPS download links "
|
|
330
|
+
"of each test data file. This cache can significantly "
|
|
331
|
+
"increase overall test speed, ensure the correctness of "
|
|
332
|
+
"links at 'contentctl validate' time, and reduce errors "
|
|
333
|
+
"associated with failed responses from file servers.",
|
|
334
|
+
)
|
|
335
|
+
|
|
294
336
|
@property
|
|
295
337
|
def external_repos_path(self) -> pathlib.Path:
|
|
296
338
|
return self.path / "external_repos"
|
|
297
339
|
|
|
340
|
+
# We can't make this a validator because the constructor
|
|
341
|
+
# is called many times - we don't want to print this out many times.
|
|
342
|
+
def check_test_data_caches(self) -> Self:
|
|
343
|
+
"""
|
|
344
|
+
Check that the test data caches actually exist at the specified paths.
|
|
345
|
+
If they do exist, then do nothing. If they do not, then emit the helpext, but
|
|
346
|
+
do not raise an exception. They are not required, but can significantly speed up
|
|
347
|
+
and reduce the flakiness of tests by reducing failed HTTP requests.
|
|
348
|
+
"""
|
|
349
|
+
if not self.verbose:
|
|
350
|
+
# Ignore the check and error output if we are not in verbose mode
|
|
351
|
+
return self
|
|
352
|
+
for cache in self.test_data_caches:
|
|
353
|
+
cache_path = self.path / cache.base_directory_name
|
|
354
|
+
if not cache_path.is_dir():
|
|
355
|
+
print(cache.helptext)
|
|
356
|
+
else:
|
|
357
|
+
build_date_file = cache_path / "cache_build_date.txt"
|
|
358
|
+
git_hash_file = cache_path / "git_hash.txt"
|
|
359
|
+
|
|
360
|
+
if build_date_file.is_file():
|
|
361
|
+
# This is a cache that was built by contentctl. We can use this to
|
|
362
|
+
# determine if the cache is out of date.
|
|
363
|
+
with open(build_date_file, "r") as f:
|
|
364
|
+
build_date = f.read().strip()
|
|
365
|
+
else:
|
|
366
|
+
build_date = "<UNKNOWN_DATE>"
|
|
367
|
+
if git_hash_file.is_file():
|
|
368
|
+
# This is a cache that was built by contentctl. We can use this to
|
|
369
|
+
# determine if the cache is out of date.
|
|
370
|
+
with open(git_hash_file, "r") as f:
|
|
371
|
+
git_hash = f.read().strip()
|
|
372
|
+
else:
|
|
373
|
+
git_hash = "<UNKNOWN_HASH>"
|
|
374
|
+
|
|
375
|
+
print(
|
|
376
|
+
f"Found attack data cache at [{cache_path}]\n**Cache Build Date: {build_date}\n**Repo Git Hash : {git_hash}\n"
|
|
377
|
+
)
|
|
378
|
+
|
|
379
|
+
return self
|
|
380
|
+
|
|
381
|
+
def map_to_attack_data_cache(
|
|
382
|
+
self, filename: HttpUrl | FilePath, verbose: bool = False
|
|
383
|
+
) -> HttpUrl | FilePath:
|
|
384
|
+
if str(filename) in ATTACK_DATA_CACHE_MAPPING_EXCEPTIONS:
|
|
385
|
+
# This is already something that we have emitted a warning or
|
|
386
|
+
# Exception for. We don't want to emit it again as it will
|
|
387
|
+
# pollute the output.
|
|
388
|
+
return filename
|
|
389
|
+
|
|
390
|
+
# If this is simply a link to a file directly, then no mapping
|
|
391
|
+
# needs to take place. Return the link to the file.
|
|
392
|
+
if isinstance(filename, pathlib.Path):
|
|
393
|
+
return filename
|
|
394
|
+
|
|
395
|
+
if len(self.test_data_caches) == 0:
|
|
396
|
+
return filename
|
|
397
|
+
|
|
398
|
+
# Otherwise, this is a URL. See if its prefix matches one of the
|
|
399
|
+
# prefixes in the list of caches
|
|
400
|
+
for cache in self.test_data_caches:
|
|
401
|
+
root_folder_path = self.path / cache.base_directory_name
|
|
402
|
+
# See if this data file was in that path
|
|
403
|
+
|
|
404
|
+
if str(filename).startswith(cache.base_url):
|
|
405
|
+
new_file_name = str(filename).replace(cache.base_url, "")
|
|
406
|
+
new_file_path = root_folder_path / new_file_name
|
|
407
|
+
|
|
408
|
+
if not root_folder_path.is_dir():
|
|
409
|
+
# This has not been checked out. Even though we want to use this cache
|
|
410
|
+
# whenever possible, we don't want to force it.
|
|
411
|
+
return filename
|
|
412
|
+
|
|
413
|
+
if new_file_path.is_file():
|
|
414
|
+
# We found the file in the cache. Return the new path
|
|
415
|
+
return new_file_path
|
|
416
|
+
|
|
417
|
+
# Any thing below here is non standard behavior that will produce either a warning message,
|
|
418
|
+
# an error, or both. We onyl want to do this once for each file, even if it is used
|
|
419
|
+
# across multiple different detections.
|
|
420
|
+
ATTACK_DATA_CACHE_MAPPING_EXCEPTIONS.add(str(filename))
|
|
421
|
+
|
|
422
|
+
# The cache exists, but we didn't find the file. We will emit an informational warning
|
|
423
|
+
# for this, but this is not an exception. Instead, we will just fall back to using
|
|
424
|
+
# the original URL.
|
|
425
|
+
if verbose:
|
|
426
|
+
# Give some extra context about missing attack data files/bad mapping
|
|
427
|
+
try:
|
|
428
|
+
h = head(str(filename))
|
|
429
|
+
h.raise_for_status()
|
|
430
|
+
|
|
431
|
+
except RequestException:
|
|
432
|
+
raise ValueError(
|
|
433
|
+
f"Error resolving the attack_data file {filename}. "
|
|
434
|
+
f"It was missing from the cache {cache.base_directory_name} and a download from the server failed."
|
|
435
|
+
)
|
|
436
|
+
print(
|
|
437
|
+
f"\nFilename {filename} not found in cache {cache.base_directory_name}, but exists on the server. "
|
|
438
|
+
f"Your cache {cache.base_directory_name} may be out of date."
|
|
439
|
+
)
|
|
440
|
+
return filename
|
|
441
|
+
if verbose:
|
|
442
|
+
# Any thing below here is non standard behavior that will produce either a warning message,
|
|
443
|
+
# an error, or both. We onyl want to do this once for each file, even if it is used
|
|
444
|
+
# across multiple different detections.
|
|
445
|
+
ATTACK_DATA_CACHE_MAPPING_EXCEPTIONS.add(str(filename))
|
|
446
|
+
|
|
447
|
+
# Give some extra context about missing attack data files/bad mapping
|
|
448
|
+
url = f"Attack Data : {filename}"
|
|
449
|
+
prefixes = "".join(
|
|
450
|
+
[
|
|
451
|
+
f"\n Valid Prefix: {cache.base_url}"
|
|
452
|
+
for cache in self.test_data_caches
|
|
453
|
+
]
|
|
454
|
+
)
|
|
455
|
+
# Give some extra context about missing attack data files/bad mapping
|
|
456
|
+
try:
|
|
457
|
+
h = head(str(filename))
|
|
458
|
+
h.raise_for_status()
|
|
459
|
+
except RequestException:
|
|
460
|
+
raise ValueError(
|
|
461
|
+
f"Error resolving the attack_data file {filename}. It was missing from all caches and a download from the server failed.\n"
|
|
462
|
+
f"{url}{prefixes}\n"
|
|
463
|
+
)
|
|
464
|
+
|
|
465
|
+
print(
|
|
466
|
+
f"\nAttack Data Missing from all caches, but present at URL:\n{url}{prefixes}"
|
|
467
|
+
)
|
|
468
|
+
|
|
469
|
+
return filename
|
|
470
|
+
|
|
298
471
|
@property
|
|
299
472
|
def mitre_cti_repo_path(self) -> pathlib.Path:
|
|
300
473
|
return self.external_repos_path / "cti"
|
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
|
-
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
if TYPE_CHECKING:
|
|
6
|
+
from contentctl.objects.config import validate
|
|
7
|
+
|
|
8
|
+
from pydantic import (
|
|
9
|
+
BaseModel,
|
|
10
|
+
ConfigDict,
|
|
11
|
+
Field,
|
|
12
|
+
FilePath,
|
|
13
|
+
HttpUrl,
|
|
14
|
+
ValidationInfo,
|
|
15
|
+
field_validator,
|
|
16
|
+
)
|
|
3
17
|
|
|
4
18
|
|
|
5
19
|
class TestAttackData(BaseModel):
|
|
@@ -11,3 +25,24 @@ class TestAttackData(BaseModel):
|
|
|
11
25
|
sourcetype: str = Field(...)
|
|
12
26
|
custom_index: str | None = None
|
|
13
27
|
host: str | None = None
|
|
28
|
+
|
|
29
|
+
@field_validator("data", mode="after")
|
|
30
|
+
@classmethod
|
|
31
|
+
def check_for_existence_of_attack_data_repo(
|
|
32
|
+
cls, value: HttpUrl | FilePath, info: ValidationInfo
|
|
33
|
+
) -> HttpUrl | FilePath:
|
|
34
|
+
# this appears to be called more than once, the first time
|
|
35
|
+
# info.context is always None. In this case, just return what
|
|
36
|
+
# was passed.
|
|
37
|
+
if not info.context:
|
|
38
|
+
return value
|
|
39
|
+
|
|
40
|
+
# When the config is passed, used it to determine if we can map
|
|
41
|
+
# the test data to a file on disk
|
|
42
|
+
if info.context.get("config", None):
|
|
43
|
+
config: validate = info.context.get("config", None)
|
|
44
|
+
return config.map_to_attack_data_cache(value, verbose=config.verbose)
|
|
45
|
+
else:
|
|
46
|
+
raise ValueError(
|
|
47
|
+
"config not passed to TestAttackData constructor in context"
|
|
48
|
+
)
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
### {{app.label}} DETECTIONS ###
|
|
2
2
|
|
|
3
3
|
{% for detection in objects %}
|
|
4
|
-
{% if (detection.type == 'TTP' or detection.type == 'Anomaly' or detection.type == 'Hunting' or detection.type == 'Correlation') %}
|
|
5
4
|
[{{ detection.get_conf_stanza_name(app) }}]
|
|
6
5
|
action.escu = 0
|
|
7
6
|
action.escu.enabled = 1
|
|
@@ -9,23 +8,13 @@ description = {{ detection.status_aware_description | escapeNewlines() }}
|
|
|
9
8
|
action.escu.mappings = {{ detection.mappings | tojson }}
|
|
10
9
|
action.escu.data_models = {{ detection.datamodel | tojson }}
|
|
11
10
|
action.escu.eli5 = {{ detection.status_aware_description | escapeNewlines() }}
|
|
12
|
-
{% if detection.how_to_implement %}
|
|
13
11
|
action.escu.how_to_implement = {{ detection.how_to_implement | escapeNewlines() }}
|
|
14
|
-
{% else %}
|
|
15
|
-
action.escu.how_to_implement = none
|
|
16
|
-
{% endif %}
|
|
17
|
-
{% if detection.known_false_positives %}
|
|
18
12
|
action.escu.known_false_positives = {{ detection.known_false_positives | escapeNewlines() }}
|
|
19
|
-
{% else %}
|
|
20
|
-
action.escu.known_false_positives = None
|
|
21
|
-
{% endif %}
|
|
22
13
|
action.escu.creation_date = {{ detection.date }}
|
|
23
14
|
action.escu.modification_date = {{ detection.date }}
|
|
24
15
|
action.escu.confidence = high
|
|
25
16
|
action.escu.search_type = detection
|
|
26
|
-
{% if detection.tags.product is defined %}
|
|
27
17
|
action.escu.product = {{ detection.tags.product | tojson }}
|
|
28
|
-
{% endif %}
|
|
29
18
|
{% if detection.tags.atomic_guid %}
|
|
30
19
|
action.escu.atomic_red_team_guids = {{ detection.tags.getAtomicGuidStringArray() | tojson }}
|
|
31
20
|
{% endif %}
|
|
@@ -34,7 +23,6 @@ action.escu.providing_technologies = {{ detection.providing_technologies | tojso
|
|
|
34
23
|
{% else %}
|
|
35
24
|
action.escu.providing_technologies = null
|
|
36
25
|
{% endif %}
|
|
37
|
-
{% if detection.tags.analytic_story %}
|
|
38
26
|
action.escu.analytic_story = {{ objectListToNameList(detection.tags.analytic_story) | tojson }}
|
|
39
27
|
{% if detection.deployment.alert_action.rba.enabled%}
|
|
40
28
|
action.risk = 1
|
|
@@ -43,25 +31,19 @@ action.risk.param._risk = {{ detection.risk | tojson }}
|
|
|
43
31
|
action.risk.param._risk_score = 0
|
|
44
32
|
action.risk.param.verbose = 0
|
|
45
33
|
{% endif %}
|
|
46
|
-
{% else %}
|
|
47
|
-
action.escu.analytic_story = []
|
|
48
|
-
{% endif %}
|
|
49
34
|
cron_schedule = {{ detection.deployment.scheduling.cron_schedule }}
|
|
50
35
|
dispatch.earliest_time = {{ detection.deployment.scheduling.earliest_time }}
|
|
51
36
|
dispatch.latest_time = {{ detection.deployment.scheduling.latest_time }}
|
|
52
37
|
action.correlationsearch.enabled = 1
|
|
38
|
+
action.correlationsearch.detection_type = ebd
|
|
53
39
|
action.correlationsearch.label = {{ detection.get_action_dot_correlationsearch_dot_label(app) }}
|
|
54
40
|
action.correlationsearch.annotations = {{ detection.annotations | tojson }}
|
|
55
41
|
action.correlationsearch.metadata = {{ detection.metadata | tojson }}
|
|
56
|
-
{% if detection.deployment.scheduling.schedule_window is defined %}
|
|
57
42
|
schedule_window = {{ detection.deployment.scheduling.schedule_window }}
|
|
58
|
-
{% endif %}
|
|
59
|
-
{% if detection.deployment is defined %}
|
|
60
43
|
{% if detection.deployment.alert_action.notable %}
|
|
61
44
|
action.notable = 1
|
|
62
|
-
{
|
|
45
|
+
action.notable.param._entities = [{"risk_object_field": "N/A", "risk_object_type": "N/A", "risk_score": "0"}]
|
|
63
46
|
action.notable.param.nes_fields = {{ detection.nes_fields }}
|
|
64
|
-
{% endif %}
|
|
65
47
|
action.notable.param.rule_description = {{ detection.deployment.alert_action.notable.rule_description | custom_jinja2_enrichment_filter(detection) | escapeNewlines()}}
|
|
66
48
|
action.notable.param.rule_title = {% if detection.type | lower == "correlation" %}RBA: {{ detection.deployment.alert_action.notable.rule_title | custom_jinja2_enrichment_filter(detection) }}{% else %}{{ detection.deployment.alert_action.notable.rule_title | custom_jinja2_enrichment_filter(detection) }}{% endif +%}
|
|
67
49
|
action.notable.param.security_domain = {{ detection.tags.security_domain }}
|
|
@@ -87,13 +69,8 @@ action.sendtophantom.param.phantom_server = {{ detection.deployment.alert_action
|
|
|
87
69
|
action.sendtophantom.param.sensitivity = {{ detection.deployment.alert_action.phantom.sensitivity | custom_jinja2_enrichment_filter(detection) }}
|
|
88
70
|
action.sendtophantom.param.severity = {{ detection.deployment.alert_action.phantom.severity | custom_jinja2_enrichment_filter(detection) }}
|
|
89
71
|
{% endif %}
|
|
90
|
-
{% endif %}
|
|
91
72
|
alert.digest_mode = 1
|
|
92
|
-
{
|
|
93
|
-
disabled = false
|
|
94
|
-
{% else %}
|
|
95
|
-
disabled = true
|
|
96
|
-
{% endif %}
|
|
73
|
+
disabled = {{ (not detection.enabled_by_default) | lower }}
|
|
97
74
|
enableSched = 1
|
|
98
75
|
allow_skew = 100%
|
|
99
76
|
counttype = number of events
|
|
@@ -108,7 +85,6 @@ alert.suppress.period = {{ detection.tags.throttling.period }}
|
|
|
108
85
|
{% endif %}
|
|
109
86
|
search = {{ detection.search | escapeNewlines() }}
|
|
110
87
|
action.notable.param.drilldown_searches = {{ detection.drilldowns_in_JSON | tojson | escapeNewlines() }}
|
|
111
|
-
{% endif %}
|
|
112
88
|
|
|
113
89
|
{% endfor %}
|
|
114
90
|
### END {{ app.label }} DETECTIONS ###
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: contentctl
|
|
3
|
-
Version: 5.
|
|
3
|
+
Version: 5.5.0
|
|
4
4
|
Summary: Splunk Content Control Tool
|
|
5
5
|
License: Apache 2.0
|
|
6
6
|
Author: STRT
|
|
@@ -24,7 +24,7 @@ Requires-Dist: questionary (>=2.0.1,<3.0.0)
|
|
|
24
24
|
Requires-Dist: requests (>=2.32.3,<2.33.0)
|
|
25
25
|
Requires-Dist: rich (>=14.0.0,<15.0.0)
|
|
26
26
|
Requires-Dist: semantic-version (>=2.10.0,<3.0.0)
|
|
27
|
-
Requires-Dist: setuptools (>=69.5.1,<
|
|
27
|
+
Requires-Dist: setuptools (>=69.5.1,<81.0.0)
|
|
28
28
|
Requires-Dist: splunk-sdk (>=2.0.2,<3.0.0)
|
|
29
29
|
Requires-Dist: tqdm (>=4.66.5,<5.0.0)
|
|
30
30
|
Requires-Dist: tyro (>=0.9.2,<0.10.0)
|
|
@@ -4,7 +4,7 @@ contentctl/actions/deploy_acs.py,sha256=w3OqO8GXzB_5zHrE8lDYbadAy4Etw7F2o84Gze74
|
|
|
4
4
|
contentctl/actions/detection_testing/DetectionTestingManager.py,sha256=94apBwLkXWsgdLSvE9f_KqCfQSdmDChMncMcsEdY1A8,10974
|
|
5
5
|
contentctl/actions/detection_testing/GitService.py,sha256=HU1fKkb5463weqSZ3LrTVHtNrzBH_f5pE99-zD2j1A8,11345
|
|
6
6
|
contentctl/actions/detection_testing/generate_detection_coverage_badge.py,sha256=bGUVKjKv96lTw1GZ4Kw1JX-Yicu4aOJWm-IL524e9HI,2302
|
|
7
|
-
contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructure.py,sha256=
|
|
7
|
+
contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructure.py,sha256=Th2QFQeGL3a-aUp83fUCm0aApQIcXjspetNj920fd34,60776
|
|
8
8
|
contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructureContainer.py,sha256=qYWgRW7uc-15jzwv5xSUF2xyLDmtyGyMfuXkQK9j-aM,7221
|
|
9
9
|
contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructureServer.py,sha256=Q1ZfCYOp54O39bgTScZMInkmZiU-bGAM9Hiwr2mq5ms,370
|
|
10
10
|
contentctl/actions/detection_testing/progress_bar.py,sha256=UrpNCqxTmQ4hfoRZgxPJ1xvDVwMrTq0UnotdryHN0gM,3232
|
|
@@ -21,7 +21,7 @@ contentctl/actions/reporting.py,sha256=GF32i7sHdc47bw-VWSW-nZ1QBaUl6Ni1JjV5_SOyi
|
|
|
21
21
|
contentctl/actions/test.py,sha256=ftZazqoqv7bLNhyW23aRnDpetG9zltS8wr4Xq9Hls0k,6268
|
|
22
22
|
contentctl/actions/validate.py,sha256=teqRVxNlUgzDvKQm-sXb05TST05duA2-NhJOzNxlBTw,6152
|
|
23
23
|
contentctl/api.py,sha256=6s17vNOW1E1EzQqOCXAa5uWuhwwShu-JkGSgrsOFEMs,6329
|
|
24
|
-
contentctl/contentctl.py,sha256=
|
|
24
|
+
contentctl/contentctl.py,sha256=Wpy5GmgQIVoWs9PBDDKS0E1U2810J8WEfzfxckMGB-c,11791
|
|
25
25
|
contentctl/enrichments/attack_enrichment.py,sha256=68C9xQ8Q3YX-luRdK2hLnwWtRFpheFA2kE4v5GOLGEo,6358
|
|
26
26
|
contentctl/enrichments/cve_enrichment.py,sha256=TsZ52ef2njt19lPf_VyclY_-5Z5iQ1boVOAxFbjGdSQ,2431
|
|
27
27
|
contentctl/enrichments/splunk_app_enrichment.py,sha256=Xynxjjkqlw0_RtQ1thGSFwy1I3HdmPAJmNKZezyqniU,3419
|
|
@@ -29,10 +29,10 @@ contentctl/helper/link_validator.py,sha256=kzEi2GdncPWSi-UKNerXm2jtTJfFQ5goS9pqy
|
|
|
29
29
|
contentctl/helper/logger.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
30
30
|
contentctl/helper/splunk_app.py,sha256=Zq_C9rjNVqCjBNgm-5CWdBpXyeX5jSpbE-QTGptEZlk,14571
|
|
31
31
|
contentctl/helper/utils.py,sha256=1_6cbvvbPXWxym3ZhRhL18ttmXLXiHbavpXAkROtGcg,21154
|
|
32
|
-
contentctl/input/director.py,sha256=
|
|
32
|
+
contentctl/input/director.py,sha256=Em7ZwYdIapgr7Qd--uX6UxkeSyT1oxwyY0pius2Wbqc,17431
|
|
33
33
|
contentctl/input/new_content_questions.py,sha256=z2C4Mg7-EyxtiF2z9m4SnSbi6QO4CUPB3wg__JeMXIQ,4067
|
|
34
34
|
contentctl/input/yml_reader.py,sha256=L27b14_xXQYypUV1eAzZrfMtwtkzAZx--6nRo3RNZnE,2729
|
|
35
|
-
contentctl/objects/abstract_security_content_objects/detection_abstract.py,sha256=
|
|
35
|
+
contentctl/objects/abstract_security_content_objects/detection_abstract.py,sha256=bvX3iDQvR7BI1KIgKFBbyX7QkijhPANo9GzBCMVwtm8,46905
|
|
36
36
|
contentctl/objects/abstract_security_content_objects/security_content_object_abstract.py,sha256=un1tjoZdxKi07dSddPD5MXRN3tea3dj0poKmFrbCu-k,34206
|
|
37
37
|
contentctl/objects/alert_action.py,sha256=iEvdEOT4TrTXT0z4rQ_W5v79hPJpPhFPSzo7TuHDxwA,1376
|
|
38
38
|
contentctl/objects/annotated_types.py,sha256=xR4EKvdOpNDEt0doGs8XjxCzKK99J2NHZgHFAmt7p2c,424
|
|
@@ -42,7 +42,7 @@ contentctl/objects/base_test.py,sha256=JG6qlr7xe9P71n3CzKOro8_bsmDQGYDfTG9YooHQS
|
|
|
42
42
|
contentctl/objects/base_test_result.py,sha256=TYYzTPKWqp9rHTebWoid50uxAp_iALZouril4sFwIcA,5197
|
|
43
43
|
contentctl/objects/baseline.py,sha256=EMcuz_9sVgOFh3YCj871GSAA6v3FIkRTf90-LAHq-J0,3700
|
|
44
44
|
contentctl/objects/baseline_tags.py,sha256=Eomy8y3HV-E6Lym5B5ZZTtsmQJYi6Jd4y8GZpTWGYgQ,1643
|
|
45
|
-
contentctl/objects/config.py,sha256=
|
|
45
|
+
contentctl/objects/config.py,sha256=3sJN48BuWXsCRjrdYyHTjZnUmVHGt9aW3_QaMBaghRw,57953
|
|
46
46
|
contentctl/objects/constants.py,sha256=VwwQtJBGC_zb3ukjb3A7P0CwAlyhacWiXczwAW5Jiog,5466
|
|
47
47
|
contentctl/objects/content_versioning_service.py,sha256=BDk_TV1PTVoXpPcUxqTLa5_bjkfOs9PFYgqTuzOS9UI,20566
|
|
48
48
|
contentctl/objects/correlation_search.py,sha256=tvcFeHGFRyZso50KHsEVCjTmHh3rHDegcfqaD-TjCFg,51473
|
|
@@ -84,7 +84,7 @@ contentctl/objects/savedsearches_conf.py,sha256=Dn_Pxd9i3RT6DwNh6JrgmfxjsO3q15xz
|
|
|
84
84
|
contentctl/objects/security_content_object.py,sha256=2mEf-wt3hMsLEyo4yatyK66jKjgUOVjJHIN9fgQB5nA,246
|
|
85
85
|
contentctl/objects/story.py,sha256=1JCiF9D1EZeVcoMXXDoWkOqHXQn4TsQgl8EtUN59a2E,5796
|
|
86
86
|
contentctl/objects/story_tags.py,sha256=IYumFuBF2Bt7HtW4lBfCRo2EUpjMYlnNjpx24jBErs4,2365
|
|
87
|
-
contentctl/objects/test_attack_data.py,sha256=
|
|
87
|
+
contentctl/objects/test_attack_data.py,sha256=jXpaThVBntzKkRm1CPjTpaXDbKPu8xNghwuQ1pcGkoo,1513
|
|
88
88
|
contentctl/objects/test_group.py,sha256=r-dXyddok4yslv8SIjwOpqylbN1rdjsRi-HIijvpWD0,2602
|
|
89
89
|
contentctl/objects/threat_object.py,sha256=CB3igcmiq06lqnEh7h-btxFrBfgZbHaA9p8kFDKY6lQ,712
|
|
90
90
|
contentctl/objects/throttling.py,sha256=oupWmdtvwAXzLmD3MBJyAU18SD2L2ciEZWUcnL8MuGk,2309
|
|
@@ -124,7 +124,7 @@ contentctl/output/templates/header.j2,sha256=3usV7jm1q6J-QNnQrZzII9cN0XEGQjg_eVK
|
|
|
124
124
|
contentctl/output/templates/macros.j2,sha256=SLcQQ5X7TZS8j-2qP06BTXqdIcnwoYqTAaBLX2Dge7Y,390
|
|
125
125
|
contentctl/output/templates/panel.j2,sha256=Cw_W6p-14n6UivVfpS75KKJiJ2VpdGsSBceYsUYe9gk,221
|
|
126
126
|
contentctl/output/templates/savedsearches_baselines.j2,sha256=WHZB4e0vmeym8832VxRmuUfDJ-YRYt6emcYaJrghI58,1709
|
|
127
|
-
contentctl/output/templates/savedsearches_detections.j2,sha256=
|
|
127
|
+
contentctl/output/templates/savedsearches_detections.j2,sha256=4WVJtVdFNzSx83SygzpcTYS0McBX41mpRPFm0HxkpuU,5328
|
|
128
128
|
contentctl/output/templates/savedsearches_investigations.j2,sha256=KH2r8SgyAMiettSHypSbA2-1XmQ_8_8xzk3BkbZ1Re4,1196
|
|
129
129
|
contentctl/output/templates/server.conf.j2,sha256=sPZUkiuJNGm9R8rpjfRKyuAvmmQb0C4w9Q6hpmvmPeU,127
|
|
130
130
|
contentctl/output/templates/transforms.j2,sha256=TEKZi8DWpcCysRTNvuLEgAwx-g1SZ2E0CkLiu6v6AlU,1339
|
|
@@ -164,8 +164,8 @@ contentctl/templates/detections/web/.gitkeep,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRk
|
|
|
164
164
|
contentctl/templates/macros/security_content_ctime.yml,sha256=Gg1YNllHVsX_YB716H1SJLWzxXZEfuJlnsgB2fuyoHU,159
|
|
165
165
|
contentctl/templates/macros/security_content_summariesonly.yml,sha256=9BYUxAl2E4Nwh8K19F3AJS8Ka7ceO6ZDBjFiO3l3LY0,162
|
|
166
166
|
contentctl/templates/stories/cobalt_strike.yml,sha256=uj8idtDNOAIqpZ9p8usQg6mop1CQkJ5TlB4Q7CJdTIE,3082
|
|
167
|
-
contentctl-5.
|
|
168
|
-
contentctl-5.
|
|
169
|
-
contentctl-5.
|
|
170
|
-
contentctl-5.
|
|
171
|
-
contentctl-5.
|
|
167
|
+
contentctl-5.5.0.dist-info/LICENSE.md,sha256=hQWUayRk-pAiOZbZnuy8djmoZkjKBx8MrCFpW-JiOgo,11344
|
|
168
|
+
contentctl-5.5.0.dist-info/METADATA,sha256=howJqbvOr3-HZmRA1k887Cj-yiGCPpR_pwJrVEYDUUY,5134
|
|
169
|
+
contentctl-5.5.0.dist-info/WHEEL,sha256=fGIA9gx4Qxk2KDKeNJCbOEwSrmLtjWCwzBz351GyrPQ,88
|
|
170
|
+
contentctl-5.5.0.dist-info/entry_points.txt,sha256=5bjZ2NkbQfSwK47uOnA77yCtjgXhvgxnmCQiynRF_-U,57
|
|
171
|
+
contentctl-5.5.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|