contentctl 3.6.0__py3-none-any.whl → 4.0.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.
Files changed (142) hide show
  1. contentctl/actions/build.py +89 -0
  2. contentctl/actions/detection_testing/DetectionTestingManager.py +48 -49
  3. contentctl/actions/detection_testing/GitService.py +148 -230
  4. contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructure.py +14 -24
  5. contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructureContainer.py +43 -17
  6. contentctl/actions/detection_testing/views/DetectionTestingView.py +3 -2
  7. contentctl/actions/detection_testing/views/DetectionTestingViewFile.py +0 -8
  8. contentctl/actions/doc_gen.py +1 -1
  9. contentctl/actions/initialize.py +28 -65
  10. contentctl/actions/inspect.py +260 -0
  11. contentctl/actions/new_content.py +106 -13
  12. contentctl/actions/release_notes.py +168 -144
  13. contentctl/actions/reporting.py +24 -13
  14. contentctl/actions/test.py +39 -20
  15. contentctl/actions/validate.py +25 -48
  16. contentctl/contentctl.py +196 -754
  17. contentctl/enrichments/attack_enrichment.py +69 -19
  18. contentctl/enrichments/cve_enrichment.py +28 -13
  19. contentctl/helper/link_validator.py +24 -26
  20. contentctl/helper/utils.py +7 -3
  21. contentctl/input/director.py +139 -201
  22. contentctl/input/new_content_questions.py +63 -61
  23. contentctl/input/sigma_converter.py +1 -2
  24. contentctl/input/ssa_detection_builder.py +16 -7
  25. contentctl/input/yml_reader.py +4 -3
  26. contentctl/objects/abstract_security_content_objects/detection_abstract.py +487 -154
  27. contentctl/objects/abstract_security_content_objects/security_content_object_abstract.py +155 -51
  28. contentctl/objects/alert_action.py +40 -0
  29. contentctl/objects/atomic.py +212 -0
  30. contentctl/objects/baseline.py +44 -43
  31. contentctl/objects/baseline_tags.py +69 -20
  32. contentctl/objects/config.py +857 -125
  33. contentctl/objects/constants.py +0 -1
  34. contentctl/objects/correlation_search.py +1 -1
  35. contentctl/objects/data_source.py +2 -4
  36. contentctl/objects/deployment.py +61 -21
  37. contentctl/objects/deployment_email.py +2 -2
  38. contentctl/objects/deployment_notable.py +4 -4
  39. contentctl/objects/deployment_phantom.py +2 -2
  40. contentctl/objects/deployment_rba.py +3 -4
  41. contentctl/objects/deployment_scheduling.py +2 -3
  42. contentctl/objects/deployment_slack.py +2 -2
  43. contentctl/objects/detection.py +1 -5
  44. contentctl/objects/detection_tags.py +210 -119
  45. contentctl/objects/enums.py +312 -24
  46. contentctl/objects/integration_test.py +1 -1
  47. contentctl/objects/integration_test_result.py +0 -2
  48. contentctl/objects/investigation.py +62 -53
  49. contentctl/objects/investigation_tags.py +30 -6
  50. contentctl/objects/lookup.py +80 -31
  51. contentctl/objects/macro.py +29 -45
  52. contentctl/objects/mitre_attack_enrichment.py +29 -5
  53. contentctl/objects/observable.py +3 -7
  54. contentctl/objects/playbook.py +60 -30
  55. contentctl/objects/playbook_tags.py +45 -8
  56. contentctl/objects/security_content_object.py +1 -5
  57. contentctl/objects/ssa_detection.py +8 -4
  58. contentctl/objects/ssa_detection_tags.py +19 -26
  59. contentctl/objects/story.py +142 -44
  60. contentctl/objects/story_tags.py +46 -33
  61. contentctl/objects/unit_test.py +7 -2
  62. contentctl/objects/unit_test_attack_data.py +10 -19
  63. contentctl/objects/unit_test_baseline.py +1 -1
  64. contentctl/objects/unit_test_old.py +4 -3
  65. contentctl/objects/unit_test_result.py +5 -3
  66. contentctl/objects/unit_test_ssa.py +31 -0
  67. contentctl/output/api_json_output.py +202 -130
  68. contentctl/output/attack_nav_output.py +20 -9
  69. contentctl/output/attack_nav_writer.py +3 -3
  70. contentctl/output/ba_yml_output.py +3 -3
  71. contentctl/output/conf_output.py +125 -391
  72. contentctl/output/conf_writer.py +169 -31
  73. contentctl/output/jinja_writer.py +2 -2
  74. contentctl/output/json_writer.py +17 -5
  75. contentctl/output/new_content_yml_output.py +8 -7
  76. contentctl/output/svg_output.py +17 -27
  77. contentctl/output/templates/analyticstories_detections.j2 +8 -4
  78. contentctl/output/templates/analyticstories_investigations.j2 +1 -1
  79. contentctl/output/templates/analyticstories_stories.j2 +6 -6
  80. contentctl/output/templates/app.conf.j2 +2 -2
  81. contentctl/output/templates/app.manifest.j2 +2 -2
  82. contentctl/output/templates/detection_coverage.j2 +6 -8
  83. contentctl/output/templates/doc_detection_page.j2 +2 -2
  84. contentctl/output/templates/doc_detections.j2 +2 -2
  85. contentctl/output/templates/doc_stories.j2 +1 -1
  86. contentctl/output/templates/es_investigations_investigations.j2 +1 -1
  87. contentctl/output/templates/es_investigations_stories.j2 +1 -1
  88. contentctl/output/templates/header.j2 +2 -1
  89. contentctl/output/templates/macros.j2 +6 -10
  90. contentctl/output/templates/savedsearches_baselines.j2 +5 -5
  91. contentctl/output/templates/savedsearches_detections.j2 +36 -33
  92. contentctl/output/templates/savedsearches_investigations.j2 +4 -4
  93. contentctl/output/templates/transforms.j2 +4 -4
  94. contentctl/output/yml_writer.py +2 -2
  95. contentctl/templates/app_template/README.md +7 -0
  96. contentctl/{output/templates/splunk_app → templates/app_template}/default/data/ui/nav/default.xml +1 -0
  97. contentctl/templates/app_template/lookups/mitre_enrichment.csv +638 -0
  98. contentctl/templates/deployments/{00_default_anomaly.yml → escu_default_configuration_anomaly.yml} +1 -2
  99. contentctl/templates/deployments/{00_default_baseline.yml → escu_default_configuration_baseline.yml} +1 -2
  100. contentctl/templates/deployments/{00_default_correlation.yml → escu_default_configuration_correlation.yml} +2 -2
  101. contentctl/templates/deployments/{00_default_hunting.yml → escu_default_configuration_hunting.yml} +2 -2
  102. contentctl/templates/deployments/{00_default_ttp.yml → escu_default_configuration_ttp.yml} +1 -2
  103. contentctl/templates/detections/anomalous_usage_of_7zip.yml +0 -1
  104. contentctl/templates/stories/cobalt_strike.yml +0 -1
  105. {contentctl-3.6.0.dist-info → contentctl-4.0.2.dist-info}/METADATA +36 -15
  106. contentctl-4.0.2.dist-info/RECORD +168 -0
  107. contentctl/actions/detection_testing/DataManipulation.py +0 -149
  108. contentctl/actions/generate.py +0 -91
  109. contentctl/helper/config_handler.py +0 -75
  110. contentctl/input/baseline_builder.py +0 -66
  111. contentctl/input/basic_builder.py +0 -58
  112. contentctl/input/detection_builder.py +0 -370
  113. contentctl/input/investigation_builder.py +0 -42
  114. contentctl/input/new_content_generator.py +0 -95
  115. contentctl/input/playbook_builder.py +0 -68
  116. contentctl/input/story_builder.py +0 -106
  117. contentctl/objects/app.py +0 -214
  118. contentctl/objects/repo_config.py +0 -163
  119. contentctl/objects/test_config.py +0 -630
  120. contentctl/output/templates/macros_detections.j2 +0 -7
  121. contentctl/output/templates/splunk_app/README.md +0 -7
  122. contentctl-3.6.0.dist-info/RECORD +0 -176
  123. /contentctl/{output/templates/splunk_app → templates/app_template}/README/essoc_story_detail.txt +0 -0
  124. /contentctl/{output/templates/splunk_app → templates/app_template}/README/essoc_summary.txt +0 -0
  125. /contentctl/{output/templates/splunk_app → templates/app_template}/README/essoc_usage_dashboard.txt +0 -0
  126. /contentctl/{output/templates/splunk_app → templates/app_template}/default/analytic_stories.conf +0 -0
  127. /contentctl/{output/templates/splunk_app → templates/app_template}/default/app.conf +0 -0
  128. /contentctl/{output/templates/splunk_app → templates/app_template}/default/commands.conf +0 -0
  129. /contentctl/{output/templates/splunk_app → templates/app_template}/default/content-version.conf +0 -0
  130. /contentctl/{output/templates/splunk_app → templates/app_template}/default/data/ui/views/escu_summary.xml +0 -0
  131. /contentctl/{output/templates/splunk_app → templates/app_template}/default/data/ui/views/feedback.xml +0 -0
  132. /contentctl/{output/templates/splunk_app → templates/app_template}/default/distsearch.conf +0 -0
  133. /contentctl/{output/templates/splunk_app → templates/app_template}/default/usage_searches.conf +0 -0
  134. /contentctl/{output/templates/splunk_app → templates/app_template}/default/use_case_library.conf +0 -0
  135. /contentctl/{output/templates/splunk_app → templates/app_template}/metadata/default.meta +0 -0
  136. /contentctl/{output/templates/splunk_app → templates/app_template}/static/appIcon.png +0 -0
  137. /contentctl/{output/templates/splunk_app → templates/app_template}/static/appIconAlt.png +0 -0
  138. /contentctl/{output/templates/splunk_app → templates/app_template}/static/appIconAlt_2x.png +0 -0
  139. /contentctl/{output/templates/splunk_app → templates/app_template}/static/appIcon_2x.png +0 -0
  140. {contentctl-3.6.0.dist-info → contentctl-4.0.2.dist-info}/LICENSE.md +0 -0
  141. {contentctl-3.6.0.dist-info → contentctl-4.0.2.dist-info}/WHEEL +0 -0
  142. {contentctl-3.6.0.dist-info → contentctl-4.0.2.dist-info}/entry_points.txt +0 -0
