|
30 | 30 | get_inputs, |
31 | 31 | get_marker_options, |
32 | 32 | get_overrides, |
33 | | - temporary_ini_file, |
| 33 | + temporary_ini_file, get_type, |
34 | 34 | ) |
35 | 35 | from rpdk.core.utils.handler_utils import generate_handler_name |
36 | 36 |
|
@@ -719,3 +719,106 @@ def test_use_both_sam_and_docker_arguments(): |
719 | 719 | "Cannot specify both --docker-image and --endpoint or --function-name" |
720 | 720 | in str(e) |
721 | 721 | ) |
| 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