Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b257846981 | |||
| 986171a165 |
+10
@@ -19,3 +19,13 @@ terraform/bootstrap/.bootstrap_state.json
|
|||||||
**/.terraform.lock.hcl
|
**/.terraform.lock.hcl
|
||||||
**/tfplan
|
**/tfplan
|
||||||
**/*.tfstate*
|
**/*.tfstate*
|
||||||
|
|
||||||
|
# Credential patterns (v1.14, REQ-146)
|
||||||
|
*.pem
|
||||||
|
*.key
|
||||||
|
*.p12
|
||||||
|
*.pfx
|
||||||
|
*.cer
|
||||||
|
*.crt
|
||||||
|
*.jks
|
||||||
|
*.keystore
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
@@ -97,3 +97,63 @@ def test_account_id_is_12_digits():
|
|||||||
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)
|
||||||
@@ -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}"
|
||||||
Reference in New Issue
Block a user