feat(posture): add deterministic feedback proposals
Assistant: codex Assistant-Model: gpt-5.6-sol Assistant-Session: 01a02929-244b-7391-b933-c04010e8eedb
This commit is contained in:
parent
dc8da422f8
commit
cfc9e7d0cb
19 changed files with 1428 additions and 16 deletions
13
Makefile
13
Makefile
|
|
@ -199,6 +199,16 @@ security-scenario-compose-c0: ## Compose the plan-only C0 local-identity referen
|
||||||
--scenario examples/security-scenarios/c0-local-identity.yaml \
|
--scenario examples/security-scenarios/c0-local-identity.yaml \
|
||||||
capabilities/playbooks/net-kingdom.local-identity.yaml
|
capabilities/playbooks/net-kingdom.local-identity.yaml
|
||||||
|
|
||||||
|
posture-feedback-test: ## Run deterministic posture-feedback tests
|
||||||
|
python3 -m pytest tools/posture-feedback/tests tools/tenancy-posture/test_validate.py
|
||||||
|
|
||||||
|
posture-feedback-reference: ## Emit the proposal-only reference report at its fixed example time
|
||||||
|
python3 tools/posture-feedback/posture_feedback.py \
|
||||||
|
--as-of 2026-08-23T12:00:00Z \
|
||||||
|
--horizon-days 14 \
|
||||||
|
--fail-on none \
|
||||||
|
examples/posture-feedback/expired-e2.yaml
|
||||||
|
|
||||||
security-bootstrap-console-test: ## Run automated tests for bootstrap console UI/sections/runbooks (NET-WP-0018-T07)
|
security-bootstrap-console-test: ## Run automated tests for bootstrap console UI/sections/runbooks (NET-WP-0018-T07)
|
||||||
python3 -m pytest tools/security-bootstrap-console/tests
|
python3 -m pytest tools/security-bootstrap-console/tests
|
||||||
|
|
||||||
|
|
@ -358,7 +368,8 @@ security-bootstrap-ui: security-bootstrap-metadata-init ## Serve local custody a
|
||||||
creds-status creds-rotate \
|
creds-status creds-rotate \
|
||||||
creds-agent-init creds-agent-status creds-emergency-reprint \
|
creds-agent-init creds-agent-status creds-emergency-reprint \
|
||||||
openbao-init-unseal openbao-init-unseal-dry-run \
|
openbao-init-unseal openbao-init-unseal-dry-run \
|
||||||
iam-profile-conformance-test playbook-contract-test \
|
iam-profile-conformance-test playbook-contract-test posture-feedback-test \
|
||||||
|
posture-feedback-reference \
|
||||||
security-bootstrap-console-test security-bootstrap-scripts-syntax \
|
security-bootstrap-console-test security-bootstrap-scripts-syntax \
|
||||||
security-bootstrap-console security-bootstrap-king-kit \
|
security-bootstrap-console security-bootstrap-king-kit \
|
||||||
security-bootstrap-validate-kit security-bootstrap-validate-t02 \
|
security-bootstrap-validate-kit security-bootstrap-validate-t02 \
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,8 @@ The dynamic, self-optimizing security platform is the long-term direction in
|
||||||
- [SCOPE.md](SCOPE.md) — what this repo owns, current state, and when it is relevant
|
- [SCOPE.md](SCOPE.md) — what this repo owns, current state, and when it is relevant
|
||||||
- [Security scenario composition](canon/standards/security-scenario-composition_v0.1.md)
|
- [Security scenario composition](canon/standards/security-scenario-composition_v0.1.md)
|
||||||
— deterministic, plan-only capability and trust composition
|
— deterministic, plan-only capability and trust composition
|
||||||
|
- [Posture feedback](canon/standards/posture-feedback_v0.1.md) — deterministic,
|
||||||
|
proposal-only posture and evidence remediation findings
|
||||||
|
|
||||||
## Security Infrastructure Documents
|
## Security Infrastructure Documents
|
||||||
|
|
||||||
|
|
|
||||||
26
SCOPE.md
26
SCOPE.md
|
|
@ -44,6 +44,8 @@ about the current implementation.
|
||||||
selection/parameterization and Railiance execution.
|
selection/parameterization and Railiance execution.
|
||||||
- Security Scenario Composition v0.1 for deterministic provider selection,
|
- Security Scenario Composition v0.1 for deterministic provider selection,
|
||||||
trust ordering, responsibility mapping, and non-executing owner handoff.
|
trust ordering, responsibility mapping, and non-executing owner handoff.
|
||||||
|
- Posture Feedback v0.1 for deterministic, proposal-only routing of posture
|
||||||
|
review, evidence-freshness, implementation, and declared-gap findings.
|
||||||
- Tenancy Posture v0.1 and Security Zones v0.1 proposed standards, their schemas,
|
- Tenancy Posture v0.1 and Security Zones v0.1 proposed standards, their schemas,
|
||||||
validators, evidence rules, and publication stewardship. Zone semantics are
|
validators, evidence rules, and publication stewardship. Zone semantics are
|
||||||
owned by `zone-engine`; NetKingdom owns their canon publication.
|
owned by `zone-engine`; NetKingdom owns their canon publication.
|
||||||
|
|
@ -54,11 +56,13 @@ about the current implementation.
|
||||||
|
|
||||||
- `local-identity/`: minimal file-backed OIDC identity for bootstrap,
|
- `local-identity/`: minimal file-backed OIDC identity for bootstrap,
|
||||||
development, test, and sandbox use.
|
development, test, and sandbox use.
|
||||||
- IAM Profile, playbook-capability, tenancy-posture, custody, evidence, and
|
- IAM Profile, playbook-capability, tenancy-posture, posture-feedback, custody,
|
||||||
bootstrap-policy validators.
|
evidence, and bootstrap-policy validators.
|
||||||
- `tools/security-scenario-composer/`: fail-closed, plan-only composition of
|
- `tools/security-scenario-composer/`: fail-closed, plan-only composition of
|
||||||
declared capabilities, parameters, trust transitions, responsibilities, and
|
declared capabilities, parameters, trust transitions, responsibilities, and
|
||||||
readiness obligations.
|
readiness obligations.
|
||||||
|
- `tools/posture-feedback/`: deterministic, time-explicit remediation proposals
|
||||||
|
with authoritative owner resolution and no external or policy writes.
|
||||||
- `tools/security-bootstrap-console/`: guarded platform-root and OpenBao
|
- `tools/security-bootstrap-console/`: guarded platform-root and OpenBao
|
||||||
bootstrap workflow, including refusal of unsafe live initialization.
|
bootstrap workflow, including refusal of unsafe live initialization.
|
||||||
- SOPS/age bootstrap integration, credential-generation and rotation helpers,
|
- SOPS/age bootstrap integration, credential-generation and rotation helpers,
|
||||||
|
|
@ -110,14 +114,16 @@ repositories while NetKingdom retains the contracts and reference evidence.
|
||||||
| C3 — Runtime secrets | OpenBao custody, bootstrap, policy, delivery, and recovery contracts | Integrated with an externally deployed runtime; production evidence remains gated |
|
| C3 — Runtime secrets | OpenBao custody, bootstrap, policy, delivery, and recovery contracts | Integrated with an externally deployed runtime; production evidence remains gated |
|
||||||
| C4 — Fine-grained authorization | flex-auth caller identity and boundary integration | Partially delivered; full estate/PDP readiness is not established here |
|
| C4 — Fine-grained authorization | flex-auth caller identity and boundary integration | Partially delivered; full estate/PDP readiness is not established here |
|
||||||
| C5 — Enterprise federation | Keycloak/SAML/enterprise-IdP design | Backlog; not a current provided runtime capability |
|
| C5 — Enterprise federation | Keycloak/SAML/enterprise-IdP design | Backlog; not a current provided runtime capability |
|
||||||
| C6 — Self-optimizing security | Declarations, validators, evidence freshness, workplans, and drift surfacing | Early governance mechanisms only; no autonomous closed loop |
|
| C6 — Self-optimizing security | Declarations, validators, evidence freshness, and deterministic owner-routed remediation proposals | First proposal-only feedback loop delivered; no autonomous policy mutation or closed loop |
|
||||||
|
|
||||||
Current open work as of 2026-08-23 is either externally blocked, date-gated, or
|
Current open work as of 2026-08-23 is either externally blocked, date-gated, or
|
||||||
explicit backlog: reef carrier/public-classification decisions in NK-WP-0027,
|
explicit backlog: reef carrier/public-classification decisions in NK-WP-0027,
|
||||||
the NK-WP-0022 retirement gate, security tutorials in NK-WP-0009, and
|
the NK-WP-0022 retirement gate, security tutorials in NK-WP-0009, and
|
||||||
enterprise federation in NK-WP-0011. NK-WP-0030 has delivered the local C0
|
enterprise federation in NK-WP-0011. NK-WP-0030 has delivered the local C0
|
||||||
scenario-composition slice and waits for externally owned C1/C2 provider
|
scenario-composition slice and waits for externally owned C1/C2 provider
|
||||||
declarations before the lightweight SSO scenario can compose.
|
declarations before the lightweight SSO scenario can compose. NK-WP-0031 has
|
||||||
|
delivered the local proposal-only feedback evaluator and waits for authoritative
|
||||||
|
freshness adoption by `audit-core`.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -126,8 +132,8 @@ declarations before the lightweight SSO scenario can compose.
|
||||||
- Defining or reviewing identity, tenancy, workload-zone, credential, and
|
- Defining or reviewing identity, tenancy, workload-zone, credential, and
|
||||||
security-composition canon.
|
security-composition canon.
|
||||||
- Bootstrapping identity and trust before the normal platform is available.
|
- Bootstrapping identity and trust before the normal platform is available.
|
||||||
- Validating an IAM issuer, posture declaration, or Railiance capability
|
- Validating an IAM issuer, posture declaration, posture feedback report, or
|
||||||
declaration against NetKingdom contracts.
|
Railiance capability declaration against NetKingdom contracts.
|
||||||
- Integrating KeyCape, flex-auth, OpenBao, user-engine, tenant-engine, or a
|
- Integrating KeyCape, flex-auth, OpenBao, user-engine, tenant-engine, or a
|
||||||
Railiance package across an explicit security boundary.
|
Railiance package across an explicit security boundary.
|
||||||
- Deciding which repository owns a security semantic, runtime, deployment, or
|
- Deciding which repository owns a security semantic, runtime, deployment, or
|
||||||
|
|
@ -151,15 +157,15 @@ declarations before the lightweight SSO scenario can compose.
|
||||||
```capability
|
```capability
|
||||||
type: governance
|
type: governance
|
||||||
title: NetKingdom security canon
|
title: NetKingdom security canon
|
||||||
description: Provider-neutral IAM v0.3, user/tenant boundaries, credential, playbook and scenario composition, tenancy-posture, and workload-zone standards with explicit ownership and conformance rules.
|
description: Provider-neutral IAM v0.3, user/tenant boundaries, credential, playbook and scenario composition, tenancy-posture, posture-feedback, and workload-zone standards with explicit ownership and conformance rules.
|
||||||
keywords: [iam, oidc, tenancy, workload-identity, security-zones, credentials, canon]
|
keywords: [iam, oidc, tenancy, workload-identity, security-zones, credentials, canon]
|
||||||
```
|
```
|
||||||
|
|
||||||
```capability
|
```capability
|
||||||
type: validation
|
type: validation
|
||||||
title: Security contract conformance
|
title: Security contract conformance
|
||||||
description: Executable validation for IAM Profile issuers, playbook capability declarations, tenancy posture, bootstrap custody, and non-secret evidence records.
|
description: Executable validation for IAM Profile issuers, playbook capability declarations, tenancy posture, deterministic proposal-only feedback, bootstrap custody, and non-secret evidence records.
|
||||||
keywords: [validation, conformance, iam, posture, evidence, playbooks]
|
keywords: [validation, conformance, iam, posture, feedback, evidence, playbooks]
|
||||||
```
|
```
|
||||||
|
|
||||||
```capability
|
```capability
|
||||||
|
|
@ -192,5 +198,5 @@ keywords: [meta-orchestration, railiance, responsibility, capability, trust-stat
|
||||||
`tools/security-bootstrap-console/`
|
`tools/security-bootstrap-console/`
|
||||||
- Executable surfaces: `local-identity/`, `tools/iam-profile-conformance/`,
|
- Executable surfaces: `local-identity/`, `tools/iam-profile-conformance/`,
|
||||||
`tools/playbook-capability-contract/`, `tools/security-scenario-composer/`,
|
`tools/playbook-capability-contract/`, `tools/security-scenario-composer/`,
|
||||||
and `tools/tenancy-posture/`
|
`tools/tenancy-posture/`, and `tools/posture-feedback/`
|
||||||
- Work state: `.custodian-brief.md` and `workplans/`
|
- Work state: `.custodian-brief.md` and `workplans/`
|
||||||
|
|
|
||||||
145
canon/schemas/posture-feedback-report_v0.1.schema.json
Normal file
145
canon/schemas/posture-feedback-report_v0.1.schema.json
Normal file
|
|
@ -0,0 +1,145 @@
|
||||||
|
{
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"$id": "https://netkingdom.local/schemas/posture-feedback-report_v0.1.schema.json",
|
||||||
|
"title": "NetKingdom Posture Feedback Report v0.1",
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": [
|
||||||
|
"apiVersion",
|
||||||
|
"kind",
|
||||||
|
"ok",
|
||||||
|
"as_of",
|
||||||
|
"horizon_days",
|
||||||
|
"fail_on",
|
||||||
|
"automation",
|
||||||
|
"summary",
|
||||||
|
"validation_errors",
|
||||||
|
"findings",
|
||||||
|
"report_digest"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"apiVersion": {
|
||||||
|
"const": "netkingdom.io/posture-feedback/v0.1"
|
||||||
|
},
|
||||||
|
"kind": {
|
||||||
|
"const": "PostureFeedbackReport"
|
||||||
|
},
|
||||||
|
"ok": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"as_of": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "date-time"
|
||||||
|
},
|
||||||
|
"horizon_days": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 0
|
||||||
|
},
|
||||||
|
"fail_on": {
|
||||||
|
"enum": ["none", "low", "medium", "high"]
|
||||||
|
},
|
||||||
|
"automation": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": [
|
||||||
|
"mode",
|
||||||
|
"external_write_permitted",
|
||||||
|
"policy_mutation_permitted",
|
||||||
|
"declaration_mutation_permitted"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"mode": {"const": "proposal-only"},
|
||||||
|
"external_write_permitted": {"const": false},
|
||||||
|
"policy_mutation_permitted": {"const": false},
|
||||||
|
"declaration_mutation_permitted": {"const": false}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"summary": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": ["total", "high", "medium", "low", "unknown_owner"],
|
||||||
|
"properties": {
|
||||||
|
"total": {"type": "integer", "minimum": 0},
|
||||||
|
"high": {"type": "integer", "minimum": 0},
|
||||||
|
"medium": {"type": "integer", "minimum": 0},
|
||||||
|
"low": {"type": "integer", "minimum": 0},
|
||||||
|
"unknown_owner": {"type": "integer", "minimum": 0}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"validation_errors": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {"type": "string", "minLength": 1}
|
||||||
|
},
|
||||||
|
"findings": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/$defs/finding"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"report_digest": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^sha256:[0-9a-f]{64}$"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"$defs": {
|
||||||
|
"finding": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": [
|
||||||
|
"id",
|
||||||
|
"class",
|
||||||
|
"severity",
|
||||||
|
"source",
|
||||||
|
"service",
|
||||||
|
"control",
|
||||||
|
"owner",
|
||||||
|
"evidence_state",
|
||||||
|
"reason",
|
||||||
|
"recommended_action"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^NKFB-[0-9a-f]{16}$"
|
||||||
|
},
|
||||||
|
"class": {
|
||||||
|
"enum": [
|
||||||
|
"posture-review-overdue",
|
||||||
|
"posture-review-due-soon",
|
||||||
|
"zone-review-overdue",
|
||||||
|
"zone-review-due-soon",
|
||||||
|
"evidence-freshness-unknown",
|
||||||
|
"evidence-expired",
|
||||||
|
"evidence-due-soon",
|
||||||
|
"implemented-not-evidenced",
|
||||||
|
"declared-gap"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"severity": {
|
||||||
|
"enum": ["low", "medium", "high"]
|
||||||
|
},
|
||||||
|
"source": {"type": "string", "minLength": 1},
|
||||||
|
"service": {"type": "string", "minLength": 1},
|
||||||
|
"control": {"type": "string", "minLength": 1},
|
||||||
|
"owner": {"type": "string", "minLength": 1},
|
||||||
|
"evidence_state": {
|
||||||
|
"enum": [
|
||||||
|
"not-applicable",
|
||||||
|
"due-soon",
|
||||||
|
"overdue",
|
||||||
|
"unknown",
|
||||||
|
"expired",
|
||||||
|
"implemented",
|
||||||
|
"gap"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"due": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1
|
||||||
|
},
|
||||||
|
"reason": {"type": "string", "minLength": 1},
|
||||||
|
"recommended_action": {"type": "string", "minLength": 1}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -21,6 +21,9 @@
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"minLength": 1
|
"minLength": 1
|
||||||
},
|
},
|
||||||
|
"responsible_repo": {
|
||||||
|
"$ref": "#/$defs/repoName"
|
||||||
|
},
|
||||||
"workload_identity": {
|
"workload_identity": {
|
||||||
"$ref": "#/$defs/workloadIdentity"
|
"$ref": "#/$defs/workloadIdentity"
|
||||||
},
|
},
|
||||||
|
|
@ -36,6 +39,9 @@
|
||||||
"evidence": {
|
"evidence": {
|
||||||
"$ref": "#/$defs/evidence"
|
"$ref": "#/$defs/evidence"
|
||||||
},
|
},
|
||||||
|
"evidence_freshness": {
|
||||||
|
"$ref": "#/$defs/evidenceFreshness"
|
||||||
|
},
|
||||||
"notes": {
|
"notes": {
|
||||||
"$ref": "#/$defs/stringList"
|
"$ref": "#/$defs/stringList"
|
||||||
},
|
},
|
||||||
|
|
@ -100,6 +106,16 @@
|
||||||
"required": [
|
"required": [
|
||||||
"evidence"
|
"evidence"
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"required": [
|
||||||
|
"responsible_repo"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"required": [
|
||||||
|
"evidence_freshness"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
@ -601,6 +617,76 @@
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"evidenceFreshnessEntry": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"kind",
|
||||||
|
"observed_at",
|
||||||
|
"responsible_repo",
|
||||||
|
"scope",
|
||||||
|
"remediation"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"kind": {
|
||||||
|
"enum": [
|
||||||
|
"mechanical",
|
||||||
|
"adversarial"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"observed_at": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "date-time"
|
||||||
|
},
|
||||||
|
"valid_until": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "date-time"
|
||||||
|
},
|
||||||
|
"responsible_repo": {
|
||||||
|
"$ref": "#/$defs/repoName"
|
||||||
|
},
|
||||||
|
"scope": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1
|
||||||
|
},
|
||||||
|
"remediation": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1
|
||||||
|
},
|
||||||
|
"source_revision": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"if": {
|
||||||
|
"properties": {
|
||||||
|
"kind": {
|
||||||
|
"const": "adversarial"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"kind"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"then": {
|
||||||
|
"required": [
|
||||||
|
"valid_until"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
"evidenceFreshness": {
|
||||||
|
"type": "object",
|
||||||
|
"patternProperties": {
|
||||||
|
"^[IAEPRV][0-4]$": {
|
||||||
|
"$ref": "#/$defs/evidenceFreshnessEntry"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
"service": {
|
"service": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": [
|
||||||
|
|
@ -616,6 +702,9 @@
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"minLength": 1
|
"minLength": 1
|
||||||
},
|
},
|
||||||
|
"responsible_repo": {
|
||||||
|
"$ref": "#/$defs/repoName"
|
||||||
|
},
|
||||||
"workload_identity": {
|
"workload_identity": {
|
||||||
"$ref": "#/$defs/workloadIdentity"
|
"$ref": "#/$defs/workloadIdentity"
|
||||||
},
|
},
|
||||||
|
|
@ -628,6 +717,9 @@
|
||||||
"evidence": {
|
"evidence": {
|
||||||
"$ref": "#/$defs/evidence"
|
"$ref": "#/$defs/evidence"
|
||||||
},
|
},
|
||||||
|
"evidence_freshness": {
|
||||||
|
"$ref": "#/$defs/evidenceFreshness"
|
||||||
|
},
|
||||||
"notes": {
|
"notes": {
|
||||||
"$ref": "#/$defs/stringList"
|
"$ref": "#/$defs/stringList"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
109
canon/standards/posture-feedback_v0.1.md
Normal file
109
canon/standards/posture-feedback_v0.1.md
Normal file
|
|
@ -0,0 +1,109 @@
|
||||||
|
---
|
||||||
|
id: netkingdom-posture-feedback-v0.1
|
||||||
|
type: standard
|
||||||
|
title: "NetKingdom Posture Feedback v0.1"
|
||||||
|
domain: netkingdom
|
||||||
|
status: proposed
|
||||||
|
version: "0.1"
|
||||||
|
owner: net-kingdom
|
||||||
|
created: "2026-08-23"
|
||||||
|
updated: "2026-08-23"
|
||||||
|
last_reviewed: "2026-08-23"
|
||||||
|
review_interval: 3m
|
||||||
|
scope: self-optimizing-security
|
||||||
|
schema:
|
||||||
|
- canon/schemas/posture-feedback-report_v0.1.schema.json
|
||||||
|
validator:
|
||||||
|
- tools/posture-feedback/posture_feedback.py
|
||||||
|
related:
|
||||||
|
- canon/standards/tenancy-posture_v0.1.md
|
||||||
|
- canon/standards/security-zones_v0.1.md
|
||||||
|
---
|
||||||
|
|
||||||
|
# NetKingdom Posture Feedback v0.1
|
||||||
|
|
||||||
|
## 1. Purpose
|
||||||
|
|
||||||
|
This contract is the first bounded C6 feedback mechanism. It turns explicit
|
||||||
|
posture review dates, evidence freshness, implemented-but-unevidenced controls,
|
||||||
|
and declared gaps into deterministic remediation **proposals**.
|
||||||
|
|
||||||
|
It does not modify a posture level, policy, declaration, workplan, State Hub,
|
||||||
|
or runtime. Human or separately governed automation decides whether a proposal
|
||||||
|
becomes work.
|
||||||
|
|
||||||
|
## 2. Deterministic time
|
||||||
|
|
||||||
|
Evaluation requires an explicit RFC 3339 `as_of` timestamp. Wall-clock time is
|
||||||
|
never read implicitly. A date-only `review_due` remains current through that
|
||||||
|
calendar date in UTC and becomes overdue on the following UTC date.
|
||||||
|
|
||||||
|
A non-negative horizon in days identifies items due soon. Changing the horizon
|
||||||
|
changes the report digest and is therefore visible.
|
||||||
|
|
||||||
|
## 3. Owner resolution
|
||||||
|
|
||||||
|
The evaluator routes only from authoritative declaration fields:
|
||||||
|
|
||||||
|
- posture review, gaps, and implemented controls: `responsible_repo`;
|
||||||
|
- evidence replacement: `evidence_freshness.<level>.responsible_repo`;
|
||||||
|
- security-zone review: `zones.responsible_party`.
|
||||||
|
|
||||||
|
If the relevant field is absent, owner resolution is `unknown`. The evaluator
|
||||||
|
must not infer ownership from the service name, repository path, Git remote,
|
||||||
|
previous work, or another policy subject.
|
||||||
|
|
||||||
|
## 4. Finding classes
|
||||||
|
|
||||||
|
| Finding | Trigger | Severity |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `posture-review-overdue` | `as_of` is after `tenancy.review_due` | high |
|
||||||
|
| `posture-review-due-soon` | review is within the horizon | medium |
|
||||||
|
| `zone-review-overdue` | `as_of` is after `zones.review_due` | high |
|
||||||
|
| `zone-review-due-soon` | zone review is within the horizon | medium |
|
||||||
|
| `evidence-freshness-unknown` | a current adversarial level has no complete freshness entry | high |
|
||||||
|
| `evidence-expired` | `as_of` is after `valid_until` | high |
|
||||||
|
| `evidence-due-soon` | evidence expires within the horizon | medium |
|
||||||
|
| `implemented-not-evidenced` | an `implemented` level is above `current` | medium |
|
||||||
|
| `declared-gap` | a non-empty `tenancy.gap` entry exists | low |
|
||||||
|
|
||||||
|
The review horizon does not generate a due-soon finding for an item already
|
||||||
|
overdue or expired. Exact equality with a timestamp is still valid; expiry is
|
||||||
|
strictly `as_of > valid_until`.
|
||||||
|
|
||||||
|
Current adversarial levels are `E2`, `R4`, and `V2`–`V4`. This vocabulary comes
|
||||||
|
from Tenancy Posture §13. Mechanical evidence is evaluated for expiry only when
|
||||||
|
its declaration explicitly supplies `valid_until`.
|
||||||
|
|
||||||
|
## 5. Proposal and safety boundary
|
||||||
|
|
||||||
|
Every finding receives a stable id derived from its source declaration,
|
||||||
|
service, finding class, control, and due value. It contains the authoritative
|
||||||
|
owner or `unknown`, current evidence state, reason, and recommended action.
|
||||||
|
For declarations under the workspace containing this repository, the source is
|
||||||
|
normalized to `<repo>/<path>` so absolute checkout locations do not perturb the
|
||||||
|
identity. This source normalization identifies an input only; it is never an
|
||||||
|
ownership inference.
|
||||||
|
|
||||||
|
Every report declares:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
automation:
|
||||||
|
mode: proposal-only
|
||||||
|
external_write_permitted: false
|
||||||
|
policy_mutation_permitted: false
|
||||||
|
declaration_mutation_permitted: false
|
||||||
|
```
|
||||||
|
|
||||||
|
Expired or unknown evidence does not silently inherit freshness and does not
|
||||||
|
silently downgrade a level. It makes the uncertainty visible for governed
|
||||||
|
review. Consumers that use the report as an admission gate may fail closed on
|
||||||
|
high findings, but that is a separate owner decision.
|
||||||
|
|
||||||
|
## 6. Exit behavior
|
||||||
|
|
||||||
|
The CLI emits a report conforming to
|
||||||
|
`posture-feedback-report_v0.1.schema.json`. `--fail-on high` exits non-zero when
|
||||||
|
at least one high-severity finding exists; `medium` includes medium and high;
|
||||||
|
`low` includes every finding; `none` reports without a finding-based failure.
|
||||||
|
Invalid declarations always exit non-zero.
|
||||||
|
|
@ -8,7 +8,7 @@ version: "0.1"
|
||||||
created: "2026-08-17"
|
created: "2026-08-17"
|
||||||
updated: "2026-08-23"
|
updated: "2026-08-23"
|
||||||
scope: multi-tenancy-security-framework
|
scope: multi-tenancy-security-framework
|
||||||
revision: "draft-13"
|
revision: "draft-14"
|
||||||
owner: net-kingdom
|
owner: net-kingdom
|
||||||
last_reviewed: "2026-08-23"
|
last_reviewed: "2026-08-23"
|
||||||
review_interval: 6m
|
review_interval: 6m
|
||||||
|
|
@ -87,6 +87,11 @@ and the tenant-engine boundary contract, not in the work-factory canon.
|
||||||
required by §13.2. The claim remains explicitly bounded and freshness-dated:
|
required by §13.2. The claim remains explicitly bounded and freshness-dated:
|
||||||
the attempted cross-tenant attacks did not work; this is not a universal
|
the attempted cross-tenant attacks did not work; this is not a universal
|
||||||
isolation proof.
|
isolation proof.
|
||||||
|
- **draft-14** makes evidence freshness and remediation ownership declarable.
|
||||||
|
Adversarial evidence may now carry its observation and expiry timestamps,
|
||||||
|
bounded scope, responsible repository, and replacement action. A separate
|
||||||
|
proposal-only evaluator treats absent authoritative owner or freshness as
|
||||||
|
`unknown`; it does not infer either or mutate the declared posture.
|
||||||
|
|
||||||
**Reviewed by all six. The score:** six repos found three live defects in their
|
**Reviewed by all six. The score:** six repos found three live defects in their
|
||||||
own code by reading the ladders — `tenant-engine`'s unfiltered
|
own code by reading the ladders — `tenant-engine`'s unfiltered
|
||||||
|
|
@ -826,6 +831,11 @@ declaration but do not replace it. The schema carries `current`, `implemented`,
|
||||||
`target`, `reviewed`, `review_due`, `gap`, `placement_exceptions`,
|
`target`, `reviewed`, `review_due`, `gap`, `placement_exceptions`,
|
||||||
`service_class` (§8.3), per-path detail (§5.2), and provider reachability
|
`service_class` (§8.3), per-path detail (§5.2), and provider reachability
|
||||||
(§5.5), plus the workload identity prerequisite for zone membership (§5.6.2).
|
(§5.5), plus the workload identity prerequisite for zone membership (§5.6.2).
|
||||||
|
It also permits `responsible_repo` for authoritative posture routing and
|
||||||
|
`evidence_freshness` for machine-readable evidence observation, expiry, scope,
|
||||||
|
owner, and remediation metadata. Their absence remains valid declaration
|
||||||
|
syntax; feedback resolution must report `unknown`, never infer them from a
|
||||||
|
directory, service name, or previous owner.
|
||||||
From the `net-kingdom` repo, owners validate one or more declarations
|
From the `net-kingdom` repo, owners validate one or more declarations
|
||||||
with `uv run tools/tenancy-posture/validate.py <path>...`; the validator applies
|
with `uv run tools/tenancy-posture/validate.py <path>...`; the validator applies
|
||||||
the JSON Schema and the evidence, date, implemented/current and provider-range
|
the JSON Schema and the evidence, date, implemented/current and provider-range
|
||||||
|
|
@ -1129,6 +1139,22 @@ rather than a green build. Cross-tenant findings are the category external
|
||||||
testing practice identifies as needing human review. **A passing CI run is not
|
testing practice identifies as needing human review. **A passing CI run is not
|
||||||
E2 evidence.**
|
E2 evidence.**
|
||||||
|
|
||||||
|
**Decision 13.5 — freshness and ownership are explicit inputs.** A declaration
|
||||||
|
may attach `evidence_freshness.<level>` to an evidence key. An adversarial entry
|
||||||
|
requires `observed_at`, `valid_until`, `responsible_repo`, `scope`, and
|
||||||
|
`remediation`; a mechanical entry may omit `valid_until` when the artifact is
|
||||||
|
continuously re-established by the referenced revision or CI control. The
|
||||||
|
timestamps use RFC 3339 and the responsible repository is the authority for
|
||||||
|
replacement evidence.
|
||||||
|
|
||||||
|
The feedback evaluator does not parse prose for dates, infer ownership from a
|
||||||
|
file path, or silently extend a validity window. A current adversarial claim
|
||||||
|
without freshness metadata resolves to **freshness `unknown`**. An expired
|
||||||
|
artifact resolves to **freshness `expired`**. Neither automatically rewrites the
|
||||||
|
declared level: the evaluator emits a deterministic owner-routed remediation
|
||||||
|
proposal so review remains observable and controlled. The proposal contract is
|
||||||
|
`posture-feedback_v0.1`; it performs no State Hub write or policy mutation.
|
||||||
|
|
||||||
| Level | Evidence | Kind |
|
| Level | Evidence | Kind |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| **I2** | Identifiers validated against the vocabulary; rejection test for a malformed id; binding shown to come from a verified token | Mechanical |
|
| **I2** | Identifiers validated against the vocabulary; rejection test for a malformed id; binding shown to come from a verified token | Mechanical |
|
||||||
|
|
|
||||||
16
examples/posture-feedback/README.md
Normal file
16
examples/posture-feedback/README.md
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
# Posture Feedback Example
|
||||||
|
|
||||||
|
`expired-e2.yaml` is deliberately valid as a posture declaration while carrying
|
||||||
|
expired E2 evidence at the example evaluation time. It also has posture and
|
||||||
|
zone reviews due within 14 days, an E3 control awaiting evidence, and one
|
||||||
|
declared retention gap.
|
||||||
|
|
||||||
|
The evaluator reports these as proposals and performs no mutation:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv run tools/posture-feedback/posture_feedback.py \
|
||||||
|
--as-of 2026-08-23T12:00:00Z \
|
||||||
|
--horizon-days 14 \
|
||||||
|
--fail-on none \
|
||||||
|
examples/posture-feedback/expired-e2.yaml
|
||||||
|
```
|
||||||
60
examples/posture-feedback/expired-e2.yaml
Normal file
60
examples/posture-feedback/expired-e2.yaml
Normal file
|
|
@ -0,0 +1,60 @@
|
||||||
|
schema_version: "0.1"
|
||||||
|
framework: netkingdom-tenancy-posture
|
||||||
|
service: example-security-api
|
||||||
|
role: tenant-api
|
||||||
|
responsible_repo: net-kingdom
|
||||||
|
|
||||||
|
workload_identity:
|
||||||
|
name: example-security-api
|
||||||
|
kind: application
|
||||||
|
responsible_repo: net-kingdom
|
||||||
|
identity_bindings:
|
||||||
|
- scheme: iam-profile
|
||||||
|
authority: key-cape
|
||||||
|
subject: example-security-api
|
||||||
|
principal_type: service
|
||||||
|
environment: reference
|
||||||
|
|
||||||
|
tenancy:
|
||||||
|
current: {I: 1, A: 2, E: 2, P: 0, R: 1, V: 0}
|
||||||
|
implemented: {E: 3}
|
||||||
|
target: {I: 2, A: 3, E: 3, P: 1, R: 2, V: 1}
|
||||||
|
reviewed: "2026-08-01"
|
||||||
|
review_due: "2026-08-31"
|
||||||
|
service_class: interactive
|
||||||
|
reason:
|
||||||
|
I: Request identity is not verified in this reference fixture.
|
||||||
|
P: Reference fixture has no provisioned datastore.
|
||||||
|
R: Platform default retention applies.
|
||||||
|
V: No availability exercise exists.
|
||||||
|
gap:
|
||||||
|
R: Publish and evidence an explicit erasure horizon.
|
||||||
|
|
||||||
|
zones:
|
||||||
|
standard: security-zones_v0.1
|
||||||
|
membership: z1-operational
|
||||||
|
responsible_party: team:platform-security
|
||||||
|
justification: Reference workload with bounded internal data.
|
||||||
|
context:
|
||||||
|
maturity: M1
|
||||||
|
criticality: medium
|
||||||
|
data_classification: internal
|
||||||
|
evidence:
|
||||||
|
- ref: docs/evidence/reference-zone.md
|
||||||
|
supports: [M1, reference]
|
||||||
|
reviewed: "2026-08-01"
|
||||||
|
review_due: "2026-08-25"
|
||||||
|
|
||||||
|
evidence:
|
||||||
|
A2: docs/evidence/reference-authorization.md
|
||||||
|
E2: docs/evidence/reference-adversarial-e2.md
|
||||||
|
|
||||||
|
evidence_freshness:
|
||||||
|
E2:
|
||||||
|
kind: adversarial
|
||||||
|
observed_at: "2026-08-20T12:00:00Z"
|
||||||
|
valid_until: "2026-08-22T12:00:00Z"
|
||||||
|
responsible_repo: net-kingdom
|
||||||
|
scope: Two bounded cross-tenant read probes against the reference fixture.
|
||||||
|
remediation: Repeat the bounded E2 probes and replace the evidence metadata.
|
||||||
|
source_revision: reference@0000000
|
||||||
47
history/2026-08-23-posture-feedback-estate-baseline.md
Normal file
47
history/2026-08-23-posture-feedback-estate-baseline.md
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
# Posture feedback estate baseline
|
||||||
|
|
||||||
|
Date: 2026-08-23
|
||||||
|
Workplan: NK-WP-0031
|
||||||
|
Mode: read-only, proposal-only
|
||||||
|
|
||||||
|
## Evaluation
|
||||||
|
|
||||||
|
The proposed Posture Feedback v0.1 evaluator was run with:
|
||||||
|
|
||||||
|
- `as_of`: `2026-08-23T11:05:14Z`
|
||||||
|
- `horizon_days`: `30`
|
||||||
|
- `fail_on`: `none`
|
||||||
|
- inputs: the tenancy declarations in `adaptive-pricing`, `audit-core`,
|
||||||
|
`flex-auth`, `ops-warden`, `railiance-platform`, `rapp-postgres`, and
|
||||||
|
`tenant-engine`
|
||||||
|
|
||||||
|
The report digest was
|
||||||
|
`sha256:66174f08cf5feb1e5618477a374975100a4712bcaa354fcb1b1702103482b6c1`.
|
||||||
|
It contained 34 proposals: one high, six medium, and 27 low. All 34 owners were
|
||||||
|
`unknown`, which is the required result because these declarations do not yet
|
||||||
|
carry the new authoritative `responsible_repo` fields.
|
||||||
|
|
||||||
|
## High finding
|
||||||
|
|
||||||
|
`NKFB-c08b78e82248bf27` reports `audit-core` E2 evidence freshness as
|
||||||
|
`unknown`. The declaration describes the Whitehat run and its 24-hour validity
|
||||||
|
in prose but does not yet supply the machine-readable `evidence_freshness.E2`
|
||||||
|
authority. NetKingdom did not infer timestamps, ownership, scope, or a
|
||||||
|
replacement action from that prose.
|
||||||
|
|
||||||
|
Adoption was routed to `audit-core` in State Hub message
|
||||||
|
`874e5fa5-e05c-4b1a-a915-26c4dba07b87`.
|
||||||
|
|
||||||
|
## Declaration validation errors
|
||||||
|
|
||||||
|
The `railiance-platform` declaration did not enter evaluation because its
|
||||||
|
`apps-pg` service declares current R2 and V1 without the evidence entries
|
||||||
|
required for current non-zero levels. Remediation was routed to
|
||||||
|
`railiance-platform` in State Hub message
|
||||||
|
`d65f8383-bdab-488e-a36f-f8cabd8a5f65`.
|
||||||
|
|
||||||
|
## Safety result
|
||||||
|
|
||||||
|
The evaluator changed no declaration, policy, workplan, State Hub record, or
|
||||||
|
runtime. The two State Hub messages above were explicit operator-process
|
||||||
|
handoffs after inspection of the report; they were not emitted by the tool.
|
||||||
|
|
@ -145,3 +145,13 @@ deterministic provider pins, trust ordering, responsibility mapping, and
|
||||||
readiness handoff without executing provider actions. G1 remains open for the
|
readiness handoff without executing provider actions. G1 remains open for the
|
||||||
lightweight SSO path until the KeyCape/Railiance owners publish conformant C1
|
lightweight SSO path until the KeyCape/Railiance owners publish conformant C1
|
||||||
and C2 declarations.
|
and C2 declarations.
|
||||||
|
|
||||||
|
NK-WP-0031 subsequently implemented the first bounded G2 increment: proposed
|
||||||
|
Posture Feedback v0.1 canon and a deterministic evaluator that turns explicit
|
||||||
|
review dates, evidence freshness, implemented-but-unevidenced controls, and
|
||||||
|
declared gaps into owner-routed remediation proposals. Time is an explicit
|
||||||
|
input; missing ownership or freshness resolves to `unknown`; and the report
|
||||||
|
forbids external, declaration, and policy writes. G2 remains open because the
|
||||||
|
tool does not create work, change policy, close remediation, or consume runtime
|
||||||
|
telemetry autonomously. Those powers require separate authority, rollback, and
|
||||||
|
review decisions.
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
id: capability.security.iam-tooling-suite
|
id: capability.security.iam-tooling-suite
|
||||||
name: NetKingdom Security/IAM Tooling Suite
|
name: NetKingdom Security/IAM Tooling Suite
|
||||||
summary: Canonical security architecture and executable reference tooling for IAM, bootstrap, posture validation,
|
summary: Canonical security architecture and executable reference tooling for IAM, bootstrap, posture validation,
|
||||||
and deterministic plan-only security scenario composition.
|
deterministic plan-only security scenario composition, and proposal-only posture feedback.
|
||||||
owner: net-kingdom
|
owner: net-kingdom
|
||||||
status: draft
|
status: draft
|
||||||
domain: infotech
|
domain: infotech
|
||||||
|
|
@ -25,7 +25,7 @@ maturity:
|
||||||
target: A3
|
target: A3
|
||||||
confidence: medium
|
confidence: medium
|
||||||
rationale: 'No top-level package manifest, but tools/ holds independently documented and runnable
|
rationale: 'No top-level package manifest, but tools/ holds independently documented and runnable
|
||||||
conformance, posture, scenario-composition, and guarded bootstrap tools; local-identity is separately
|
conformance, posture, posture-feedback, scenario-composition, and guarded bootstrap tools; local-identity is separately
|
||||||
packaged and tested.'
|
packaged and tested.'
|
||||||
external_evidence:
|
external_evidence:
|
||||||
completeness:
|
completeness:
|
||||||
|
|
@ -53,6 +53,7 @@ discovery:
|
||||||
- playbook capability contract validator
|
- playbook capability contract validator
|
||||||
- deterministic plan-only security scenario composer
|
- deterministic plan-only security scenario composer
|
||||||
- tenancy posture schema and validator
|
- tenancy posture schema and validator
|
||||||
|
- deterministic proposal-only posture feedback evaluator
|
||||||
- security bootstrap console (local, non-secret-collecting)
|
- security bootstrap console (local, non-secret-collecting)
|
||||||
- packaged local bootstrap identity CLI
|
- packaged local bootstrap identity CLI
|
||||||
excludes:
|
excludes:
|
||||||
|
|
@ -69,6 +70,7 @@ availability:
|
||||||
- tools/playbook-capability-contract
|
- tools/playbook-capability-contract
|
||||||
- tools/security-scenario-composer
|
- tools/security-scenario-composer
|
||||||
- tools/tenancy-posture
|
- tools/tenancy-posture
|
||||||
|
- tools/posture-feedback
|
||||||
- tools/security-bootstrap-console
|
- tools/security-bootstrap-console
|
||||||
- local-identity
|
- local-identity
|
||||||
target_artifacts: []
|
target_artifacts: []
|
||||||
|
|
@ -105,7 +107,7 @@ promotion_history: []
|
||||||
|
|
||||||
`net-kingdom` provides canonical IAM and security architecture, executable
|
`net-kingdom` provides canonical IAM and security architecture, executable
|
||||||
conformance and posture checks, deterministic plan-only scenario composition,
|
conformance and posture checks, deterministic plan-only scenario composition,
|
||||||
a local bootstrap identity package, and a non-secret-collecting security
|
proposal-only posture feedback, a local bootstrap identity package, and a non-secret-collecting security
|
||||||
bootstrap console. Provider implementations remain in sibling repositories such
|
bootstrap console. Provider implementations remain in sibling repositories such
|
||||||
as `key-cape`; the self-optimizing platform remains an intent rather than a
|
as `key-cape`; the self-optimizing platform remains an intent rather than a
|
||||||
current delivery claim.
|
current delivery claim.
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ capabilities:
|
||||||
- id: capability.security.iam-tooling-suite
|
- id: capability.security.iam-tooling-suite
|
||||||
name: NetKingdom Security/IAM Tooling Suite
|
name: NetKingdom Security/IAM Tooling Suite
|
||||||
summary: Canonical security architecture and executable reference tooling for IAM, bootstrap, posture
|
summary: Canonical security architecture and executable reference tooling for IAM, bootstrap, posture
|
||||||
validation, and deterministic plan-only security scenario composition.
|
validation, deterministic plan-only security scenario composition, and proposal-only posture feedback.
|
||||||
vector: D3 / A2 / C1 / R1
|
vector: D3 / A2 / C1 / R1
|
||||||
domain: infotech
|
domain: infotech
|
||||||
status: draft
|
status: draft
|
||||||
|
|
|
||||||
22
tools/posture-feedback/README.md
Normal file
22
tools/posture-feedback/README.md
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
# Posture Feedback Evaluator
|
||||||
|
|
||||||
|
This tool implements the proposal-only feedback boundary in
|
||||||
|
`canon/standards/posture-feedback_v0.1.md`. It reads tenancy declarations and
|
||||||
|
emits deterministic JSON. It never writes State Hub, edits a declaration,
|
||||||
|
changes policy, or executes remediation.
|
||||||
|
|
||||||
|
Evaluate the checked-in reference at an explicit time:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv run tools/posture-feedback/posture_feedback.py \
|
||||||
|
--as-of 2026-08-23T12:00:00Z \
|
||||||
|
--horizon-days 14 \
|
||||||
|
--fail-on none \
|
||||||
|
examples/posture-feedback/expired-e2.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
Run tests:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv run pytest tools/posture-feedback/tests
|
||||||
|
```
|
||||||
440
tools/posture-feedback/posture_feedback.py
Normal file
440
tools/posture-feedback/posture_feedback.py
Normal file
|
|
@ -0,0 +1,440 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
# /// script
|
||||||
|
# dependencies = ["jsonschema>=4.23,<5", "PyYAML>=6,<7"]
|
||||||
|
# ///
|
||||||
|
"""Emit deterministic, proposal-only feedback for tenancy posture declarations."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import datetime as dt
|
||||||
|
import hashlib
|
||||||
|
import importlib.util
|
||||||
|
import json
|
||||||
|
import pathlib
|
||||||
|
import sys
|
||||||
|
from collections import Counter
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
import jsonschema
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = pathlib.Path(__file__).resolve().parents[2]
|
||||||
|
TENANCY_SCHEMA = ROOT / "canon/schemas/tenancy-posture_v0.1.schema.json"
|
||||||
|
REPORT_SCHEMA = ROOT / "canon/schemas/posture-feedback-report_v0.1.schema.json"
|
||||||
|
TENANCY_VALIDATOR_PATH = ROOT / "tools/tenancy-posture/validate.py"
|
||||||
|
API_VERSION = "netkingdom.io/posture-feedback/v0.1"
|
||||||
|
KIND = "PostureFeedbackReport"
|
||||||
|
ADVERSARIAL_LEVELS = {"E2", "R4", "V2", "V3", "V4"}
|
||||||
|
SEVERITY_RANK = {"low": 1, "medium": 2, "high": 3}
|
||||||
|
|
||||||
|
SPEC = importlib.util.spec_from_file_location(
|
||||||
|
"tenancy_posture_validator_for_feedback", TENANCY_VALIDATOR_PATH
|
||||||
|
)
|
||||||
|
assert SPEC and SPEC.loader
|
||||||
|
TENANCY_VALIDATOR = importlib.util.module_from_spec(SPEC)
|
||||||
|
SPEC.loader.exec_module(TENANCY_VALIDATOR)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_timestamp(value: str) -> dt.datetime:
|
||||||
|
parsed = dt.datetime.fromisoformat(value.replace("Z", "+00:00"))
|
||||||
|
if parsed.tzinfo is None or parsed.utcoffset() is None:
|
||||||
|
raise ValueError("timestamp must include an explicit UTC offset")
|
||||||
|
return parsed.astimezone(dt.timezone.utc)
|
||||||
|
|
||||||
|
|
||||||
|
def format_timestamp(value: dt.datetime) -> str:
|
||||||
|
return value.astimezone(dt.timezone.utc).isoformat().replace("+00:00", "Z")
|
||||||
|
|
||||||
|
|
||||||
|
def load_document(path: pathlib.Path) -> dict[str, Any]:
|
||||||
|
document = yaml.safe_load(path.read_text(encoding="utf-8"))
|
||||||
|
if not isinstance(document, dict):
|
||||||
|
raise ValueError("document must be a YAML object")
|
||||||
|
return document
|
||||||
|
|
||||||
|
|
||||||
|
def service_entries(document: dict[str, Any]) -> list[dict[str, Any]]:
|
||||||
|
if "services" in document:
|
||||||
|
return document["services"]
|
||||||
|
return [document]
|
||||||
|
|
||||||
|
|
||||||
|
def source_identifier(path: pathlib.Path) -> str:
|
||||||
|
"""Return a portable declaration id without treating the path as ownership."""
|
||||||
|
resolved = path.resolve()
|
||||||
|
try:
|
||||||
|
return resolved.relative_to(ROOT.parent).as_posix()
|
||||||
|
except ValueError:
|
||||||
|
return resolved.as_posix()
|
||||||
|
|
||||||
|
|
||||||
|
def finding_id(
|
||||||
|
source: str,
|
||||||
|
service: str,
|
||||||
|
finding_class: str,
|
||||||
|
control: str,
|
||||||
|
due: str | None,
|
||||||
|
) -> str:
|
||||||
|
identity = "\x1f".join((source, service, finding_class, control, due or ""))
|
||||||
|
digest = hashlib.sha256(identity.encode("utf-8")).hexdigest()[:16]
|
||||||
|
return f"NKFB-{digest}"
|
||||||
|
|
||||||
|
|
||||||
|
def finding(
|
||||||
|
*,
|
||||||
|
source: str,
|
||||||
|
service: str,
|
||||||
|
finding_class: str,
|
||||||
|
severity: str,
|
||||||
|
control: str,
|
||||||
|
owner: str,
|
||||||
|
evidence_state: str,
|
||||||
|
reason: str,
|
||||||
|
recommended_action: str,
|
||||||
|
due: str | None = None,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
result = {
|
||||||
|
"id": finding_id(source, service, finding_class, control, due),
|
||||||
|
"class": finding_class,
|
||||||
|
"severity": severity,
|
||||||
|
"source": source,
|
||||||
|
"service": service,
|
||||||
|
"control": control,
|
||||||
|
"owner": owner,
|
||||||
|
"evidence_state": evidence_state,
|
||||||
|
"reason": reason,
|
||||||
|
"recommended_action": recommended_action,
|
||||||
|
}
|
||||||
|
if due is not None:
|
||||||
|
result["due"] = due
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def review_finding(
|
||||||
|
*,
|
||||||
|
source: str,
|
||||||
|
service: str,
|
||||||
|
owner: str,
|
||||||
|
control: str,
|
||||||
|
due: dt.date,
|
||||||
|
as_of: dt.datetime,
|
||||||
|
horizon_days: int,
|
||||||
|
prefix: str,
|
||||||
|
) -> dict[str, Any] | None:
|
||||||
|
due_text = due.isoformat()
|
||||||
|
if as_of.date() > due:
|
||||||
|
return finding(
|
||||||
|
source=source,
|
||||||
|
service=service,
|
||||||
|
finding_class=f"{prefix}-review-overdue",
|
||||||
|
severity="high",
|
||||||
|
control=control,
|
||||||
|
owner=owner,
|
||||||
|
evidence_state="overdue",
|
||||||
|
due=due_text,
|
||||||
|
reason=f"{control} review was due on {due_text} and is overdue at {format_timestamp(as_of)}.",
|
||||||
|
recommended_action=f"Review {control}, update its declaration and evidence, and retain the review record.",
|
||||||
|
)
|
||||||
|
days = (due - as_of.date()).days
|
||||||
|
if days <= horizon_days:
|
||||||
|
return finding(
|
||||||
|
source=source,
|
||||||
|
service=service,
|
||||||
|
finding_class=f"{prefix}-review-due-soon",
|
||||||
|
severity="medium",
|
||||||
|
control=control,
|
||||||
|
owner=owner,
|
||||||
|
evidence_state="due-soon",
|
||||||
|
due=due_text,
|
||||||
|
reason=f"{control} review is due in {days} day(s), within the {horizon_days}-day horizon.",
|
||||||
|
recommended_action=f"Schedule and evidence the {control} review before {due_text}.",
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def current_evidence_keys(entry: dict[str, Any]) -> set[str]:
|
||||||
|
current = entry["tenancy"]["current"]
|
||||||
|
return {
|
||||||
|
f"{axis}{level}"
|
||||||
|
for axis, level in current.items()
|
||||||
|
if isinstance(level, int)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def evidence_findings(
|
||||||
|
*,
|
||||||
|
source: str,
|
||||||
|
entry: dict[str, Any],
|
||||||
|
as_of: dt.datetime,
|
||||||
|
horizon_days: int,
|
||||||
|
) -> list[dict[str, Any]]:
|
||||||
|
service = entry["service"]
|
||||||
|
freshness = entry.get("evidence_freshness", {})
|
||||||
|
current_keys = current_evidence_keys(entry)
|
||||||
|
results: list[dict[str, Any]] = []
|
||||||
|
|
||||||
|
for key in sorted(ADVERSARIAL_LEVELS & current_keys):
|
||||||
|
if key not in freshness:
|
||||||
|
results.append(
|
||||||
|
finding(
|
||||||
|
source=source,
|
||||||
|
service=service,
|
||||||
|
finding_class="evidence-freshness-unknown",
|
||||||
|
severity="high",
|
||||||
|
control=key,
|
||||||
|
owner="unknown",
|
||||||
|
evidence_state="unknown",
|
||||||
|
reason=f"Current adversarial claim {key} has no authoritative evidence_freshness entry.",
|
||||||
|
recommended_action=(
|
||||||
|
f"Declare the {key} observation, expiry, bounded scope, responsible repository, "
|
||||||
|
"and replacement action; do not infer freshness from prose."
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
for key in sorted(current_keys & set(freshness)):
|
||||||
|
metadata = freshness[key]
|
||||||
|
valid_until_raw = metadata.get("valid_until")
|
||||||
|
if not valid_until_raw:
|
||||||
|
continue
|
||||||
|
valid_until = parse_timestamp(valid_until_raw)
|
||||||
|
owner = metadata["responsible_repo"]
|
||||||
|
if as_of > valid_until:
|
||||||
|
results.append(
|
||||||
|
finding(
|
||||||
|
source=source,
|
||||||
|
service=service,
|
||||||
|
finding_class="evidence-expired",
|
||||||
|
severity="high",
|
||||||
|
control=key,
|
||||||
|
owner=owner,
|
||||||
|
evidence_state="expired",
|
||||||
|
due=format_timestamp(valid_until),
|
||||||
|
reason=f"Evidence for {key} expired before {format_timestamp(as_of)}.",
|
||||||
|
recommended_action=metadata["remediation"],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
remaining = valid_until - as_of
|
||||||
|
if remaining <= dt.timedelta(days=horizon_days):
|
||||||
|
results.append(
|
||||||
|
finding(
|
||||||
|
source=source,
|
||||||
|
service=service,
|
||||||
|
finding_class="evidence-due-soon",
|
||||||
|
severity="medium",
|
||||||
|
control=key,
|
||||||
|
owner=owner,
|
||||||
|
evidence_state="due-soon",
|
||||||
|
due=format_timestamp(valid_until),
|
||||||
|
reason=(
|
||||||
|
f"Evidence for {key} expires within the {horizon_days}-day horizon "
|
||||||
|
f"at {format_timestamp(valid_until)}."
|
||||||
|
),
|
||||||
|
recommended_action=metadata["remediation"],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
def evaluate_entry(
|
||||||
|
source: str,
|
||||||
|
entry: dict[str, Any],
|
||||||
|
as_of: dt.datetime,
|
||||||
|
horizon_days: int,
|
||||||
|
) -> list[dict[str, Any]]:
|
||||||
|
results: list[dict[str, Any]] = []
|
||||||
|
service = entry["service"]
|
||||||
|
posture = entry["tenancy"]
|
||||||
|
posture_owner = entry.get("responsible_repo", "unknown")
|
||||||
|
|
||||||
|
posture_review = review_finding(
|
||||||
|
source=source,
|
||||||
|
service=service,
|
||||||
|
owner=posture_owner,
|
||||||
|
control="tenancy-posture",
|
||||||
|
due=dt.date.fromisoformat(posture["review_due"]),
|
||||||
|
as_of=as_of,
|
||||||
|
horizon_days=horizon_days,
|
||||||
|
prefix="posture",
|
||||||
|
)
|
||||||
|
if posture_review:
|
||||||
|
results.append(posture_review)
|
||||||
|
|
||||||
|
zones = entry.get("zones")
|
||||||
|
if zones:
|
||||||
|
zone_review = review_finding(
|
||||||
|
source=source,
|
||||||
|
service=service,
|
||||||
|
owner=zones["responsible_party"],
|
||||||
|
control="security-zones",
|
||||||
|
due=dt.date.fromisoformat(zones["review_due"]),
|
||||||
|
as_of=as_of,
|
||||||
|
horizon_days=horizon_days,
|
||||||
|
prefix="zone",
|
||||||
|
)
|
||||||
|
if zone_review:
|
||||||
|
results.append(zone_review)
|
||||||
|
|
||||||
|
results.extend(
|
||||||
|
evidence_findings(
|
||||||
|
source=source,
|
||||||
|
entry=entry,
|
||||||
|
as_of=as_of,
|
||||||
|
horizon_days=horizon_days,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
current = posture["current"]
|
||||||
|
for axis, level in sorted(posture.get("implemented", {}).items()):
|
||||||
|
current_level = current[axis]
|
||||||
|
results.append(
|
||||||
|
finding(
|
||||||
|
source=source,
|
||||||
|
service=service,
|
||||||
|
finding_class="implemented-not-evidenced",
|
||||||
|
severity="medium",
|
||||||
|
control=f"{axis}{level}",
|
||||||
|
owner=posture_owner,
|
||||||
|
evidence_state="implemented",
|
||||||
|
reason=f"{axis}{level} is implemented while the current evidenced level is {axis}{current_level}.",
|
||||||
|
recommended_action=f"Produce the canon-required evidence for {axis}{level} or remove the implemented claim.",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
for axis, description in sorted(posture.get("gap", {}).items()):
|
||||||
|
results.append(
|
||||||
|
finding(
|
||||||
|
source=source,
|
||||||
|
service=service,
|
||||||
|
finding_class="declared-gap",
|
||||||
|
severity="low",
|
||||||
|
control=axis,
|
||||||
|
owner=posture_owner,
|
||||||
|
evidence_state="gap",
|
||||||
|
reason=description,
|
||||||
|
recommended_action=f"Review the declared {axis}-axis gap and retain, schedule, or resolve it explicitly.",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
def report_digest(report_without_digest: dict[str, Any]) -> str:
|
||||||
|
encoded = json.dumps(
|
||||||
|
report_without_digest,
|
||||||
|
sort_keys=True,
|
||||||
|
separators=(",", ":"),
|
||||||
|
).encode("utf-8")
|
||||||
|
return f"sha256:{hashlib.sha256(encoded).hexdigest()}"
|
||||||
|
|
||||||
|
|
||||||
|
def build_report(
|
||||||
|
paths: list[pathlib.Path],
|
||||||
|
*,
|
||||||
|
as_of: dt.datetime,
|
||||||
|
horizon_days: int,
|
||||||
|
fail_on: str,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
tenancy_schema = json.loads(TENANCY_SCHEMA.read_text(encoding="utf-8"))
|
||||||
|
validation_errors: list[str] = []
|
||||||
|
findings: list[dict[str, Any]] = []
|
||||||
|
|
||||||
|
for path in paths:
|
||||||
|
source = source_identifier(path)
|
||||||
|
try:
|
||||||
|
document = load_document(path)
|
||||||
|
except Exception as exc:
|
||||||
|
validation_errors.append(f"{path}: {exc}")
|
||||||
|
continue
|
||||||
|
errors = TENANCY_VALIDATOR.validate(path, tenancy_schema)
|
||||||
|
if errors:
|
||||||
|
validation_errors.extend(errors)
|
||||||
|
continue
|
||||||
|
for entry in service_entries(document):
|
||||||
|
findings.extend(evaluate_entry(source, entry, as_of, horizon_days))
|
||||||
|
|
||||||
|
findings.sort(
|
||||||
|
key=lambda item: (
|
||||||
|
item["source"],
|
||||||
|
item["service"],
|
||||||
|
item["class"],
|
||||||
|
item["control"],
|
||||||
|
item.get("due", ""),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
counts = Counter(item["severity"] for item in findings)
|
||||||
|
threshold_failed = False
|
||||||
|
if fail_on != "none":
|
||||||
|
threshold = SEVERITY_RANK[fail_on]
|
||||||
|
threshold_failed = any(
|
||||||
|
SEVERITY_RANK[item["severity"]] >= threshold for item in findings
|
||||||
|
)
|
||||||
|
|
||||||
|
report: dict[str, Any] = {
|
||||||
|
"apiVersion": API_VERSION,
|
||||||
|
"kind": KIND,
|
||||||
|
"ok": not validation_errors and not threshold_failed,
|
||||||
|
"as_of": format_timestamp(as_of),
|
||||||
|
"horizon_days": horizon_days,
|
||||||
|
"fail_on": fail_on,
|
||||||
|
"automation": {
|
||||||
|
"mode": "proposal-only",
|
||||||
|
"external_write_permitted": False,
|
||||||
|
"policy_mutation_permitted": False,
|
||||||
|
"declaration_mutation_permitted": False,
|
||||||
|
},
|
||||||
|
"summary": {
|
||||||
|
"total": len(findings),
|
||||||
|
"high": counts["high"],
|
||||||
|
"medium": counts["medium"],
|
||||||
|
"low": counts["low"],
|
||||||
|
"unknown_owner": sum(item["owner"] == "unknown" for item in findings),
|
||||||
|
},
|
||||||
|
"validation_errors": sorted(validation_errors),
|
||||||
|
"findings": findings,
|
||||||
|
}
|
||||||
|
report["report_digest"] = report_digest(report)
|
||||||
|
report_schema = json.loads(REPORT_SCHEMA.read_text(encoding="utf-8"))
|
||||||
|
jsonschema.Draft202012Validator(
|
||||||
|
report_schema,
|
||||||
|
format_checker=jsonschema.FormatChecker(),
|
||||||
|
).validate(report)
|
||||||
|
return report
|
||||||
|
|
||||||
|
|
||||||
|
def build_parser() -> argparse.ArgumentParser:
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="Emit deterministic, proposal-only NetKingdom posture feedback."
|
||||||
|
)
|
||||||
|
parser.add_argument("declarations", nargs="+", type=pathlib.Path)
|
||||||
|
parser.add_argument("--as-of", required=True, help="RFC 3339 timestamp with explicit offset")
|
||||||
|
parser.add_argument("--horizon-days", type=int, default=30)
|
||||||
|
parser.add_argument("--fail-on", choices=("none", "low", "medium", "high"), default="high")
|
||||||
|
return parser
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv: list[str] | None = None) -> int:
|
||||||
|
args = build_parser().parse_args(argv)
|
||||||
|
if args.horizon_days < 0:
|
||||||
|
raise SystemExit("--horizon-days must be non-negative")
|
||||||
|
try:
|
||||||
|
as_of = parse_timestamp(args.as_of)
|
||||||
|
except ValueError as exc:
|
||||||
|
raise SystemExit(f"invalid --as-of: {exc}") from exc
|
||||||
|
|
||||||
|
report = build_report(
|
||||||
|
args.declarations,
|
||||||
|
as_of=as_of,
|
||||||
|
horizon_days=args.horizon_days,
|
||||||
|
fail_on=args.fail_on,
|
||||||
|
)
|
||||||
|
print(json.dumps(report, indent=2, sort_keys=True))
|
||||||
|
if report["validation_errors"]:
|
||||||
|
return 2
|
||||||
|
return 0 if report["ok"] else 1
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
261
tools/posture-feedback/tests/test_posture_feedback.py
Normal file
261
tools/posture-feedback/tests/test_posture_feedback.py
Normal file
|
|
@ -0,0 +1,261 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import copy
|
||||||
|
import importlib.util
|
||||||
|
import json
|
||||||
|
import pathlib
|
||||||
|
import sys
|
||||||
|
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
|
||||||
|
TOOL_PATH = pathlib.Path(__file__).resolve().parents[1] / "posture_feedback.py"
|
||||||
|
SPEC = importlib.util.spec_from_file_location("posture_feedback", TOOL_PATH)
|
||||||
|
feedback = importlib.util.module_from_spec(SPEC)
|
||||||
|
assert SPEC and SPEC.loader
|
||||||
|
sys.modules[SPEC.name] = feedback
|
||||||
|
SPEC.loader.exec_module(feedback)
|
||||||
|
|
||||||
|
|
||||||
|
def declaration(service="example") -> dict:
|
||||||
|
return {
|
||||||
|
"schema_version": "0.1",
|
||||||
|
"framework": "netkingdom-tenancy-posture",
|
||||||
|
"service": service,
|
||||||
|
"role": "test-service",
|
||||||
|
"responsible_repo": "example-owner",
|
||||||
|
"tenancy": {
|
||||||
|
"current": {"I": 1, "A": 1, "E": 0, "P": 0, "R": 1, "V": 0},
|
||||||
|
"target": {"I": 1, "A": 1, "E": 0, "P": 0, "R": 1, "V": 0},
|
||||||
|
"reviewed": "2026-08-01",
|
||||||
|
"review_due": "2026-12-31",
|
||||||
|
"service_class": "interactive",
|
||||||
|
"reason": {
|
||||||
|
"I": "floor explained",
|
||||||
|
"A": "floor explained",
|
||||||
|
"E": "floor explained",
|
||||||
|
"P": "floor explained",
|
||||||
|
"R": "floor explained",
|
||||||
|
"V": "floor explained",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def write_declaration(tmp_path: pathlib.Path, document: dict, name="tenancy.yaml") -> pathlib.Path:
|
||||||
|
path = tmp_path / name
|
||||||
|
path.write_text(yaml.safe_dump(document, sort_keys=False), encoding="utf-8")
|
||||||
|
return path
|
||||||
|
|
||||||
|
|
||||||
|
def build(path: pathlib.Path, *, as_of="2026-08-23T12:00:00Z", horizon=14, fail_on="none"):
|
||||||
|
return feedback.build_report(
|
||||||
|
[path],
|
||||||
|
as_of=feedback.parse_timestamp(as_of),
|
||||||
|
horizon_days=horizon,
|
||||||
|
fail_on=fail_on,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def classes(report: dict) -> list[str]:
|
||||||
|
return [item["class"] for item in report["findings"]]
|
||||||
|
|
||||||
|
|
||||||
|
def test_checked_in_reference_emits_five_proposals_and_no_authority():
|
||||||
|
repo_root = pathlib.Path(__file__).resolve().parents[3]
|
||||||
|
path = repo_root / "examples/posture-feedback/expired-e2.yaml"
|
||||||
|
|
||||||
|
report = build(path)
|
||||||
|
|
||||||
|
assert report["summary"] == {
|
||||||
|
"total": 5,
|
||||||
|
"high": 1,
|
||||||
|
"medium": 3,
|
||||||
|
"low": 1,
|
||||||
|
"unknown_owner": 0,
|
||||||
|
}
|
||||||
|
assert report["automation"] == {
|
||||||
|
"mode": "proposal-only",
|
||||||
|
"external_write_permitted": False,
|
||||||
|
"policy_mutation_permitted": False,
|
||||||
|
"declaration_mutation_permitted": False,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_missing_adversarial_freshness_is_unknown_without_owner_inference(tmp_path):
|
||||||
|
document = declaration()
|
||||||
|
document["tenancy"]["current"]["E"] = 2
|
||||||
|
document["tenancy"]["target"]["E"] = 2
|
||||||
|
document["tenancy"]["reason"].pop("E")
|
||||||
|
document["evidence"] = {"E2": "docs/evidence/e2.md"}
|
||||||
|
path = write_declaration(tmp_path, document)
|
||||||
|
|
||||||
|
report = build(path)
|
||||||
|
|
||||||
|
finding = next(item for item in report["findings"] if item["class"] == "evidence-freshness-unknown")
|
||||||
|
assert finding["owner"] == "unknown"
|
||||||
|
assert finding["evidence_state"] == "unknown"
|
||||||
|
|
||||||
|
|
||||||
|
def test_exact_evidence_expiry_is_still_valid_but_due(tmp_path):
|
||||||
|
document = declaration()
|
||||||
|
document["tenancy"]["current"]["E"] = 2
|
||||||
|
document["tenancy"]["target"]["E"] = 2
|
||||||
|
document["tenancy"]["reason"].pop("E")
|
||||||
|
document["evidence"] = {"E2": "docs/evidence/e2.md"}
|
||||||
|
document["evidence_freshness"] = {
|
||||||
|
"E2": {
|
||||||
|
"kind": "adversarial",
|
||||||
|
"observed_at": "2026-08-22T12:00:00Z",
|
||||||
|
"valid_until": "2026-08-23T12:00:00Z",
|
||||||
|
"responsible_repo": "evidence-owner",
|
||||||
|
"scope": "bounded",
|
||||||
|
"remediation": "repeat",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
path = write_declaration(tmp_path, document)
|
||||||
|
|
||||||
|
report = build(path, horizon=0)
|
||||||
|
|
||||||
|
assert "evidence-expired" not in classes(report)
|
||||||
|
assert "evidence-due-soon" in classes(report)
|
||||||
|
|
||||||
|
|
||||||
|
def test_review_due_date_expires_after_utc_calendar_day(tmp_path):
|
||||||
|
document = declaration()
|
||||||
|
document["tenancy"]["review_due"] = "2026-08-23"
|
||||||
|
path = write_declaration(tmp_path, document)
|
||||||
|
|
||||||
|
due_today = build(path, as_of="2026-08-23T23:59:59Z", horizon=0)
|
||||||
|
overdue = build(path, as_of="2026-08-24T00:00:00Z", horizon=0)
|
||||||
|
|
||||||
|
assert "posture-review-due-soon" in classes(due_today)
|
||||||
|
assert "posture-review-overdue" not in classes(due_today)
|
||||||
|
assert "posture-review-overdue" in classes(overdue)
|
||||||
|
|
||||||
|
|
||||||
|
def test_posture_owner_is_unknown_when_not_declared(tmp_path):
|
||||||
|
document = declaration()
|
||||||
|
del document["responsible_repo"]
|
||||||
|
document["tenancy"]["review_due"] = "2026-08-01"
|
||||||
|
path = write_declaration(tmp_path, document)
|
||||||
|
|
||||||
|
report = build(path)
|
||||||
|
|
||||||
|
finding = next(item for item in report["findings"] if item["class"] == "posture-review-overdue")
|
||||||
|
assert finding["owner"] == "unknown"
|
||||||
|
assert report["summary"]["unknown_owner"] == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_multi_service_declaration_is_evaluated_per_service(tmp_path):
|
||||||
|
first = declaration("first")
|
||||||
|
second = declaration("second")
|
||||||
|
for entry in (first, second):
|
||||||
|
entry.pop("schema_version")
|
||||||
|
entry.pop("framework")
|
||||||
|
first["tenancy"]["gap"] = {"A": "first gap"}
|
||||||
|
second["tenancy"]["gap"] = {"V": "second gap"}
|
||||||
|
document = {
|
||||||
|
"schema_version": "0.1",
|
||||||
|
"framework": "netkingdom-tenancy-posture",
|
||||||
|
"services": [first, second],
|
||||||
|
}
|
||||||
|
path = write_declaration(tmp_path, document)
|
||||||
|
|
||||||
|
report = build(path)
|
||||||
|
|
||||||
|
assert {(item["service"], item["control"]) for item in report["findings"]} == {
|
||||||
|
("first", "A"),
|
||||||
|
("second", "V"),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_mechanical_evidence_without_expiry_creates_no_freshness_finding(tmp_path):
|
||||||
|
document = declaration()
|
||||||
|
document["tenancy"]["current"]["A"] = 2
|
||||||
|
document["tenancy"]["target"]["A"] = 2
|
||||||
|
document["tenancy"]["reason"].pop("A")
|
||||||
|
document["evidence"] = {"A2": "tests/authorization.py"}
|
||||||
|
document["evidence_freshness"] = {
|
||||||
|
"A2": {
|
||||||
|
"kind": "mechanical",
|
||||||
|
"observed_at": "2026-08-22T12:00:00Z",
|
||||||
|
"responsible_repo": "example-owner",
|
||||||
|
"scope": "continuous test",
|
||||||
|
"remediation": "repair the test",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
path = write_declaration(tmp_path, document)
|
||||||
|
|
||||||
|
report = build(path)
|
||||||
|
|
||||||
|
assert not any(item["class"].startswith("evidence-") for item in report["findings"])
|
||||||
|
|
||||||
|
|
||||||
|
def test_report_digest_is_stable_for_identical_inputs(tmp_path):
|
||||||
|
path = write_declaration(tmp_path, declaration())
|
||||||
|
|
||||||
|
first = build(path)
|
||||||
|
second = build(path)
|
||||||
|
|
||||||
|
assert first == second
|
||||||
|
assert first["report_digest"].startswith("sha256:")
|
||||||
|
|
||||||
|
|
||||||
|
def test_workspace_source_identifier_is_portable_and_not_an_owner_inference():
|
||||||
|
repo_root = pathlib.Path(__file__).resolve().parents[3]
|
||||||
|
path = repo_root / "examples/posture-feedback/expired-e2.yaml"
|
||||||
|
|
||||||
|
report = build(path)
|
||||||
|
|
||||||
|
assert {item["source"] for item in report["findings"]} == {
|
||||||
|
"net-kingdom/examples/posture-feedback/expired-e2.yaml"
|
||||||
|
}
|
||||||
|
gap = next(item for item in report["findings"] if item["class"] == "declared-gap")
|
||||||
|
zone = next(item for item in report["findings"] if item["class"].startswith("zone-"))
|
||||||
|
assert gap["owner"] == "net-kingdom"
|
||||||
|
assert zone["owner"] == "team:platform-security"
|
||||||
|
assert all(item["owner"] != item["source"] for item in report["findings"])
|
||||||
|
|
||||||
|
|
||||||
|
def test_fail_on_threshold_changes_ok_not_findings(tmp_path):
|
||||||
|
document = declaration()
|
||||||
|
document["tenancy"]["gap"] = {"R": "declared gap"}
|
||||||
|
path = write_declaration(tmp_path, document)
|
||||||
|
|
||||||
|
report_only = build(path, fail_on="none")
|
||||||
|
failing = build(path, fail_on="low")
|
||||||
|
|
||||||
|
assert report_only["ok"] is True
|
||||||
|
assert failing["ok"] is False
|
||||||
|
assert report_only["findings"] == failing["findings"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_invalid_declaration_is_reported_and_cli_exits_two(tmp_path, capsys):
|
||||||
|
document = declaration()
|
||||||
|
del document["role"]
|
||||||
|
path = write_declaration(tmp_path, document)
|
||||||
|
|
||||||
|
exit_code = feedback.main(
|
||||||
|
[
|
||||||
|
"--as-of",
|
||||||
|
"2026-08-23T12:00:00Z",
|
||||||
|
"--fail-on",
|
||||||
|
"none",
|
||||||
|
str(path),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
payload = json.loads(capsys.readouterr().out)
|
||||||
|
|
||||||
|
assert exit_code == 2
|
||||||
|
assert payload["ok"] is False
|
||||||
|
assert payload["validation_errors"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_timestamp_without_offset_is_rejected():
|
||||||
|
try:
|
||||||
|
feedback.parse_timestamp("2026-08-23T12:00:00")
|
||||||
|
except ValueError as exc:
|
||||||
|
assert "explicit UTC offset" in str(exc)
|
||||||
|
else:
|
||||||
|
raise AssertionError("timezone-naive timestamp was accepted")
|
||||||
|
|
@ -89,6 +89,41 @@ class SemanticValidationTests(unittest.TestCase):
|
||||||
document["tenancy"]["review_due"] = "2026-08-16"
|
document["tenancy"]["review_due"] = "2026-08-16"
|
||||||
self.assertIn("review_due precedes reviewed", self.validate(document)[0])
|
self.assertIn("review_due precedes reviewed", self.validate(document)[0])
|
||||||
|
|
||||||
|
def test_evidence_freshness_must_reference_evidence_key(self) -> None:
|
||||||
|
document = declaration()
|
||||||
|
document["evidence_freshness"] = {
|
||||||
|
"E2": {
|
||||||
|
"kind": "adversarial",
|
||||||
|
"observed_at": "2026-08-22T22:10:25Z",
|
||||||
|
"valid_until": "2026-08-23T22:10:25Z",
|
||||||
|
"responsible_repo": "example",
|
||||||
|
"scope": "bounded tenant probes",
|
||||||
|
"remediation": "repeat the bounded run",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.assertIn(
|
||||||
|
"evidence_freshness E2 has no evidence entry",
|
||||||
|
self.validate(document)[0],
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_evidence_freshness_expiry_must_follow_observation(self) -> None:
|
||||||
|
document = declaration()
|
||||||
|
document["evidence"] = {"E2": "docs/evidence/e2.md"}
|
||||||
|
document["evidence_freshness"] = {
|
||||||
|
"E2": {
|
||||||
|
"kind": "adversarial",
|
||||||
|
"observed_at": "2026-08-22T22:10:25Z",
|
||||||
|
"valid_until": "2026-08-22T22:10:25Z",
|
||||||
|
"responsible_repo": "example",
|
||||||
|
"scope": "bounded tenant probes",
|
||||||
|
"remediation": "repeat the bounded run",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.assertIn(
|
||||||
|
"valid_until must be after observed_at",
|
||||||
|
self.validate(document)[0],
|
||||||
|
)
|
||||||
|
|
||||||
def test_service_names_are_unique(self) -> None:
|
def test_service_names_are_unique(self) -> None:
|
||||||
entry = declaration()
|
entry = declaration()
|
||||||
document = {
|
document = {
|
||||||
|
|
|
||||||
|
|
@ -67,6 +67,7 @@ def validate_semantics(document: dict[str, Any], path: pathlib.Path) -> list[str
|
||||||
reason = posture.get("reason", {})
|
reason = posture.get("reason", {})
|
||||||
gap = posture.get("gap", {})
|
gap = posture.get("gap", {})
|
||||||
evidence = entry.get("evidence", {})
|
evidence = entry.get("evidence", {})
|
||||||
|
evidence_freshness = entry.get("evidence_freshness", {})
|
||||||
|
|
||||||
reviewed = dt.date.fromisoformat(posture["reviewed"])
|
reviewed = dt.date.fromisoformat(posture["reviewed"])
|
||||||
review_due = dt.date.fromisoformat(posture["review_due"])
|
review_due = dt.date.fromisoformat(posture["review_due"])
|
||||||
|
|
@ -92,6 +93,22 @@ def validate_semantics(document: dict[str, Any], path: pathlib.Path) -> list[str
|
||||||
f"{name}: implemented {axis}{level} must be above current {axis}{current_level}"
|
f"{name}: implemented {axis}{level} must be above current {axis}{current_level}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
for key, freshness in evidence_freshness.items():
|
||||||
|
if key not in evidence:
|
||||||
|
errors.append(f"{name}: evidence_freshness {key} has no evidence entry")
|
||||||
|
observed_at = dt.datetime.fromisoformat(
|
||||||
|
freshness["observed_at"].replace("Z", "+00:00")
|
||||||
|
)
|
||||||
|
valid_until_raw = freshness.get("valid_until")
|
||||||
|
if valid_until_raw:
|
||||||
|
valid_until = dt.datetime.fromisoformat(
|
||||||
|
valid_until_raw.replace("Z", "+00:00")
|
||||||
|
)
|
||||||
|
if valid_until <= observed_at:
|
||||||
|
errors.append(
|
||||||
|
f"{name}: evidence_freshness {key} valid_until must be after observed_at"
|
||||||
|
)
|
||||||
|
|
||||||
provider = entry.get("provider", {})
|
provider = entry.get("provider", {})
|
||||||
for axis, reach in provider.get("axes", {}).items():
|
for axis, reach in provider.get("axes", {}).items():
|
||||||
available = reach["available"]
|
available = reach["available"]
|
||||||
|
|
|
||||||
111
workplans/NK-WP-0031-deterministic-posture-feedback.md
Normal file
111
workplans/NK-WP-0031-deterministic-posture-feedback.md
Normal file
|
|
@ -0,0 +1,111 @@
|
||||||
|
---
|
||||||
|
id: NK-WP-0031
|
||||||
|
type: workplan
|
||||||
|
title: "Implement deterministic posture and evidence feedback"
|
||||||
|
domain: infotech
|
||||||
|
repo: net-kingdom
|
||||||
|
status: blocked
|
||||||
|
owner: codex
|
||||||
|
topic_slug: netkingdom
|
||||||
|
planning_priority: P1
|
||||||
|
created: "2026-08-23"
|
||||||
|
updated: "2026-08-23"
|
||||||
|
---
|
||||||
|
|
||||||
|
# NK-WP-0031 — Deterministic posture and evidence feedback
|
||||||
|
|
||||||
|
Authorized by the operator as the next locally owned increment after
|
||||||
|
NK-WP-0030. This implements the safe first C6 loop proposed by the 2026-08-23
|
||||||
|
scope-to-intent assessment: surface explicit drift as proposals without
|
||||||
|
autonomous policy or work-record mutation.
|
||||||
|
|
||||||
|
## Define freshness, owner, and proposal semantics
|
||||||
|
|
||||||
|
```task
|
||||||
|
id: NK-WP-0031-T01
|
||||||
|
status: done
|
||||||
|
priority: high
|
||||||
|
```
|
||||||
|
|
||||||
|
Extend Tenancy Posture declarations with optional authoritative repository and
|
||||||
|
evidence-freshness metadata. Define deterministic time, owner resolution,
|
||||||
|
finding severity, stable identity, and the proposal-only safety boundary.
|
||||||
|
|
||||||
|
Implemented in Tenancy Posture draft-14, Posture Feedback v0.1, and their
|
||||||
|
schemas. Missing authoritative ownership or freshness resolves to `unknown`;
|
||||||
|
source-path normalization never becomes owner inference.
|
||||||
|
|
||||||
|
## Implement the posture feedback evaluator
|
||||||
|
|
||||||
|
```task
|
||||||
|
id: NK-WP-0031-T02
|
||||||
|
status: done
|
||||||
|
priority: high
|
||||||
|
```
|
||||||
|
|
||||||
|
Validate declarations and emit deterministic JSON findings for overdue/due-soon
|
||||||
|
reviews, unknown/expired/due-soon evidence, implemented-but-unevidenced levels,
|
||||||
|
and declared gaps. Require explicit `--as-of`; never perform external writes.
|
||||||
|
|
||||||
|
Implemented under `tools/posture-feedback/`. The emitted report declares that
|
||||||
|
external, policy, and declaration writes are all forbidden.
|
||||||
|
|
||||||
|
## Publish reference workflow and tests
|
||||||
|
|
||||||
|
```task
|
||||||
|
id: NK-WP-0031-T03
|
||||||
|
status: done
|
||||||
|
priority: high
|
||||||
|
```
|
||||||
|
|
||||||
|
Provide an example declaration and cover freshness boundaries, missing owners,
|
||||||
|
multi-service inputs, stable digests, severity exit thresholds, and invalid
|
||||||
|
declarations.
|
||||||
|
|
||||||
|
The checked-in expired-E2 example yields five deterministic proposals at its
|
||||||
|
fixed evaluation time. Focused coverage includes every listed boundary plus
|
||||||
|
portable source identity.
|
||||||
|
|
||||||
|
## Obtain audit-core freshness adoption
|
||||||
|
|
||||||
|
```task
|
||||||
|
id: NK-WP-0031-T04
|
||||||
|
status: wait
|
||||||
|
priority: high
|
||||||
|
```
|
||||||
|
|
||||||
|
Ask `audit-core` to add authoritative owner and E2 freshness metadata for
|
||||||
|
`WH-ENG-20260822-AUDIT-E2-03`. The evaluator must report its freshness as
|
||||||
|
unknown until the source declaration supplies the exact timestamps and
|
||||||
|
replacement action; NetKingdom must not parse them from prose.
|
||||||
|
|
||||||
|
Requested in State Hub message `874e5fa5-e05c-4b1a-a915-26c4dba07b87`.
|
||||||
|
The same estate baseline found pre-existing missing evidence entries for
|
||||||
|
`railiance-platform` `apps-pg` current R2 and V1; that separate correction was
|
||||||
|
routed in `d65f8383-bdab-488e-a36f-f8cabd8a5f65`.
|
||||||
|
|
||||||
|
## Verify and reconcile
|
||||||
|
|
||||||
|
```task
|
||||||
|
id: NK-WP-0031-T05
|
||||||
|
status: done
|
||||||
|
priority: medium
|
||||||
|
```
|
||||||
|
|
||||||
|
Run focused and repository regression tests, validate schemas and examples,
|
||||||
|
reconcile State Hub, and publish the implementation evidence.
|
||||||
|
|
||||||
|
Verification on 2026-08-23:
|
||||||
|
|
||||||
|
- 28 focused posture-feedback and tenancy-validator tests passed;
|
||||||
|
- 84 root tests passed with the independently packaged `local-identity`
|
||||||
|
project excluded, then all 142 `local-identity` tests passed in its own
|
||||||
|
environment;
|
||||||
|
- both JSON schemas parsed, the fixed-time reference target succeeded, and
|
||||||
|
`git diff --check` passed;
|
||||||
|
- reuse-surface validated the capability registry entry; and
|
||||||
|
- the read-only seven-declaration baseline is recorded in
|
||||||
|
`history/2026-08-23-posture-feedback-estate-baseline.md`.
|
||||||
|
|
||||||
|
Local implementation is complete. The workplan remains blocked only on T04's
|
||||||
|
externally owned audit-core declaration adoption.
|
||||||
Loading…
Add table
Add a link
Reference in a new issue