@@ -1,4 +1,5 @@
1
1
 
2
+ from __future__ import annotations
2
3
  import csv
3
4
  import os
4
5
  from posixpath import split
@@ -6,16 +7,57 @@ from typing import Optional
6
7
  import sys
7
8
  from attackcti import attack_client
8
9
  import logging
10
+ from pydantic import BaseModel, Field
11
+ from dataclasses import field
12
+ from typing import Union,Annotated
13
+ from contentctl.objects.mitre_attack_enrichment import MitreAttackEnrichment
14
+ from contentctl.objects.config import validate
9
15
  logging.getLogger('taxii2client').setLevel(logging.CRITICAL)
10
16
 
11
17
 
12
- class AttackEnrichment():
18
+ class AttackEnrichment(BaseModel):
19
+ data: dict[str, MitreAttackEnrichment] = field(default_factory=dict)
20
+ use_enrichment:bool = True
21
+
22
+ @staticmethod
23
+ def getAttackEnrichment(config:validate)->AttackEnrichment:
24
+ enrichment = AttackEnrichment(use_enrichment=config.enrichments)
25
+ _ = enrichment.get_attack_lookup(str(config.path))
26
+ return enrichment
27
+
28
+ def getEnrichmentByMitreID(self, mitre_id:Annotated[str, Field(pattern="^T\d{4}(.\d{3})?$")])->Union[MitreAttackEnrichment,None]:
29
+ if not self.use_enrichment:
30
+ return None
31
+
32
+ enrichment = self.data.get(mitre_id, None)
33
+ if enrichment is not None:
34
+ return enrichment
35
+ else:
36
+ raise ValueError(f"Error, Unable to find Mitre Enrichment for MitreID {mitre_id}")
37
+
13
38
 
