Skip to content
Merged
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
Original file line number Diff line number Diff line change
@@ -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.
Original file line number Diff line number Diff line change
@@ -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"
}
}
Original file line number Diff line number Diff line change
@@ -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"
}
}
161 changes: 161 additions & 0 deletions tests/test_passrole_lambda_live_binding_fixture.py
Original file line number Diff line number Diff line change
@@ -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 "<redacted" not in text.lower()
assert "AdministratorAccess" not in text
assert "iam:*_permission" not in text
assert "admin-equivalent" not in selected["title"].lower()
assert "downstream" not in selected["title"].lower()
assert "exploit" not in selected["title"].lower()
assert "/tmp/iamscope-live-passrole-lambda-validation/result.json" not in text
assert FORBIDDEN_GENERATED_NAMES.isdisjoint({path.name for path in FIXTURE_DIR.iterdir()})
assert not any(path.suffix == ".tfplan" for path in FIXTURE_DIR.iterdir())


def test_fixture_documents_boundary_and_next_slice() -> 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