# SecurityProfileProposal CustomResourceDefinition — internal/proposal.
#
# Cluster-level, one-time setup: installing a CRD needs cluster-admin,
# categorically different from the tracer's own runtime RBAC
# (deploy/rbac.yaml, deploy/rbac-restart.yaml, deploy/rbac-history.yaml,
# deploy/rbac-proposal.yaml). Apply this once, as a cluster operator,
# before anyone runs `landlock-genprof trace` — publishing this resource
# is mandatory, not opt-in, so every run needs it:
#
#   kubectl apply -f deploy/crd-securityprofileproposal.yaml
#
# Same landlockgenprof.io API group as deploy/crd-traininghistory.yaml —
# a second Kind under it, not a reason for a second group.
#
# First slice of a three-stage evidence/proposal/approved-policy model:
# TrainingHistory is the evidence stage (already built), this is the
# proposal stage — a reviewable snapshot of what a training run
# generated, published as a cluster object instead of only local files.
# No controller reads or reconciles this resource, same as
# TrainingHistory — `trace` writes it directly, on every run, via the
# dynamic client (internal/proposal/store.go). An eventual approved-
# policy stage (WorkloadSecurityProfile) plus an operator to enforce it
# are deliberately NOT part of this — see docs/roadmap.md.
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  name: securityprofileproposals.landlockgenprof.io
spec:
  group: landlockgenprof.io
  scope: Namespaced
  names:
    kind: SecurityProfileProposal
    listKind: SecurityProfileProposalList
    plural: securityprofileproposals
    singular: securityprofileproposal
  versions:
    - name: v1alpha1
      served: true
      storage: true
      subresources:
        # .status is only writable via the /status subresource endpoint,
        # even on Create — see internal/proposal/store.go's Save/setStatus.
        # Without this, an approve/reject decision would get silently
        # clobbered by the next `trace` re-run against the same pod,
        # since Save() overwrites the whole object every time.
        status: {}
      additionalPrinterColumns:
        - name: Binary
          type: string
          jsonPath: .spec.binary
        - name: GeneratedAt
          type: string
          jsonPath: .spec.generatedAt
        - name: Approval
          type: string
          jsonPath: .status.approvalState
      schema:
        openAPIV3Schema:
          type: object
          properties:
            spec:
              type: object
              properties:
                candidateVersion:
                  type: string
                  enum: [candidate-v1, candidate-v2]
                container:
                  type: string
                binary:
                  type: string
                generatedAt:
                  type: string
                historyUsed:
                  type: boolean
                targetBinding:
                  type: object
                  properties:
                    namespace:
                      type: string
                    group:
                      type: string
                    kind:
                      type: string
                    name:
                      type: string
                # The three fields below hold the exact rendered text
                # (YAML) each exporter's own ToYAML already produces for
                # the corresponding local file — a full, directly usable
                # artifact (apiVersion/kind/metadata included), not a
                # structured sub-spec or bare fragment. Plain strings:
                # copy one out of `kubectl get securityprofileproposal -o
                # yaml` and it's exactly what you'd `kubectl apply -f -`
                # or save as the target file.
                # patchedManifest is the live owner's (or bare pod's)
                # full manifest with the generated securityContext
                # merged in — same content as --patched-manifest-out,
                # not the bare capabilities/seccompProfile fragment
                # --security-context-out produces (that one has no
                # apiVersion/kind/metadata, so isn't directly appliable).
                # spoSeccompProfile is the sole seccomp-related field — a
                # security-profiles-operator (SPO) SeccompProfile custom
                # resource, named to match the exact path
                # patchedManifest's securityContext.seccompProfile.
                # localhostProfile references. No separate raw-JSON
                # seccomp field: spoSeccompProfile.spec.syscalls already
                # carries the same data, and duplicating it here would
                # just be dead weight — actually taking effect requires
                # SPO installed in the cluster; see README.md.
                podLock:
                  type: string
                networkPolicy:
                  type: string
                patchedManifest:
                  type: string
                spoSeccompProfile:
                  type: string
                subject:
                  type: object
                  required: [scope, target, container, imageIdentity]
                  properties:
                    scope:
                      type: string
                      enum: [CONTAINER]
                    target:
                      type: string
                    container:
                      type: string
                    imageIdentity:
                      type: string
                capabilityArtifact:
                  type: object
                  required: [type, containerCapabilities]
                  properties:
                    type:
                      type: string
                      enum: [CONTAINER_CAPABILITIES]
                    containerCapabilities:
                      type: object
                      required: [drop, add]
                      properties:
                        drop:
                          type: array
                          minItems: 1
                          maxItems: 1
                          items:
                            type: string
                            enum: [ALL]
                        add:
                          type: array
                          maxItems: 41
                          items:
                            type: string
                provenance:
                  type: object
                  required: [populationScope, observationIDs]
                  properties:
                    populationScope:
                      type: string
                      enum: [CONTAINER]
                    observationIDs:
                      type: array
                      maxItems: 256
                      items:
                        type: string
                qualification:
                  type: object
                  required: [filesystem, exec, networkConnect, networkBind, capabilities]
                  properties:
                    filesystem: {type: string, enum: [AVAILABLE, UNKNOWN, EMPTY]}
                    exec: {type: string, enum: [AVAILABLE, UNKNOWN, EMPTY]}
                    networkConnect: {type: string, enum: [AVAILABLE, UNKNOWN, EMPTY]}
                    networkBind: {type: string, enum: [AVAILABLE, UNKNOWN, EMPTY]}
                    capabilities: {type: string, enum: [AVAILABLE, UNKNOWN, EMPTY]}
                derivationStatus:
                  type: object
                  required: [capabilities, podLock, networkPolicy, seccomp]
                  properties:
                    capabilities: {type: string, enum: [SUPPORTED]}
                    podLock: {type: string, enum: [NOT_AVAILABLE, UNSUPPORTED]}
                    networkPolicy: {type: string, enum: [NOT_AVAILABLE, UNSUPPORTED]}
                    seccomp: {type: string, enum: [NOT_AVAILABLE, UNSUPPORTED]}
            status:
              type: object
              properties:
                # Draft (set once on Create) -> Reviewed (set by `review`,
                # never downgraded) -> Approved/Rejected (only an explicit
                # `approve`/`reject` sets these, in either direction).
                # See internal/proposal.ApprovalState and this project's
                # docs/product-roadmap-v1.md v0.2 section.
                approvalState:
                  type: string
                  enum: ["Draft", "Reviewed", "Approved", "Rejected"]
                reason:
                  type: string
                updatedAt:
                  type: string
                # ApprovedCandidateDigest records the exact candidate
                # representation that was approved. See ADR-0006: the
                # digest binds approval to the exact persisted candidate
                # bytes the reviewer inspected. This field is required
                # at runtime for apply-time authorization, but the CRD
                # schema does not enforce presence conditionally here.
                approvedCandidateDigest:
                  type: string
                  description: "Content digest of the approved candidate (format: sha256:<hex>)"
                  pattern: "^sha256:[0-9a-f]{64}$"
                approvedReviewContextDigest:
                  type: string
                  description: "Digest of the reviewed candidate-v2 context"
                  pattern: "^sha256:[0-9a-f]{64}$"
                # ApprovalMechanismVersion records which canonicalization
                # / digest mechanism produced approvedCandidateDigest
                # (e.g. candidate-v1). Verifiers must reject unsupported
                # versions. Not enumerated here to allow future versions.
                approvalMechanismVersion:
                  type: string
                  description: "Canonicalization/digest mechanism that produced approvedCandidateDigest (e.g. candidate-v1)"
                lastApprovalSnapshot:
                  type: object
                  description: "Most recent successful approval evidence; not current authorization or complete approval history."
                  required: [proposalUID, approvalMechanismVersion, approvedCandidateDigest, approvedAt]
                  properties:
                    proposalUID:
                      type: string
                    approvalMechanismVersion:
                      type: string
                      enum: [candidate-v1, candidate-v2]
                    approvedCandidateDigest:
                      type: string
                      pattern: "^sha256:[0-9a-f]{64}$"
                    reviewContextDigest:
                      type: string
                    approvedAt:
                      type: string
