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.
- contentctl/actions/build.py +89 -0
- contentctl/actions/detection_testing/DetectionTestingManager.py +48 -49
- contentctl/actions/detection_testing/GitService.py +148 -230
- contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructure.py +14 -24
- contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructureContainer.py +43 -17
- contentctl/actions/detection_testing/views/DetectionTestingView.py +3 -2
- contentctl/actions/detection_testing/views/DetectionTestingViewFile.py +0 -8
- contentctl/actions/doc_gen.py +1 -1
- contentctl/actions/initialize.py +28 -65
- contentctl/actions/inspect.py +260 -0
- contentctl/actions/new_content.py +106 -13
- contentctl/actions/release_notes.py +168 -144
- contentctl/actions/reporting.py +24 -13
- contentctl/actions/test.py +39 -20
- contentctl/actions/validate.py +25 -48
- contentctl/contentctl.py +196 -754
- contentctl/enrichments/attack_enrichment.py +69 -19
- contentctl/enrichments/cve_enrichment.py +28 -13
- contentctl/helper/link_validator.py +24 -26
- contentctl/helper/utils.py +7 -3
- contentctl/input/director.py +139 -201
- contentctl/input/new_content_questions.py +63 -61
- contentctl/input/sigma_converter.py +1 -2
- contentctl/input/ssa_detection_builder.py +16 -7
- contentctl/input/yml_reader.py +4 -3
- contentctl/objects/abstract_security_content_objects/detection_abstract.py +487 -154
- contentctl/objects/abstract_security_content_objects/security_content_object_abstract.py +155 -51
- contentctl/objects/alert_action.py +40 -0
- contentctl/objects/atomic.py +212 -0
- contentctl/objects/baseline.py +44 -43
- contentctl/objects/baseline_tags.py +69 -20
- contentctl/objects/config.py +857 -125
- contentctl/objects/constants.py +0 -1
- contentctl/objects/correlation_search.py +1 -1
- contentctl/objects/data_source.py +2 -4
- contentctl/objects/deployment.py +61 -21
- contentctl/objects/deployment_email.py +2 -2
- contentctl/objects/deployment_notable.py +4 -4
- contentctl/objects/deployment_phantom.py +2 -2
- contentctl/objects/deployment_rba.py +3 -4
- contentctl/objects/deployment_scheduling.py +2 -3
- contentctl/objects/deployment_slack.py +2 -2
- contentctl/objects/detection.py +1 -5
- contentctl/objects/detection_tags.py +210 -119
- contentctl/objects/enums.py +312 -24
- contentctl/objects/integration_test.py +1 -1
- contentctl/objects/integration_test_result.py +0 -2
- contentctl/objects/investigation.py +62 -53
- contentctl/objects/investigation_tags.py +30 -6
- contentctl/objects/lookup.py +80 -31
- contentctl/objects/macro.py +29 -45
- contentctl/objects/mitre_attack_enrichment.py +29 -5
- contentctl/objects/observable.py +3 -7
- contentctl/objects/playbook.py +60 -30
- contentctl/objects/playbook_tags.py +45 -8
- contentctl/objects/security_content_object.py +1 -5
- contentctl/objects/ssa_detection.py +8 -4
- contentctl/objects/ssa_detection_tags.py +19 -26
- contentctl/objects/story.py +142 -44
- contentctl/objects/story_tags.py +46 -33
- contentctl/objects/unit_test.py +7 -2
- contentctl/objects/unit_test_attack_data.py +10 -19
- contentctl/objects/unit_test_baseline.py +1 -1
- contentctl/objects/unit_test_old.py +4 -3
- contentctl/objects/unit_test_result.py +5 -3
- contentctl/objects/unit_test_ssa.py +31 -0
- contentctl/output/api_json_output.py +202 -130
- contentctl/output/attack_nav_output.py +20 -9
- contentctl/output/attack_nav_writer.py +3 -3
- contentctl/output/ba_yml_output.py +3 -3
- contentctl/output/conf_output.py +125 -391
- contentctl/output/conf_writer.py +169 -31
- contentctl/output/jinja_writer.py +2 -2
- contentctl/output/json_writer.py +17 -5
- contentctl/output/new_content_yml_output.py +8 -7
- contentctl/output/svg_output.py +17 -27
- contentctl/output/templates/analyticstories_detections.j2 +8 -4
- contentctl/output/templates/analyticstories_investigations.j2 +1 -1
- contentctl/output/templates/analyticstories_stories.j2 +6 -6
- contentctl/output/templates/app.conf.j2 +2 -2
- contentctl/output/templates/app.manifest.j2 +2 -2
- contentctl/output/templates/detection_coverage.j2 +6 -8
- contentctl/output/templates/doc_detection_page.j2 +2 -2
- contentctl/output/templates/doc_detections.j2 +2 -2
- contentctl/output/templates/doc_stories.j2 +1 -1
- contentctl/output/templates/es_investigations_investigations.j2 +1 -1
- contentctl/output/templates/es_investigations_stories.j2 +1 -1
- contentctl/output/templates/header.j2 +2 -1
- contentctl/output/templates/macros.j2 +6 -10
- contentctl/output/templates/savedsearches_baselines.j2 +5 -5
- contentctl/output/templates/savedsearches_detections.j2 +36 -33
- contentctl/output/templates/savedsearches_investigations.j2 +4 -4
- contentctl/output/templates/transforms.j2 +4 -4
- contentctl/output/yml_writer.py +2 -2
- contentctl/templates/app_template/README.md +7 -0
- contentctl/{output/templates/splunk_app → templates/app_template}/default/data/ui/nav/default.xml +1 -0
- contentctl/templates/app_template/lookups/mitre_enrichment.csv +638 -0
- contentctl/templates/deployments/{00_default_anomaly.yml → escu_default_configuration_anomaly.yml} +1 -2
- contentctl/templates/deployments/{00_default_baseline.yml → escu_default_configuration_baseline.yml} +1 -2
- contentctl/templates/deployments/{00_default_correlation.yml → escu_default_configuration_correlation.yml} +2 -2
- contentctl/templates/deployments/{00_default_hunting.yml → escu_default_configuration_hunting.yml} +2 -2
- contentctl/templates/deployments/{00_default_ttp.yml → escu_default_configuration_ttp.yml} +1 -2
- contentctl/templates/detections/anomalous_usage_of_7zip.yml +0 -1
- contentctl/templates/stories/cobalt_strike.yml +0 -1
- {contentctl-3.6.0.dist-info → contentctl-4.0.2.dist-info}/METADATA +36 -15
- contentctl-4.0.2.dist-info/RECORD +168 -0
- contentctl/actions/detection_testing/DataManipulation.py +0 -149
- contentctl/actions/generate.py +0 -91
- contentctl/helper/config_handler.py +0 -75
- contentctl/input/baseline_builder.py +0 -66
- contentctl/input/basic_builder.py +0 -58
- contentctl/input/detection_builder.py +0 -370
- contentctl/input/investigation_builder.py +0 -42
- contentctl/input/new_content_generator.py +0 -95
- contentctl/input/playbook_builder.py +0 -68
- contentctl/input/story_builder.py +0 -106
- contentctl/objects/app.py +0 -214
- contentctl/objects/repo_config.py +0 -163
- contentctl/objects/test_config.py +0 -630
- contentctl/output/templates/macros_detections.j2 +0 -7
- contentctl/output/templates/splunk_app/README.md +0 -7
- contentctl-3.6.0.dist-info/RECORD +0 -176
- /contentctl/{output/templates/splunk_app → templates/app_template}/README/essoc_story_detail.txt +0 -0
- /contentctl/{output/templates/splunk_app → templates/app_template}/README/essoc_summary.txt +0 -0
- /contentctl/{output/templates/splunk_app → templates/app_template}/README/essoc_usage_dashboard.txt +0 -0
- /contentctl/{output/templates/splunk_app → templates/app_template}/default/analytic_stories.conf +0 -0
- /contentctl/{output/templates/splunk_app → templates/app_template}/default/app.conf +0 -0
- /contentctl/{output/templates/splunk_app → templates/app_template}/default/commands.conf +0 -0
- /contentctl/{output/templates/splunk_app → templates/app_template}/default/content-version.conf +0 -0
- /contentctl/{output/templates/splunk_app → templates/app_template}/default/data/ui/views/escu_summary.xml +0 -0
- /contentctl/{output/templates/splunk_app → templates/app_template}/default/data/ui/views/feedback.xml +0 -0
- /contentctl/{output/templates/splunk_app → templates/app_template}/default/distsearch.conf +0 -0
- /contentctl/{output/templates/splunk_app → templates/app_template}/default/usage_searches.conf +0 -0
- /contentctl/{output/templates/splunk_app → templates/app_template}/default/use_case_library.conf +0 -0
- /contentctl/{output/templates/splunk_app → templates/app_template}/metadata/default.meta +0 -0
- /contentctl/{output/templates/splunk_app → templates/app_template}/static/appIcon.png +0 -0
- /contentctl/{output/templates/splunk_app → templates/app_template}/static/appIconAlt.png +0 -0
- /contentctl/{output/templates/splunk_app → templates/app_template}/static/appIconAlt_2x.png +0 -0
- /contentctl/{output/templates/splunk_app → templates/app_template}/static/appIcon_2x.png +0 -0
- {contentctl-3.6.0.dist-info → contentctl-4.0.2.dist-info}/LICENSE.md +0 -0
- {contentctl-3.6.0.dist-info → contentctl-4.0.2.dist-info}/WHEEL +0 -0
- {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
|
-
|
|
15
|
-
|
|
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{'
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
44
|
-
|
|
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['
|
|
93
|
+
if (relationship['target_object'] == technique['id']) and relationship['source_object'].startswith('intrusion-set'):
|
|
50
94
|
for group in enterprise_groups:
|
|
51
|
-
if relationship['
|
|
52
|
-
apt_groups.append(group['
|
|
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
|
-
|
|
60
|
-
|
|
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('
|
|
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
|
|
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(
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
|
2
|
-
from
|
|
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
|
-
@
|
|
46
|
-
def check_reference(cls,
|
|
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 =
|
|
50
|
-
reference =
|
|
51
|
-
timeout_seconds =
|
|
52
|
-
headers =
|
|
53
|
-
allow_redirects =
|
|
54
|
-
verify_ssl =
|
|
55
|
-
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
|
-
|
|
65
|
-
|
|
62
|
+
data['status_code'] = get.status_code
|
|
63
|
+
data['resolution_time'] = resolution_time
|
|
66
64
|
if reference != get.url:
|
|
67
|
-
|
|
65
|
+
data['redirect'] = get.url
|
|
68
66
|
else:
|
|
69
|
-
|
|
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
|
-
|
|
71
|
+
data['valid'] = True
|
|
74
72
|
else:
|
|
75
73
|
#print(f"Unacceptable HTTP Status Code {get.status_code} received for {reference}")
|
|
76
|
-
|
|
77
|
-
return
|
|
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
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
return
|
|
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):
|
contentctl/helper/utils.py
CHANGED
|
@@ -6,13 +6,17 @@ import random
|
|
|
6
6
|
import string
|
|
7
7
|
from timeit import default_timer
|
|
8
8
|
import pathlib
|
|
9
|
-
|
|
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
|
|