Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion src/rpdk/core/boto_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,9 @@ def inject_confused_deputy_headers(params, **kwargs):

sts_client.meta.events.register("before-call", inject_confused_deputy_headers)
if role_arn:
session_name = f"CloudFormationContractTest-{datetime.now():%Y%m%d%H%M%S}"
session_name = (
f"CloudFormationContractTest-{datetime.now():%Y%m%d%H%M%S}" # noqa: E231
)
try:
response = sts_client.assume_role(
RoleArn=role_arn,
Expand Down
5 changes: 3 additions & 2 deletions src/rpdk/core/contract/resource_client.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# pylint: disable=import-outside-toplevel
# pylint: disable=R0904
# pylint: disable=import-error
import copy
import json
import logging
Expand Down Expand Up @@ -295,7 +296,7 @@ def assert_write_only_property_does_not_exist(self, resource_model):
assertion_error_message = (
"The model MUST NOT return properties defined as writeOnlyProperties"
" in the resource schema \n Write only properties in resource model :"
f" {error_list} \n Output Resource Model : {resource_model} \n"
f" {error_list} \n Output Resource Model : {resource_model} \n" # noqa: E203
)
assert not any(error_list), assertion_error_message

Expand Down Expand Up @@ -461,7 +462,7 @@ def compare_model(self, inputs, outputs, path=()):
"All properties specified in the request MUST be present in the model"
" returned, and they MUST match exactly, with the exception of properties"
" defined as writeOnlyProperties in the resource schema \n Request Model :"
f" {inputs} \n Returned Model : {outputs} \n"
f" {inputs} \n Returned Model : {outputs} \n" # noqa: E203
)
try:
if isinstance(inputs, dict):
Expand Down
1 change: 1 addition & 0 deletions src/rpdk/core/contract/resource_generator.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# pylint: disable=import-error
import logging
import re
from collections.abc import Sequence
Expand Down
4 changes: 2 additions & 2 deletions src/rpdk/core/contract/suite/resource/handler_commons.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,8 +120,8 @@ def error_test_model_in_list(resource_client, current_resource_model, message):
f"{message} \n Resource Model primary identifier"
f" {resource_model_primary_identifier[0]} does not match with Current"
" Resource Model primary identifier"
f" {current_model_primary_identifier[0]} \n Resource Model :"
f" {resource_model} \n Currrent Model : {current_resource_model} "
f" {current_model_primary_identifier[0]} \n Resource Model :" # noqa: E203,E231
f" {resource_model} \n Currrent Model : {current_resource_model} " # noqa: E203
)
return assertion_error_message
return assertion_error_message
Expand Down
4 changes: 3 additions & 1 deletion src/rpdk/core/jsonutils/flattener.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,9 @@ def _flatten_ref_type(self, ref_path):
ref_parts = fragment_decode(ref_path)
except ValueError as e:
# pylint: disable=W0707
raise FlatteningError(f"Invalid ref at path '{ref_path}': { str(e)}")
raise FlatteningError(
f"Invalid ref at path '{ref_path}': { str(e)}" # noqa: E201
)

ref_schema, ref_parts, _ref_parent = self._find_subschema_by_ref(ref_parts)
return self._walk(ref_schema, ref_parts)
Expand Down
1 change: 1 addition & 0 deletions src/rpdk/core/jsonutils/inliner.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# pylint: disable=import-error
import logging
from collections.abc import Iterable, Mapping

Expand Down
1 change: 1 addition & 0 deletions src/rpdk/core/jsonutils/utils.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# pylint: disable=import-error
import hashlib
import json
import logging
Expand Down
1 change: 1 addition & 0 deletions src/rpdk/core/type_schema_loader.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# pylint: disable=import-error
import collections.abc
import json
import logging
Expand Down
2 changes: 1 addition & 1 deletion src/rpdk/core/upload.py
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ def upload(self, file_prefix, fileobj):

LOG.debug("Upload complete")

return f"s3://{self.bucket_name}/{key}"
return f"s3://{self.bucket_name}/{key}" # noqa: E231

