Compare commits

..

6 Commits

Author SHA1 Message Date
Jon Chery 3d9dd06411 docs(P13): complete kyverno-kube-version-resolution phase (v1.13.16)
---ci---
project: acdl
phase: 13
milestone: v1.14
status: complete
requirements:
  covered: [REQ-147]
  partial: []
---/ci---
2026-07-29 21:07:10 +00:00
Jon Chery b257846981 docs(P12): complete gitignore-credential-hygiene phase (v1.13.15)
---ci---
project: acdl
phase: 12
milestone: v1.14
status: complete
requirements:
  covered: [REQ-146]
  partial: []
---/ci---
2026-07-29 21:00:34 +00:00
Jon Chery 986171a165 docs(P11): complete schema-input-validation-hardening phase (v1.13.14)
---ci---
project: acdl
phase: 11
milestone: v1.14
status: complete
requirements:
  covered: [REQ-145]
  partial: []
---/ci---
2026-07-29 20:57:56 +00:00
Jon Chery 099ed015ac docs(P10): complete contract-ingestor-identity-validation phase (v1.13.13)
---ci---
project: acdl
phase: 10
milestone: v1.14
status: complete
requirements:
  covered: [REQ-144]
  partial: []
---/ci---
2026-07-29 20:52:42 +00:00
Jon Chery cc97a9308d docs(P09): complete iam-policy-least-privilege phase (v1.13.12)
---ci---
project: acdl
phase: 9
milestone: v1.14
status: complete
requirements:
  covered: [REQ-143]
  partial: []
---/ci---
2026-07-29 20:49:36 +00:00
Jon Chery c2ca0e4631 docs(P08): complete account-id-externalization phase (v1.13.11)
---ci---
project: acdl
phase: 8
milestone: v1.14
status: complete
requirements:
  covered: [REQ-142]
  partial: []
