contentctl 5.1.0__py3-none-any.whl → 5.2.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.
@@ -89,7 +89,7 @@ class DetectionTestingManagerOutputDto:
89
89
  start_time: Union[datetime.datetime, None] = None
90
90
  replay_index: str = "contentctl_testing_index"
91
91
  replay_host: str = "CONTENTCTL_HOST"
92
- timeout_seconds: int = 60
92
+ timeout_seconds: int = 120
93
93
  terminate: bool = False
94
94
 
95
95
 
@@ -474,7 +474,7 @@ class Detection_Abstract(SecurityContentObject):
474
474
  "name": lookup.name,
475
475
  "description": lookup.description,
476
476
  "filename": lookup.filename.name,
477
- "default_match": "true" if lookup.default_match else "false",
477
+ "default_match": lookup.default_match,
478
478
  "case_sensitive_match": "true"
479
479
  if lookup.case_sensitive_match
480
480
  else "false",
@@ -1055,3 +1055,30 @@ class Detection_Abstract(SecurityContentObject):
1055
1055
  # Return the summary
1056
1056
 
1057
1057
  return summary_dict
1058
+
1059
+ @model_validator(mode="after")
1060
+ def validate_data_source_output_fields(self):
1061
+ # Skip validation for Hunting and Correlation types, or non-production detections
1062
+ if self.status != DetectionStatus.production or self.type in {
1063
+ AnalyticsType.Hunting,
1064
+ AnalyticsType.Correlation,
1065
+ }:
1066
+ return self
1067
+
1068
+ # Validate that all required output fields are present in the search
1069
+ for data_source in self.data_source_objects:
1070
+ if not data_source.output_fields:
1071
+ continue
1072
+
1073
+ missing_fields = [
1074
+ field for field in data_source.output_fields if field not in self.search
1075
+ ]
1076
+
1077
+ if missing_fields:
1078
+ raise ValueError(
1079
+ f"Data source '{data_source.name}' has output fields "
1080
+ f"{missing_fields} that are not present in the search "
1081
+ f"for detection '{self.name}'"
1082
+ )
1083
+
1084
+ return self
@@ -17,10 +17,12 @@ class DataSource(SecurityContentObject):
17
17
  source: str = Field(...)
18
18
  sourcetype: str = Field(...)
19
19
  separator: Optional[str] = None
20
+ separator_value: None | str = None
20
21
  configuration: Optional[str] = None
21
22
  supported_TA: list[TA] = []
22
23
  fields: None | list = None
23
24
  field_mappings: None | list = None
25
+ mitre_components: list[str] = []
24
26
  convert_to_log_source: None | list = None
25
27
  example_log: None | str = None
26
28
  output_fields: list[str] = []
@@ -6,9 +6,10 @@ import pathlib
6
6
  import re
7
7
  from enum import StrEnum, auto
8
8
  from functools import cached_property
9
- from typing import TYPE_CHECKING, Annotated, Any, Literal, Optional, Self
9
+ from typing import TYPE_CHECKING, Annotated, Any, Literal, Self
10
10
 