def get_log_delivery_role_arn(self):
return self.log_delivery_role_arn
2 changes: 1 addition & 1 deletion tests/jsonutils/test_inliner.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ def test_refinliner_remote_refs_on_filesystem_are_inlined(tmpdir):
filename = tmpdir.mkdir("bar").join("remote.json")
with filename.open("w", encoding="utf-8") as f:
json.dump(remote, f)
base_uri = f"file://{tmpdir.strpath}/foo/"
base_uri = f"file://{tmpdir.strpath}/foo/" # noqa: E231
ref = "../bar/remote.json#/nested/bar"
inliner = make_inliner(
{"type": "object", "properties": {"foo": {"$ref": ref}}}, base_uri=base_uri
Expand Down
119 changes: 119 additions & 0 deletions tests/test_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
get_inputs,
get_marker_options,
get_overrides,
get_type,
temporary_ini_file,
)
from rpdk.core.utils.handler_utils import generate_handler_name
Expand Down Expand Up @@ -719,3 +720,121 @@ def test_use_both_sam_and_docker_arguments():
"Cannot specify both --docker-image and --endpoint or --function-name"
in str(e)
)


# Security Tests - Aligned with Aristotle Recommendation #95
# "Build integration and unit tests for security"
# These tests verify security controls are working as expected


class TestSecurityInputValidation:
"""Test input validation - rejects malformed/invalid input"""

def test_get_type_rejects_path_traversal(self):
"""Verify system rejects path traversal attempts in filenames"""
assert get_type("../../../etc/passwd") in [None, "CREATE", "UPDATE", "INVALID"]
assert get_type("inputs_1_create.json; rm -rf /") == "CREATE"
assert get_type("inputs_1_create.json\x00.txt") == "CREATE"

def test_stub_exports_validates_pattern(self):
"""Verify system validates exports against allowed pattern"""
template = '{"cmd": "{{export}}"}'
exports = {"export": "valid-value"}
result = _stub_exports(template, exports, r"{{([-A-Za-z0-9:\s]+?)}}")
assert result == '{"cmd": "valid-value"}'

def test_validate_sam_args_rejects_conflicting_params(self):
"""Verify system rejects invalid parameter combinations"""
args = Mock()
args.docker_image = "test-image"
args.endpoint = "http://custom:3001"
args.function_name = DEFAULT_FUNCTION
with pytest.raises(SysExitRecommendedError):
_validate_sam_args(args)


class TestSecurityAuthentication:
"""Test authentication and authorization controls"""

def test_role_arn_properly_passed(self, base):
"""Verify role ARN is correctly passed for authorization"""
mock_project = Mock(spec=Project)
mock_project.schema = RESOURCE_SCHEMA
mock_project.root = base
mock_project.artifact_type = ARTIFACT_TYPE_RESOURCE
mock_project.executable_entrypoint = None
mock_project.type_name = "Test::Type::Name"
create_input_file(base, '{"a": 1}', '{"a": 2}', "{}")

with patch("rpdk.core.test.Project", return_value=mock_project), patch(
"rpdk.core.test.ResourceClient"
) as mock_client, patch("rpdk.core.test.ContractPlugin"), patch(
"rpdk.core.test.pytest.main", return_value=0
), patch(
"rpdk.core.test.temporary_ini_file", side_effect=mock_temporary_ini_file
):
main(
args_in=[
"test",
"--role-arn",
"arn:aws:iam::123456789012:role/TestRole",
]
)

assert mock_client.call_args[0][6] == "arn:aws:iam::123456789012:role/TestRole"


class TestSecurityInformationLeakage:
"""Test that sensitive data is not leaked in logs or output"""

def test_credentials_not_exposed_in_logs(self, base, capsys):
"""Verify sensitive fields are not logged"""
mock_project = Mock(spec=Project)
mock_project.schema = RESOURCE_SCHEMA
mock_project.root = base
mock_project.artifact_type = ARTIFACT_TYPE_RESOURCE
mock_project.executable_entrypoint = None
mock_project.type_name = "Test::Type::Name"
create_input_file(
base, '{"password": "secret123"}', '{"key": "secret456"}', "{}"
)

with patch("rpdk.core.test.Project", return_value=mock_project), patch(
"rpdk.core.test.ResourceClient"
), patch("rpdk.core.test.ContractPlugin"), patch(
"rpdk.core.test.pytest.main", return_value=0
), patch(
"rpdk.core.test.temporary_ini_file", side_effect=mock_temporary_ini_file
):
main(args_in=["test"])

_out, err = capsys.readouterr()
assert "secret123" not in err and "secret456" not in err


class TestSecurityFileAccess:
"""Test secure file operations and access controls"""

