diff --git a/tests/fixtures/live_binding/passrole_lambda_selected_finding/README.md b/tests/fixtures/live_binding/passrole_lambda_selected_finding/README.md new file mode 100644 index 0000000..e4d77bd --- /dev/null +++ b/tests/fixtures/live_binding/passrole_lambda_selected_finding/README.md @@ -0,0 +1,7 @@ +# PassRole Lambda Selected Finding Fixture + +This sanitized local fixture selects one IAMScope `passrole_lambda` finding/path for the controlled live PassRole-to-Lambda binding follow-up. + +It is local-only and synthetic/redacted. It does not run live AWS, Terraform, AWS CLI, STS, Lambda APIs, or `iam:PassRole`. It does not commit raw live output, a real AWS account id, or a real role ARN. + +The selected path represents only service-mediated Lambda `CreateFunction` plus `iam:PassRole` plus Lambda trust. It does not include an admin-equivalent execution role edge. It does not claim Lambda invocation behavior, downstream authorization, or exploitability proof. It does not claim broad IAMScope correctness, broad PassRole correctness, production readiness, composite benchmark scoring, or pass/fail benchmark labeling. diff --git a/tests/fixtures/live_binding/passrole_lambda_selected_finding/expected_finding.json b/tests/fixtures/live_binding/passrole_lambda_selected_finding/expected_finding.json new file mode 100644 index 0000000..bb1f340 --- /dev/null +++ b/tests/fixtures/live_binding/passrole_lambda_selected_finding/expected_finding.json @@ -0,0 +1,49 @@ +{ + "binding_status": "ready_for_next_checkpoint_comparison", + "fixture_id": "passrole_lambda_selected_live_binding_finding_001", + "next_slice": "Recommended next slice: bind selected IAMScope PassRole finding to live AWS result.", + "non_claims": { + "no_admin_equivalent_execution_role_claim": true, + "no_broad_iamscope_correctness": true, + "no_broad_passrole_correctness": true, + "no_composite_benchmark_score": true, + "no_downstream_authorization_proof": true, + "no_exploitability_proof": true, + "no_lambda_invocation_behavior": true, + "no_live_aws": true, + "no_pass_fail_benchmark_label": true, + "no_production_readiness": true + }, + "selected_finding": { + "assumptions": [ + { + "detail": "no session policy restricts lambda:CreateFunction or iam:PassRole; session policies are not visible to IAMScope collectors at collection time", + "kind": "session_policy" + } + ], + "blockers_observed_count": 0, + "expected_classification": "selected_local_createfunction_passrole_finding", + "expected_verdict": "validated", + "finding_id": "dc284c673334e54974e229c9ac006684b3e928d0d03936f857fe93068dc74dc8", + "finding_key": "e7aa122330b61ce55fdb3cd017139b3c9b8c941fdd95fec676e2c337cde58b1e", + "live_behavior_alignment": "service-mediated CreateFunction plus PassRole plus Lambda trust only", + "pattern_id": "passrole_lambda", + "pattern_version": "1.0.0", + "required_check_states": { + "no_boundary_blocks_lambda_create_function": "pass", + "no_boundary_blocks_passrole": "pass", + "no_identity_deny_blocks_lambda_create_function": "pass", + "no_identity_deny_blocks_passrole": "pass", + "no_scp_blocks_lambda_create_function": "pass", + "no_scp_blocks_passrole": "pass", + "passrole_condition_scoped_to_lambda_or_absent": "pass", + "source_has_lambda_create_function": "pass", + "source_has_passrole_to_target": "pass", + "target_trusts_lambda_service": "pass" + }, + "severity": "high", + "source_principal_arn": "arn:aws:iam::000000000000:user/IAMScopeLiveBindingSource", + "target_role_arn": "arn:aws:iam::000000000000:role/IAMScopeLiveBindingLambdaExecutionRole", + "title": "arn:aws:iam::000000000000:user/IAMScopeLiveBindingSource can assume role arn:aws:iam::000000000000:role/IAMScopeLiveBindingLambdaExecutionRole via Lambda PassRole chain" + } +} diff --git a/tests/fixtures/live_binding/passrole_lambda_selected_finding/scenario.json b/tests/fixtures/live_binding/passrole_lambda_selected_finding/scenario.json new file mode 100644 index 0000000..19f81b6 --- /dev/null +++ b/tests/fixtures/live_binding/passrole_lambda_selected_finding/scenario.json @@ -0,0 +1,170 @@ +{ + "account_id": "000000000000", + "aws_calls_made": 0, + "constraints": [], + "description": "Sanitized local fixture for selecting one IAMScope PassRole-to-Lambda finding for live-result binding.", + "edge_constraints": [], + "edges": [ + { + "dst": { + "node_type": "IAMRole", + "provider": "aws", + "provider_id": "arn:aws:iam::000000000000:role/IAMScopeLiveBindingLambdaExecutionRole", + "region": "-" + }, + "edge_id": "bcf0f131f4a66f2a73a77bae0ad7bc4b6928c92bf924da5f93a359547425b522", + "edge_type": "lambda:CreateFunction_permission", + "features": { + "allow_controls": [ + { + "control_type": "PERMISSION", + "digest": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "policy_arn": "arn:aws:iam::000000000000:policy/IAMScopeLiveBindingSanitizedPolicy", + "statement_index": 0, + "summary": "lambda:CreateFunction" + } + ], + "effect": "Allow", + "has_conditions": false, + "is_wildcard_resource": false, + "layer": "permission", + "raw_conditions": {}, + "resource_pattern": "arn:aws:lambda:us-east-1:000000000000:function:iamscope-live-passrole-lambda-test-redacted", + "statement_index": 0 + }, + "region": "-", + "src": { + "node_type": "IAMUser", + "provider": "aws", + "provider_id": "arn:aws:iam::000000000000:user/IAMScopeLiveBindingSource", + "region": "-" + } + }, + { + "dst": { + "node_type": "IAMRole", + "provider": "aws", + "provider_id": "arn:aws:iam::000000000000:role/IAMScopeLiveBindingLambdaExecutionRole", + "region": "-" + }, + "edge_id": "8fbe3500a1361fd80425ac88a46d7767c2413c97cc82b940525dead1001879b9", + "edge_type": "iam:PassRole_permission", + "features": { + "allow_controls": [ + { + "control_type": "PERMISSION", + "digest": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "policy_arn": "arn:aws:iam::000000000000:policy/IAMScopeLiveBindingSanitizedPolicy", + "statement_index": 1, + "summary": "iam:PassRole" + } + ], + "effect": "Allow", + "has_conditions": false, + "is_wildcard_resource": false, + "layer": "permission", + "raw_conditions": {}, + "resource_pattern": "arn:aws:iam::000000000000:role/IAMScopeLiveBindingLambdaExecutionRole", + "statement_index": 1 + }, + "region": "-", + "src": { + "node_type": "IAMUser", + "provider": "aws", + "provider_id": "arn:aws:iam::000000000000:user/IAMScopeLiveBindingSource", + "region": "-" + } + }, + { + "dst": { + "node_type": "IAMRole", + "provider": "aws", + "provider_id": "arn:aws:iam::000000000000:role/IAMScopeLiveBindingLambdaExecutionRole", + "region": "-" + }, + "edge_id": "24ad4d35b4441651fb0a4a321b53addfbdd843dc3653ff54dea29c220c478668", + "edge_type": "sts:AssumeRole_trust", + "features": { + "allow_controls": [ + { + "control_type": "TRUST", + "digest": "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd", + "policy_arn": "arn:aws:iam::000000000000:role/IAMScopeLiveBindingLambdaExecutionRole", + "statement_index": 0, + "summary": "lambda.amazonaws.com trust" + } + ], + "effect": "Allow", + "has_conditions": false, + "is_wildcard_principal": false, + "layer": "trust", + "principal_type": "Service", + "raw_conditions": {}, + "statement_index": 0 + }, + "region": "-", + "src": { + "node_type": "AWSService", + "provider": "aws", + "provider_id": "lambda.amazonaws.com", + "region": "-" + } + } + ], + "fixture_id": "passrole_lambda_selected_live_binding_finding_001", + "generation_mode": "local_reasoner_fixture", + "live_aws_used": false, + "nodes": [ + { + "node_id": "c23e6af725515592a63bf88a0d1bf77bbd18927e492295d573e5f058bcb84425", + "node_type": "IAMUser", + "properties": { + "account_id": "000000000000", + "sanitized": true + }, + "provider": "aws", + "provider_id": "arn:aws:iam::000000000000:user/IAMScopeLiveBindingSource", + "region": "-" + }, + { + "node_id": "9a8beffce76dcd9a52962f1be19c603a4bc97287d51b54296eec70942325d4ec", + "node_type": "IAMRole", + "properties": { + "account_id": "000000000000", + "sanitized": true + }, + "provider": "aws", + "provider_id": "arn:aws:iam::000000000000:role/IAMScopeLiveBindingLambdaExecutionRole", + "region": "-" + }, + { + "node_id": "dd76780b75405f72452650208a1129ca4dc959c43bcea761cbc3686e576d16d0", + "node_type": "AWSService", + "properties": {}, + "provider": "aws", + "provider_id": "lambda.amazonaws.com", + "region": "-" + } + ], + "redaction": { + "raw_live_account_id_committed": false, + "raw_live_role_arn_committed": false, + "raw_tmp_result_committed": false + }, + "scenario_hash": "facefeedfacefeedfacefeedfacefeedfacefeedfacefeedfacefeedfacefeed", + "scope_boundary": { + "represents_admin_equivalent_execution_role": false, + "represents_downstream_authorization": false, + "represents_exploitability": false, + "represents_lambda_invocation": false, + "represents_service_mediated_create_function": true + }, + "selected_path": { + "attempted_action": "lambda:CreateFunction", + "candidate_function_arn": "arn:aws:lambda:us-east-1:000000000000:function:iamscope-live-passrole-lambda-test-redacted", + "live_behavior_alignment": "service-mediated CreateFunction plus PassRole plus Lambda trust only", + "service_principal": "lambda.amazonaws.com", + "source_principal_arn": "arn:aws:iam::000000000000:user/IAMScopeLiveBindingSource", + "target_role_arn": "arn:aws:iam::000000000000:role/IAMScopeLiveBindingLambdaExecutionRole" + } +} diff --git a/tests/test_passrole_lambda_live_binding_fixture.py b/tests/test_passrole_lambda_live_binding_fixture.py new file mode 100644 index 0000000..db1a511 --- /dev/null +++ b/tests/test_passrole_lambda_live_binding_fixture.py @@ -0,0 +1,161 @@ +from __future__ import annotations + +import json +import re +from pathlib import Path +from typing import Any + +from iamscope.models import Edge, Node, NodeRef +from iamscope.reasoner import FactGraph, PassRoleLambdaReasoner, Verdict + +REPO_ROOT = Path(__file__).resolve().parents[1] +FIXTURE_DIR = REPO_ROOT / "tests" / "fixtures" / "live_binding" / "passrole_lambda_selected_finding" +SCENARIO = FIXTURE_DIR / "scenario.json" +EXPECTED = FIXTURE_DIR / "expected_finding.json" +README = FIXTURE_DIR / "README.md" +ALLOWED_SYNTHETIC_ACCOUNT = "000000000000" +FORBIDDEN_GENERATED_NAMES = { + "result.json", + "terraform.tfstate", + "terraform.tfstate.backup", + ".terraform.lock.hcl", + "terraform.tfvars", + "terraform-outputs.json", +} + + +def _load_json(path: Path) -> dict[str, Any]: + return json.loads(path.read_text()) + + +def _node_ref(payload: dict[str, Any]) -> NodeRef: + return NodeRef( + provider=payload["provider"], + node_type=payload["node_type"], + provider_id=payload["provider_id"], + region=payload["region"], + ) + + +def _fact_graph_from_scenario(payload: dict[str, Any]) -> FactGraph: + nodes = tuple( + Node( + provider=node["provider"], + node_type=node["node_type"], + provider_id=node["provider_id"], + region=node["region"], + properties=node.get("properties", {}), + ) + for node in payload["nodes"] + ) + edges = tuple( + Edge( + edge_type=edge["edge_type"], + src=_node_ref(edge["src"]), + dst=_node_ref(edge["dst"]), + region=edge["region"], + features=edge.get("features", {}), + ) + for edge in payload["edges"] + ) + return FactGraph( + nodes=nodes, + edges=edges, + constraints=(), + edge_constraints=(), + scenario_hash=payload["scenario_hash"], + edge_budget_exhausted=False, + ) + + +def test_selected_passrole_finding_is_generated_by_existing_reasoner() -> None: + scenario = _load_json(SCENARIO) + expected = _load_json(EXPECTED)["selected_finding"] + + findings = PassRoleLambdaReasoner().run(_fact_graph_from_scenario(scenario)) + + assert len(findings) == 1 + finding = findings[0] + assert finding.finding_id == expected["finding_id"] + assert finding.finding_key == expected["finding_key"] + assert finding.pattern_id == "passrole_lambda" + assert finding.verdict is Verdict.VALIDATED + assert finding.verdict.value == expected["expected_verdict"] + assert finding.severity == expected["severity"] + assert finding.severity == "high" + assert "admin-equivalent" not in finding.title.lower() + assert "exploit" not in finding.title.lower() + assert "downstream" not in finding.title.lower() + assert finding.source.provider_id == expected["source_principal_arn"] + assert finding.target.provider_id == expected["target_role_arn"] + assert {check.name: check.state.value for check in finding.required_checks} == expected["required_check_states"] + assert all(check.state.value == "pass" for check in finding.required_checks) + + +def test_fixture_is_passrole_lambda_specific_and_sanitized() -> None: + scenario = _load_json(SCENARIO) + selected = _load_json(EXPECTED)["selected_finding"] + + assert scenario["generation_mode"] == "local_reasoner_fixture" + assert scenario["live_aws_used"] is False + assert scenario["aws_calls_made"] == 0 + assert selected["pattern_id"] == "passrole_lambda" + assert selected["source_principal_arn"].startswith(f"arn:aws:iam::{ALLOWED_SYNTHETIC_ACCOUNT}:") + assert selected["target_role_arn"].startswith(f"arn:aws:iam::{ALLOWED_SYNTHETIC_ACCOUNT}:") + assert selected["expected_classification"] == "selected_local_createfunction_passrole_finding" + assert selected["live_behavior_alignment"] == "service-mediated CreateFunction plus PassRole plus Lambda trust only" + assert scenario["scope_boundary"] == { + "represents_admin_equivalent_execution_role": False, + "represents_downstream_authorization": False, + "represents_exploitability": False, + "represents_lambda_invocation": False, + "represents_service_mediated_create_function": True, + } + assert any(edge["edge_type"] == "lambda:CreateFunction_permission" for edge in scenario["edges"]) + assert any(edge["edge_type"] == "iam:PassRole_permission" for edge in scenario["edges"]) + assert any(edge["edge_type"] == "sts:AssumeRole_trust" for edge in scenario["edges"]) + assert not any(edge["edge_type"] == "iam:*_permission" for edge in scenario["edges"]) + + +def test_fixture_contains_no_raw_live_ids_or_generated_artifacts() -> None: + text = "\n".join(path.read_text() for path in FIXTURE_DIR.iterdir() if path.is_file()) + account_ids = set(re.findall(r"\b\d{12}\b", text)) + + assert account_ids <= {ALLOWED_SYNTHETIC_ACCOUNT} + selected = _load_json(EXPECTED)["selected_finding"] + + assert " None: + expected = _load_json(EXPECTED) + readme = README.read_text() + + assert expected["binding_status"] == "ready_for_next_checkpoint_comparison" + assert ( + expected["next_slice"] == "Recommended next slice: bind selected IAMScope PassRole finding to live AWS result." + ) + assert expected["non_claims"] == { + "no_live_aws": True, + "no_lambda_invocation_behavior": True, + "no_admin_equivalent_execution_role_claim": True, + "no_broad_iamscope_correctness": True, + "no_broad_passrole_correctness": True, + "no_exploitability_proof": True, + "no_downstream_authorization_proof": True, + "no_production_readiness": True, + "no_composite_benchmark_score": True, + "no_pass_fail_benchmark_label": True, + } + assert "does not run live AWS" in readme + assert "does not include an admin-equivalent execution role edge" in readme + assert "does not claim Lambda invocation behavior" in readme + assert "does not claim broad IAMScope correctness" in readme