contentctl 4.3.1__py3-none-any.whl → 4.3.2__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.
@@ -7,7 +7,7 @@ from attackcti import attack_client
7
7
  import logging
8
8
  from pydantic import BaseModel, Field
9
9
  from dataclasses import field
10
- from typing import Annotated
10
+ from typing import Annotated,Any
11
11
  from contentctl.objects.mitre_attack_enrichment import MitreAttackEnrichment
12
12
  from contentctl.objects.config import validate
13
13
  logging.getLogger('taxii2client').setLevel(logging.CRITICAL)
@@ -33,21 +33,33 @@ class AttackEnrichment(BaseModel):
33
33
  else:
34
34
  raise Exception(f"Error, Unable to find Mitre Enrichment for MitreID {mitre_id}")
35
35
 
36
-
37
- def addMitreID(self, technique:dict, tactics:list[str], groups:list[str])->None:
38
-
36
+ def addMitreIDViaGroupNames(self, technique:dict, tactics:list[str], groupNames:list[str])->None:
39
37
  technique_id = technique['technique_id']
40
38
  technique_obj = technique['technique']
41
39
  tactics.sort()
42
- groups.sort()
43
-
40
+
44
41
  if technique_id in self.data:
45
42
  raise Exception(f"Error, trying to redefine MITRE ID '{technique_id}'")
43
+ self.data[technique_id] = MitreAttackEnrichment(mitre_attack_id=technique_id,
44
+ mitre_attack_technique=technique_obj,
45
+ mitre_attack_tactics=tactics,
46
+ mitre_attack_groups=groupNames,
47
+ mitre_attack_group_objects=[])
48
+
49
+ def addMitreIDViaGroupObjects(self, technique:dict, tactics:list[str], groupObjects:list[dict[str,Any]])->None:
50
+ technique_id = technique['technique_id']
51
+ technique_obj = technique['technique']
52
+ tactics.sort()
46
53
 
54
+ groupNames:list[str] = sorted([group['group'] for group in groupObjects])
55
+
56
+ if technique_id in self.data:
57
+ raise Exception(f"Error, trying to redefine MITRE ID '{technique_id}'")
47
58
  self.data[technique_id] = MitreAttackEnrichment(mitre_attack_id=technique_id,
48
59
  mitre_attack_technique=technique_obj,
49
60
  mitre_attack_tactics=tactics,
50
- mitre_attack_groups=groups)
61
+ mitre_attack_groups=groupNames,
62
+ mitre_attack_group_objects=groupObjects)
51
63
 
52
64
 
53
65
  def get_attack_lookup(self, input_path: str, store_csv: bool = False, force_cached_or_offline: bool = False, skip_enrichment:bool = False) -> dict:
@@ -86,19 +98,20 @@ class AttackEnrichment(BaseModel):
86
98
  progress_percent = ((index+1)/len(all_enterprise_techniques)) * 100
87
99
  if (sys.stdout.isatty() and sys.stdin.isatty() and sys.stderr.isatty()):
88
100
  print(f"\r\t{'MITRE Technique Progress'.rjust(23)}: [{progress_percent:3.0f}%]...", end="", flush=True)
89
- apt_groups = []
101
+ apt_groups:list[dict[str,Any]] = []
90
102
  for relationship in enterprise_relationships:
91
103
  if (relationship['target_object'] == technique['id']) and relationship['source_object'].startswith('intrusion-set'):
92
104
  for group in enterprise_groups:
93
105
  if relationship['source_object'] == group['id']:
94
- apt_groups.append(group['group'])
106
+ apt_groups.append(group)
107
+ #apt_groups.append(group['group'])
95
108
 
96
109
  tactics = []
97
110
  if ('tactic' in technique):
98
111
  for tactic in technique['tactic']:
99
112
  tactics.append(tactic.replace('-',' ').title())
100
113
 
101
- self.addMitreID(technique, tactics, apt_groups)
114
+ self.addMitreIDViaGroupObjects(technique, tactics, apt_groups)
102
115
  attack_lookup[technique['technique_id']] = {'technique': technique['technique'], 'tactics': tactics, 'groups': apt_groups}
