regscale-cli 6.19.1.0__py3-none-any.whl → 6.20.0.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.
Potentially problematic release.
This version of regscale-cli might be problematic. Click here for more details.
- regscale/__init__.py +1 -1
- regscale/airflow/config.py +2 -0
- regscale/airflow/tasks/groups.py +11 -47
- regscale/core/app/internal/login.py +49 -43
- regscale/core/app/internal/model_editor.py +2 -1
- regscale/dev/code_gen.py +2 -5
- regscale/integrations/commercial/amazon/common.py +5 -4
- regscale/integrations/commercial/aws/scanner.py +3 -2
- regscale/integrations/commercial/synqly/assets.py +20 -0
- regscale/integrations/commercial/synqly/ticketing.py +25 -0
- regscale/integrations/commercial/wizv2/click.py +3 -3
- regscale/integrations/public/fedramp/appendix_parser.py +499 -104
- regscale/integrations/public/fedramp/fedramp_five.py +89 -43
- regscale/integrations/scanner_integration.py +1 -1
- regscale/models/app_models/import_validater.py +2 -0
- regscale/models/integration_models/cisa_kev_data.json +355 -27
- regscale/models/integration_models/flat_file_importer/__init__.py +26 -9
- regscale/models/integration_models/synqly_models/capabilities.json +1 -1
- regscale/models/regscale_models/__init__.py +5 -0
- regscale/models/regscale_models/business_impact_assessment.py +71 -0
- regscale/models/regscale_models/control_implementation.py +15 -0
- regscale/models/regscale_models/master_assessment.py +19 -0
- regscale/models/regscale_models/policy.py +90 -0
- regscale/models/regscale_models/question.py +30 -2
- regscale/models/regscale_models/questionnaire.py +4 -3
- regscale/models/regscale_models/questionnaire_instance.py +37 -14
- regscale/models/regscale_models/rbac.py +0 -1
- regscale/models/regscale_models/regscale_model.py +16 -15
- regscale/models/regscale_models/risk_trend.py +67 -0
- regscale/utils/graphql_client.py +2 -1
- {regscale_cli-6.19.1.0.dist-info → regscale_cli-6.20.0.0.dist-info}/METADATA +130 -71
- {regscale_cli-6.19.1.0.dist-info → regscale_cli-6.20.0.0.dist-info}/RECORD +36 -33
- {regscale_cli-6.19.1.0.dist-info → regscale_cli-6.20.0.0.dist-info}/LICENSE +0 -0
- {regscale_cli-6.19.1.0.dist-info → regscale_cli-6.20.0.0.dist-info}/WHEEL +0 -0
- {regscale_cli-6.19.1.0.dist-info → regscale_cli-6.20.0.0.dist-info}/entry_points.txt +0 -0
- {regscale_cli-6.19.1.0.dist-info → regscale_cli-6.20.0.0.dist-info}/top_level.txt +0 -0
regscale/__init__.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "6.
|
|
1
|
+
__version__ = "6.20.0.0"
|
regscale/airflow/config.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""Provide configurations for Airflow."""
|
|
2
2
|
|
|
3
3
|
from datetime import datetime, timedelta
|
|
4
|
+
from regscale.airflow.tasks.groups import email_on_fail
|
|
4
5
|
|
|
5
6
|
|
|
6
7
|
def yesterday():
|
|
@@ -18,6 +19,7 @@ DEFAULT_ARGS = {
|
|
|
18
19
|
"retries": 2,
|
|
19
20
|
"retry_delay": timedelta(minutes=2),
|
|
20
21
|
"execution_timeout": timedelta(hours=3),
|
|
22
|
+
"on_failure_callback": email_on_fail,
|
|
21
23
|
# 'queue': 'whatever queue we want to implement', # left here for an example
|
|
22
24
|
# 'pool': 'backfill', # another example default arg
|
|
23
25
|
# 'priority_weight': 10, # give this a high priority weight
|
regscale/airflow/tasks/groups.py
CHANGED
|
@@ -2,14 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
from uuid import uuid4
|
|
4
4
|
|
|
5
|
-
from airflow import AirflowException, DAG
|
|
6
|
-
from airflow.models import TaskInstance
|
|
7
5
|
from airflow.operators.python import PythonOperator
|
|
8
|
-
from airflow.utils.db import provide_session
|
|
9
|
-
from airflow.utils.state import State
|
|
10
6
|
from airflow.utils.task_group import TaskGroup
|
|
11
|
-
from airflow.utils.trigger_rule import TriggerRule
|
|
12
7
|
|
|
8
|
+
from airflow import AirflowException, DAG
|
|
13
9
|
from regscale.airflow.hierarchy import AIRFLOW_CLICK_OPERATORS as OPERATORS
|
|
14
10
|
from regscale.airflow.tasks.click import execute_click_command
|
|
15
11
|
|
|
@@ -54,63 +50,31 @@ def setup_task_group(
|
|
|
54
50
|
return setup
|
|
55
51
|
|
|
56
52
|
|
|
57
|
-
|
|
58
|
-
def _email_on_fail(task, execution_date, dag, session=None, **kwargs):
|
|
53
|
+
def email_on_fail(task, **_):
|
|
59
54
|
"""
|
|
60
55
|
Send an email if any of the upstream DAGs failed
|
|
61
56
|
|
|
62
|
-
:param
|
|
63
|
-
:param execution_date: The execution date of the DAG
|
|
64
|
-
:param session: DAG session
|
|
57
|
+
:param task: The task that has failed
|
|
65
58
|
:return: The PythonOperator to send an email if any of the DAG tasks fail
|
|
66
59
|
:rtype: TaskGroup
|
|
67
60
|
"""
|
|
68
61
|
from regscale.core.app.application import Application
|
|
69
62
|
from regscale.models import Email
|
|
70
63
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
TaskInstance.dag_id == dag.dag_id,
|
|
75
|
-
TaskInstance.execution_date == execution_date,
|
|
76
|
-
TaskInstance.task_id.in_(task.upstream_task_ids),
|
|
77
|
-
)
|
|
78
|
-
.all()
|
|
79
|
-
)
|
|
80
|
-
upstream_states = [ti.state for ti in upstream_task_instances]
|
|
81
|
-
fail_this_task = State.FAILED in upstream_states
|
|
82
|
-
dag_config = kwargs["dag_run"].conf
|
|
83
|
-
app = Application(config=dag_config)
|
|
64
|
+
dag = task["dag"]
|
|
65
|
+
config = task["params"]
|
|
66
|
+
app = Application(config=config)
|
|
84
67
|
|
|
85
|
-
if email :=
|
|
68
|
+
if email := config.get("email"):
|
|
86
69
|
email = Email(
|
|
87
70
|
fromEmail="Support@RegScale.com",
|
|
88
71
|
emailSenderId=app.config["userId"],
|
|
89
72
|
to=email,
|
|
90
|
-
subject=f"An Automated Job Has Failed: {
|
|
91
|
-
body=f"{dag.dag_id} with ID: {
|
|
92
|
-
+ f"Please check the logs for more information: {
|
|
73
|
+
subject=f"An Automated Job Has Failed: {config['jobName']} ({config['cadence']})",
|
|
74
|
+
body=f"{dag.dag_id} with ID: {task['run_id']} job has failed. "
|
|
75
|
+
+ f"Please check the logs for more information: {config['job_url']}",
|
|
93
76
|
)
|
|
94
77
|
|
|
95
|
-
if fail_this_task:
|
|
96
78
|
if email.send():
|
|
97
|
-
|
|
79
|
+
app.logger.info(f"Email sent to {email} about {task['run_id']} job failure.")
|
|
98
80
|
raise AirflowException("Failing task because one or more upstream tasks failed.")
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
def email_on_fail_operator(dag, email_tag: str = None) -> PythonOperator:
|
|
102
|
-
"""
|
|
103
|
-
Create a PythonOperator to send an email if any of the DAGs fail
|
|
104
|
-
|
|
105
|
-
:param DAG dag: The DAG to add the operator to
|
|
106
|
-
:param str email_tag: A unique identifier for the task, if not provided one will be generated
|
|
107
|
-
:return: The PythonOperator to send an email if any of the DAG tasks fail
|
|
108
|
-
:rtype: TaskGroup
|
|
109
|
-
"""
|
|
110
|
-
return PythonOperator(
|
|
111
|
-
task_id=generate_name("email", email_tag),
|
|
112
|
-
python_callable=_email_on_fail,
|
|
113
|
-
trigger_rule=TriggerRule.ALL_DONE,
|
|
114
|
-
provide_context=True,
|
|
115
|
-
dag=dag,
|
|
116
|
-
)
|
|
@@ -74,35 +74,47 @@ def login(
|
|
|
74
74
|
else:
|
|
75
75
|
config["domain"] = host or config["domain"]
|
|
76
76
|
token = token or os.getenv("REGSCALE_TOKEN")
|
|
77
|
+
if token is not None:
|
|
78
|
+
token = token if token.startswith("Bearer ") else f"Bearer {token}"
|
|
77
79
|
if config and token:
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
80
|
+
try:
|
|
81
|
+
if verify_token(app, token):
|
|
82
|
+
config["userId"] = parse_user_id_from_jwt(app, token)
|
|
83
|
+
config["token"] = token
|
|
84
|
+
if not running_in_airflow:
|
|
85
|
+
logger.info("RegScale Token and userId has been saved in init.yaml")
|
|
86
|
+
app.save_config(conf=config)
|
|
87
|
+
return token
|
|
88
|
+
else:
|
|
89
|
+
logger.error("Invalid token provided.")
|
|
90
|
+
sys.exit(1)
|
|
91
|
+
except AttributeError as e:
|
|
92
|
+
logger.error("Unexpected error when verifying token: %s", e)
|
|
87
93
|
sys.exit(1)
|
|
88
94
|
from regscale.core.app.api import Api
|
|
89
95
|
|
|
90
|
-
|
|
91
|
-
if
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
96
|
+
try:
|
|
97
|
+
if str_user and str_password:
|
|
98
|
+
if config and "REGSCALE_DOMAIN" not in os.environ and host is None:
|
|
99
|
+
host = config["domain"]
|
|
100
|
+
regscale_auth = RegScaleAuth.authenticate(
|
|
101
|
+
api=Api(),
|
|
102
|
+
username=str_user,
|
|
103
|
+
password=str_password,
|
|
104
|
+
domain=host,
|
|
105
|
+
mfa_token=mfa_token,
|
|
106
|
+
)
|
|
107
|
+
else:
|
|
108
|
+
regscale_auth = RegScaleAuth.authenticate(Api(), mfa_token=mfa_token)
|
|
109
|
+
if config and config["domain"] is None:
|
|
110
|
+
raise ValueError("No domain set in the init.yaml configuration file.")
|
|
111
|
+
if config and config["domain"] == "":
|
|
112
|
+
raise ValueError("The domain is blank in the init.yaml configuration file.")
|
|
113
|
+
except TypeError as ex:
|
|
114
|
+
logger.error("TypeError: %s", ex)
|
|
115
|
+
except SSLCertVerificationError as sslex:
|
|
116
|
+
logger.error("SSLError, python requests requires a valid ssl certificate.\n%s", sslex)
|
|
117
|
+
sys.exit(1)
|
|
106
118
|
|
|
107
119
|
# create object to authenticate
|
|
108
120
|
auth = {
|
|
@@ -112,24 +124,18 @@ def login(
|
|
|
112
124
|
"mfaToken": mfa_token,
|
|
113
125
|
}
|
|
114
126
|
if auth["password"]:
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
logger.debug("Token: %s", regscale_auth.token[:20])
|
|
128
|
-
except TypeError as ex:
|
|
129
|
-
logger.error("TypeError: %s", ex)
|
|
130
|
-
except SSLCertVerificationError as sslex:
|
|
131
|
-
logger.error("SSLError, python requests requires a valid ssl certificate.\n%s", sslex)
|
|
132
|
-
sys.exit(1)
|
|
127
|
+
# update init file from login
|
|
128
|
+
if config:
|
|
129
|
+
config["token"] = regscale_auth.token
|
|
130
|
+
config["userId"] = regscale_auth.user_id
|
|
131
|
+
config["domain"] = regscale_auth.domain
|
|
132
|
+
# write the changes back to file
|
|
133
|
+
app.save_config(config)
|
|
134
|
+
# set variables
|
|
135
|
+
logger.info("User ID: %s", regscale_auth.user_id)
|
|
136
|
+
logger.info("New RegScale Token has been updated and saved in init.yaml")
|
|
137
|
+
# Truncate token for logging purposes
|
|
138
|
+
logger.debug("Token: %s", regscale_auth.token[:20])
|
|
133
139
|
config["domain"] = host
|
|
134
140
|
app.save_config(config)
|
|
135
141
|
return regscale_auth.token
|
|
@@ -944,10 +944,11 @@ def map_workbook_to_lookups(file_path: str, workbook_data: Optional["pd.DataFram
|
|
|
944
944
|
wb_data = workbook_data
|
|
945
945
|
else:
|
|
946
946
|
wb_data = pd.read_excel(file_path)
|
|
947
|
+
|
|
948
|
+
wb_data = wb_data.dropna()
|
|
947
949
|
for cur_row in obj_fields:
|
|
948
950
|
if len(cur_row.lookup_field) > 0 and cur_row.lookup_field != "module":
|
|
949
951
|
if cur_row.column_name in wb_data.columns:
|
|
950
|
-
wb_data.fillna("None")
|
|
951
952
|
lookup_wb = pd.read_excel(file_path, sheet_name=cur_row.column_name)
|
|
952
953
|
if cur_row.lookup_field == "user":
|
|
953
954
|
lookup_wb = lookup_wb.rename(
|
regscale/dev/code_gen.py
CHANGED
|
@@ -80,14 +80,13 @@ from airflow import DAG
|
|
|
80
80
|
|
|
81
81
|
from regscale.airflow.config import DEFAULT_ARGS, yesterday
|
|
82
82
|
from regscale.airflow.hierarchy import AIRFLOW_CLICK_OPERATORS as OPERATORS
|
|
83
|
-
from regscale.airflow.tasks.groups import email_on_fail_operator
|
|
84
83
|
|
|
85
84
|
DAG_NAME = "{integration}"
|
|
86
85
|
|
|
87
86
|
dag = DAG(
|
|
88
87
|
DAG_NAME,
|
|
89
88
|
default_args=DEFAULT_ARGS,
|
|
90
|
-
|
|
89
|
+
schedule=None,
|
|
91
90
|
start_date=yesterday(),
|
|
92
91
|
description=__doc__,
|
|
93
92
|
is_paused_upon_creation=False,
|
|
@@ -100,9 +99,7 @@ integration_operator = OPERATORS["{integration.split('_')[0].lower()}__sync_{int
|
|
|
100
99
|
op_kwargs={{{op_kwargs}\n }},
|
|
101
100
|
)
|
|
102
101
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
integration_operator >> email
|
|
102
|
+
integration_operator
|
|
106
103
|
"""
|
|
107
104
|
with open(f"{save_dir}/{integration_name.lower()}.py", "w") as f:
|
|
108
105
|
f.write("# flake8: noqa E501\n# pylint: disable=line-too-long\n")
|
|
@@ -56,17 +56,18 @@ def determine_status_and_results(finding: Any) -> Tuple[str, Optional[str]]:
|
|
|
56
56
|
results = None
|
|
57
57
|
if "Compliance" in finding.keys():
|
|
58
58
|
status = "Fail" if finding["Compliance"]["Status"] == "FAILED" else "Pass"
|
|
59
|
-
results = ", ".join(finding
|
|
59
|
+
results = ", ".join(finding.get("Compliance", {}).get("RelatedRequirements", [])) or "N/A"
|
|
60
60
|
if "FindingProviderFields" in finding.keys():
|
|
61
61
|
status = (
|
|
62
62
|
"Fail"
|
|
63
|
-
if finding
|
|
63
|
+
if finding.get("FindingProviderFields", {}).get("Severity", {}).get("Label", "")
|
|
64
|
+
in ["CRITICAL", "HIGH", "MEDIUM", "LOW"]
|
|
64
65
|
else "Pass"
|
|
65
66
|
)
|
|
66
67
|
if "PatchSummary" in finding.keys() and not results:
|
|
67
68
|
results = (
|
|
68
|
-
f"{finding
|
|
69
|
-
"{finding
|
|
69
|
+
f"{finding.get('PatchSummary', {}).get('MissingCount', 0)} Missing Patch(s) of "
|
|
70
|
+
"{finding.get('PatchSummary', {}).get('InstalledCount', 0)}"
|
|
70
71
|
)
|
|
71
72
|
return status, results
|
|
72
73
|
|
|
@@ -711,12 +711,13 @@ Description: {description if isinstance(description, str) else ''}"""
|
|
|
711
711
|
)
|
|
712
712
|
if not region:
|
|
713
713
|
logger.warning("AWS region not provided. Defaulting to 'us-east-1'.")
|
|
714
|
-
|
|
715
|
-
"securityhub",
|
|
714
|
+
session = boto3.Session(
|
|
716
715
|
region_name=kwargs.get(region, "us-east-1"),
|
|
717
716
|
aws_access_key_id=aws_secret_key_id,
|
|
718
717
|
aws_secret_access_key=aws_secret_access_key,
|
|
718
|
+
aws_session_token=kwargs.get("aws_session_token"),
|
|
719
719
|
)
|
|
720
|
+
client = session.client("securityhub")
|
|
720
721
|
aws_findings = fetch_aws_findings(aws_client=client)
|
|
721
722
|
self.num_findings_to_process = len(aws_findings)
|
|
722
723
|
for finding in aws_findings:
|
|
@@ -33,6 +33,16 @@ def sync_nozomi_vantage(regscale_ssp_id: int) -> None:
|
|
|
33
33
|
assets_nozomi_vantage.run_sync(regscale_ssp_id=regscale_ssp_id)
|
|
34
34
|
|
|
35
35
|
|
|
36
|
+
@assets.command(name="sync_qualys_cloud")
|
|
37
|
+
@regscale_ssp_id()
|
|
38
|
+
def sync_qualys_cloud(regscale_ssp_id: int) -> None:
|
|
39
|
+
"""Sync Assets from Qualys Cloud to RegScale."""
|
|
40
|
+
from regscale.models.integration_models.synqly_models.connectors import Assets
|
|
41
|
+
|
|
42
|
+
assets_qualys_cloud = Assets("qualys_cloud")
|
|
43
|
+
assets_qualys_cloud.run_sync(regscale_ssp_id=regscale_ssp_id)
|
|
44
|
+
|
|
45
|
+
|
|
36
46
|
@assets.command(name="sync_servicenow")
|
|
37
47
|
@regscale_ssp_id()
|
|
38
48
|
def sync_servicenow(regscale_ssp_id: int) -> None:
|
|
@@ -43,4 +53,14 @@ def sync_servicenow(regscale_ssp_id: int) -> None:
|
|
|
43
53
|
assets_servicenow.run_sync(regscale_ssp_id=regscale_ssp_id)
|
|
44
54
|
|
|
45
55
|
|
|
56
|
+
@assets.command(name="sync_tanium_cloud")
|
|
57
|
+
@regscale_ssp_id()
|
|
58
|
+
def sync_tanium_cloud(regscale_ssp_id: int) -> None:
|
|
59
|
+
"""Sync Assets from Tanium Cloud to RegScale."""
|
|
60
|
+
from regscale.models.integration_models.synqly_models.connectors import Assets
|
|
61
|
+
|
|
62
|
+
assets_tanium_cloud = Assets("tanium_cloud")
|
|
63
|
+
assets_tanium_cloud.run_sync(regscale_ssp_id=regscale_ssp_id)
|
|
64
|
+
|
|
65
|
+
|
|
46
66
|
# pylint: enable=line-too-long
|
|
@@ -158,4 +158,29 @@ def sync_torq(regscale_id: int, regscale_module: str, name: str) -> None:
|
|
|
158
158
|
ticketing_torq.run_sync(regscale_id=regscale_id, regscale_module=regscale_module, name=name)
|
|
159
159
|
|
|
160
160
|
|
|
161
|
+
@ticketing.command(name="sync_zendesk")
|
|
162
|
+
@regscale_id()
|
|
163
|
+
@regscale_module()
|
|
164
|
+
@click.option(
|
|
165
|
+
"--name",
|
|
166
|
+
type=click.STRING,
|
|
167
|
+
help="zendesk name",
|
|
168
|
+
required=True,
|
|
169
|
+
prompt="zendesk name",
|
|
170
|
+
)
|
|
171
|
+
@click.option(
|
|
172
|
+
"--subject",
|
|
173
|
+
type=click.STRING,
|
|
174
|
+
help="zendesk subject",
|
|
175
|
+
required=True,
|
|
176
|
+
prompt="zendesk subject",
|
|
177
|
+
)
|
|
178
|
+
def sync_zendesk(regscale_id: int, regscale_module: str, name: str, subject: str) -> None:
|
|
179
|
+
"""Sync Ticketing data between Zendesk and RegScale."""
|
|
180
|
+
from regscale.models.integration_models.synqly_models.connectors import Ticketing
|
|
181
|
+
|
|
182
|
+
ticketing_zendesk = Ticketing("zendesk")
|
|
183
|
+
ticketing_zendesk.run_sync(regscale_id=regscale_id, regscale_module=regscale_module, name=name, subject=subject)
|
|
184
|
+
|
|
185
|
+
|
|
161
186
|
# pylint: enable=line-too-long
|
|
@@ -128,7 +128,7 @@ def inventory(
|
|
|
128
128
|
)
|
|
129
129
|
def issues(
|
|
130
130
|
wiz_project_id: str,
|
|
131
|
-
|
|
131
|
+
regscale_ssp_id: int,
|
|
132
132
|
client_id: str,
|
|
133
133
|
client_secret: str,
|
|
134
134
|
filter_by_override: Optional[str] = None,
|
|
@@ -152,9 +152,9 @@ def issues(
|
|
|
152
152
|
|
|
153
153
|
filter_by["project"] = wiz_project_id
|
|
154
154
|
|
|
155
|
-
scanner = WizIssue(plan_id=
|
|
155
|
+
scanner = WizIssue(plan_id=regscale_ssp_id)
|
|
156
156
|
scanner.sync_findings(
|
|
157
|
-
plan_id=
|
|
157
|
+
plan_id=regscale_ssp_id,
|
|
158
158
|
filter_by_override=filter_by_override, # type: ignore
|
|
159
159
|
client_id=client_id, # type: ignore
|
|
160
160
|
client_secret=client_secret, # type: ignore
|