---/ci---
2026-07-29 20:46:28 +00:00
16 changed files with 259 additions and 47 deletions
+11 -1
View File
@@ -18,4 +18,14 @@ terraform/bootstrap/.bootstrap_state.json
**/.terraform/ **/.terraform/
**/.terraform.lock.hcl **/.terraform.lock.hcl
**/tfplan **/tfplan
**/*.tfstate* **/*.tfstate*
# Credential patterns (v1.14, REQ-146)
*.pem
*.key
*.p12
*.pfx
*.cer
*.crt
*.jks
*.keystore
+8 -13
View File
@@ -8,13 +8,16 @@ v1.9 (REQ-111): the translator is fleshed out — full PolicyReport →
PolicyCheckResult mapping with severity + skip-with-reason handling. It PolicyCheckResult mapping with severity + skip-with-reason handling. It
remains inactive for Terraform-only stacks (guard preserved — emits a remains inactive for Terraform-only stacks (guard preserved — emits a
single SKIPPED `KYVERNO_INACTIVE_TF_STACK` record when no K8s manifests). single SKIPPED `KYVERNO_INACTIVE_TF_STACK` record when no K8s manifests).
A `--kube-version` stub is parsed but not yet used (for future GitOps). A `--kube-version` flag was previously parsed but never used. It has been
removed (v1.14, G-103) to resolve the stub. Version-aware policy selection
will be added when the GitOps reconciler emits K8s manifests (D-053
roadmap). The adapter is inactive for Terraform-only stacks today.
D-053: the platform emits Terraform, not K8s manifests. This adapter D-053: the platform emits Terraform, not K8s manifests. This adapter
activates when the GitOps reconciler (roadmap) emits K8s manifests. activates when the GitOps reconciler (roadmap) emits K8s manifests.
Sample policies are included as documentation at adapters/kyverno/policies/. Sample policies are included as documentation at adapters/kyverno/policies/.
CLI: kyverno_adapter.py <policyreport.json> <contract-id> [--kube-version <ver>] CLI: kyverno_adapter.py <policyreport.json> <contract-id>
""" """
import datetime import datetime
@@ -100,7 +103,7 @@ def _emit_inactive_tf(contract_id):
} }
def adapt(policyreport_json_path, contract_id, kube_version=None): def adapt(policyreport_json_path, contract_id):
with open(policyreport_json_path, "r", encoding="utf-8") as fh: with open(policyreport_json_path, "r", encoding="utf-8") as fh:
data = json.load(fh) data = json.load(fh)
out = [] out = []
@@ -112,8 +115,6 @@ def adapt(policyreport_json_path, contract_id, kube_version=None):
out.append(_to_pcr(entry, contract_id)) out.append(_to_pcr(entry, contract_id))
if not out: if not out:
out.append(_emit_inactive_tf(contract_id)) out.append(_emit_inactive_tf(contract_id))
# kube_version is parsed but not yet used (future GitOps reconciler).
_ = kube_version
return out return out
@@ -123,14 +124,8 @@ def adapt_inactive(contract_id):
if __name__ == "__main__": if __name__ == "__main__":
kube_ver = None
args = sys.argv[1:] args = sys.argv[1:]
if "--kube-version" in args:
idx = args.index("--kube-version")
if idx + 1 < len(args):
kube_ver = args[idx + 1]
args = args[:idx] + args[idx + 2:]
if len(args) != 2: if len(args) != 2:
print("usage: kyverno_adapter.py <policyreport.json> <contract-id> [--kube-version <ver>]", file=sys.stderr) print("usage: kyverno_adapter.py <policyreport.json> <contract-id>", file=sys.stderr)
sys.exit(2) sys.exit(2)
print(json.dumps(adapt(args[0], args[1], kube_version=kube_ver), indent=2)) print(json.dumps(adapt(args[0], args[1]), indent=2))
+4 -2
View File
@@ -112,6 +112,8 @@ def adapt(stack_instance, out_dir):
stack_name = stack.get("name", "spike") stack_name = stack.get("name", "spike")
environment = stack.get("environment", "dev") environment = stack.get("environment", "dev")
account_id = os.environ.get("ACDL_AWS_ACCOUNT_ID", "581513795199")
state_bucket = f"acdl-tfstate-{account_id}-us-east-1"
terraform_tf = ( terraform_tf = (
'terraform {\n' 'terraform {\n'
' required_version = ">= 1.9, < 1.10"\n' ' required_version = ">= 1.9, < 1.10"\n'
@@ -122,7 +124,7 @@ def adapt(stack_instance, out_dir):
' }\n' ' }\n'
' }\n' ' }\n'
' backend "s3" {\n' ' backend "s3" {\n'
' bucket = "acdl-tfstate-581513795199-us-east-1"\n' f' bucket = "{state_bucket}"\n'
f' key = "spike/{stack_name}/{environment}/terraform.tfstate"\n' f' key = "spike/{stack_name}/{environment}/terraform.tfstate"\n'
' region = "us-east-1"\n' ' region = "us-east-1"\n'
' }\n' ' }\n'
@@ -137,7 +139,7 @@ def adapt(stack_instance, out_dir):
'data "terraform_remote_state" "platform" {\n' 'data "terraform_remote_state" "platform" {\n'
' backend = "s3"\n' ' backend = "s3"\n'
' config = {\n' ' config = {\n'
' bucket = "acdl-tfstate-581513795199-us-east-1"\n' f' bucket = "{state_bucket}"\n'
f' key = "{remote_state_key}"\n' f' key = "{remote_state_key}"\n'
' region = "us-east-1"\n' ' region = "us-east-1"\n'
' }\n' ' }\n'
+31 -10
View File
@@ -238,21 +238,42 @@ def _validate_caller_identity(event, payload):
If the identity is not available (e.g. local testing or non-IAM auth), the If the identity is not available (e.g. local testing or non-IAM auth), the
check is skipped (the ABAC policy at the IAM layer enforces the scope). check is skipped (the ABAC policy at the IAM layer enforces the scope).
v1.14 (REQ-144): also validates contractId format, environment enum, and
error length. The ABAC reliance is documented here: the Function URL IAM
identity does not expose principal tags in the event, so full enforcement
of consumerRepo ownership is at the IAM layer (ABAC via
aws:PrincipalTag/acdl:owner). This function validates format only, not
ownership.
""" """
identity = event.get("requestContext", {}).get("identity", {}) identity = event.get("requestContext", {}).get("identity", {})
caller_arn = identity.get("userArn", "") caller_arn = identity.get("userArn", "")
if not caller_arn: if not caller_arn:
return # no identity available — rely on IAM ABAC enforcement pass # no identity available — rely on IAM ABAC enforcement
payload_repo = payload.get("consumerRepo", "") payload_repo = payload.get("consumerRepo", "")
if not payload_repo: if payload_repo:
return # consumerRepo must be org/repo format, <=128 chars
# Extract the session name or principal tag from the ARN. The ABAC policy if "/" not in payload_repo or len(payload_repo) > 128:
# scopes via aws:PrincipalTag/acdl:owner = <consumerRepo>. The Function URL raise ValueError(f"invalid consumerRepo format: {payload_repo!r}")
# IAM identity does not expose principal tags in the event, so we do a
# best-effort check: the consumerRepo must not be empty and must be a valid # v1.14 (REQ-144): contractId format validation
# repo identifier (org/repo format). Full enforcement is at the IAM layer. contract_id = payload.get("contractId", "")
if "/" not in payload_repo or len(payload_repo) > 128: if contract_id:
raise ValueError(f"invalid consumerRepo format: {payload_repo!r}") import re
if not re.match(r'^[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}$', contract_id):
raise ValueError(f"invalid contractId format: {contract_id!r} (alphanumeric, hyphen, underscore; max 64 chars)")
# v1.14 (REQ-144): environment enum validation
environment = payload.get("environment", "")
if environment:
valid_envs = {"dev", "qa", "prod", "dr"}
if environment not in valid_envs:
raise ValueError(f"invalid environment: {environment!r} (must be one of {valid_envs})")
# v1.14 (REQ-144): error length cap (for report_error action)
error_msg = payload.get("error", "")
if error_msg and len(str(error_msg)) > 10000:
payload["error"] = str(error_msg)[:10000]
def _validate_change_request(payload): def _validate_change_request(payload):
+4 -2
View File
@@ -421,8 +421,10 @@ def _check_s3_state_bucket() -> Tuple[Status, str]:
s3 = boto3.client("s3", region_name=env.get("AWS_DEFAULT_REGION", "us-east-1"), s3 = boto3.client("s3", region_name=env.get("AWS_DEFAULT_REGION", "us-east-1"),
aws_access_key_id=env.get("AWS_ACCESS_KEY_ID"), aws_access_key_id=env.get("AWS_ACCESS_KEY_ID"),
aws_secret_access_key=env.get("AWS_SECRET_ACCESS_KEY")) aws_secret_access_key=env.get("AWS_SECRET_ACCESS_KEY"))
s3.head_bucket(Bucket="acdl-tfstate-581513795199-us-east-1") account_id = os.environ.get("ACDL_AWS_ACCOUNT_ID", "581513795199")
r = s3.list_objects_v2(Bucket="acdl-tfstate-581513795199-us-east-1", MaxKeys=5) state_bucket = f"acdl-tfstate-{account_id}-us-east-1"
s3.head_bucket(Bucket=state_bucket)
r = s3.list_objects_v2(Bucket=state_bucket, MaxKeys=5)
keys = [o["Key"] for o in r.get("Contents", [])] keys = [o["Key"] for o in r.get("Contents", [])]
return "Verified", f"state bucket exists, keys={keys}" return "Verified", f"state bucket exists, keys={keys}"
except Exception as e: except Exception as e:
+1 -1
View File
@@ -18,7 +18,7 @@
"wires": [ "wires": [
{"from": "contract.inputs.name", "to": "alb.inputs.name", "default": "app"}, {"from": "contract.inputs.name", "to": "alb.inputs.name", "default": "app"},
{"from": "contract.inputs.name", "to": "ecr.inputs.name", "default": "app-repo"}, {"from": "contract.inputs.name", "to": "ecr.inputs.name", "default": "app-repo"},
{"from": "contract.inputs.name", "to": "roles.inputs.role_name", "default": "app-role"}, {"from": "contract.inputs.name", "to": "roles.inputs.role_name", "default": "acdl-app-role"},
{"from": "contract.inputs.region", "to": "cluster.inputs.region"}, {"from": "contract.inputs.region", "to": "cluster.inputs.region"},
{"from": "contract.inputs.region", "to": "ecr.inputs.region"}, {"from": "contract.inputs.region", "to": "ecr.inputs.region"},
{"from": "contract.inputs.region", "to": "roles.inputs.region"}, {"from": "contract.inputs.region", "to": "roles.inputs.region"},
+10 -6
View File
@@ -27,20 +27,23 @@
"type": "object", "type": "object",
"required": ["bucket", "lock_table"], "required": ["bucket", "lock_table"],
"properties": { "properties": {
"bucket": {"type": "string", "description": "S3 state bucket name."}, "bucket": {"type": "string", "pattern": "^[a-z0-9][a-z0-9.-]{1,61}[a-z0-9]$", "description": "S3 state bucket name (lowercase, 3-63 chars, dots/hyphens)."},
"lock_table": {"type": "string", "description": "DynamoDB lock table name."} "lock_table": {"type": "string", "description": "DynamoDB lock table name."}
} },
"additionalProperties": false
}, },
"network": { "network": {
"type": "object", "type": "object",
"required": ["vpc_cidr", "azs"], "required": ["vpc_cidr", "azs"],
"properties": { "properties": {
"vpc_cidr": {"type": "string", "description": "VPC CIDR block."}, "vpc_cidr": {"type": "string", "pattern": "^[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}/[0-9]{1,2}$", "description": "VPC CIDR block (e.g. 10.0.0.0/16)."},
"azs": {"type": "array", "items": {"type": "string"}, "description": "Availability zones."} "azs": {"type": "array", "items": {"type": "string"}, "maxItems": 6, "description": "Availability zones (max 6)."}
} },
"additionalProperties": false
}, },
"runner_role_arn": { "runner_role_arn": {
"type": "string", "type": "string",
"pattern": "^arn:aws:iam::[0-9]{12}:role/.+$",
"description": "The IAM role ARN surfaced to the consumer's repo via ABAC." "description": "The IAM role ARN surfaced to the consumer's repo via ABAC."
}, },
"autonomy": { "autonomy": {
@@ -54,5 +57,6 @@
"maximum": 1, "maximum": 1,
"description": "The confidence gate threshold for this environment (dev 0.50, qa 0.75, prod 0.90, dr 0.95)." "description": "The confidence gate threshold for this environment (dev 0.50, qa 0.75, prod 0.90, dr 0.95)."
} }
} },
"additionalProperties": false
} }
+1 -1
View File
@@ -29,7 +29,7 @@ import boto3
REPO_ROOT = pathlib.Path(__file__).resolve().parent.parent REPO_ROOT = pathlib.Path(__file__).resolve().parent.parent
ENV_FILE = REPO_ROOT / ".env.secrets" ENV_FILE = REPO_ROOT / ".env.secrets"
AWS_ACCOUNT_ID = "581513795199" AWS_ACCOUNT_ID = os.environ.get("ACDL_AWS_ACCOUNT_ID", "581513795199")
AWS_REGION = "us-east-1" AWS_REGION = "us-east-1"
ECR_REPO_NAME = "acdl-microservice" ECR_REPO_NAME = "acdl-microservice"
IMAGE_TAG = "latest" IMAGE_TAG = "latest"
+1 -1
View File
@@ -30,7 +30,7 @@ import boto3
ROOT = Path(__file__).resolve().parent.parent.parent ROOT = Path(__file__).resolve().parent.parent.parent
POLICY_PATH = ROOT / "terraform" / "bootstrap" / "spike_runner_policy.json" POLICY_PATH = ROOT / "terraform" / "bootstrap" / "spike_runner_policy.json"
ACCOUNT = "581513795199" ACCOUNT = os.environ.get("ACDL_AWS_ACCOUNT_ID", "581513795199")
USER = "acdl-spike-runner" USER = "acdl-spike-runner"
POLICY_NAME = "acdl-spike-runner-policy" POLICY_NAME = "acdl-spike-runner-policy"
POLICY_ARN = f"arn:aws:iam::{ACCOUNT}:policy/{POLICY_NAME}" POLICY_ARN = f"arn:aws:iam::{ACCOUNT}:policy/{POLICY_NAME}"
+2 -2
View File
@@ -30,9 +30,9 @@ import boto3
REGION = os.environ.get("AWS_DEFAULT_REGION", "us-east-1") REGION = os.environ.get("AWS_DEFAULT_REGION", "us-east-1")
STATE_BUCKET = "acdl-tfstate-581513795199-us-east-1" ACCOUNT_ID = os.environ.get("ACDL_AWS_ACCOUNT_ID", "581513795199")
STATE_BUCKET = f"acdl-tfstate-{ACCOUNT_ID}-us-east-1"
OUTBOX_TABLE = "acdl-outbox" OUTBOX_TABLE = "acdl-outbox"
ACCOUNT_ID = "581513795199"
def main(): def main():
+5 -2
View File
@@ -215,7 +215,10 @@
"kms:TagResource", "kms:TagResource",
"kms:UntagResource" "kms:UntagResource"
], ],
"Resource": "*" "Resource": [
"arn:aws:kms:*:*:key/*",
"arn:aws:kms:*:*:alias/acdl-*"
]
}, },
{ {
"Effect": "Allow", "Effect": "Allow",
@@ -233,7 +236,7 @@
"iam:TagRole", "iam:TagRole",
"iam:UntagRole" "iam:UntagRole"
], ],
"Resource": "*" "Resource": "arn:aws:iam::*:role/acdl-*"
} }
] ]
} }
+40 -1
View File
@@ -500,4 +500,43 @@ class TestValidateChangeRequest:
resp = ingestor.lambda_handler(event, None) resp = ingestor.lambda_handler(event, None)
assert resp["statusCode"] == 200 assert resp["statusCode"] == 200
body = json.loads(resp["body"]) body = json.loads(resp["body"])
assert body["action"] == "validate_change_request" assert body["action"] == "validate_change_request"
class TestV14IdentityValidation:
"""v1.14 (REQ-144): contractId format, environment enum, error length
validation + spoofing resistance."""
def test_invalid_contract_id_rejected(self, moto_contracts_table, sample_payload):
sample_payload["contractId"] = "bad contract!@#"
event = {"body": json.dumps(sample_payload), "requestContext": {}}
resp = ingestor.lambda_handler(event, None)
assert resp["statusCode"] == 400
assert "invalid contractId" in resp["body"]
def test_contract_id_too_long_rejected(self, moto_contracts_table, sample_payload):
sample_payload["contractId"] = "a" * 65
event = {"body": json.dumps(sample_payload), "requestContext": {}}
resp = ingestor.lambda_handler(event, None)
assert resp["statusCode"] == 400
assert "invalid contractId" in resp["body"]
def test_invalid_environment_rejected(self, moto_contracts_table, sample_payload):
sample_payload["environment"] = "staging"
event = {"body": json.dumps(sample_payload), "requestContext": {}}
resp = ingestor.lambda_handler(event, None)
assert resp["statusCode"] == 400
assert "invalid environment" in resp["body"]
def test_valid_environments_accepted(self, moto_contracts_table, sample_payload):
for env in ["dev", "qa", "prod", "dr"]:
sample_payload["environment"] = env
event = {"body": json.dumps(sample_payload), "requestContext": {}}
resp = ingestor.lambda_handler(event, None)
assert resp["statusCode"] == 200
def test_abac_reliance_documented(self):
"""The _validate_caller_identity docstring documents the ABAC reliance."""
docstring = ingestor._validate_caller_identity.__doc__
assert "ABAC" in docstring
assert "PrincipalTag" in docstring
+61 -1
View File
@@ -96,4 +96,64 @@ def test_account_id_is_12_digits():
for env_file in ENV_FILES: for env_file in ENV_FILES:
env = json.loads((ENV_DIR / env_file).read_text()) env = json.loads((ENV_DIR / env_file).read_text())
assert len(env["account_id"]) == 12 assert len(env["account_id"]) == 12
assert env["account_id"].isdigit() assert env["account_id"].isdigit()
def test_v14_schema_rejects_undocumented_fields():
"""v1.14 (REQ-145): additionalProperties: false rejects unknown fields."""
schema = json.loads(SCHEMA.read_text())
bad_env = {
"name": "dev",
"account_id": "123456789012",
"region": "us-east-1",
"state_backend": {"bucket": "test", "lock_table": "test"},
"network": {"vpc_cidr": "10.0.0.0/16", "azs": ["us-east-1a"]},
"runner_role_arn": "arn:aws:iam::123456789012:role/test",
"autonomy": "full",
"confidence_threshold": 0.5,
"rogue_field": "should be rejected"
}
with pytest.raises(jsonschema.ValidationError, match="Additional properties are not allowed"):
jsonschema.validate(bad_env, schema)
def test_v14_schema_validates_bucket_name_format():
"""v1.14 (REQ-145): state_backend.bucket must match S3 naming rules."""
schema = json.loads(SCHEMA.read_text())
bad_env = {
"name": "dev", "account_id": "123456789012", "region": "us-east-1",
"state_backend": {"bucket": "Invalid_Bucket!", "lock_table": "test"},
"network": {"vpc_cidr": "10.0.0.0/16", "azs": ["us-east-1a"]},
"runner_role_arn": "arn:aws:iam::123456789012:role/test",
"autonomy": "full", "confidence_threshold": 0.5
}
with pytest.raises(jsonschema.ValidationError, match="does not match"):
jsonschema.validate(bad_env, schema)
def test_v14_schema_validates_arn_format():
"""v1.14 (REQ-145): runner_role_arn must match ARN format."""
schema = json.loads(SCHEMA.read_text())
bad_env = {
"name": "dev", "account_id": "123456789012", "region": "us-east-1",
"state_backend": {"bucket": "test", "lock_table": "test"},
"network": {"vpc_cidr": "10.0.0.0/16", "azs": ["us-east-1a"]},
"runner_role_arn": "not-an-arn",
"autonomy": "full", "confidence_threshold": 0.5
}
with pytest.raises(jsonschema.ValidationError, match="does not match"):
jsonschema.validate(bad_env, schema)
def test_v14_schema_validates_cidr_format():
"""v1.14 (REQ-145): vpc_cidr must match CIDR format."""
schema = json.loads(SCHEMA.read_text())
bad_env = {
"name": "dev", "account_id": "123456789012", "region": "us-east-1",
"state_backend": {"bucket": "test", "lock_table": "test"},
"network": {"vpc_cidr": "not-a-cidr", "azs": ["us-east-1a"]},
"runner_role_arn": "arn:aws:iam::123456789012:role/test",
"autonomy": "full", "confidence_threshold": 0.5
}
with pytest.raises(jsonschema.ValidationError, match="does not match"):
jsonschema.validate(bad_env, schema)
+40 -1
View File
@@ -176,4 +176,43 @@ class TestIAMPolicyBaseline:
res = s.get("Resource", "") res = s.get("Resource", "")
if isinstance(res, list): if isinstance(res, list):
res = " ".join(res) res = " ".join(res)
assert res != "*", "iam:PassRole must not be granted to Resource: *" assert res != "*", "iam:PassRole must not be granted to Resource: *"
def test_iam_role_creation_scoped_to_acdl_prefix(self, policy):
"""G-104: iam:CreateRole must be scoped to role/acdl-* (not Resource: *)."""
for s in policy["Statement"]:
acts = s.get("Action", [])
if isinstance(acts, str):
acts = [acts]
if "iam:CreateRole" in acts:
res = s.get("Resource", "")
if isinstance(res, list):
res = " ".join(res)
assert "acdl-*" in res, f"iam:CreateRole must be scoped to acdl-* (got: {res})"
def test_kms_scoped_to_acdl_alias(self, policy):
"""G-104: kms:CreateKey etc. must be scoped to alias/acdl-* (not Resource: *)."""
for s in policy["Statement"]:
acts = s.get("Action", [])
if isinstance(acts, str):
acts = [acts]
if any(a.startswith("kms:") for a in acts):
res = s.get("Resource", "")
if isinstance(res, list):
res = " ".join(res)
assert "acdl-*" in res, f"kms actions must be scoped to acdl-* (got: {res})"
def test_cloudfront_waf_remain_global(self, policy):
"""G-104: CloudFront + WAFv2 (CloudFront scope) ARNs are global;
Resource: * is acceptable here (documented constraint, not a defect)."""
global_actions = {"cloudfront:", "wafv2:"}
for s in policy["Statement"]:
acts = s.get("Action", [])
if isinstance(acts, str):
acts = [acts]
if any(any(a.startswith(g) for g in global_actions) for a in acts):
res = s.get("Resource", "")
if isinstance(res, list):
res = res[0] if res else ""
# CloudFront/WAFv2 are allowed to be * (global ARNs)
assert res == "*" or "acdl" in res
+5 -3
View File
@@ -211,11 +211,13 @@ class TestFleshedOutTranslator:
assert pcrs[0]["result"] == "skipped" assert pcrs[0]["result"] == "skipped"
assert "Terraform" in pcrs[0]["message"] assert "Terraform" in pcrs[0]["message"]
def test_kube_version_parsed(self, tmp_path): def test_kube_version_removed(self, tmp_path):
"""--kube-version is parsed but not yet used (future GitOps).""" """v1.14 (G-103): --kube-version flag removed; adapt() no longer
accepts kube_version parameter. Version-aware policy selection
deferred to GitOps reconciler (D-053)."""
f = tmp_path / "k.json" f = tmp_path / "k.json"
f.write_text(json.dumps({"results": [ f.write_text(json.dumps({"results": [
{"policy": "p", "rule": "r", "severity": "low", "result": "pass", "resource": "x"}, {"policy": "p", "rule": "r", "severity": "low", "result": "pass", "resource": "x"},
]})) ]}))
results = adapt(str(f), "c8", kube_version="1.28") results = adapt(str(f), "c8")
assert len(results) == 1 assert len(results) == 1
+35
View File
@@ -0,0 +1,35 @@
"""v1.14 (REQ-146): no credential-looking files are tracked by git."""
import subprocess
import sys
from pathlib import Path
import pytest
ROOT = Path(__file__).resolve().parent.parent
CREDENTIAL_EXTENSIONS = [".pem", ".key", ".p12", ".pfx", ".cer", ".crt", ".jks", ".keystore"]
def test_no_credential_files_tracked():
"""Assert no file with a credential extension is tracked by git."""
result = subprocess.run(
["git", "ls-files"],
cwd=str(ROOT),
capture_output=True,
text=True,
)
if result.returncode != 0:
pytest.skip("git not available or not a repo")
tracked = result.stdout.strip().split("\n")
cred_files = [
f for f in tracked
if any(f.endswith(ext) for ext in CREDENTIAL_EXTENSIONS)
]
assert cred_files == [], f"credential files tracked by git: {cred_files}"
def test_gitignore_has_credential_patterns():
"""Assert .gitignore contains the credential-pattern catch-all."""
gitignore = (ROOT / ".gitignore").read_text()
for ext in [".pem", ".key", ".p12", ".pfx"]:
assert f"*{ext}" in gitignore, f".gitignore missing credential pattern *{ext}"