14
- @classmethod
15
- def get_attack_lookup(self, input_path: str, store_csv = False, force_cached_or_offline: bool = False, skip_enrichment:bool = False) -> dict:
39
+ def addMitreID(self, technique:dict, tactics:list[str], groups:list[str])->None:
40
+
41
+ technique_id = technique['technique_id']
42
+ technique_obj = technique['technique']
43
+ tactics.sort()
44
+ groups.sort()
45
+
46
+ if technique_id in self.data:
47
+ raise ValueError(f"Error, trying to redefine MITRE ID '{technique_id}'")
48
+
49
+ self.data[technique_id] = MitreAttackEnrichment(mitre_attack_id=technique_id,
50
+ mitre_attack_technique=technique_obj,
51
+ mitre_attack_tactics=tactics,
52
+ mitre_attack_groups=groups)
53
+
54
+
55
+ def get_attack_lookup(self, input_path: str, store_csv: bool = False, force_cached_or_offline: bool = False, skip_enrichment:bool = False) -> dict:
56
+ if self.use_enrichment is False:
57
+ return {}
16
58
  print("Getting MITRE Attack Enrichment Data. This may take some time...")
17
59
  attack_lookup = dict()
18
- file_path = os.path.join(input_path, "lookups", "mitre_enrichment.csv")
60
+ file_path = os.path.join(input_path, "app_template", "lookups", "mitre_enrichment.csv")
19
61
 