103
116
 
104
117
  if store_csv:
@@ -131,7 +144,7 @@ class AttackEnrichment(BaseModel):
131
144
  technique_input = {'technique_id': key , 'technique': attack_lookup[key]['technique'] }
132
145
  tactics_input = attack_lookup[key]['tactics']
133
146
  groups_input = attack_lookup[key]['groups']
134
- self.addMitreID(technique=technique_input, tactics=tactics_input, groups=groups_input)
147
+ self.addMitreIDViaGroupNames(technique=technique_input, tactics=tactics_input, groups=groups_input)
135
148
 
136
149
 
137
150
 
@@ -1,8 +1,8 @@
1
1
  from __future__ import annotations
2
- from pydantic import BaseModel, Field, ConfigDict
2
+ from pydantic import BaseModel, Field, ConfigDict, HttpUrl, field_validator
3
3
  from typing import List, Annotated
4
4
  from enum import StrEnum
5
-
5
+ import datetime
6
6
 
7
7
  class MitreTactics(StrEnum):
8
8
  RECONNAISSANCE = "Reconnaissance"
@@ -21,12 +21,75 @@ class MitreTactics(StrEnum):
21
21
  IMPACT = "Impact"
22
22
 
23
23
 
24
+ class AttackGroupMatrix(StrEnum):
25
+ enterprise_attack = "enterprise-attack"
26
+ ics_attack = "ics-attack"
27
+ mobile_attack = "mobile-attack"
28
+
29
+
30
+ class AttackGroupType(StrEnum):
31
+ intrusion_set = "intrusion-set"
32
+
33
+ class MitreExternalReference(BaseModel):
34
+ model_config = ConfigDict(extra='forbid')
35
+ source_name: str
36
+ external_id: None | str = None
37
+ url: None | HttpUrl = None
38
+ description: None | str = None
39
+
40
+
41
+ class MitreAttackGroup(BaseModel):
42
+ model_config = ConfigDict(extra='forbid')
43
+ contributors: list[str] = []
44
+ created: datetime.datetime
45
+ created_by_ref: str
46
+ external_references: list[MitreExternalReference]
47
+ group: str
48
+ group_aliases: list[str]
49
+ group_description: str
50
+ group_id: str
51
+ id: str
52
+ matrix: list[AttackGroupMatrix]
53
+ mitre_attack_spec_version: None | str
54
+ mitre_version: str
55
+ #assume that if the deprecated field is not present, then the group is not deprecated
56
+ mitre_deprecated: bool
57
+ modified: datetime.datetime
58
+ modified_by_ref: str
59
+ object_marking_refs: list[str]
60
+ type: AttackGroupType
61
+ url: HttpUrl
62
+
63
+
64
+ @field_validator("mitre_deprecated", mode="before")
65
+ def standardize_mitre_deprecated(cls, mitre_deprecated:bool | None) -> bool:
66
+ '''
67
+ For some reason, the API will return either a bool for mitre_deprecated OR
68
+ None. We simplify our typing by converting None to False, and assuming that
69
+ if deprecated is None, then the group is not deprecated.
70
+ '''
71
+ if mitre_deprecated is None:
72
+ return False
73
+ return mitre_deprecated
74
+
75
+ @field_validator("contributors", mode="before")
76
+ def standardize_contributors(cls, contributors:list[str] | None) -> list[str]:
77
+ '''
78
+ For some reason, the API will return either a list of strings for contributors OR
79
+ None. We simplify our typing by converting None to an empty list.
80
+ '''
81
+ if contributors is None:
82
+ return []
83
+ return contributors
84
+
24
85
  class MitreAttackEnrichment(BaseModel):
25
86
  ConfigDict(use_enum_values=True)
26
87
  mitre_attack_id: Annotated[str, Field(pattern=r"^T\d{4}(.\d{3})?$")] = Field(...)
27
88
  mitre_attack_technique: str = Field(...)
28
89
  mitre_attack_tactics: List[MitreTactics] = Field(...)