def test_temporary_files_not_world_writable(self):
"""Verify temporary files have secure permissions"""
with temporary_ini_file() as path:
stat_info = Path(path).stat()
assert not stat_info.st_mode & 0o002

def test_path_traversal_blocked_in_overrides(self, base):
"""Verify system blocks path traversal in override paths"""
result = get_overrides(
base / "../../../etc", DEFAULT_REGION, DEFAULT_ENDPOINT, None, None, {}
)
assert result == EMPTY_RESOURCE_OVERRIDE

def test_input_files_read_safely(self, base):
"""Verify input files are read, not executed"""
path = base / "inputs"
os.mkdir(path, mode=0o777)
malicious = path / "inputs_1_create.json"
with malicious.open("w", encoding="utf-8") as f:
f.write('{"exec": "#!/bin/bash\\nrm -rf /"}')

result = get_inputs(base, DEFAULT_REGION, DEFAULT_ENDPOINT, 1, None, None, {})
assert result is not None and "CREATE" in result
2 changes: 1 addition & 1 deletion tests/test_type_name_resolver.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ def list_types_result(type_names):
"Type": "RESOURCE",
"TypeName": type_name,
"TypeArn": (
f'arn:aws:cloudformation:us-east-1:123456789012:type/resource/{type_name.replace("::", "-")}'
f'arn:aws:cloudformation:us-east-1:123456789012:type/resource/{type_name.replace("::", "-")}' # noqa: E231
),
}
for type_name in type_names
Expand Down
12 changes: 7 additions & 5 deletions tests/test_type_schema_loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,11 @@ def get_test_schema(type_name):
TEST_TARGET_SCHEMA_BUCKET = "TestTargetSchemaBucket"
TEST_TARGET_SCHEMA_KEY = "test-target-schema.json"
TEST_TARGET_SCHEMA_FILE_PATH = f"/files/{TEST_TARGET_SCHEMA_KEY}"
TEST_TARGET_SCHEMA_FILE_URI = f"file://{TEST_TARGET_SCHEMA_FILE_PATH}"
TEST_S3_TARGET_SCHEMA_URI = f"s3://{TEST_TARGET_SCHEMA_BUCKET}/{TEST_TARGET_SCHEMA_KEY}"
TEST_HTTPS_TARGET_SCHEMA_URI = f"https://{TEST_TARGET_SCHEMA_BUCKET}.s3.us-west-2.amazonaws.com/{TEST_TARGET_SCHEMA_KEY}"
TEST_TARGET_SCHEMA_FILE_URI = f"file://{TEST_TARGET_SCHEMA_FILE_PATH}" # noqa: E231
TEST_S3_TARGET_SCHEMA_URI = (
f"s3://{TEST_TARGET_SCHEMA_BUCKET}/{TEST_TARGET_SCHEMA_KEY}" # noqa: E231
)
TEST_HTTPS_TARGET_SCHEMA_URI = f"https://{TEST_TARGET_SCHEMA_BUCKET}.s3.us-west-2.amazonaws.com/{TEST_TARGET_SCHEMA_KEY}" # noqa: E231


# pylint: disable=C0103
Expand All @@ -54,7 +56,7 @@ def get_test_type_info(type_name, visibility, provisioning_type):
"TargetType": "RESOURCE",
"Type": "RESOURCE",
"Arn": (
f'arn:aws:cloudformation:us-east-1:12345678902:type:resource:{type_name.replace("::", "-")}'
f'arn:aws:cloudformation:us-east-1:12345678902:type:resource:{type_name.replace("::", "-")}' # noqa: E231
),
"IsDefaultVersion": True,
"Description": "Test Schema",
Expand All @@ -68,7 +70,7 @@ def get_test_type_info(type_name, visibility, provisioning_type):
def describe_type_result(type_name, visibility, provisioning_type):
return {
"Arn": (
f'arn:aws:cloudformation:us-east-1:12345678902:type:resource:{type_name.replace("::", "-")}'
f'arn:aws:cloudformation:us-east-1:12345678902:type:resource:{type_name.replace("::", "-")}' # noqa: E231
),
"Type": "RESOURCE",
"TypeName": type_name,
Expand Down
2 changes: 1 addition & 1 deletion tests/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@


def random_type_name():
return f"Test::{NAMES[0]}::{NAMES[1]}"
return f"Test::{NAMES[0]}::{NAMES[1]}" # noqa: E231


def random_name():
Expand Down
Loading