20
62
  if skip_enrichment is True:
21
63
  print("Skipping enrichment")
@@ -28,36 +70,38 @@ class AttackEnrichment():
28
70
  lift = attack_client()
29
71
  print(f"\r{'Client'.rjust(23)}: [{100:3.0f}%]...Done!", end="\n", flush=True)
30
72
 
31
- print(f"\r{'Enterprise'.rjust(23)}: [{0.0:3.0f}%]...", end="", flush=True)
32
- all_enterprise = lift.get_enterprise(stix_format=False)
33
- print(f"\r{'Enterprise'.rjust(23)}: [{100:3.0f}%]...Done!", end="\n", flush=True)
73
+ print(f"\r{'Techniques'.rjust(23)}: [{0.0:3.0f}%]...", end="", flush=True)
74
+ all_enterprise_techniques = lift.get_enterprise_techniques(stix_format=False)
75
+
76
+ print(f"\r{'Techniques'.rjust(23)}: [{100:3.0f}%]...Done!", end="\n", flush=True)
34
77
 
35
78
  print(f"\r{'Relationships'.rjust(23)}: [{0.0:3.0f}%]...", end="", flush=True)
36
- enterprise_relationships = lift.get_enterprise_relationships()
79
+ enterprise_relationships = lift.get_enterprise_relationships(stix_format=False)
37
80
  print(f"\r{'Relationships'.rjust(23)}: [{100:3.0f}%]...Done!", end="\n", flush=True)
38
81
 
39
82
  print(f"\r{'Groups'.rjust(23)}: [{0:3.0f}%]...", end="", flush=True)
40
- enterprise_groups = lift.get_enterprise_groups()
83
+ enterprise_groups = lift.get_enterprise_groups(stix_format=False)
41
84
  print(f"\r{'Groups'.rjust(23)}: [{100:3.0f}%]...Done!", end="\n", flush=True)
42
85
 
43
- for index, technique in enumerate(all_enterprise['techniques']):
44
- progress_percent = ((index+1)/len(all_enterprise['techniques'])) * 100
86
+
87
+ for index, technique in enumerate(all_enterprise_techniques):
88
+ progress_percent = ((index+1)/len(all_enterprise_techniques)) * 100
45
89
  if (sys.stdout.isatty() and sys.stdin.isatty() and sys.stderr.isatty()):
46
90
  print(f"\r\t{'MITRE Technique Progress'.rjust(23)}: [{progress_percent:3.0f}%]...", end="", flush=True)
47
91
  apt_groups = []
48
92
  for relationship in enterprise_relationships:
49
- if (relationship['target_ref'] == technique['id']) and relationship['source_ref'].startswith('intrusion-set'):
93
+ if (relationship['target_object'] == technique['id']) and relationship['source_object'].startswith('intrusion-set'):
50
94
  for group in enterprise_groups:
51
- if relationship['source_ref'] == group['id']:
52
- apt_groups.append(group['name'])
95
+ if relationship['source_object'] == group['id']:
96
+ apt_groups.append(group['group'])
53
97
 
54
98
  tactics = []
55
99
  if ('tactic' in technique):
56
100
  for tactic in technique['tactic']:
57
101
  tactics.append(tactic.replace('-',' ').title())
58
102
 
59
- if not ('revoked' in technique):
60
- attack_lookup[technique['technique_id']] = {'technique': technique['technique'], 'tactics': tactics, 'groups': apt_groups}
103
+ self.addMitreID(technique, tactics, apt_groups)
104
+ attack_lookup[technique['technique_id']] = {'technique': technique['technique'], 'tactics': tactics, 'groups': apt_groups}
61
105
 
62
106
  if store_csv:
63
107
  f = open(file_path, 'w')
@@ -79,13 +123,19 @@ class AttackEnrichment():
79
123
  f.close()
80
124
 
81
125
  except Exception as err:
