diff --git a/tests/live/aws/prod_like_accuracy_sandbox/README.md b/tests/live/aws/prod_like_accuracy_sandbox/README.md new file mode 100644 index 0000000..beae260 --- /dev/null +++ b/tests/live/aws/prod_like_accuracy_sandbox/README.md @@ -0,0 +1,86 @@ +# IAMScope Prod-Like Accuracy Sandbox Terraform Source + +This directory contains Terraform source for a future dedicated IAMScope +prod-like accuracy sandbox. This PR only adds source files. It does not run +Terraform, does not call AWS, and does not create AWS resources. + +## Boundary + +- Terraform source only. +- Do not run in production. +- Dedicated sandbox account only. +- No Terraform apply in this PR. +- No AWS resources are created by this PR. +- Future command examples below are examples only. Do not run until Phase 4 approval. + +## Required Variables + +- `aws_profile`: explicit AWS profile for the future sandbox operator. +- `aws_region`: explicit AWS region. +- `expected_account_id`: expected dedicated sandbox AWS account ID. +- `resource_prefix`: defaults to `iamscope-prodlike-v1-`. +- `live_ack`: must equal `I_UNDERSTAND_THIS_IS_A_DEDICATED_IAMSCOPE_SANDBOX`. + +## Account Guard + +The Terraform source uses `data "aws_caller_identity" "current"` and a +Terraform-native `terraform_data` guard. Future Terraform evaluation must fail +unless: + +- the caller account matches `expected_account_id`; +- `live_ack` matches the required acknowledgement; +- `resource_prefix` begins with `iamscope-prodlike-v1-`. + +## Future Commands, Do Not Run Until Phase 4 Approval + +```bash +terraform init +terraform plan \ + -var 'aws_profile=' \ + -var 'aws_region=' \ + -var 'expected_account_id=' \ + -var 'live_ack=I_UNDERSTAND_THIS_IS_A_DEDICATED_IAMSCOPE_SANDBOX' +terraform apply +``` + +Do not run these commands until the Terraform source has its own review and +Phase 4 is explicitly approved. + +## Cleanup Expectations For Future Phase 4 + +Future Phase 4 must include: + +- `terraform destroy` instructions; +- cleanup verification that created IAM users, roles, policies, and attachments are gone; +- sanitized cleanup proof only; +- no orphaned IAM resources. + +## Artifact Hygiene + +Do not commit: + +- `.terraform/` +- `.terraform.lock.hcl` +- `terraform.tfstate` +- `terraform.tfstate.backup` +- `*.tfplan` +- `terraform-outputs.json` +- `result.json` +- `*.log` +- raw account IDs +- raw IAM ARNs +- secrets + +## Non-Claims + +- not broad IAMScope correctness +- not production readiness +- not real production AWS +- not exploitability proof +- not downstream authorization proof +- not Lambda invocation behavior +- not generic Deny correctness +- not resource-policy Deny support except unsupported/static-only row labeling +- not SCP Deny support beyond selected benchmark behavior +- no composite benchmark score +- no pass/fail benchmark label diff --git a/tests/live/aws/prod_like_accuracy_sandbox/terraform/.gitignore b/tests/live/aws/prod_like_accuracy_sandbox/terraform/.gitignore new file mode 100644 index 0000000..118fc20 --- /dev/null +++ b/tests/live/aws/prod_like_accuracy_sandbox/terraform/.gitignore @@ -0,0 +1,8 @@ +.terraform/ +.terraform.lock.hcl +terraform.tfstate +terraform.tfstate.backup +*.tfplan +terraform-outputs.json +result.json +*.log diff --git a/tests/live/aws/prod_like_accuracy_sandbox/terraform/main.tf b/tests/live/aws/prod_like_accuracy_sandbox/terraform/main.tf new file mode 100644 index 0000000..d506174 --- /dev/null +++ b/tests/live/aws/prod_like_accuracy_sandbox/terraform/main.tf @@ -0,0 +1,380 @@ +terraform { + required_version = ">= 1.5.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 5.0" + } + } +} + +provider "aws" { + profile = var.aws_profile + region = var.aws_region + + default_tags { + tags = local.common_tags + } +} + +data "aws_caller_identity" "current" {} + +locals { + required_live_ack = "I_UNDERSTAND_THIS_IS_A_DEDICATED_IAMSCOPE_SANDBOX" + + common_tags = { + Project = "IAMScope" + Purpose = "ProdLikeAccuracySandbox" + Owner = "TestOnly" + ManagedBy = "Terraform" + } + + source_principals = { + ci_deployer = "ci-deployer" + ecs_deployer = "ecs-deployer" + helpdesk = "helpdesk" + build = "build" + audit = "audit" + boundary_probe = "boundary-probe" + deny_probe = "deny-probe" + uncertainty_probe = "uncertainty-probe" + } + + target_roles = { + lambda_exec_scoped = "lambda-exec-scoped" + ecs_task_scoped = "ecs-task-scoped" + readonly_ops = "readonly-ops" + prod_observer = "prod-observer" + audit_b = "audit-b" + service_mediated_target = "service-mediated-target" + lambda_exec_boundary = "lambda-exec-boundary" + chain_target = "chain-target" + scp_passrole_target = "scp-passrole-target" + denied_assume = "denied-assume" + } + + permission_boundary_names = { + passrole_lambda = "boundary-passrole-lambda" + assume_chain = "boundary-assume-chain" + session_context = "boundary-session-context" + } + + account_guardrail_policy_names = { + passrole_guardrail = "guardrail-passrole" + service_guardrail = "guardrail-service" + scope_guardrail = "guardrail-scope" + } + + unsupported_static_only_rows = [ + "oracle-u-001", + "oracle-u-002", + "oracle-u-003", + "oracle-u-004", + ] + + oracle_row_mapping = { + oracle-v-001 = { + resource_group = "passrole_lambda_allowed" + source_principal = "ci_deployer" + target_role = "lambda_exec_scoped" + live_representable = "yes" + cleanup_requirement = "delete policies, source principal, target role, and any future test Lambda function" + risk_note = "no Lambda invocation" + } + oracle-v-002 = { + resource_group = "passrole_ecs_allowed" + source_principal = "ecs_deployer" + target_role = "ecs_task_scoped" + live_representable = "partial" + cleanup_requirement = "delete policies, source principal, and target role" + risk_note = "no service launch by default" + } + oracle-v-003 = { + resource_group = "assume_role_direct_allowed" + source_principal = "helpdesk" + target_role = "readonly_ops" + live_representable = "yes" + cleanup_requirement = "delete policies, source principal, and target role" + risk_note = "optional assume role probe only if reviewed" + } + oracle-v-004 = { + resource_group = "assume_role_two_hop_allowed" + source_principal = "build" + target_role = "prod_observer" + live_representable = "partial" + cleanup_requirement = "delete policies and chain roles" + risk_note = "avoid credential chaining unless separately approved" + } + oracle-v-005 = { + resource_group = "cross_account_trust_condition_satisfied" + source_principal = "audit" + target_role = "audit_b" + live_representable = "partial" + cleanup_requirement = "delete trust roles and policies" + risk_note = "use aliases if second account is too risky" + } + oracle-v-006 = { + resource_group = "service_mediated_role_path" + source_principal = "ci_deployer" + target_role = "service_mediated_target" + live_representable = "partial" + cleanup_requirement = "delete policies and roles" + risk_note = "no downstream action" + } + oracle-b-001 = { + resource_group = "boundary_blocks_passrole_lambda" + source_principal = "boundary_probe" + target_role = "lambda_exec_boundary" + live_representable = "yes" + cleanup_requirement = "delete boundary policy, source principal, and target role" + risk_note = "boundary must not affect operator identity" + } + oracle-b-002 = { + resource_group = "boundary_blocks_assume_chain" + source_principal = "boundary_probe" + target_role = "chain_target" + live_representable = "partial" + cleanup_requirement = "delete boundary policy and roles" + risk_note = "avoid credential chaining unless separately approved" + } + oracle-b-003 = { + resource_group = "scp_like_blocks_passrole" + source_principal = "deny_probe" + target_role = "scp_passrole_target" + live_representable = "partial" + cleanup_requirement = "delete guardrail policies and roles" + risk_note = "IAM-only guardrail simulation, not generic SCP support" + } + oracle-b-004 = { + resource_group = "identity_deny_suppresses_assume" + source_principal = "deny_probe" + target_role = "denied_assume" + live_representable = "partial" + cleanup_requirement = "delete deny policy and roles" + risk_note = "not generic Deny correctness" + } + oracle-b-005 = { + resource_group = "explicit_deny_service_permission" + source_principal = "deny_probe" + target_role = "service_mediated_target" + live_representable = "partial" + cleanup_requirement = "delete deny policy and roles" + risk_note = "no destructive service action" + } + oracle-p-001 = { + resource_group = "missing_passrole_precondition" + source_principal = "ci_deployer" + target_role = "lambda_exec_boundary" + live_representable = "yes" + cleanup_requirement = "delete policies and roles" + risk_note = "candidate for denied CreateFunction probe" + } + oracle-p-002 = { + resource_group = "missing_target_service_trust" + source_principal = "ci_deployer" + target_role = "readonly_ops" + live_representable = "partial" + cleanup_requirement = "delete target role and policies" + risk_note = "no service launch" + } + oracle-p-003 = { + resource_group = "missing_service_action" + source_principal = "helpdesk" + target_role = "lambda_exec_scoped" + live_representable = "yes" + cleanup_requirement = "delete policies and roles" + risk_note = "no live service action" + } + oracle-p-004 = { + resource_group = "missing_assume_role_permission" + source_principal = "build" + target_role = "readonly_ops" + live_representable = "yes" + cleanup_requirement = "delete trust role and policies" + risk_note = "optional denied AssumeRole probe only if reviewed" + } + oracle-i-001 = { + resource_group = "wildcard_resource_scope_unknown" + source_principal = "uncertainty_probe" + target_role = "lambda_exec_scoped" + live_representable = "yes" + cleanup_requirement = "delete policies and roles" + risk_note = "must remain inconclusive unless resolved" + } + oracle-i-002 = { + resource_group = "unresolved_condition_key" + source_principal = "uncertainty_probe" + target_role = "readonly_ops" + live_representable = "partial" + cleanup_requirement = "delete conditional policies and roles" + risk_note = "do not assume condition satisfaction" + } + oracle-i-003 = { + resource_group = "session_or_boundary_context_missing" + source_principal = "uncertainty_probe" + target_role = "chain_target" + live_representable = "partial" + cleanup_requirement = "delete roles and policies" + risk_note = "do not claim downstream authorization" + } + oracle-i-004 = { + resource_group = "scp_like_scope_unknown" + source_principal = "deny_probe" + target_role = "scp_passrole_target" + live_representable = "partial" + cleanup_requirement = "delete guardrail policies and roles" + risk_note = "not generic SCP support" + } + oracle-i-005 = { + resource_group = "cross_account_trust_condition_unknown" + source_principal = "audit" + target_role = "audit_b" + live_representable = "partial" + cleanup_requirement = "delete trust roles and policies" + risk_note = "use aliases if second account is too risky" + } + oracle-u-001 = { + resource_group = "unsupported_resource_policy_deny" + source_principal = null + target_role = null + live_representable = "static-only" + cleanup_requirement = "no live resource" + risk_note = "unsupported static-only row, not false positive or false negative" + } + oracle-u-002 = { + resource_group = "unsupported_service_condition_semantics" + source_principal = null + target_role = null + live_representable = "static-only" + cleanup_requirement = "no live resource" + risk_note = "unsupported static-only row, not false positive or false negative" + } + oracle-u-003 = { + resource_group = "unsupported_lambda_invocation_behavior" + source_principal = null + target_role = null + live_representable = "static-only" + cleanup_requirement = "no live resource" + risk_note = "unsupported static-only row, no Lambda invocation" + } + oracle-u-004 = { + resource_group = "unsupported_downstream_authorization" + source_principal = null + target_role = null + live_representable = "static-only" + cleanup_requirement = "no live resource" + risk_note = "unsupported static-only row, no exploitability proof" + } + } +} + +resource "terraform_data" "safety_guards" { + input = { + account_id = data.aws_caller_identity.current.account_id + resource_prefix = var.resource_prefix + } + + lifecycle { + precondition { + condition = data.aws_caller_identity.current.account_id == var.expected_account_id + error_message = "Caller account does not match expected_account_id." + } + + precondition { + condition = var.live_ack == local.required_live_ack + error_message = "live_ack does not match the required dedicated sandbox acknowledgement." + } + + precondition { + condition = startswith(var.resource_prefix, "iamscope-prodlike-v1-") + error_message = "resource_prefix must begin with iamscope-prodlike-v1-." + } + } +} + +data "aws_iam_policy_document" "target_role_trust" { + for_each = local.target_roles + + statement { + effect = "Allow" + actions = ["sts:AssumeRole"] + + principals { + type = "AWS" + identifiers = [ + "arn:aws:iam::${data.aws_caller_identity.current.account_id}:root", + ] + } + } +} + +resource "aws_iam_user" "source" { + for_each = local.source_principals + + name = "${var.resource_prefix}${each.value}" + + tags = merge(local.common_tags, { + OracleFixture = "prod_like_aws_accuracy_oracle_v1" + SandboxRole = "source-principal" + }) +} + +resource "aws_iam_role" "target" { + for_each = local.target_roles + + name = "${var.resource_prefix}${each.value}" + assume_role_policy = data.aws_iam_policy_document.target_role_trust[each.key].json + + tags = merge(local.common_tags, { + OracleFixture = "prod_like_aws_accuracy_oracle_v1" + SandboxRole = "target-role" + }) +} + +data "aws_iam_policy_document" "boundary" { + for_each = local.permission_boundary_names + + statement { + effect = "Allow" + actions = ["iam:GetRole", "iam:ListRolePolicies", "iam:GetPolicy", "iam:GetPolicyVersion"] + resources = ["*"] + } +} + +resource "aws_iam_policy" "permission_boundary" { + for_each = local.permission_boundary_names + + name = "${var.resource_prefix}${each.value}" + description = "IAMScope prod-like v1 permission boundary fixture policy." + policy = data.aws_iam_policy_document.boundary[each.key].json + + tags = merge(local.common_tags, { + OracleFixture = "prod_like_aws_accuracy_oracle_v1" + SandboxRole = "permission-boundary" + }) +} + +data "aws_iam_policy_document" "guardrail_simulation" { + for_each = local.account_guardrail_policy_names + + statement { + sid = "DenySelectedSandboxAction" + effect = "Deny" + actions = ["iam:PassRole"] + resources = ["*"] + } +} + +resource "aws_iam_policy" "guardrail_simulation" { + for_each = local.account_guardrail_policy_names + + name = "${var.resource_prefix}${each.value}" + description = "IAMScope prod-like v1 IAM-only guardrail simulation policy." + policy = data.aws_iam_policy_document.guardrail_simulation[each.key].json + + tags = merge(local.common_tags, { + OracleFixture = "prod_like_aws_accuracy_oracle_v1" + SandboxRole = "guardrail-simulation" + }) +} diff --git a/tests/live/aws/prod_like_accuracy_sandbox/terraform/outputs.tf b/tests/live/aws/prod_like_accuracy_sandbox/terraform/outputs.tf new file mode 100644 index 0000000..05425eb --- /dev/null +++ b/tests/live/aws/prod_like_accuracy_sandbox/terraform/outputs.tf @@ -0,0 +1,29 @@ +output "source_principal_user_names" { + description = "IAM user names for future controlled collection. No credentials or secrets." + value = { for key, user in aws_iam_user.source : key => user.name } +} + +output "source_principal_user_arns" { + description = "IAM user ARNs for future controlled collection. These must be sanitized before commit." + value = { for key, user in aws_iam_user.source : key => user.arn } +} + +output "target_role_names" { + description = "IAM role names for future controlled collection." + value = { for key, role in aws_iam_role.target : key => role.name } +} + +output "target_role_arns" { + description = "IAM role ARNs for future controlled collection. These must be sanitized before commit." + value = { for key, role in aws_iam_role.target : key => role.arn } +} + +output "oracle_row_mapping" { + description = "Static mapping from frozen oracle rows to planned Terraform resource groups." + value = local.oracle_row_mapping +} + +output "unsupported_static_only_rows" { + description = "Unsupported rows that have no live resources in v1." + value = local.unsupported_static_only_rows +} diff --git a/tests/live/aws/prod_like_accuracy_sandbox/terraform/variables.tf b/tests/live/aws/prod_like_accuracy_sandbox/terraform/variables.tf new file mode 100644 index 0000000..3cccf50 --- /dev/null +++ b/tests/live/aws/prod_like_accuracy_sandbox/terraform/variables.tf @@ -0,0 +1,44 @@ +variable "aws_profile" { + description = "Explicit AWS profile for the future dedicated IAMScope sandbox." + type = string + nullable = false +} + +variable "aws_region" { + description = "Explicit AWS region for the future dedicated IAMScope sandbox." + type = string + nullable = false +} + +variable "expected_account_id" { + description = "Expected dedicated sandbox AWS account ID. Must match the caller identity account." + type = string + nullable = false + + validation { + condition = can(regex("^[0-9]{12}$", var.expected_account_id)) + error_message = "expected_account_id must be a 12-digit AWS account ID for the future dedicated sandbox." + } +} + +variable "resource_prefix" { + description = "Resource name prefix for the IAMScope prod-like v1 sandbox." + type = string + default = "iamscope-prodlike-v1-" + + validation { + condition = startswith(var.resource_prefix, "iamscope-prodlike-v1-") + error_message = "resource_prefix must begin with iamscope-prodlike-v1-." + } +} + +variable "live_ack" { + description = "Explicit acknowledgement required before any future live sandbox use." + type = string + nullable = false + + validation { + condition = var.live_ack == "I_UNDERSTAND_THIS_IS_A_DEDICATED_IAMSCOPE_SANDBOX" + error_message = "live_ack must equal I_UNDERSTAND_THIS_IS_A_DEDICATED_IAMSCOPE_SANDBOX." + } +} diff --git a/tests/test_prod_like_terraform_sandbox_files.py b/tests/test_prod_like_terraform_sandbox_files.py new file mode 100644 index 0000000..cf88992 --- /dev/null +++ b/tests/test_prod_like_terraform_sandbox_files.py @@ -0,0 +1,188 @@ +from __future__ import annotations + +import re +from pathlib import Path + +REPO_ROOT = Path(__file__).resolve().parents[1] +SANDBOX_DIR = REPO_ROOT / "tests" / "live" / "aws" / "prod_like_accuracy_sandbox" +TERRAFORM_DIR = SANDBOX_DIR / "terraform" +README = SANDBOX_DIR / "README.md" +REQUIRED_FILES = { + "main.tf", + "variables.tf", + "outputs.tf", + ".gitignore", +} +REQUIRED_GITIGNORE = { + ".terraform/", + ".terraform.lock.hcl", + "terraform.tfstate", + "terraform.tfstate.backup", + "*.tfplan", + "terraform-outputs.json", + "result.json", + "*.log", +} +REQUIRED_ROW_IDS = { + *(f"oracle-v-{index:03d}" for index in range(1, 7)), + *(f"oracle-b-{index:03d}" for index in range(1, 6)), + *(f"oracle-p-{index:03d}" for index in range(1, 5)), + *(f"oracle-i-{index:03d}" for index in range(1, 6)), + *(f"oracle-u-{index:03d}" for index in range(1, 5)), +} +UNSUPPORTED_ROW_IDS = { + "oracle-u-001", + "oracle-u-002", + "oracle-u-003", + "oracle-u-004", +} +NON_CLAIMS = { + "not broad IAMScope correctness", + "not production readiness", + "not real production AWS", + "not exploitability proof", + "not downstream authorization proof", + "not Lambda invocation behavior", + "not generic Deny correctness", + "not resource-policy Deny support except unsupported/static-only row labeling", + "not SCP Deny support beyond selected benchmark behavior", + "no composite benchmark score", + "no pass/fail benchmark label", +} + + +def _terraform_text() -> str: + return "\n".join((TERRAFORM_DIR / name).read_text(encoding="utf-8") for name in REQUIRED_FILES) + + +def _all_sandbox_text() -> str: + return README.read_text(encoding="utf-8") + "\n" + _terraform_text() + + +def test_required_terraform_files_and_readme_exist() -> None: + assert README.is_file() + assert {path.name for path in TERRAFORM_DIR.iterdir() if path.is_file()} == REQUIRED_FILES + + +def test_gitignore_excludes_local_terraform_and_live_artifacts() -> None: + patterns = { + line.strip() for line in (TERRAFORM_DIR / ".gitignore").read_text(encoding="utf-8").splitlines() if line.strip() + } + assert patterns >= REQUIRED_GITIGNORE + + +def test_terraform_contains_account_ack_and_prefix_guards() -> None: + text = _terraform_text() + + assert 'variable "aws_profile"' in text + assert 'variable "aws_region"' in text + assert 'variable "expected_account_id"' in text + assert 'variable "resource_prefix"' in text + assert 'variable "live_ack"' in text + assert 'data "aws_caller_identity" "current"' in text + assert 'resource "terraform_data" "safety_guards"' in text + assert "data.aws_caller_identity.current.account_id == var.expected_account_id" in text + assert 'var.live_ack == "I_UNDERSTAND_THIS_IS_A_DEDICATED_IAMSCOPE_SANDBOX"' in text + assert 'startswith(var.resource_prefix, "iamscope-prodlike-v1-")' in text + + +def test_provider_and_tags_are_bounded_to_aws_provider() -> None: + text = _terraform_text() + + assert 'source = "hashicorp/aws"' in text + assert 'provider "aws"' in text + assert 'Project = "IAMScope"' in text + assert 'Purpose = "ProdLikeAccuracySandbox"' in text + assert 'Owner = "TestOnly"' in text + assert 'ManagedBy = "Terraform"' in text + + +def test_no_raw_non_synthetic_account_ids_or_iam_arns() -> None: + text = _all_sandbox_text() + account_ids = set(re.findall(r"\b[0-9]{12}\b", text)) + raw_iam_arn_accounts = set(re.findall(r"arn:aws:iam::([0-9]{12})", text)) + + assert account_ids <= {"000000000000"} + assert raw_iam_arn_accounts <= {"000000000000"} + + +def test_terraform_files_avoid_work_identifiers() -> None: + text = _terraform_text().lower() + + assert "work account" not in text + assert "corporate" not in text + assert "company" not in text + assert "employee" not in text + + +def test_all_oracle_rows_are_mapped() -> None: + text = _terraform_text() + + for row_id in REQUIRED_ROW_IDS: + assert row_id in text + assert len(re.findall(r"oracle-[vbpiu]-[0-9]{3}", text)) >= len(REQUIRED_ROW_IDS) + + +def test_unsupported_rows_are_static_only_or_no_live_resource() -> None: + text = _terraform_text() + + for row_id in UNSUPPORTED_ROW_IDS: + start = text.index(f"{row_id} = {{") + block = text[start : start + 500] + assert "static-only" in block + assert "no live resource" in block + + +def test_no_lambda_invocation_compute_networking_or_organizations_resources() -> None: + text = _terraform_text() + lowered = text.lower() + + assert "lambda:invokefunction" not in lowered + assert "aws_lambda_function" not in lowered + assert "aws_lambda_function_url" not in lowered + assert "aws_lambda_event_source_mapping" not in lowered + assert "aws_vpc" not in lowered + assert "aws_subnet" not in lowered + assert "aws_internet_gateway" not in lowered + assert "aws_route_table" not in lowered + assert "aws_organizations" not in lowered + + +def test_readme_warns_not_to_run_until_phase4_and_lists_non_claims() -> None: + readme = README.read_text(encoding="utf-8") + + assert "Do not run in production." in readme + assert "Dedicated sandbox account only." in readme + assert "No Terraform apply in this PR." in readme + assert "No AWS resources are created by this PR." in readme + assert "Do not run until Phase 4 approval" in readme + assert "expected_account_id" in readme + assert "I_UNDERSTAND_THIS_IS_A_DEDICATED_IAMSCOPE_SANDBOX" in readme + for claim in NON_CLAIMS: + assert claim in readme + + +def test_no_generated_terraform_or_live_artifacts_are_committed() -> None: + forbidden_names = { + "result.json", + "terraform.tfstate", + "terraform.tfstate.backup", + ".terraform.lock.hcl", + "terraform-outputs.json", + } + forbidden_suffixes = (".tfplan", ".log") + + files = [path for path in SANDBOX_DIR.rglob("*") if path.is_file()] + assert forbidden_names.isdisjoint({path.name for path in files}) + assert not any(path.name.endswith(forbidden_suffixes) for path in files) + assert not any(".terraform" in path.parts for path in files) + + +def test_no_score_or_pass_fail_machine_fields_are_introduced() -> None: + text = _all_sandbox_text().lower() + + assert "composite_score" not in text + assert "benchmark_passed" not in text + assert "pass_fail" not in text + assert "no composite benchmark score" in text + assert "no pass/fail benchmark label" in text