29
90
  mitre_attack_groups: List[str] = Field(...)
30
-
91
+ #Exclude this field from serialization - it is very large and not useful in JSON objects
92
+ mitre_attack_group_objects: list[MitreAttackGroup] = Field(..., exclude=True)
31
93
  def __hash__(self) -> int:
32
94
  return id(self)
95
+
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: contentctl
3
- Version: 4.3.1
3
+ Version: 4.3.2
4
4
  Summary: Splunk Content Control Tool
5
5
  License: Apache 2.0
6
6
  Author: STRT
@@ -11,20 +11,20 @@ Classifier: Programming Language :: Python :: 3
11
11
  Classifier: Programming Language :: Python :: 3.11
12
12
  Classifier: Programming Language :: Python :: 3.12
13
13
  Requires-Dist: Jinja2 (>=3.1.4,<4.0.0)
14
- Requires-Dist: PyYAML (>=6.0.1,<7.0.0)
15
- Requires-Dist: attackcti (>=0.3.7,<0.5.0)
14
+ Requires-Dist: PyYAML (>=6.0.2,<7.0.0)
15
+ Requires-Dist: attackcti (>=0.4.0,<0.5.0)
16
16
  Requires-Dist: bottle (>=0.12.25,<0.13.0)
17
17
  Requires-Dist: docker (>=7.1.0,<8.0.0)
18
18
  Requires-Dist: gitpython (>=3.1.43,<4.0.0)
19
19
  Requires-Dist: pycvesearch (>=1.2,<2.0)
20
- Requires-Dist: pydantic (>=2.7.1,<3.0.0)
21
- Requires-Dist: pygit2 (>=1.14.1,<2.0.0)
20
+ Requires-Dist: pydantic (>=2.8.2,<3.0.0)
21
+ Requires-Dist: pygit2 (>=1.15.1,<2.0.0)
22
22
  Requires-Dist: questionary (>=2.0.1,<3.0.0)
23
- Requires-Dist: requests (>=2.32.2,<2.33.0)
23
+ Requires-Dist: requests (>=2.32.3,<2.33.0)
24
24
  Requires-Dist: semantic-version (>=2.10.0,<3.0.0)
25
25
  Requires-Dist: setuptools (>=69.5.1,<74.0.0)
26
- Requires-Dist: splunk-sdk (>=2.0.1,<3.0.0)
27
- Requires-Dist: tqdm (>=4.66.4,<5.0.0)
26
+ Requires-Dist: splunk-sdk (>=2.0.2,<3.0.0)
27
+ Requires-Dist: tqdm (>=4.66.5,<5.0.0)
28
28
  Requires-Dist: tyro (>=0.8.3,<0.9.0)
29
29
  Requires-Dist: xmltodict (>=0.13.0,<0.14.0)
30
30
  Description-Content-Type: text/markdown
@@ -23,7 +23,7 @@ contentctl/actions/test.py,sha256=dx7f750_MrlvysxOmOdIro1bH0iVKF4K54TSwhvU2MU,51
23
23
  contentctl/actions/validate.py,sha256=2MQ8yumCKj7zD8iUuA5gfFEMcE-GPRzYqkvuOexn0JA,5633
24
24
  contentctl/api.py,sha256=FBOpRhbBCBdjORmwe_8MPQ3PRZ6T0KrrFcfKovVFkug,6343
25
25
  contentctl/contentctl.py,sha256=SxWFMYquSYQAATrTBpvfj4j5DRedsOF2xO96ASs74wA,10505
26
- contentctl/enrichments/attack_enrichment.py,sha256=dVwXcULSeZJuQbeTlPpKDyEB9Y6uCy0UGWI83gPLTI0,6735
26
+ contentctl/enrichments/attack_enrichment.py,sha256=HsfHfcrRmsHT6pILN457jmCGOCdAhOlRBGfAP8aZY78,7834
27
27
  contentctl/enrichments/cve_enrichment.py,sha256=SjiytaZktVNbfICXcZ2vZzBiQpOkug5taPtiJK-S1OE,2313