82
- print('Warning: ' + str(err))
83
- print('Use local copy lookups/mitre_enrichment.csv')
84
- dict_from_csv = {}
126
+ print(f'\nError: {str(err)}')
127
+ print('Use local copy app_template/lookups/mitre_enrichment.csv')
85
128
  with open(file_path, mode='r') as inp:
86
129
  reader = csv.reader(inp)
87
130
  attack_lookup = {rows[0]:{'technique': rows[1], 'tactics': rows[2].split('|'), 'groups': rows[3].split('|')} for rows in reader}
88
131
  attack_lookup.pop('mitre_id')
132
+ for key in attack_lookup.keys():
133
+ technique_input = {'technique_id': key , 'technique': attack_lookup[key]['technique'] }
134
+ tactics_input = attack_lookup[key]['tactics']
135
+ groups_input = attack_lookup[key]['groups']
136
+ self.addMitreID(technique=technique_input, tactics=tactics_input, groups=groups_input)
137
+
138
+
89
139
 
90
140
  print("Done!")
91
141
  return attack_lookup
@@ -1,10 +1,13 @@
1
-
1
+ from __future__ import annotations
2
2
  from pycvesearch import CVESearch
3
3
  import functools
4
4
  import os
5
5
  import shelve
6
6
  import time
7
- import sys
7
+ from typing import Annotated
8
+ from pydantic import BaseModel,Field,ConfigDict
9
+
10
+ from decimal import Decimal
8
11
  CVESSEARCH_API_URL = 'https://cve.circl.lu'
9
12
 
10
13
  CVE_CACHE_FILENAME = "lookups/CVE_CACHE.db"
@@ -12,7 +15,7 @@ CVE_CACHE_FILENAME = "lookups/CVE_CACHE.db"
12
15
  NON_PERSISTENT_CACHE = {}
13
16
 
14
17
 
15
-
18
+ ''''''
16
19
  @functools.cache
17
20
  def cvesearch_helper(url:str, cve_id:str, force_cached_or_offline:bool=False, max_api_attempts:int=3, retry_sleep_seconds:int=5):
18
21
  if max_api_attempts < 1:
@@ -63,10 +66,22 @@ def cvesearch_id_helper(url:str):
63
66
 
64
67
 
65
68
 
66
- class CveEnrichment():
67
69
 
70
+ class CveEnrichmentObj(BaseModel):
71
+ id:Annotated[str, "^CVE-[1|2][0-9]{3}-[0-9]+$"]
72
+ cvss:Annotated[Decimal, Field(ge=.1, le=10, decimal_places=1)]
73
+ summary:str
74
+
75
+
76
+ @staticmethod
77
+ def buildEnrichmentOnFailure(id:Annotated[str, "^CVE-[1|2][0-9]{3}-[0-9]+$"], errorMessage:str)->CveEnrichmentObj:
78
+ message = f"{errorMessage}. Default CVSS of 5.0 used"
79
+ print(message)
80
+ return CveEnrichmentObj(id=id, cvss=Decimal(5.0), summary=message)
81
+
82
+ class CveEnrichment():
68
83
  @classmethod
69
- def enrich_cve(self, cve_id: str, force_cached_or_offline: bool = False) -> dict:
84
+ def enrich_cve(cls, cve_id: str, force_cached_or_offline: bool = False, treat_failures_as_warnings:bool=True) -> CveEnrichmentObj:
70
85
  cve_enriched = dict()
71
86
  try:
72
87
 
@@ -74,12 +89,12 @@ class CveEnrichment():
74
89
  cve_enriched['id'] = cve_id
75
90
  cve_enriched['cvss'] = result['cvss']
76
91
  cve_enriched['summary'] = result['summary']
77
- except TypeError as TypeErr:
78
- # there was a error calling the circl api lets just empty the object
79
- print("WARNING, issue enriching {0}, with error: {1}".format(cve_id, str(TypeErr)))
80
- cve_enriched = dict()
81
-
82
92
  except Exception as e:
83
- print("WARNING - {0}".format(str(e)))
84
-
85
- return cve_enriched
93
+ message = f"issue enriching {cve_id}, with error: {str(e)}"
94
+ if treat_failures_as_warnings:
95
+ return CveEnrichmentObj.buildEnrichmentOnFailure(id = cve_id, errorMessage=f"WARNING, {message}")
96
+ else:
97
+ raise ValueError(f"ERROR, {message}")
98
+
99
+ return CveEnrichmentObj.model_validate(cve_enriched)
100
+
@@ -1,8 +1,5 @@
1
- import re
2
- from tracemalloc import start
3
- from unittest.mock import DEFAULT
4
- from pydantic import BaseModel, validator, root_validator,Field
5
- from typing import Union, Callable
1
+ from pydantic import BaseModel, model_validator
2
+ from typing import Union, Callable, Any
6
3
  import requests
