Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3d9dd06411 | |||
| b257846981 | |||
| 986171a165 | |||
| 099ed015ac | |||
| cc97a9308d |
+11
-1
@@ -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 +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))
|
||||||
@@ -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):
|
||||||
|
|||||||
@@ -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"},
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
@@ -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-*"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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)
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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