11
11
  from pydantic import (
12
+ BeforeValidator,
12
13
  Field,
13
14
  FilePath,
14
15
  NonNegativeInt,
@@ -69,7 +70,19 @@ class Lookup_Type(StrEnum):
69
70
 
70
71
  # TODO (#220): Split Lookup into 2 classes
71
72
  class Lookup(SecurityContentObject, abc.ABC):
72
- default_match: Optional[bool] = None
73
+ # We need to make sure that this is converted to a string because we widely
74
+ # use the string "False" in our lookup content. However, PyYAML reads this
75
+ # as a BOOL and this causes parsing to fail. As such, we will always
76
+ # convert this to a string if it is passed as a bool
77
+ default_match: Annotated[
78
+ str, BeforeValidator(lambda dm: str(dm).lower() if isinstance(dm, bool) else dm)
79
+ ] = Field(
80
+ default="",
81
+ description="This field is given a default value of ''"
82
+ "because it is the default value specified in the transforms.conf "
83
+ "docs. Giving it a type of str rather than str | None simplifies "
84
+ "the typing for the field.",
85
+ )
73
86
  # Per the documentation for transforms.conf, EXACT should not be specified in this list,
74
87
  # so we include only WILDCARD and CIDR
75
88
  match_type: list[Annotated[str, Field(pattern=r"(^WILDCARD|CIDR)\(.+\)$")]] = Field(
@@ -88,7 +101,7 @@ class Lookup(SecurityContentObject, abc.ABC):
88
101
 
89
102
  # All fields custom to this model
90
103
  model = {
91
- "default_match": "true" if self.default_match is True else "false",
104
+ "default_match": self.default_match,
92
105
  "match_type": self.match_type_to_conf_format,
93
106
  "min_matches": self.min_matches,
94
107
  "max_matches": self.max_matches,
@@ -1,5 +1,5 @@
1
- from typing import List, Union
2
1
  import pathlib
2
+ from typing import List, Union
3
3
 
4
4
  from contentctl.objects.detection import Detection
5
5
  from contentctl.output.attack_nav_writer import AttackNavWriter
@@ -10,14 +10,21 @@ class AttackNavOutput:
10
10
  self, detections: List[Detection], output_path: pathlib.Path
11
11
  ) -> None:
12
12
  techniques: dict[str, dict[str, Union[List[str], int]]] = {}
13
+
13
14
  for detection in detections:
14
15
  for tactic in detection.tags.mitre_attack_id:
15
16
  if tactic not in techniques:
16
17
  techniques[tactic] = {"score": 0, "file_paths": []}
17
18
 
18
- detection_url = f"https://github.com/splunk/security_content/blob/develop/detections/{detection.source}/{detection.file_path.name}"
19
- techniques[tactic]["score"] += 1
20
- techniques[tactic]["file_paths"].append(detection_url)
19
+ detection_type = detection.source
20
+ detection_id = detection.id
21
+
22
+ # Store all three pieces of information separately
23
+ detection_info = f"{detection_type}|{detection_id}|{detection.name}"
24
+
25
+ techniques[tactic]["score"] = techniques[tactic].get("score", 0) + 1
26
+ if isinstance(techniques[tactic]["file_paths"], list):
27
+ techniques[tactic]["file_paths"].append(detection_info)
21
28
 
22
29
  """
23
30
  for detection in objects:
@@ -1,11 +1,11 @@
1
1
  import json
2
- from typing import Union, List
3
2
  import pathlib
3
+ from typing import List, Union
4
4
 
5
- VERSION = "4.3"
5
+ VERSION = "4.5"
6
6
  NAME = "Detection Coverage"
7
- DESCRIPTION = "security_content detection coverage"
8
- DOMAIN = "mitre-enterprise"
7
+ DESCRIPTION = "Security Content Detection Coverage"
8
+ DOMAIN = "enterprise-attack"
9
9
 
10
10
 
11
11
  class AttackNavWriter:
@@ -14,52 +14,68 @@ class AttackNavWriter:
14
14
  mitre_techniques: dict[str, dict[str, Union[List[str], int]]],
15
15
  output_path: pathlib.Path,
16
16
  ) -> None:
17
- max_count = 0
18
- for technique_id in mitre_techniques.keys():
19
- if mitre_techniques[technique_id]["score"] > max_count:
20
- max_count = mitre_techniques[technique_id]["score"]
17
+ max_count = max(
18
+ (technique["score"] for technique in mitre_techniques.values()), default=0
19
+ )
21
20
 
22
21
  layer_json = {
23
- "version": VERSION,
22
+ "versions": {"attack": "16", "navigator": "5.1.0", "layer": VERSION},
24
23
  "name": NAME,
25
24
  "description": DESCRIPTION,
26
25
  "domain": DOMAIN,
27
26
  "techniques": [],
27
+ "gradient": {
28
+ "colors": ["#ffffff", "#66b1ff", "#096ed7"],
29
+ "minValue": 0,
30
+ "maxValue": max_count,
31
+ },
32
+ "filters": {
33
+ "platforms": [
34
+ "Windows",
35
+ "Linux",
36
+ "macOS",
37
+ "Network",
38
+ "AWS",
39
+ "GCP",
40
+ "Azure",
41
+ "Azure AD",
42
+ "Office 365",
43
+ "SaaS",
44
+ ]
45
+ },
46
+ "layout": {
47
+ "layout": "side",
48
+ "showName": True,
49
+ "showID": True,
50
+ "showAggregateScores": False,
51
+ },
52
+ "legendItems": [
53
+ {"label": "No detections", "color": "#ffffff"},
54
+ {"label": "Has detections", "color": "#66b1ff"},
55
+ ],
56
+ "showTacticRowBackground": True,
57
+ "tacticRowBackground": "#dddddd",
58
+ "selectTechniquesAcrossTactics": True,
28
59
  }
29
60
 
30
- layer_json["gradient"] = {
31
- "colors": ["#ffffff", "#66b1ff", "#096ed7"],
32
- "minValue": 0,
33
- "maxValue": max_count,
34
- }
35
-
36
- layer_json["filters"] = {
37
- "platforms": [
38
- "Windows",
39
- "Linux",
40
- "macOS",
41
- "AWS",
42
- "GCP",
43
- "Azure",
44
- "Office 365",
45
- "SaaS",
46
- ]
47
- }
61
+ for technique_id, data in mitre_techniques.items():
62
+ links = []
63
+ for detection_info in data["file_paths"]:
64
+ # Split the detection info into its components
65
+ detection_type, detection_id, detection_name = detection_info.split("|")
48
66
 
49
- layer_json["legendItems"] = [
50
- {"label": "NO available detections", "color": "#ffffff"},
51
- {"label": "Some detections available", "color": "#66b1ff"},
52
- ]
67
+ # Construct research website URL (without the name)
68
+ research_url = (
69
+ f"https://research.splunk.com/{detection_type}/{detection_id}/"
70
+ )
53
71
 
54
- layer_json["showTacticRowBackground"] = True
55
- layer_json["tacticRowBackground"] = "#dddddd"
56
- layer_json["sorting"] = 3
72
+ links.append({"label": detection_name, "url": research_url})
57
73
 
58
- for technique_id in mitre_techniques.keys():
59
74
  layer_technique = {
60
75
  "techniqueID": technique_id,
61
- "score": mitre_techniques[technique_id]["score"],
62
- "comment": "\n\n".join(mitre_techniques[technique_id]["file_paths"]),
76
+ "score": data["score"],
77
+ "enabled": True,
78
+ "links": links,
63
79
  }
64
80
  layer_json["techniques"].append(layer_technique)
65
81
 
@@ -7,8 +7,8 @@ filename = {{ lookup.app_filename.name }}
7
7
  collection = {{ lookup.collection }}
8
8
  external_type = kvstore
9
9
  {% endif %}
10
- {% if lookup.default_match is defined and lookup.default_match != None %}
11
- default_match = {{ lookup.default_match | lower }}
10
+ {% if lookup.default_match != '' %}
11
+ default_match = {{ lookup.default_match }}
12
12
  {% endif %}
13
13
  {% if lookup.case_sensitive_match is defined and lookup.case_sensitive_match != None %}
14
14
  case_sensitive_match = {{ lookup.case_sensitive_match | lower }}
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: contentctl
3
- Version: 5.1.0
3
+ Version: 5.2.0
4
4
  Summary: Splunk Content Control Tool
5
5
  License: Apache 2.0
6
6
  Author: STRT
@@ -4,7 +4,7 @@ contentctl/actions/deploy_acs.py,sha256=w3OqO8GXzB_5zHrE8lDYbadAy4Etw7F2o84Gze74
4
4
  contentctl/actions/detection_testing/DetectionTestingManager.py,sha256=TWZpmDjMqWRWyzsLyiYol_jAovAr6ok9J_GzE9-kNN0,9079
5
5
  contentctl/actions/detection_testing/GitService.py,sha256=a6y7lqCgSL1KdSVEgJDxawea8ZgEkGNfOKEf9v_BgLo,11135
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=52Xsbyq4M913kXuQ8JcjYfP2BvwRJo3chK1p2hK76o0,57281
7
+ contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructure.py,sha256=dus-8ANS0VgKq1HOdis4wVF5DKtdot6csbcpayxqXDo,57282
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
@@ -32,7 +32,7 @@ contentctl/helper/utils.py,sha256=rigwZzCwWzn11sKTVWDkYEtLmRSf0yBbJ671OSRQnOM,19
32
32
  contentctl/input/director.py,sha256=7mx93-k_8DUc4pwYjxwq39n8-BlDYZOvRBkdzdGA8Qc,10919
33
33
  contentctl/input/new_content_questions.py,sha256=z2C4Mg7-EyxtiF2z9m4SnSbi6QO4CUPB3wg__JeMXIQ,4067
34
34
  contentctl/input/yml_reader.py,sha256=ymmAqsWsf9Oj56waDOhCh_E4SomkSCmu4dAx7iURFt8,2050
35
- contentctl/objects/abstract_security_content_objects/detection_abstract.py,sha256=T5SAFOwEeCDCe79IyVryoLO9k6VaTiwbQPWSNo1epk0,43001
35
+ contentctl/objects/abstract_security_content_objects/detection_abstract.py,sha256=VMt_0eA8eFa64DD2j96kIqfhREVNMPBeJAclsv0eFUw,43978
36
36
  contentctl/objects/abstract_security_content_objects/security_content_object_abstract.py,sha256=P1v5n8hM4XzJOjNZbJ5m3y4Bu-cQYaddLPdbE6K-4Xw,12164
37
37
  contentctl/objects/alert_action.py,sha256=iEvdEOT4TrTXT0z4rQ_W5v79hPJpPhFPSzo7TuHDxwA,1376
38
38
  contentctl/objects/annotated_types.py,sha256=xR4EKvdOpNDEt0doGs8XjxCzKK99J2NHZgHFAmt7p2c,424
@@ -45,7 +45,7 @@ contentctl/objects/config.py,sha256=3l8tFVwrBDpAnS7aBgj6to0Kc8_s4bxuZY5Bm5vel8k,
45
45
  contentctl/objects/constants.py,sha256=VwwQtJBGC_zb3ukjb3A7P0CwAlyhacWiXczwAW5Jiog,5466
46
46
  contentctl/objects/correlation_search.py,sha256=ab6v-0nbzujhTMpwaXynQiInWpRO1zB5KR4eZLCav_M,45234
47
47
  contentctl/objects/dashboard.py,sha256=9DiHP_Nx2XBQv4-zUaz6v9XH5yeTJhxaGDlaQqCsbIU,4468
48
- contentctl/objects/data_source.py,sha256=qt4W14DEwKGO69oLGdJeuYqbWvGkZ6j5Nz0R1RhDQEQ,1491
48
+ contentctl/objects/data_source.py,sha256=AtB6lAe43mx0GicphWTkmYupp8Fb1hmWgm_GpbX73wo,1567
49
49
  contentctl/objects/deployment.py,sha256=FRsgsX2T1gvA_0A44_sFPr22rsedxXVIhtO7o9F7eZM,2902
50
50
  contentctl/objects/deployment_email.py,sha256=_Sdr_BNjvXECiFonRHLkiOrIQp3slnUaERbptqRbD0Q,206
51
51
  contentctl/objects/deployment_notable.py,sha256=j5AniTRDcw32El5H91qKOXDVZvUYxnIuM4Zzlhrm9cM,258
@@ -64,7 +64,7 @@ contentctl/objects/integration_test.py,sha256=TYjKyH4YinUnYXOse5BQGCa4-ez_5mtoMw
64
64
  contentctl/objects/integration_test_result.py,sha256=_uUSgqgjFhEZM8UwOJI6Q9K-ekIrbKU6OPdqHZycl-s,279
65
65
  contentctl/objects/investigation.py,sha256=kmvQ0grCd1YVEW5wVF4-_Rx87PnOxPiNoN_ufTLc3ZM,3659
66
66
  contentctl/objects/investigation_tags.py,sha256=qDGNusrWDvCX_GcBEzag2MydSV0LIhGxoXZGgxDXfHA,1317
67
- contentctl/objects/lookup.py,sha256=mzOPhMDyoNZKLAj8zf6Wg6i9FJKMu3qHWinATtH75I8,13015
67
+ contentctl/objects/lookup.py,sha256=uP8N2Munu9vDGOzkC3mBlM93xkICyVBi62fAXAloI_I,13656
68
68
  contentctl/objects/macro.py,sha256=usbxyOPIRIJoDmvawfP2DxtFNf71GaDwffxiZsRkP5A,3594
69
69
  contentctl/objects/manual_test.py,sha256=cx_XAtQ8VG8Ui_F553Xnut75vFEOtRwm1dDIIWNpOaM,952
70
70
  contentctl/objects/manual_test_result.py,sha256=FyCVVf-f1DKs-qBkM4tbKfY6mkrW25NcIEBqyaDC2rE,156
@@ -89,8 +89,8 @@ contentctl/objects/unit_test.py,sha256=-rtSmZ8N2UZ4NkDsfzNXzXiF6dTDwt_jsQ_14xp0h
89
89
  contentctl/objects/unit_test_baseline.py,sha256=ezg8Ctih_3che2ln2tuVCAtRPHaf5tDMR3dGb34MqaA,287
90
90
  contentctl/objects/unit_test_result.py,sha256=gqHqYN5XGBKdV-mdKhAdwfOw4_PpN3i9z_b6ciByDSc,2928
91
91
  contentctl/output/api_json_output.py,sha256=QWe_KWlHHxE4Mhd3BHRfJbUJ4z2mLHZn_eMWfMVInik,8237
92
- contentctl/output/attack_nav_output.py,sha256=2_JISJ3sL4dVAwrIfZ7c426CGz5gjUBVkWh0uFO2MXU,2276
93
- contentctl/output/attack_nav_writer.py,sha256=FEua57vv347PjFiu1skOEGAbxIqPWMN8Iyp8nDrIvAA,2044
92
+ contentctl/output/attack_nav_output.py,sha256=cbQNZkcNCKaQm7Ht70_tcmTvixtsuVDjQB4BpZ8s-Ts,2489
93
+ contentctl/output/attack_nav_writer.py,sha256=AiQU3q8hzz_lJECI-sjyqOsWx64HUugg3aAHEeZl-qM,2750
94
94
  contentctl/output/conf_output.py,sha256=2_ofRqMro4xmFzf6ZmPRDd93pCG-LQhOiB_kE4owADc,10609
95
95
  contentctl/output/conf_writer.py,sha256=9eqt2tm1xjs397pwWLz5oPJcMHbs62ejRG7KghGQQCI,15137
96
96
  contentctl/output/data_source_writer.py,sha256=hjr0b5zfJ2UHcDLbCkmTrqma1ngu8F5vWFPJEwOZwU8,1860
@@ -124,7 +124,7 @@ contentctl/output/templates/savedsearches_baselines.j2,sha256=WHZB4e0vmeym8832Vx
124
124
  contentctl/output/templates/savedsearches_detections.j2,sha256=B7_b8aBjEKAV7mJm0DxUS_HyCt63bQZtpacCQfDgDqc,6033
125
125
  contentctl/output/templates/savedsearches_investigations.j2,sha256=KH2r8SgyAMiettSHypSbA2-1XmQ_8_8xzk3BkbZ1Re4,1196
126
126
  contentctl/output/templates/server.conf.j2,sha256=sPZUkiuJNGm9R8rpjfRKyuAvmmQb0C4w9Q6hpmvmPeU,127
127
- contentctl/output/templates/transforms.j2,sha256=mM9vIIGqxlhLk1W8sHoUgppfK1FO3ESQD1WCVAewhBw,1386
127
+ contentctl/output/templates/transforms.j2,sha256=TEKZi8DWpcCysRTNvuLEgAwx-g1SZ2E0CkLiu6v6AlU,1339
128
128
  contentctl/output/templates/workflow_actions.j2,sha256=DFoZVnCa8dMRHjW2AdpoydBC0THgiH_W-Nx7WI4-uR4,925
129
129
  contentctl/output/yml_writer.py,sha256=gGgbamHWunHKjj47TcqB04k0xliX6w3H7iajZtUZRSU,2124
130
130
  contentctl/templates/README.md,sha256=GoRmywUqwnjaehY_GLmGqxsFXCLP9plpDYwB6W6nVPs,428
@@ -161,8 +161,8 @@ contentctl/templates/detections/web/.gitkeep,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRk
161
161
  contentctl/templates/macros/security_content_ctime.yml,sha256=Gg1YNllHVsX_YB716H1SJLWzxXZEfuJlnsgB2fuyoHU,159
162
162
  contentctl/templates/macros/security_content_summariesonly.yml,sha256=9BYUxAl2E4Nwh8K19F3AJS8Ka7ceO6ZDBjFiO3l3LY0,162
163
163
  contentctl/templates/stories/cobalt_strike.yml,sha256=uj8idtDNOAIqpZ9p8usQg6mop1CQkJ5TlB4Q7CJdTIE,3082
164
- contentctl-5.1.0.dist-info/LICENSE.md,sha256=hQWUayRk-pAiOZbZnuy8djmoZkjKBx8MrCFpW-JiOgo,11344
165
- contentctl-5.1.0.dist-info/METADATA,sha256=F4a_TCVzB6ayCKCjSNSaX0cPIaZl12-DoLClq1xliNc,5097
166
- contentctl-5.1.0.dist-info/WHEEL,sha256=XbeZDeTWKc1w7CSIyre5aMDU_-PohRwTQceYnisIYYY,88
167
- contentctl-5.1.0.dist-info/entry_points.txt,sha256=5bjZ2NkbQfSwK47uOnA77yCtjgXhvgxnmCQiynRF_-U,57
168
- contentctl-5.1.0.dist-info/RECORD,,
164
+ contentctl-5.2.0.dist-info/LICENSE.md,sha256=hQWUayRk-pAiOZbZnuy8djmoZkjKBx8MrCFpW-JiOgo,11344
165
+ contentctl-5.2.0.dist-info/METADATA,sha256=SxtNJ4XnhZzf7fFzZR3NWsl7hPwh3gX1lqwRTzQFaGc,5097
166
+ contentctl-5.2.0.dist-info/WHEEL,sha256=XbeZDeTWKc1w7CSIyre5aMDU_-PohRwTQceYnisIYYY,88
167
+ contentctl-5.2.0.dist-info/entry_points.txt,sha256=5bjZ2NkbQfSwK47uOnA77yCtjgXhvgxnmCQiynRF_-U,57
168
+ contentctl-5.2.0.dist-info/RECORD,,