7
4
  import urllib3, urllib3.exceptions
8
5
  import time
@@ -14,6 +11,7 @@ import shelve
14
11
  DEFAULT_USER_AGENT_STRING = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.4951.41 Safari/537.36"
15
12
  ALLOWED_HTTP_CODES = [200]
16
13
  class LinkStats(BaseModel):
14
+
17
15
  #Static Values
18
16
  method: Callable = requests.get
19
17
  allowed_http_codes: list[int] = ALLOWED_HTTP_CODES
@@ -42,17 +40,17 @@ class LinkStats(BaseModel):
42
40
  self.referencing_files.add(referencing_file)
43
41
  return self.valid
44
42
 
45
- @root_validator
46
- def check_reference(cls, values):
43
+ @model_validator(mode="before")
44
+ def check_reference(cls, data:Any)->Any:
47
45
  start_time = time.time()
48
46
  #Get out all the fields names to make them easier to reference
49
- method = values['method']
50
- reference = values['reference']
51
- timeout_seconds = values['timeout_seconds']
52
- headers = values['headers']
53
- allow_redirects = values['allow_redirects']
54
- verify_ssl = values['verify_ssl']
55
- allowed_http_codes = values['allowed_http_codes']
47
+ method = data['method']
48
+ reference = data['reference']
49
+ timeout_seconds = data['timeout_seconds']
50
+ headers = data['headers']
51
+ allow_redirects = data['allow_redirects']
52
+ verify_ssl = data['verify_ssl']
53
+ allowed_http_codes = data['allowed_http_codes']
56
54
  if not (reference.startswith("http://") or reference.startswith("https://")):
57
55
  raise(ValueError(f"Reference {reference} does not begin with http(s). Only http(s) references are supported"))
58
56
 
@@ -61,29 +59,29 @@ class LinkStats(BaseModel):
61
59
  headers = headers,
62
60
  allow_redirects=allow_redirects, verify=verify_ssl)
63
61
  resolution_time = time.time() - start_time
64
- values['status_code'] = get.status_code
65
- values['resolution_time'] = resolution_time
62
+ data['status_code'] = get.status_code
63
+ data['resolution_time'] = resolution_time
66
64
  if reference != get.url:
67
- values['redirect'] = get.url
65
+ data['redirect'] = get.url
68
66
  else:
69
- values['redirect'] = None #None is also already the default
67
+ data['redirect'] = None #None is also already the default
70
68
 
71
69
  #Returns the updated values and sets them for the object
72
70
  if get.status_code in allowed_http_codes:
73
- values['valid'] = True
71
+ data['valid'] = True
74
72
  else:
75
73
  #print(f"Unacceptable HTTP Status Code {get.status_code} received for {reference}")
76
- values['valid'] = False
77
- return values
74
+ data['valid'] = False
75
+ return data
78
76
 
79
77
  except Exception as e:
80
78
  resolution_time = time.time() - start_time
81
79
  #print(f"Reference {reference} was not reachable after {resolution_time:.2f} seconds")
82
- values['status_code'] = 0
83
- values['valid'] = False
84
- values['redirect'] = None
85
- values['resolution_time'] = resolution_time
86
- return values
80
+ data['status_code'] = 0
81
+ data['valid'] = False
82
+ data['redirect'] = None
83
+ data['resolution_time'] = resolution_time
84
+ return data
87
85
 
88
86
 
89
87
  class LinkValidator(abc.ABC):
@@ -6,13 +6,17 @@ import random
6
6
  import string
7
7
  from timeit import default_timer
8
8
  import pathlib
9
- import datetime
9
+
10
10
  from typing import Union, Tuple
11
- from pydantic import ValidationError
12
11
  import tqdm
13
- from contentctl.objects.security_content_object import SecurityContentObject
14
12
  from math import ceil
15
13
 
14
+ from typing import TYPE_CHECKING
15
+ if TYPE_CHECKING:
16
+ from contentctl.objects.security_content_object import SecurityContentObject
17
+ from contentctl.objects.security_content_object import SecurityContentObject
18
+
19
+
16
20
  TOTAL_BYTES = 0
17
21
  ALWAYS_PULL = True
18
22