28
28
  contentctl/enrichments/splunk_app_enrichment.py,sha256=zDNHFLZTi2dJ1gdnh0sHkD6F1VtkblqFnhacFcCMBfc,3418
29
29
  contentctl/helper/link_validator.py,sha256=-XorhxfGtjLynEL1X4hcpRMiyemogf2JEnvLwhHq80c,7139
@@ -63,7 +63,7 @@ contentctl/objects/investigation.py,sha256=JRoZxc_qi1fu_VFTRaxOc3B7zzSzCfEURsNzW
63
63
  contentctl/objects/investigation_tags.py,sha256=nFpMRKBVBsW21YW_vy2G1lXaSARX-kfFyrPoCyE77Q8,1280
64
64
  contentctl/objects/lookup.py,sha256=oZwBiHfRRrv2ZXdGyWIJWSWZMpuUbsXydaDDfpenk-4,7219
65
65
  contentctl/objects/macro.py,sha256=9nE-bxkFhtaltHOUCr0luU8jCCthmglHjhKs6Q2YzLU,2684
66
- contentctl/objects/mitre_attack_enrichment.py,sha256=JqSDnKF0-ZTaxUgvhdYNzIAt-7kNaEBvGr_5Bbfdwr8,1072
66
+ contentctl/objects/mitre_attack_enrichment.py,sha256=vhoB0oHlmPs-aDcEYT19BWrylO_hiDxOo5IWP4LBlNk,3293
67
67
  contentctl/objects/notable_action.py,sha256=ValkblBaG-60TF19y_vSnNzoNZ3eg48wIfr0qZxyKTA,1605
68
68
  contentctl/objects/notable_event.py,sha256=ITcwLzeatSGpe8267PYN-EhgqOSoWTfciCBVu8zjOXE,682
69
69
  contentctl/objects/observable.py,sha256=pw0Ehi_KMb7nXzw2kuw1FnCknpD8zDkCAqBTa-M_F28,1313
@@ -163,8 +163,8 @@ contentctl/templates/detections/web/.gitkeep,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRk
163
163
  contentctl/templates/macros/security_content_ctime.yml,sha256=Gg1YNllHVsX_YB716H1SJLWzxXZEfuJlnsgB2fuyoHU,159
164
164
  contentctl/templates/macros/security_content_summariesonly.yml,sha256=9BYUxAl2E4Nwh8K19F3AJS8Ka7ceO6ZDBjFiO3l3LY0,162
165
165
  contentctl/templates/stories/cobalt_strike.yml,sha256=rlaXxMN-5k8LnKBLPafBoksyMtlmsPMHPJOjTiMiZ-M,3063
166
- contentctl-4.3.1.dist-info/LICENSE.md,sha256=hQWUayRk-pAiOZbZnuy8djmoZkjKBx8MrCFpW-JiOgo,11344
167
- contentctl-4.3.1.dist-info/METADATA,sha256=MAmOisMABa1nqU_QRdevnCbhYfgBWH8N3q441doHiTc,20939
168
- contentctl-4.3.1.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
169
- contentctl-4.3.1.dist-info/entry_points.txt,sha256=5bjZ2NkbQfSwK47uOnA77yCtjgXhvgxnmCQiynRF_-U,57
170
- contentctl-4.3.1.dist-info/RECORD,,
166
+ contentctl-4.3.2.dist-info/LICENSE.md,sha256=hQWUayRk-pAiOZbZnuy8djmoZkjKBx8MrCFpW-JiOgo,11344
167
+ contentctl-4.3.2.dist-info/METADATA,sha256=AkxYjJ2zP-wRhoyOVMq5cGNJDmA8QFRkRy6FQaIKCbY,20939
168
+ contentctl-4.3.2.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
169
+ contentctl-4.3.2.dist-info/entry_points.txt,sha256=5bjZ2NkbQfSwK47uOnA77yCtjgXhvgxnmCQiynRF_-U,57
170
+ contentctl-4.3.2.dist-info/RECORD,,