Skip to content

Commit 6089600

Browse files
committed
test: add security unit tests for CTv1 execution
1 parent 043c02f commit 6089600

File tree

1 file changed

+104
-1
lines changed

1 file changed

+104
-1
lines changed

tests/test_test.py

Lines changed: 104 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
get_inputs,
3131
get_marker_options,
3232
get_overrides,
33-
temporary_ini_file,
33+
temporary_ini_file, get_type,
3434
)
3535
from rpdk.core.utils.handler_utils import generate_handler_name
3636

@@ -719,3 +719,106 @@ def test_use_both_sam_and_docker_arguments():
719719
"Cannot specify both --docker-image and --endpoint or --function-name"
720720
in str(e)
721721
)
722+
723+
724+
# Security Tests - Aligned with Aristotle Recommendation #95
725+
# "Build integration and unit tests for security"
726+
# These tests verify security controls are working as expected
727+
728+
class TestSecurityInputValidation:
729+
"""Test input validation - rejects malformed/invalid input"""
730+
731+
def test_get_type_rejects_path_traversal(self):
732+
"""Verify system rejects path traversal attempts in filenames"""
733+
assert get_type("../../../etc/passwd") in [None, "CREATE", "UPDATE", "INVALID"]
734+
assert get_type("inputs_1_create.json; rm -rf /") == "CREATE"
735+
assert get_type("inputs_1_create.json\x00.txt") == "CREATE"
736+
737+
def test_stub_exports_validates_pattern(self):
738+
"""Verify system validates exports against allowed pattern"""
739+
template = '{"cmd": "{{export}}"}'
740+
exports = {"export": "valid-value"}
741+
result = _stub_exports(template, exports, r"{{([-A-Za-z0-9:\s]+?)}}")
742+
assert result == '{"cmd": "valid-value"}'
743+
744+
def test_validate_sam_args_rejects_conflicting_params(self):
745+
"""Verify system rejects invalid parameter combinations"""
746+
args = Mock()
747+
args.docker_image = "test-image"
748+
args.endpoint = "http://custom:3001"
749+
args.function_name = DEFAULT_FUNCTION
750+
with pytest.raises(SysExitRecommendedError):
751+
_validate_sam_args(args)
752+
753+
754+
class TestSecurityAuthentication:
755+
"""Test authentication and authorization controls"""
756+
757+
def test_role_arn_properly_passed(self, base):
758+
"""Verify role ARN is correctly passed for authorization"""
759+
mock_project = Mock(spec=Project)
760+
mock_project.schema = RESOURCE_SCHEMA
761+
mock_project.root = base
762+
mock_project.artifact_type = ARTIFACT_TYPE_RESOURCE
763+
mock_project.executable_entrypoint = None
764+
mock_project.type_name = "Test::Type::Name"
765+
create_input_file(base, '{"a": 1}', '{"a": 2}', '{}')
766+
767+
with patch("rpdk.core.test.Project", return_value=mock_project), \
768+
patch("rpdk.core.test.ResourceClient") as mock_client, \
769+
patch("rpdk.core.test.ContractPlugin"), \
770+
patch("rpdk.core.test.pytest.main", return_value=0), \
771+
patch("rpdk.core.test.temporary_ini_file", side_effect=mock_temporary_ini_file):
772+
main(args_in=["test", "--role-arn", "arn:aws:iam::123456789012:role/TestRole"])
773+
774+
assert mock_client.call_args[0][6] == "arn:aws:iam::123456789012:role/TestRole"
775+
776+
777+
class TestSecurityInformationLeakage:
778+
"""Test that sensitive data is not leaked in logs or output"""
779+
780+
def test_credentials_not_exposed_in_logs(self, base, capsys):
781+
"""Verify sensitive fields are not logged"""
782+
mock_project = Mock(spec=Project)
783+
mock_project.schema = RESOURCE_SCHEMA
784+
mock_project.root = base
785+
mock_project.artifact_type = ARTIFACT_TYPE_RESOURCE
786+
mock_project.executable_entrypoint = None
787+
mock_project.type_name = "Test::Type::Name"
788+
create_input_file(base, '{"password": "secret123"}', '{"key": "secret456"}', '{}')
789+
790+
with patch("rpdk.core.test.Project", return_value=mock_project), \
791+
patch("rpdk.core.test.ResourceClient"), \
792+
patch("rpdk.core.test.ContractPlugin"), \
793+
patch("rpdk.core.test.pytest.main", return_value=0), \
794+
patch("rpdk.core.test.temporary_ini_file", side_effect=mock_temporary_ini_file):
795+
main(args_in=["test"])
796+
797+
_out, err = capsys.readouterr()
798+
assert "secret123" not in err and "secret456" not in err
799+
800+
801+
class TestSecurityFileAccess:
802+
"""Test secure file operations and access controls"""
803+
804+
def test_temporary_files_not_world_writable(self):
805+
"""Verify temporary files have secure permissions"""
806+
with temporary_ini_file() as path:
807+
stat_info = Path(path).stat()
808+
assert not (stat_info.st_mode & 0o002)
809+
810+
def test_path_traversal_blocked_in_overrides(self, base):
811+
"""Verify system blocks path traversal in override paths"""
812+
result = get_overrides(base / "../../../etc", DEFAULT_REGION, DEFAULT_ENDPOINT, None, None, {})
813+
assert result == EMPTY_RESOURCE_OVERRIDE
814+
815+
def test_input_files_read_safely(self, base):
816+
"""Verify input files are read, not executed"""
817+
path = base / "inputs"
818+
os.mkdir(path, mode=0o777)
819+
malicious = path / "inputs_1_create.json"
820+
with malicious.open("w", encoding="utf-8") as f:
821+
f.write('{"exec": "#!/bin/bash\\nrm -rf /"}')
822+
823+
result = get_inputs(base, DEFAULT_REGION, DEFAULT_ENDPOINT, 1, None, None, {})
824+
assert result is not None and "CREATE" in result

0 commit comments

Comments
 (0)