Skip to content
Draft
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
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,11 @@ chains. Its allowed keys come from `Step.to_dict()`, `Chain.to_dict()` and `writ
techniques, adapters, classifications and scope modes reuse the validation report's vocabularies.
Unknown fields and invalid types fail, including agent-supplied approval or scope grants.

A step may contain a structured `target.http` probe with method, destination
selection, headers and authentication preconditions. Such a step cannot carry
command or rollback text. The shape does not grant endpoint access; the
executor checks the engagement and method before issuing the request.

This schema **does not authorize execution**. A caller must bind the approved artifact digest
to a verified principal and current scope, classify each command against its actual operands,
and re-check the target immediately before dispatch. Browser plans use a different envelope and
Expand All @@ -227,6 +232,13 @@ precondition references and chain links also need service checks: JSON Schema va
item, not relationships among items. An edited or recon-extended plan is a new artifact and
needs a new authorization decision.

`validation-authorization.schema.json` also preserves the approved scope snapshot.
Its optional `expires_at` is a timezone-aware deadline; date-only `expires` retains
its existing meaning. The snapshot includes exact `http_targets` and each cluster's
API identity and HTTP discovery policy, including route domains, node networks,
credential permissions and TLS/session settings. These fields record grants; the
execution service enforces their expiry and scope.

## Complete impact preparation snapshots

`impact-analysis` metadata may name a `preparation_scope`: a content digest and
Expand Down
54 changes: 53 additions & 1 deletion artifacts_schema/v1/validation-authorization.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
"authorized_by": {"type": ["string", "null"]},
"environment": {"type": ["string", "null"]},
"expires": {"oneOf": [{"type": "string", "format": "date"}, {"type": "null"}]},
"expires_at": {"oneOf": [{"type": "string", "format": "date-time"}, {"type": "null"}]},
"modes": {
"type": "array", "minItems": 1, "uniqueItems": true,
"items": {
Expand All @@ -57,6 +58,7 @@
}
},
"clusters": {"type": "array", "items": {"$ref": "#/$defs/cluster"}},
"http_targets": {"type": "array", "items": {"$ref": "#/$defs/http_target"}},
"off_limits": {"type": "array", "items": {"$ref": "#/$defs/off_limit"}},
"browser": {"type": "array", "items": {"$ref": "#/$defs/browser"}},
"containers": {"$ref": "#/$defs/names"},
Expand All @@ -75,7 +77,57 @@
"context": {"type": "string", "minLength": 1},
"namespaces": {"$ref": "#/$defs/names"},
"explicit_namespaces": {"$ref": "#/$defs/names"},
"verbs_denied": {"$ref": "#/$defs/names"}
"verbs_denied": {"$ref": "#/$defs/names"},
"api": {"type": ["string", "null"]},
"http_discovery": {"oneOf": [{"$ref": "#/$defs/http_discovery"}, {"type": "null"}]}
}
},
"http_target": {
"type": "object",
"required": ["host", "context"],
"additionalProperties": false,
"properties": {
"host": {"type": "string", "minLength": 1},
"context": {"type": "string", "minLength": 1},
"namespace": {"type": ["string", "null"]},
"resource": {"enum": ["routes", "nodes", "http_targets", null]},
"name": {"type": ["string", "null"]},
"credentials": {"type": "boolean"}
}
},
"http_discovery": {
"type": "object",
"additionalProperties": false,
"properties": {
"routes": {"type": "array", "items": {"$ref": "#/$defs/http_route_grant"}},
"nodes": {"oneOf": [{"$ref": "#/$defs/http_node_grant"}, {"type": "null"}]},
"port_forward_credentials": {"type": "boolean"},
"kube_env": {"type": "array", "items": {"type": "string"}},
"ca_bundle": {"type": ["string", "null"]},
"insecure_tls": {"type": "boolean"},
"session_check_path": {"type": ["string", "null"]},
"session_check_contains": {"type": ["string", "null"]}
}
},
"http_route_grant": {
"type": "object",
"required": ["namespace"],
"additionalProperties": false,
"properties": {
"namespace": {"type": "string", "minLength": 1},
"names": {"type": "array", "items": {"type": "string", "minLength": 1}},
"domains": {"type": "array", "items": {"type": "string", "minLength": 1}},
"credentials": {"type": "boolean"}
}
},
"http_node_grant": {
"type": "object",
"additionalProperties": false,
"properties": {
"names": {"type": "array", "items": {"type": "string", "minLength": 1}},
"networks": {"type": "array", "items": {"type": "string", "minLength": 1}},
"address_types": {"type": "array", "items": {"enum": ["InternalIP", "ExternalIP"]}},
"credentials": {"type": "boolean"}
}
},
"off_limit": {
Expand Down
30 changes: 28 additions & 2 deletions artifacts_schema/v1/validation-plan.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,13 @@
"skip": {"type": "string", "minLength": 1},
"summary": {"type": "string"},
"triage_was_needs_review": {"type": "boolean"}
}
},
"allOf": [
{
"if": {"properties": {"target": {"required": ["http"]}}, "required": ["target"]},
"then": {"properties": {"cmd": {"maxLength": 0}, "rollback": {"maxLength": 0}}}
}
]
},
"step_target": {
"type": "object",
Expand All @@ -67,7 +73,27 @@
"method": {"type": "string"},
"path": {"type": "string"},
"port": {"type": "integer", "minimum": 1, "maximum": 65535},
"pqc": {"type": "boolean"}
"pqc": {"type": "boolean"},
"http": {"$ref": "#/$defs/http_probe"}
}
},
"http_probe": {
"type": "object",
"additionalProperties": false,
"properties": {
"mode": {"enum": ["route", "service", "node", "direct"]},
"method": {"enum": ["GET", "HEAD", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"]},
"path": {"type": "string"},
"port": {"type": ["integer", "null"], "minimum": 1, "maximum": 65535},
"scheme": {"enum": ["auto", "http", "https"]},
"hint": {"type": "string"},
"namespaces": {"type": "array", "items": {"type": "string"}},
"name": {"type": ["string", "null"]},
"url": {"type": ["string", "null"]},
"headers": {"type": "object", "additionalProperties": {"type": "string"}},
"authenticate": {"type": "boolean"},
"csrf": {"type": "boolean"},
"service_account": {"type": ["string", "null"]}
}
}
}
Expand Down
7 changes: 7 additions & 0 deletions enums/v1/validation-http-method.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"name": "validation_http_method",
"source_schema": "validation-plan.schema.json#/$defs/http_probe/properties/method",
"description": "HTTP methods supported by structured validation probes.",
"values": ["GET", "HEAD", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"],
"used_in_schemas": ["validation-plan.schema.json"]
}
7 changes: 7 additions & 0 deletions enums/v1/validation-http-node-address-type.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"name": "validation_http_node_address_type",
"source_schema": "validation-authorization.schema.json#/$defs/http_node_grant/properties/address_types/items",
"description": "Kubernetes node address types eligible for HTTP discovery.",
"values": ["InternalIP", "ExternalIP"],
"used_in_schemas": ["validation-authorization.schema.json"]
}
7 changes: 7 additions & 0 deletions enums/v1/validation-http-probe-mode.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"name": "validation_http_probe_mode",
"source_schema": "validation-plan.schema.json#/$defs/http_probe/properties/mode",
"description": "How a structured HTTP validation probe selects its endpoint.",
"values": ["route", "service", "node", "direct"],
"used_in_schemas": ["validation-plan.schema.json"]
}
7 changes: 7 additions & 0 deletions enums/v1/validation-http-scheme.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"name": "validation_http_scheme",
"source_schema": "validation-plan.schema.json#/$defs/http_probe/properties/scheme",
"description": "Transport choices for structured HTTP validation probes.",
"values": ["auto", "http", "https"],
"used_in_schemas": ["validation-plan.schema.json"]
}
7 changes: 7 additions & 0 deletions enums/v1/validation-http-target-resource.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"name": "validation_http_target_resource",
"source_schema": "validation-authorization.schema.json#/$defs/http_target/properties/resource",
"description": "Resource kinds for an explicit HTTP validation target.",
"values": ["routes", "nodes", "http_targets"],
"used_in_schemas": ["validation-authorization.schema.json"]
}
57 changes: 57 additions & 0 deletions tests/test_validation_authorization.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,59 @@ def test_authorization_pins_plan_and_complete_scope(validator: Draft202012Valida
validator.validate(inline)


@pytest.mark.parametrize(
"nodes",
[
None,
{
"names": ["worker-0"],
"networks": ["10.0.0.0/24"],
"address_types": ["InternalIP"],
"credentials": False,
},
],
)
def test_authorization_preserves_complete_http_grants_and_expiry(
validator: Draft202012Validator, nodes: dict[str, Any] | None
) -> None:
approval = deepcopy(APPROVAL)
scope = approval["scope"]
scope["expires_at"] = "2026-12-31T16:30:00Z"
scope["http_targets"] = [
{
"host": "console.example",
"context": "example-lab",
"namespace": "example-a",
"resource": "routes",
"name": "console",
"credentials": True,
}
]
cluster = scope["clusters"][0]
cluster["api"] = "https://api.example:6443"
cluster["http_discovery"] = {
"routes": [
{
"namespace": "example-a",
"names": ["console"],
"domains": ["apps.example"],
"credentials": True,
}
],
"nodes": nodes,
"port_forward_credentials": False,
"kube_env": [],
"ca_bundle": None,
"insecure_tls": False,
"session_check_path": None,
"session_check_contains": None,
}
validator.validate(approval)
scope["expires_at"] = None
cluster["http_discovery"] = None
validator.validate(approval)


@pytest.mark.parametrize(
("path", "value"),
[
Expand All @@ -84,6 +137,10 @@ def test_authorization_pins_plan_and_complete_scope(validator: Draft202012Valida
(("scope", "clusters", 0, "unsafe"), True),
(("scope", "off_limits", 0, "unsafe"), True),
(("scope", "expires"), "tomorrow"),
(("scope", "expires_at"), "2026-12-31T16:30:00"),
(("scope", "http_targets"), [{"context": "example-lab"}]),
(("scope", "clusters", 0, "http_discovery"), {"nodes": {"address_types": ["DNS"]}}),
(("scope", "clusters", 0, "api"), 1),
(("scope", "wasm_artifact_ids"), ["file:///var/example.wasm"]),
],
)
Expand Down
58 changes: 58 additions & 0 deletions tests/test_validation_plan.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,64 @@ def test_validate_findings_plan_shape(validator: Draft202012Validator) -> None:
validator.validate({**PLAN, "steps": [recon]})


def http_plan() -> dict[str, Any]:
plan = deepcopy(PLAN)
step = plan["steps"][0]
step.pop("payload")
step.pop("rollback")
step["verb"] = "port-forward+http"
step["target"] = {
"context": "authorized-lab",
"namespace": "example-a",
"resource": "routes",
"method": "GET",
"path": "/healthz",
"http": {
"mode": "route",
"method": "GET",
"path": "/healthz",
"port": None,
"scheme": "auto",
"hint": "console",
"namespaces": ["example-a"],
"name": None,
"url": None,
"headers": {},
"authenticate": False,
"csrf": False,
"service_account": None,
},
}
return plan


def test_structured_http_probe_is_a_valid_plan(validator: Draft202012Validator) -> None:
validator.validate(http_plan())


@pytest.mark.parametrize("field", ["cmd", "rollback"])
def test_structured_http_probe_rejects_command_text(
validator: Draft202012Validator, field: str
) -> None:
plan = http_plan()
plan["steps"][0][field] = "curl https://example.invalid"
with pytest.raises(ValidationError):
validator.validate(plan)


@pytest.mark.parametrize(
("field", "value"),
[("method", "CONNECT"), ("port", 0), ("unexpected", True), ("headers", {"X": 1})],
)
def test_structured_http_probe_rejects_invalid_fields(
validator: Draft202012Validator, field: str, value: object
) -> None:
plan = http_plan()
plan["steps"][0]["target"]["http"][field] = value
with pytest.raises(ValidationError):
validator.validate(plan)


@pytest.mark.parametrize(
("path", "value"),
[
Expand Down
Loading