# ---------------------------------------------------------------------------
#  Copyright (c) DevSecCode. All rights reserved.
#  Licensed under the MIT License. See LICENSE.txt in the project root for license information.
# ---------------------------------------------------------------------------
"""Pydantic schema for the signed evidence-package manifest.

The manifest is the load-bearing document inside an audit-grade evidence
tarball. Its canonical JSON serialization is what gets signed; verifiers
re-serialize and re-hash to validate that the package is intact.

Layout of an evidence package::

    evidence-{uuid}.tar.gz
    +-- manifest.json           # this schema
    +-- signature.sig           # raw Ed25519 signature over manifest bytes
    +-- findings/
    |   +-- findings.sarif.json
    |   +-- findings.oscal-ar.json
    +-- compliance/
    |   +-- report.json
    |   +-- report.html         # optional, for human review
    +-- attestations/
    |   +-- attestations.json   # active attestations at evidence time
    +-- chain-of-custody.json   # append-only event log

Verification is two layers:

1. The Ed25519 signature in ``signature.sig`` validates against the
   ``signer.public_key_pem`` field of the manifest. This proves the
   manifest has not been tampered with since signing.
2. Every artifact path in ``artifacts`` is re-hashed and compared to the
   recorded ``sha256``. This proves that no artifact has been modified
   since the manifest was built.

Together these two checks make evidence packages tamper-evident. They are
not a chain of trust to any external authority; the signing key's identity
is established by other means (organizational policy, key publication).
"""
from __future__ import annotations

from typing import Literal

from pydantic import BaseModel, Field


SCHEMA_VERSION = "1.0.0"


class EvidenceTool(BaseModel):
	"""The producer of the evidence package."""
	name: str = "deva-ide"
	version: str = "1.0.0"


class EvidencePackageInfo(BaseModel):
	uuid: str = Field(description="Stable identifier for the package.")
	generated_at: str = Field(description="ISO-8601 UTC timestamp.")
	tool: EvidenceTool = Field(default_factory=EvidenceTool)
	scan_id: str | None = Field(
		default=None,
		description="Identifier of the scan whose results back this package.",
	)
	repo_identifier: str | None = Field(
		default=None,
		description="Optional repo identifier (e.g., github.com/org/repo).",
	)


class EvidenceSigner(BaseModel):
	"""Identity and public material for the signing key.

	The public key is embedded in the manifest so verifiers do not need
	out-of-band access to it. ``key_id`` is a stable fingerprint useful for
	indexing and audit trails. Identity attestation (which human or org
	owns the key) is established outside this manifest.
	"""
	key_id: str = Field(
		description=(
			"Stable identifier for the signing key (typically "
			"``ed25519:{first 16 hex of sha256 of pubkey}``)."
		),
	)
	algorithm: Literal["Ed25519"] = "Ed25519"
	mode: Literal["local", "cloud-kms"] = "local"
	public_key_pem: str = Field(
		description="PEM-encoded Ed25519 public key, used for verification.",
	)


class EvidenceArtifact(BaseModel):
	"""A single file inside the evidence tarball.

	The ``path`` is relative to the tarball root and uses forward slashes.
	The ``sha256`` is the lowercase hex digest of the file's bytes.
	"""
	path: str = Field(description="Path inside the tarball, forward-slash separated.")
	sha256: str = Field(description="Hex SHA-256 of the file's bytes.")
	size_bytes: int = Field(ge=0, description="File size in bytes.")
	media_type: str = Field(description="IANA media type or Deva-specific equivalent.")


class EvidenceManifest(BaseModel):
	"""Top-level manifest. Signed bytes are this object's canonical JSON."""
	schema_version: str = SCHEMA_VERSION
	package: EvidencePackageInfo
	frameworks_evaluated: list[str] = Field(default_factory=list)
	signer: EvidenceSigner
	artifacts: list[EvidenceArtifact] = Field(default_factory=list)
	chain_of_custody_sha256: str | None = Field(
		default=None,
		description=(
			"SHA-256 of ``chain-of-custody.json`` if present. Recorded "
			"separately so the chain log itself is signed-over but a "
			"verifier can independently re-hash and confirm."
		),
	)

	def canonical_json(self) -> bytes:
		"""Return the canonical JSON bytes that get signed.

		Stable across runs: keys sorted, no extra whitespace, UTF-8.
		"""
		import json
		return json.dumps(
			self.model_dump(mode="json", exclude_none=False),
			sort_keys=True,
			separators=(",", ":"),
			ensure_ascii=False,
		).encode("utf-8")
