Skip to content

Commit 81af001

Browse files
authored
DS-943 Fixing high severity sonarcloud reported vulnerabilities (#1144)
# Task Branch Pull Request **<https://nhsd-jira.digital.nhs.uk/browse/DS-943>** ## Description of Changes Fixing issues reported by sonarcloud for DoS-Integration repo ## Type of change Delete not appropriate - Bug fix (non-breaking change which fixes sonarcloud vulnerabilities issues) ## Development Checklist - [x] I have performed a self-review of my own code - [x] Tests have added that prove my fix is effective or that my feature works (Integration tests) - [x] I have updated Dependabot to include my changes (if applicable) ## Code Reviewer Checklist - [x] I can confirm the changes have been tested or approved by a tester
1 parent 4cb8398 commit 81af001

30 files changed

Lines changed: 133 additions & 86 deletions

File tree

application/common/dos.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -254,7 +254,7 @@ def db_rows_to_spec_open_times(db_rows: Iterable[dict]) -> list[SpecifiedOpening
254254
for date, rows in groupby(date_sorted_rows, lambda row: row["date"]):
255255
is_open = True
256256
open_periods = []
257-
for row in list(rows):
257+
for row in rows:
258258
if row["isclosed"] is True:
259259
is_open = False
260260
else:

application/common/errors.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,7 @@ class ValidationError(Exception):
44

55
class DynamoDBError(Exception):
66
"""Exception raised for all DynamoDB errors."""
7+
8+
9+
class SecretsManagerError(Exception):
10+
"""Exception raised for AWS Secrets Manager errors."""

application/common/nhs.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,7 @@ def _get_specified_opening_times(self: Self) -> list[SpecifiedOpeningTime]:
165165
date = datetime.strptime(date_str, "%b %d %Y").date()
166166
is_open = True
167167

168-
for item in list(op_dict_list):
168+
for item in op_dict_list:
169169
if item["IsOpen"]:
170170
open_periods.append(OpenPeriod.from_string_times(item["OpeningTime"], item["ClosingTime"]))
171171
else:

application/common/secretsmanager.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
from boto3 import client
55
from botocore.exceptions import ClientError
66

7+
from .errors import SecretsManagerError
8+
79
logger = Logger()
810

911
secrets_manager = client(service_name="secretsmanager")
@@ -16,7 +18,7 @@ def get_secret(secret_name: str) -> dict[str, str]:
1618
secret_name (str): Secret name to get
1719
1820
Raises:
19-
e: ClientError caused by secrets manager
21+
SecretsManagerError: When unable to retrieve secret from AWS Secrets Manager
2022
2123
Returns:
2224
Dict[str, str]: Secrets as a dictionary
@@ -25,6 +27,6 @@ def get_secret(secret_name: str) -> dict[str, str]:
2527
secret_value_response = secrets_manager.get_secret_value(SecretId=secret_name)
2628
except ClientError as err:
2729
msg = f"Failed getting secret '{secret_name}' from secrets manager"
28-
raise Exception(msg) from err # noqa: TRY002
30+
raise SecretsManagerError(msg) from err
2931
secrets_json_str = secret_value_response["SecretString"]
3032
return loads(secrets_json_str)

application/common/tests/test_secretsmanager.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
import pytest
55
from moto import mock_aws
66

7+
from application.common.errors import SecretsManagerError
8+
79
FILE_PATH = "application.common.secretsmanager"
810

911

@@ -26,5 +28,5 @@ def test_get_secret() -> None:
2628
def test_get_secret_resource_not_found() -> None:
2729
from application.common.secretsmanager import get_secret
2830

29-
with pytest.raises(Exception, match="Failed getting secret 'fake_secret_name' from secrets manager"):
31+
with pytest.raises(SecretsManagerError, match="Failed getting secret 'fake_secret_name' from secrets manager"):
3032
get_secret("fake_secret_name")

application/event_replay/event_replay.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,19 @@
88
from aws_lambda_powertools.utilities.typing import LambdaContext
99
from boto3 import client
1010
from boto3.dynamodb.types import TypeDeserializer
11+
from botocore.config import Config
1112
from simplejson import dumps
1213

1314
from common.middlewares import unhandled_exception_logging
1415

16+
# Configure boto3 client with explicit timeout to prevent hanging in Lambda
17+
# Timeouts tuned for typical DynamoDB/SQS operations while preventing indefinite hangs
18+
boto_config = Config(connect_timeout=10, read_timeout=15)
19+
20+
# Create clients at module level for connection reuse across Lambda invocations
21+
dynamodb_client = client("dynamodb", config=boto_config)
22+
sqs_client = client("sqs", config=boto_config)
23+
1524
tracer = Tracer()
1625
logger = Logger()
1726

@@ -75,7 +84,7 @@ def get_change_event(odscode: str, sequence_number: Decimal) -> dict[str, Any]:
7584
Returns:
7685
dict[str, Any]: The change event
7786
"""
78-
response = client("dynamodb").query(
87+
response = dynamodb_client.query(
7988
TableName=getenv("CHANGE_EVENTS_TABLE_NAME"),
8089
IndexName="gsi_ods_sequence",
8190
ProjectionExpression="Event",
@@ -112,11 +121,10 @@ def send_change_event(change_event: dict[str, Any], odscode: str, sequence_numbe
112121
sequence_number (int): The sequence number of the change event
113122
correlation_id (str): The correlation id of the event replay
114123
"""
115-
sqs = client("sqs")
116124
queue_url = getenv("CHANGE_EVENT_SQS_URL")
117125
logger.info("Sending change event to SQS", queue_url=queue_url)
118126
change_event_str = dumps(change_event)
119-
response = sqs.send_message(
127+
response = sqs_client.send_message(
120128
QueueUrl=queue_url,
121129
MessageBody=change_event_str,
122130
MessageGroupId=odscode,

application/event_replay/tests/test_event_replay.py

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -102,21 +102,20 @@ def test_build_correlation_id(mock_time_ns: MagicMock) -> None:
102102
assert response == f"{time}-local-replayed-event"
103103

104104

105-
@patch(f"{FILE_PATH}.client")
105+
@patch(f"{FILE_PATH}.dynamodb_client")
106106
def test_get_change_event(mock_client: MagicMock, change_event: dict[str, str], event: dict[str, str]) -> None:
107107
# Arrange
108108
table_name = "my-table"
109109
environ["CHANGE_EVENTS_TABLE_NAME"] = table_name
110110
environ["AWS_REGION"] = "eu-west-1"
111111
serializer = TypeSerializer()
112112

113-
mock_client.return_value.query.return_value = {"Items": [{"Event": serializer.serialize(change_event)}]}
113+
mock_client.query.return_value = {"Items": [{"Event": serializer.serialize(change_event)}]}
114114
# Act
115115
response = get_change_event(event["odscode"], Decimal(event["sequence_number"]))
116116
# Assert
117117
assert response == change_event
118-
mock_client.assert_called_with("dynamodb")
119-
mock_client().query.assert_called_with(
118+
mock_client.query.assert_called_with(
120119
TableName=table_name,
121120
IndexName="gsi_ods_sequence",
122121
ProjectionExpression="Event",
@@ -130,21 +129,20 @@ def test_get_change_event(mock_client: MagicMock, change_event: dict[str, str],
130129
del environ["AWS_REGION"]
131130

132131

133-
@patch(f"{FILE_PATH}.client")
132+
@patch(f"{FILE_PATH}.dynamodb_client")
134133
def test_get_change_event_no_change_event_in_dynamodb(
135134
mock_client: MagicMock, change_event: dict[str, str], event: dict[str, str]
136135
) -> None:
137136
# Arrange
138137
table_name = "my-table"
139138
environ["CHANGE_EVENTS_TABLE_NAME"] = table_name
140139
environ["AWS_REGION"] = "eu-west-1"
141-
mock_client.return_value.query.return_value = {"Items": []}
140+
mock_client.query.return_value = {"Items": []}
142141
# Act
143142
with pytest.raises(ValueError, match="No change event found for ods code FXXX1 and sequence number 1"):
144143
get_change_event(event["odscode"], Decimal(event["sequence_number"]))
145144
# Assert
146-
mock_client.assert_called_with("dynamodb")
147-
mock_client().query.assert_called_with(
145+
mock_client.query.assert_called_with(
148146
TableName=table_name,
149147
IndexName="gsi_ods_sequence",
150148
ProjectionExpression="Event",
@@ -158,16 +156,15 @@ def test_get_change_event_no_change_event_in_dynamodb(
158156
del environ["AWS_REGION"]
159157

160158

161-
@patch(f"{FILE_PATH}.client")
159+
@patch(f"{FILE_PATH}.sqs_client")
162160
def test_send_change_event(mock_client: MagicMock, change_event: dict[str, str], event: dict[str, str]) -> None:
163161
# Arrange
164162
correlation_id = "CORRELATION_ID"
165163
environ["CHANGE_EVENT_SQS_URL"] = queue_url = "https://sqs.eu-west-1.amazonaws.com/123456789/my-queue"
166164
# Act
167165
send_change_event(change_event, event["odscode"], int(event["sequence_number"]), correlation_id)
168166
# Assert
169-
mock_client.assert_called_with("sqs")
170-
mock_client().send_message.assert_called_with(
167+
mock_client.send_message.assert_called_with(
171168
QueueUrl=queue_url,
172169
MessageBody=dumps(change_event),
173170
MessageGroupId=event["odscode"],

application/ingest_change_event/ingest_change_event.py

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,22 @@
77
from aws_lambda_powertools.utilities.data_classes import SQSEvent, event_source
88
from aws_lambda_powertools.utilities.typing.lambda_context import LambdaContext
99
from boto3 import client
10+
from botocore.config import Config
11+
from botocore.exceptions import ClientError
1012

1113
from .change_event_validation import validate_change_event
1214
from common.dynamodb import add_change_event_to_dynamodb, get_latest_sequence_id_for_a_given_odscode_from_dynamodb
1315
from common.middlewares import redact_staff_key_from_event, unhandled_exception_logging
1416
from common.types import HoldingQueueChangeEventItem
1517
from common.utilities import extract_body, get_sequence_number
1618

19+
# Configure boto3 client with explicit timeout to prevent hanging in Lambda
20+
boto_config = Config(connect_timeout=10, read_timeout=15)
21+
1722
logger = Logger()
1823
tracer = Tracer()
19-
sqs = client("sqs")
24+
# Create SQS client at module level for connection reuse across Lambda invocations
25+
sqs_client = client("sqs", config=boto_config)
2026

2127

2228
@redact_staff_key_from_event()
@@ -82,8 +88,16 @@ def lambda_handler(event: SQSEvent, context: LambdaContext) -> None: # noqa: AR
8288
correlation_id=logger.get_correlation_id(),
8389
)
8490
logger.debug("Change event validated", holding_queue_change_event_item=holding_queue_change_event_item)
85-
sqs.send_message(
86-
QueueUrl=getenv("HOLDING_QUEUE_URL"),
87-
MessageBody=dumps(holding_queue_change_event_item),
88-
MessageGroupId=ods_code,
89-
)
91+
try:
92+
sqs_client.send_message(
93+
QueueUrl=getenv("HOLDING_QUEUE_URL"),
94+
MessageBody=dumps(holding_queue_change_event_item),
95+
MessageGroupId=ods_code,
96+
)
97+
except ClientError as err:
98+
logger.exception(
99+
"Failed to send message to holding queue",
100+
error_code=err.response["Error"]["Code"],
101+
queue_url=getenv("HOLDING_QUEUE_URL"),
102+
)
103+
raise

application/ingest_change_event/tests/test_ingest_change_event.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
FILE_PATH = "application.ingest_change_event.ingest_change_event"
1313

1414

15-
@patch(f"{FILE_PATH}.sqs")
15+
@patch(f"{FILE_PATH}.sqs_client")
1616
@patch(f"{FILE_PATH}.HoldingQueueChangeEventItem")
1717
@patch(f"{FILE_PATH}.add_change_event_to_dynamodb")
1818
@patch(f"{FILE_PATH}.get_latest_sequence_id_for_a_given_odscode_from_dynamodb")
@@ -72,7 +72,7 @@ def test_lambda_handler(
7272
del environ["HOLDING_QUEUE_URL"]
7373

7474

75-
@patch(f"{FILE_PATH}.sqs")
75+
@patch(f"{FILE_PATH}.sqs_client")
7676
@patch(f"{FILE_PATH}.HoldingQueueChangeEventItem")
7777
@patch(f"{FILE_PATH}.add_change_event_to_dynamodb")
7878
@patch(f"{FILE_PATH}.get_latest_sequence_id_for_a_given_odscode_from_dynamodb")
@@ -130,7 +130,7 @@ def test_lambda_handler_with_sensitive_staff_key(
130130

131131

132132
@patch.object(Logger, "error")
133-
@patch(f"{FILE_PATH}.sqs")
133+
@patch(f"{FILE_PATH}.sqs_client")
134134
@patch(f"{FILE_PATH}.HoldingQueueChangeEventItem")
135135
@patch(f"{FILE_PATH}.add_change_event_to_dynamodb")
136136
@patch(f"{FILE_PATH}.get_latest_sequence_id_for_a_given_odscode_from_dynamodb")
@@ -183,7 +183,7 @@ def test_lambda_handler_no_sequence_number(
183183

184184

185185
@patch.object(Logger, "error")
186-
@patch(f"{FILE_PATH}.sqs")
186+
@patch(f"{FILE_PATH}.sqs_client")
187187
@patch(f"{FILE_PATH}.HoldingQueueChangeEventItem")
188188
@patch(f"{FILE_PATH}.add_change_event_to_dynamodb")
189189
@patch(f"{FILE_PATH}.get_latest_sequence_id_for_a_given_odscode_from_dynamodb")
@@ -239,7 +239,7 @@ def test_lambda_handler_less_than_latest_sequence_number(
239239
del environ["HOLDING_QUEUE_URL"]
240240

241241

242-
@patch(f"{FILE_PATH}.sqs")
242+
@patch(f"{FILE_PATH}.sqs_client")
243243
@patch(f"{FILE_PATH}.HoldingQueueChangeEventItem")
244244
@patch(f"{FILE_PATH}.add_change_event_to_dynamodb")
245245
@patch(f"{FILE_PATH}.get_latest_sequence_id_for_a_given_odscode_from_dynamodb")
Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,2 @@
11
aws-lambda-powertools[tracer] ~= 3.20.0
22
psycopg[binary]
3-
pytz

0 commit comments

Comments
 (0)