1738 lines
80 KiB
Go
1738 lines
80 KiB
Go
package releaseevidence
|
|
|
|
import (
|
|
"archive/zip"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
"slices"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
|
|
"data-terminal-cloud/internal/agentmodelsconformance"
|
|
"data-terminal-cloud/internal/alertreceiverconformance"
|
|
"data-terminal-cloud/internal/cisupplychainconformance"
|
|
"data-terminal-cloud/internal/firmwareconformance"
|
|
"data-terminal-cloud/internal/infrastructureconformance"
|
|
"data-terminal-cloud/internal/kmsrotationconformance"
|
|
"data-terminal-cloud/internal/recoveryconformance"
|
|
"data-terminal-cloud/internal/rtcconformance"
|
|
"data-terminal-cloud/internal/safariconformance"
|
|
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
const (
|
|
IndexSchemaVersion = "release-evidence-index-v2"
|
|
ManifestSchemaVersion = "release-candidate-evidence-bundle-v3"
|
|
maxEvidenceFileBytes = 100 << 20
|
|
firmwareEvidenceCode = "REAL_DEVICE_FIRMWARE"
|
|
realDeviceRTCEvidenceCode = "REAL_DEVICE_RTC"
|
|
realAgentModelsEvidenceCode = "REAL_AGENT_MODELS"
|
|
firmwareDeviceMinimum = 3
|
|
alertReceiverEvidenceCode = "REAL_ALERT_RECEIVER"
|
|
kmsRotationEvidenceCode = "KMS_ROTATION"
|
|
productionInfrastructureEvidenceCode = "PRODUCTION_INFRASTRUCTURE"
|
|
productionRecoveryEvidenceCode = "PRODUCTION_RECOVERY"
|
|
safariCompatibilityEvidenceCode = "SAFARI_COMPATIBILITY"
|
|
remoteCISupplyChainEvidenceCode = "REMOTE_CI_SUPPLY_CHAIN"
|
|
)
|
|
|
|
var (
|
|
versionPattern = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9._+-]{0,127}$`)
|
|
digestPattern = regexp.MustCompile(`^sha256:[a-f0-9]{64}$`)
|
|
commitPattern = regexp.MustCompile(`^[a-f0-9]{40}([a-f0-9]{24})?$`)
|
|
localSourcePattern = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9._-]{2,127}$`)
|
|
openAPIVersionPattern = regexp.MustCompile(`(?m)^ version: ([^\r\n ]+)[\r]?$`)
|
|
requiredCodes = []string{
|
|
remoteCISupplyChainEvidenceCode,
|
|
realDeviceRTCEvidenceCode,
|
|
firmwareEvidenceCode,
|
|
realAgentModelsEvidenceCode,
|
|
productionInfrastructureEvidenceCode,
|
|
productionRecoveryEvidenceCode,
|
|
kmsRotationEvidenceCode,
|
|
alertReceiverEvidenceCode,
|
|
safariCompatibilityEvidenceCode,
|
|
"BUSINESS_APPROVAL",
|
|
}
|
|
requiredFirmwareScenarios = []string{
|
|
"AB_SUCCESS",
|
|
"AB_ROLLBACK",
|
|
"POWER_LOSS_AFTER_STAGE",
|
|
"POWER_LOSS_DURING_BOOT",
|
|
}
|
|
requiredFirmwareChecks = []string{
|
|
"DEVICE_EVIDENCE_SCHEMA", "EVIDENCE_SOURCE", "CANDIDATE_IDENTITY", "DEVICE_IDENTITY",
|
|
"DEVICE_PLATFORM_METADATA", "ARTIFACT_IDENTITY", "CLOUD_ASSIGNMENT", "RELEASE_BINDING",
|
|
"CLOUD_ARTIFACT_BINDING", "COMMAND_BINDING", "CLOUD_TIMELINE_INTEGRITY", "DEVICE_TIMELINE_INTEGRITY",
|
|
"SCENARIO_SUPPORTED", "CLOUD_PHASE_SEQUENCE", "DEVICE_PHASE_SEQUENCE", "BOOT_ATTEMPT_BINDING",
|
|
"PHASE_TIME_CORRELATION", "SLOT_TRANSITION", "VERSION_TRANSITION", "TERMINAL_OUTCOME",
|
|
}
|
|
requiredAlertReceiverChecks = []string{
|
|
"CAPTURE_SCHEMA", "EVIDENCE_SOURCE", "CANDIDATE_IDENTITY", "CAPTURE_CANDIDATE_BINDING", "RECEIVER_IDENTITY",
|
|
"ACTIVE_ROUTES", "RECEIVER_GROUP_MAPPING", "DELIVERY_TEST_COVERAGE", "DELIVERY_TIME_CORRELATION", "FORMAL_ALERT_DRILL",
|
|
}
|
|
requiredKMSRotationChecks = []string{
|
|
"CAPTURE_SCHEMA", "EVIDENCE_SOURCE", "CANDIDATE_IDENTITY", "CAPTURE_CANDIDATE_BINDING", "PROVIDER_IDENTITY",
|
|
"ROTATION_SCOPE", "VERSION_TRANSITION", "NEW_CREDENTIAL_ACCEPTED", "OLD_CREDENTIAL_REJECTED", "BUSINESS_CONTINUITY", "AUDIT_REFERENCES",
|
|
}
|
|
requiredProductionRecoveryChecks = []string{
|
|
"CAPTURE_SCHEMA", "EVIDENCE_SOURCE", "CANDIDATE_IDENTITY", "CAPTURE_CANDIDATE_BINDING", "ENVIRONMENT_IDENTITY",
|
|
"DRILL_COVERAGE", "TIMELINE_INTEGRITY", "RECOVERY_OUTCOME", "DATA_INTEGRITY", "BUSINESS_CONTINUITY", "AUDIT_REFERENCES",
|
|
}
|
|
requiredProductionInfrastructureChecks = []string{
|
|
"CAPTURE_SCHEMA", "EVIDENCE_SOURCE", "CANDIDATE_IDENTITY", "CAPTURE_CANDIDATE_BINDING", "ENVIRONMENT_IDENTITY",
|
|
"COMPONENT_COVERAGE", "PRODUCTION_CONTROLS", "OBSERVATION_TIMELINE", "EVIDENCE_REFERENCES",
|
|
}
|
|
requiredSafariCompatibilityChecks = []string{
|
|
"CAPTURE_SCHEMA", "EVIDENCE_SOURCE", "CANDIDATE_IDENTITY", "CAPTURE_CANDIDATE_BINDING", "ENVIRONMENT_IDENTITY",
|
|
"SAFARI_RELEASE_COVERAGE", "VIEWPORT_COVERAGE", "WORKFLOW_COVERAGE", "FUNCTIONAL_OUTCOME", "EVIDENCE_REFERENCES", "EXECUTION_TIMELINE",
|
|
}
|
|
requiredRealDeviceRTCChecks = []string{
|
|
"CAPTURE_SCHEMA", "EVIDENCE_SOURCE", "CANDIDATE_IDENTITY", "CAPTURE_CANDIDATE_BINDING", "PROVIDER_IDENTITY", "ENVIRONMENT_IDENTITY",
|
|
"DEVICE_COVERAGE", "SESSION_IDENTITY", "ENGINE_MODE_COVERAGE", "SCENARIO_COVERAGE", "RTC_MEDIA_PATH", "STATE_LIFECYCLE",
|
|
"TOKEN_NETWORK_RECOVERY", "PROVIDER_LIFECYCLE", "TRANSCRIPT_USAGE_QUALITY", "EVIDENCE_REFERENCES", "EXECUTION_TIMELINE",
|
|
}
|
|
requiredRealAgentModelsChecks = []string{
|
|
"CAPTURE_SCHEMA", "EVIDENCE_SOURCE", "CANDIDATE_IDENTITY", "CAPTURE_CANDIDATE_BINDING", "ENVIRONMENT_IDENTITY",
|
|
"GENERATION_PROVIDER", "GENERATION_QUALITY", "MODEL_BINDING", "EMBEDDING_PROVIDER", "EMBEDDING_COVERAGE",
|
|
"CONTENT_SAFETY_PROVIDER", "CONTENT_SAFETY_COVERAGE", "DATA_TERMS_QUOTA", "EVIDENCE_REFERENCES",
|
|
}
|
|
requiredRemoteCISupplyChainChecks = []string{
|
|
"CAPTURE_SCHEMA", "EVIDENCE_SOURCE", "CANDIDATE_IDENTITY", "CAPTURE_CANDIDATE_BINDING", "REMOTE_RUN_IDENTITY",
|
|
"REMOTE_RUN_OUTCOME", "RUN_TIMELINE", "INDEPENDENT_REVIEW", "REQUIRED_CHECKS", "IMAGE_DIGESTS", "CYCLONEDX_SBOMS",
|
|
}
|
|
)
|
|
|
|
type EvidenceIndex struct {
|
|
SchemaVersion string `json:"schema_version"`
|
|
CandidateVersion string `json:"candidate_version"`
|
|
BuildDigest string `json:"build_digest"`
|
|
Items []EvidenceItem `json:"items"`
|
|
}
|
|
|
|
type EvidenceItem struct {
|
|
Code string `json:"code"`
|
|
Status string `json:"status"`
|
|
Owner string `json:"owner"`
|
|
Note string `json:"note"`
|
|
Files []string `json:"files"`
|
|
}
|
|
|
|
type Options struct {
|
|
Root string
|
|
CandidateVersion string
|
|
BuildDigest string
|
|
SourceCommit string
|
|
AcceptanceSnapshotPath string
|
|
EvidenceIndexPath string
|
|
PilotDailySnapshotPaths []string
|
|
PilotWeeklySnapshotPath string
|
|
OutputPath string
|
|
GeneratedAt time.Time
|
|
}
|
|
|
|
type Result struct {
|
|
OutputPath string `json:"output_path"`
|
|
SHA256 string `json:"sha256"`
|
|
Status string `json:"status"`
|
|
Verified int `json:"verified"`
|
|
Pending int `json:"pending"`
|
|
Waived int `json:"waived"`
|
|
Files int `json:"files"`
|
|
}
|
|
|
|
type acceptanceSnapshot struct {
|
|
ID string `json:"id"`
|
|
CandidateVersion string `json:"candidate_version"`
|
|
BuildDigest string `json:"build_digest"`
|
|
Environment string `json:"environment"`
|
|
RunType string `json:"run_type"`
|
|
TenantID string `json:"tenant_id"`
|
|
GeneratedAt time.Time `json:"generated_at"`
|
|
RedactionPolicy string `json:"redaction_policy"`
|
|
OverallGateSatisfied bool `json:"overall_gate_satisfied"`
|
|
}
|
|
|
|
type pilotDailySnapshot struct {
|
|
ID string `json:"id"`
|
|
GeneratedAt time.Time `json:"generated_at"`
|
|
TenantID string `json:"tenant_id"`
|
|
CandidateVersion string `json:"candidate_version"`
|
|
BuildDigest string `json:"build_digest"`
|
|
LocalDate string `json:"local_date"`
|
|
Timezone string `json:"timezone"`
|
|
WindowStart time.Time `json:"window_start"`
|
|
WindowEnd time.Time `json:"window_end"`
|
|
RedactionPolicy string `json:"redaction_policy"`
|
|
}
|
|
|
|
type pilotWeeklySnapshot struct {
|
|
ID string `json:"id"`
|
|
GeneratedAt time.Time `json:"generated_at"`
|
|
TenantID string `json:"tenant_id"`
|
|
CandidateVersion string `json:"candidate_version"`
|
|
BuildDigest string `json:"build_digest"`
|
|
WeekStart string `json:"week_start"`
|
|
Timezone string `json:"timezone"`
|
|
WindowStart time.Time `json:"window_start"`
|
|
WindowEnd time.Time `json:"window_end"`
|
|
IsComplete bool `json:"is_complete"`
|
|
RedactionPolicy string `json:"redaction_policy"`
|
|
ExternalEvidence []PilotWeeklyExternalEvidence `json:"external_evidence"`
|
|
}
|
|
|
|
type PilotWeeklyExternalEvidence struct {
|
|
Code string `json:"code"`
|
|
Status string `json:"status"`
|
|
}
|
|
|
|
type pilotBundleEvidence struct {
|
|
WeekStart string
|
|
Timezone string
|
|
Daily []bundleFile
|
|
Weekly bundleFile
|
|
}
|
|
|
|
type manifest struct {
|
|
SchemaVersion string `json:"schema_version"`
|
|
CandidateVersion string `json:"candidate_version"`
|
|
BuildDigest string `json:"build_digest"`
|
|
SourceCommit string `json:"source_commit"`
|
|
OpenAPIVersion string `json:"openapi_version"`
|
|
Environment string `json:"environment"`
|
|
RunType string `json:"run_type"`
|
|
AcceptanceSnapshotID string `json:"acceptance_snapshot_id"`
|
|
AcceptanceGeneratedAt time.Time `json:"acceptance_generated_at"`
|
|
TechnicalGateSatisfied bool `json:"technical_gate_satisfied"`
|
|
EvidenceStatus string `json:"evidence_status"`
|
|
VerifiedCount int `json:"verified_count"`
|
|
PendingCount int `json:"pending_count"`
|
|
WaivedCount int `json:"waived_count"`
|
|
SBOMCount int `json:"sbom_count"`
|
|
RemoteCISupplyChainValidation RemoteCISupplyChainValidationSummary `json:"remote_ci_supply_chain_validation"`
|
|
PilotEvidenceIncluded bool `json:"pilot_evidence_included"`
|
|
PilotWeekStart string `json:"pilot_week_start,omitempty"`
|
|
PilotTimezone string `json:"pilot_timezone,omitempty"`
|
|
PilotDailyCount int `json:"pilot_daily_count"`
|
|
FirmwareValidation FirmwareValidationSummary `json:"firmware_validation"`
|
|
AlertReceiverValidation AlertReceiverValidationSummary `json:"alert_receiver_validation"`
|
|
KMSRotationValidation KMSRotationValidationSummary `json:"kms_rotation_validation"`
|
|
ProductionInfrastructureValidation ProductionInfrastructureValidationSummary `json:"production_infrastructure_validation"`
|
|
ProductionRecoveryValidation ProductionRecoveryValidationSummary `json:"production_recovery_validation"`
|
|
SafariCompatibilityValidation SafariCompatibilityValidationSummary `json:"safari_compatibility_validation"`
|
|
RealDeviceRTCValidation RealDeviceRTCValidationSummary `json:"real_device_rtc_validation"`
|
|
RealAgentModelsValidation RealAgentModelsValidationSummary `json:"real_agent_models_validation"`
|
|
GeneratedAt time.Time `json:"generated_at"`
|
|
Files []manifestFile `json:"files"`
|
|
Warnings []string `json:"warnings"`
|
|
}
|
|
|
|
type FirmwareValidationSummary struct {
|
|
Status string `json:"status"`
|
|
RequiredDeviceCount int `json:"required_device_count"`
|
|
ObservedDeviceCount int `json:"observed_device_count"`
|
|
QualifiedDeviceCount int `json:"qualified_device_count"`
|
|
ReportCount int `json:"report_count"`
|
|
CoverageComplete bool `json:"coverage_complete"`
|
|
RequiredScenarios []string `json:"required_scenarios"`
|
|
ScenarioDeviceCounts map[string]int `json:"scenario_device_counts"`
|
|
Gaps []string `json:"gaps"`
|
|
}
|
|
|
|
type AlertReceiverValidationSummary struct {
|
|
Status string `json:"status"`
|
|
ReportPresent bool `json:"report_present"`
|
|
ActiveRouteCount int `json:"active_route_count"`
|
|
MappedGroupCount int `json:"mapped_group_count"`
|
|
TestReceiptCount int `json:"test_receipt_count"`
|
|
FormalDrillVerified bool `json:"formal_drill_verified"`
|
|
CoverageComplete bool `json:"coverage_complete"`
|
|
Gaps []string `json:"gaps"`
|
|
}
|
|
|
|
type KMSRotationValidationSummary struct {
|
|
Status string `json:"status"`
|
|
ReportPresent bool `json:"report_present"`
|
|
ProviderRef string `json:"provider_ref,omitempty"`
|
|
RotationCount int `json:"rotation_count"`
|
|
ConsumptionModes []string `json:"consumption_modes"`
|
|
CoverageComplete bool `json:"coverage_complete"`
|
|
Gaps []string `json:"gaps"`
|
|
}
|
|
|
|
type ProductionRecoveryValidationSummary struct {
|
|
Status string `json:"status"`
|
|
ReportPresent bool `json:"report_present"`
|
|
InfrastructureRef string `json:"infrastructure_ref,omitempty"`
|
|
DrillCount int `json:"drill_count"`
|
|
CompletedDrillTypes []string `json:"completed_drill_types"`
|
|
RecoverySeconds map[string]int `json:"observed_recovery_seconds"`
|
|
CoverageComplete bool `json:"coverage_complete"`
|
|
Gaps []string `json:"gaps"`
|
|
}
|
|
|
|
type ProductionInfrastructureValidationSummary struct {
|
|
Status string `json:"status"`
|
|
ReportPresent bool `json:"report_present"`
|
|
Environment string `json:"environment,omitempty"`
|
|
RegionRef string `json:"region_ref,omitempty"`
|
|
ClusterRef string `json:"cluster_ref,omitempty"`
|
|
ComponentCount int `json:"component_count"`
|
|
VerifiedComponents []string `json:"verified_components"`
|
|
ControlCounts map[string]int `json:"control_counts"`
|
|
CoverageComplete bool `json:"coverage_complete"`
|
|
Gaps []string `json:"gaps"`
|
|
}
|
|
|
|
type SafariCompatibilityValidationSummary struct {
|
|
Status string `json:"status"`
|
|
ReportPresent bool `json:"report_present"`
|
|
Environment string `json:"environment,omitempty"`
|
|
BrowserVersions map[string]string `json:"browser_versions"`
|
|
RunCount int `json:"run_count"`
|
|
ResultCount int `json:"result_count"`
|
|
CompletedReleaseRanks []string `json:"completed_release_ranks"`
|
|
CoverageComplete bool `json:"coverage_complete"`
|
|
Gaps []string `json:"gaps"`
|
|
}
|
|
|
|
type RealDeviceRTCValidationSummary struct {
|
|
Status string `json:"status"`
|
|
ReportPresent bool `json:"report_present"`
|
|
Environment string `json:"environment,omitempty"`
|
|
RTCProviderRef string `json:"rtc_provider_ref,omitempty"`
|
|
VoiceProviderRef string `json:"voice_provider_ref,omitempty"`
|
|
DeviceCount int `json:"device_count"`
|
|
SessionCount int `json:"session_count"`
|
|
CompletedEngineModes []string `json:"completed_engine_modes"`
|
|
CompletedScenarios []string `json:"completed_scenarios"`
|
|
ModeSessionCounts map[string]int `json:"mode_session_counts"`
|
|
ScenarioSessionCounts map[string]int `json:"scenario_session_counts"`
|
|
CoverageComplete bool `json:"coverage_complete"`
|
|
Gaps []string `json:"gaps"`
|
|
}
|
|
|
|
type RealAgentModelsValidationSummary struct {
|
|
Status string `json:"status"`
|
|
ReportPresent bool `json:"report_present"`
|
|
Environment string `json:"environment,omitempty"`
|
|
GenerationProviderRef string `json:"generation_provider_ref,omitempty"`
|
|
AgentVersion string `json:"agent_version,omitempty"`
|
|
ModelConfigRef string `json:"model_config_ref,omitempty"`
|
|
SuiteVersion string `json:"suite_version,omitempty"`
|
|
GenerationCaseCount int `json:"generation_case_count"`
|
|
EmbeddingProviderRef string `json:"embedding_provider_ref,omitempty"`
|
|
EmbeddingModelRef string `json:"embedding_model_ref,omitempty"`
|
|
EmbeddingQueryCount int `json:"embedding_query_count"`
|
|
EmbeddingCategories []string `json:"embedding_categories"`
|
|
ContentSafetyProviderRef string `json:"content_safety_provider_ref,omitempty"`
|
|
ContentSafetyPolicyRef string `json:"content_safety_policy_ref,omitempty"`
|
|
ContentSafetyCaseCount int `json:"content_safety_case_count"`
|
|
ContentSafetyCategories []string `json:"content_safety_categories"`
|
|
CoverageComplete bool `json:"coverage_complete"`
|
|
Gaps []string `json:"gaps"`
|
|
}
|
|
|
|
type RemoteCISupplyChainValidationSummary struct {
|
|
Status string `json:"status"`
|
|
ReportPresent bool `json:"report_present"`
|
|
Provider string `json:"provider,omitempty"`
|
|
RunID string `json:"run_id,omitempty"`
|
|
RunURL string `json:"run_url,omitempty"`
|
|
CompletedChecks []string `json:"completed_checks"`
|
|
ImageDigests map[string]string `json:"image_digests"`
|
|
SBOMDigests map[string]string `json:"sbom_digests"`
|
|
CoverageComplete bool `json:"coverage_complete"`
|
|
Gaps []string `json:"gaps"`
|
|
}
|
|
|
|
type manifestFile struct {
|
|
Path string `json:"path"`
|
|
SHA256 string `json:"sha256"`
|
|
Size int64 `json:"size"`
|
|
}
|
|
|
|
type bundleFile struct {
|
|
archivePath string
|
|
data []byte
|
|
}
|
|
|
|
func RequiredEvidenceCodes() []string { return slices.Clone(requiredCodes) }
|
|
|
|
func Build(options Options) (Result, error) {
|
|
root, err := filepath.Abs(strings.TrimSpace(options.Root))
|
|
if err != nil || root == "" {
|
|
return Result{}, errors.New("project root is required")
|
|
}
|
|
options.CandidateVersion = strings.TrimSpace(options.CandidateVersion)
|
|
options.BuildDigest = strings.ToLower(strings.TrimSpace(options.BuildDigest))
|
|
options.SourceCommit = strings.ToLower(strings.TrimSpace(options.SourceCommit))
|
|
if !versionPattern.MatchString(options.CandidateVersion) || !digestPattern.MatchString(options.BuildDigest) {
|
|
return Result{}, errors.New("candidate version or build digest is invalid")
|
|
}
|
|
if strings.TrimSpace(options.OutputPath) == "" {
|
|
return Result{}, errors.New("output path is required")
|
|
}
|
|
generatedAt := options.GeneratedAt.UTC()
|
|
if generatedAt.IsZero() {
|
|
generatedAt = time.Now().UTC()
|
|
}
|
|
|
|
acceptanceBytes, err := readRegularFile(options.AcceptanceSnapshotPath)
|
|
if err != nil {
|
|
return Result{}, fmt.Errorf("read acceptance snapshot: %w", err)
|
|
}
|
|
acceptance, err := decodeAcceptance(acceptanceBytes)
|
|
if err != nil {
|
|
return Result{}, err
|
|
}
|
|
if acceptance.CandidateVersion != options.CandidateVersion || strings.ToLower(acceptance.BuildDigest) != options.BuildDigest {
|
|
return Result{}, errors.New("acceptance snapshot does not match the candidate version and build digest")
|
|
}
|
|
if !acceptance.OverallGateSatisfied {
|
|
return Result{}, errors.New("acceptance snapshot technical gate is not satisfied")
|
|
}
|
|
if acceptance.RedactionPolicy != "ops-acceptance-minimal-v1" || acceptance.ID == "" || acceptance.GeneratedAt.IsZero() {
|
|
return Result{}, errors.New("acceptance snapshot is missing required identity or redaction metadata")
|
|
}
|
|
if !slices.Contains([]string{"LAB", "STAGING", "PRODUCTION"}, acceptance.Environment) || !slices.Contains([]string{"REAL_DEVICE_PROVIDER", "PILOT", "PRODUCTION_RELEASE"}, acceptance.RunType) {
|
|
return Result{}, errors.New("acceptance snapshot environment or run type is invalid")
|
|
}
|
|
if strings.EqualFold(acceptance.Environment, "LAB") {
|
|
if !localSourcePattern.MatchString(options.SourceCommit) {
|
|
return Result{}, errors.New("source commit or explicit local source label is required")
|
|
}
|
|
} else if !commitPattern.MatchString(options.SourceCommit) {
|
|
return Result{}, errors.New("staging and production evidence bundles require a 40 or 64 character hexadecimal source commit")
|
|
}
|
|
|
|
indexBytes, err := readRegularFile(options.EvidenceIndexPath)
|
|
if err != nil {
|
|
return Result{}, fmt.Errorf("read evidence index: %w", err)
|
|
}
|
|
var index EvidenceIndex
|
|
if err = json.Unmarshal(indexBytes, &index); err != nil {
|
|
return Result{}, fmt.Errorf("decode evidence index: %w", err)
|
|
}
|
|
if err = validateIndex(index, options.CandidateVersion, options.BuildDigest); err != nil {
|
|
return Result{}, err
|
|
}
|
|
remoteCISupplyChainValidation, err := validateRemoteCISupplyChainEvidence(index, filepath.Dir(options.EvidenceIndexPath), options.CandidateVersion, options.BuildDigest, options.SourceCommit)
|
|
if err != nil {
|
|
return Result{}, err
|
|
}
|
|
firmwareValidation, err := validateFirmwareEvidence(index, filepath.Dir(options.EvidenceIndexPath), options.CandidateVersion, options.BuildDigest)
|
|
if err != nil {
|
|
return Result{}, err
|
|
}
|
|
alertReceiverValidation, err := validateAlertReceiverEvidence(index, filepath.Dir(options.EvidenceIndexPath), options.CandidateVersion, options.BuildDigest)
|
|
if err != nil {
|
|
return Result{}, err
|
|
}
|
|
kmsRotationValidation, err := validateKMSRotationEvidence(index, filepath.Dir(options.EvidenceIndexPath), options.CandidateVersion, options.BuildDigest)
|
|
if err != nil {
|
|
return Result{}, err
|
|
}
|
|
productionRecoveryValidation, err := validateProductionRecoveryEvidence(index, filepath.Dir(options.EvidenceIndexPath), options.CandidateVersion, options.BuildDigest)
|
|
if err != nil {
|
|
return Result{}, err
|
|
}
|
|
productionInfrastructureValidation, err := validateProductionInfrastructureEvidence(index, filepath.Dir(options.EvidenceIndexPath), options.CandidateVersion, options.BuildDigest)
|
|
if err != nil {
|
|
return Result{}, err
|
|
}
|
|
safariCompatibilityValidation, err := validateSafariCompatibilityEvidence(index, filepath.Dir(options.EvidenceIndexPath), options.CandidateVersion, options.BuildDigest)
|
|
if err != nil {
|
|
return Result{}, err
|
|
}
|
|
realDeviceRTCValidation, err := validateRealDeviceRTCEvidence(index, filepath.Dir(options.EvidenceIndexPath), options.CandidateVersion, options.BuildDigest)
|
|
if err != nil {
|
|
return Result{}, err
|
|
}
|
|
realAgentModelsValidation, err := validateRealAgentModelsEvidence(index, filepath.Dir(options.EvidenceIndexPath), options.CandidateVersion, options.BuildDigest)
|
|
if err != nil {
|
|
return Result{}, err
|
|
}
|
|
pilotEvidence, err := collectPilotEvidence(options, acceptance)
|
|
if err != nil {
|
|
return Result{}, err
|
|
}
|
|
|
|
files := []bundleFile{{archivePath: "acceptance/technical-acceptance-snapshot.json", data: acceptanceBytes}}
|
|
if pilotEvidence.Weekly.archivePath != "" {
|
|
files = append(files, pilotEvidence.Daily...)
|
|
files = append(files, pilotEvidence.Weekly)
|
|
}
|
|
baseline, sbomCount, warnings, err := collectBaseline(root)
|
|
if err != nil {
|
|
return Result{}, err
|
|
}
|
|
files = append(files, baseline...)
|
|
openAPIVersion := ""
|
|
for _, baselineFile := range baseline {
|
|
if baselineFile.archivePath == "baseline/openapi/openapi.yaml" {
|
|
match := openAPIVersionPattern.FindSubmatch(baselineFile.data)
|
|
if len(match) == 2 {
|
|
openAPIVersion = string(match[1])
|
|
}
|
|
break
|
|
}
|
|
}
|
|
if openAPIVersion == "" {
|
|
return Result{}, errors.New("OpenAPI info version could not be read")
|
|
}
|
|
verified, pending, waived := 0, 0, 0
|
|
indexDirectory := filepath.Dir(options.EvidenceIndexPath)
|
|
packagedIndex := index
|
|
packagedIndex.Items = slices.Clone(index.Items)
|
|
for itemPosition, item := range index.Items {
|
|
packagedIndex.Items[itemPosition].Files = []string{}
|
|
switch strings.ToUpper(strings.TrimSpace(item.Status)) {
|
|
case "VERIFIED":
|
|
verified++
|
|
case "PENDING":
|
|
pending++
|
|
case "WAIVED":
|
|
waived++
|
|
}
|
|
for fileIndex, evidencePath := range item.Files {
|
|
resolved := evidencePath
|
|
if !filepath.IsAbs(resolved) {
|
|
resolved = filepath.Join(indexDirectory, resolved)
|
|
}
|
|
data, readErr := readRegularFile(resolved)
|
|
if readErr != nil {
|
|
return Result{}, fmt.Errorf("read evidence for %s: %w", item.Code, readErr)
|
|
}
|
|
name := fmt.Sprintf("evidence/%s/%02d-%s", item.Code, fileIndex+1, safeBaseName(filepath.Base(resolved)))
|
|
files = append(files, bundleFile{archivePath: name, data: data})
|
|
packagedIndex.Items[itemPosition].Files = append(packagedIndex.Items[itemPosition].Files, name)
|
|
}
|
|
}
|
|
packagedIndexBytes, _ := json.MarshalIndent(packagedIndex, "", " ")
|
|
packagedIndexBytes = append(packagedIndexBytes, '\n')
|
|
files = append(files, bundleFile{archivePath: "evidence-index.json", data: packagedIndexBytes})
|
|
|
|
status := "READY_FOR_RELEASE_REVIEW"
|
|
if pending > 0 {
|
|
status = "EXTERNAL_VALIDATION_PENDING"
|
|
} else if waived > 0 {
|
|
status = "RELEASE_REVIEW_WITH_WAIVERS"
|
|
}
|
|
manifestFiles := make([]manifestFile, 0, len(files))
|
|
seen := map[string]bool{}
|
|
for _, file := range files {
|
|
file.archivePath = filepath.ToSlash(file.archivePath)
|
|
if file.archivePath == "" || strings.Contains(file.archivePath, "../") || seen[file.archivePath] {
|
|
return Result{}, fmt.Errorf("duplicate or unsafe archive path: %s", file.archivePath)
|
|
}
|
|
seen[file.archivePath] = true
|
|
digest := sha256.Sum256(file.data)
|
|
manifestFiles = append(manifestFiles, manifestFile{Path: file.archivePath, SHA256: hex.EncodeToString(digest[:]), Size: int64(len(file.data))})
|
|
}
|
|
sort.Slice(manifestFiles, func(i, j int) bool { return manifestFiles[i].Path < manifestFiles[j].Path })
|
|
bundleManifest := manifest{
|
|
SchemaVersion: ManifestSchemaVersion, CandidateVersion: options.CandidateVersion, BuildDigest: options.BuildDigest,
|
|
SourceCommit: options.SourceCommit, OpenAPIVersion: openAPIVersion, Environment: acceptance.Environment, RunType: acceptance.RunType,
|
|
AcceptanceSnapshotID: acceptance.ID, AcceptanceGeneratedAt: acceptance.GeneratedAt.UTC(), TechnicalGateSatisfied: true,
|
|
EvidenceStatus: status, VerifiedCount: verified, PendingCount: pending, WaivedCount: waived, SBOMCount: sbomCount,
|
|
RemoteCISupplyChainValidation: remoteCISupplyChainValidation,
|
|
PilotEvidenceIncluded: pilotEvidence.Weekly.archivePath != "", PilotWeekStart: pilotEvidence.WeekStart,
|
|
PilotTimezone: pilotEvidence.Timezone, PilotDailyCount: len(pilotEvidence.Daily),
|
|
FirmwareValidation: firmwareValidation,
|
|
AlertReceiverValidation: alertReceiverValidation,
|
|
KMSRotationValidation: kmsRotationValidation,
|
|
ProductionInfrastructureValidation: productionInfrastructureValidation,
|
|
ProductionRecoveryValidation: productionRecoveryValidation,
|
|
SafariCompatibilityValidation: safariCompatibilityValidation,
|
|
RealDeviceRTCValidation: realDeviceRTCValidation,
|
|
RealAgentModelsValidation: realAgentModelsValidation,
|
|
GeneratedAt: generatedAt, Files: manifestFiles, Warnings: warnings,
|
|
}
|
|
manifestBytes, _ := json.MarshalIndent(bundleManifest, "", " ")
|
|
manifestBytes = append(manifestBytes, '\n')
|
|
files = append(files, bundleFile{archivePath: "release-manifest.json", data: manifestBytes})
|
|
sort.Slice(files, func(i, j int) bool { return files[i].archivePath < files[j].archivePath })
|
|
|
|
outputPath, err := filepath.Abs(options.OutputPath)
|
|
if err != nil {
|
|
return Result{}, fmt.Errorf("resolve output path: %w", err)
|
|
}
|
|
if err = os.MkdirAll(filepath.Dir(outputPath), 0o755); err != nil {
|
|
return Result{}, fmt.Errorf("create output directory: %w", err)
|
|
}
|
|
if _, statErr := os.Lstat(outputPath); statErr == nil {
|
|
return Result{}, errors.New("output bundle already exists; choose a new path")
|
|
} else if !errors.Is(statErr, os.ErrNotExist) {
|
|
return Result{}, fmt.Errorf("inspect output path: %w", statErr)
|
|
}
|
|
temporary, err := os.CreateTemp(filepath.Dir(outputPath), ".release-evidence-*.zip")
|
|
if err != nil {
|
|
return Result{}, fmt.Errorf("create temporary bundle: %w", err)
|
|
}
|
|
temporaryPath := temporary.Name()
|
|
defer os.Remove(temporaryPath)
|
|
if err = writeZIP(temporary, files, generatedAt); err != nil {
|
|
temporary.Close()
|
|
return Result{}, err
|
|
}
|
|
if err = temporary.Close(); err != nil {
|
|
return Result{}, fmt.Errorf("close evidence bundle: %w", err)
|
|
}
|
|
if err = os.Rename(temporaryPath, outputPath); err != nil {
|
|
return Result{}, fmt.Errorf("publish evidence bundle: %w", err)
|
|
}
|
|
digest, err := hashFile(outputPath)
|
|
if err != nil {
|
|
return Result{}, err
|
|
}
|
|
return Result{OutputPath: outputPath, SHA256: digest, Status: status, Verified: verified, Pending: pending, Waived: waived, Files: len(files)}, nil
|
|
}
|
|
|
|
func validateIndex(index EvidenceIndex, candidateVersion, buildDigest string) error {
|
|
if index.SchemaVersion != IndexSchemaVersion || index.CandidateVersion != candidateVersion || strings.ToLower(index.BuildDigest) != buildDigest {
|
|
return errors.New("evidence index schema or candidate identity does not match")
|
|
}
|
|
seen := map[string]bool{}
|
|
for _, item := range index.Items {
|
|
if item.Code != strings.TrimSpace(item.Code) || item.Status != strings.ToUpper(strings.TrimSpace(item.Status)) {
|
|
return fmt.Errorf("evidence code or status is not canonical: %s", item.Code)
|
|
}
|
|
if !slices.Contains(requiredCodes, item.Code) || seen[item.Code] {
|
|
return fmt.Errorf("unknown or duplicate evidence code: %s", item.Code)
|
|
}
|
|
seen[item.Code] = true
|
|
if item.Status != "VERIFIED" && item.Status != "PENDING" && item.Status != "WAIVED" {
|
|
return fmt.Errorf("invalid evidence status for %s", item.Code)
|
|
}
|
|
if strings.TrimSpace(item.Owner) == "" || len([]rune(item.Owner)) > 120 || len([]rune(item.Note)) > 1000 {
|
|
return fmt.Errorf("evidence owner or note is invalid for %s", item.Code)
|
|
}
|
|
if item.Status == "VERIFIED" && len(item.Files) == 0 {
|
|
return fmt.Errorf("verified evidence %s requires at least one file", item.Code)
|
|
}
|
|
if item.Status == "WAIVED" && strings.TrimSpace(item.Note) == "" {
|
|
return fmt.Errorf("waived evidence %s requires a note", item.Code)
|
|
}
|
|
if item.Code == firmwareEvidenceCode && item.Status == "WAIVED" {
|
|
return errors.New("REAL_DEVICE_FIRMWARE cannot be waived; incomplete hardware scenarios must remain PENDING")
|
|
}
|
|
if item.Code == alertReceiverEvidenceCode && item.Status == "WAIVED" {
|
|
return errors.New("REAL_ALERT_RECEIVER cannot be waived; missing real gateway evidence must remain PENDING")
|
|
}
|
|
if item.Code == kmsRotationEvidenceCode && item.Status == "WAIVED" {
|
|
return errors.New("KMS_ROTATION cannot be waived; missing real KMS/HSM rotation evidence must remain PENDING")
|
|
}
|
|
if item.Code == productionRecoveryEvidenceCode && item.Status == "WAIVED" {
|
|
return errors.New("PRODUCTION_RECOVERY cannot be waived; missing production-equivalent drills must remain PENDING")
|
|
}
|
|
if item.Code == productionInfrastructureEvidenceCode && item.Status == "WAIVED" {
|
|
return errors.New("PRODUCTION_INFRASTRUCTURE cannot be waived; missing production infrastructure evidence must remain PENDING")
|
|
}
|
|
if item.Code == safariCompatibilityEvidenceCode && item.Status == "WAIVED" {
|
|
return errors.New("SAFARI_COMPATIBILITY cannot be waived; missing real Safari matrix must remain PENDING")
|
|
}
|
|
if item.Code == realDeviceRTCEvidenceCode && item.Status == "WAIVED" {
|
|
return errors.New("REAL_DEVICE_RTC cannot be waived; missing real device and provider matrix must remain PENDING")
|
|
}
|
|
if item.Code == realAgentModelsEvidenceCode && item.Status == "WAIVED" {
|
|
return errors.New("REAL_AGENT_MODELS cannot be waived; missing real generation, embedding, or content safety evidence must remain PENDING")
|
|
}
|
|
if item.Code == remoteCISupplyChainEvidenceCode && item.Status == "WAIVED" {
|
|
return errors.New("REMOTE_CI_SUPPLY_CHAIN cannot be waived; missing remote CI, image, or SBOM evidence must remain PENDING")
|
|
}
|
|
if len(item.Files) > 20 {
|
|
return fmt.Errorf("too many evidence files for %s", item.Code)
|
|
}
|
|
}
|
|
for _, code := range requiredCodes {
|
|
if !seen[code] {
|
|
return fmt.Errorf("required evidence code is missing: %s", code)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func validateRemoteCISupplyChainEvidence(index EvidenceIndex, indexDirectory, candidateVersion, buildDigest, sourceCommit string) (RemoteCISupplyChainValidationSummary, error) {
|
|
gaps := []string{"REMOTE_CI_REPORT:0/1"}
|
|
for _, code := range cisupplychainconformance.RequiredChecks {
|
|
gaps = append(gaps, code+":0/1")
|
|
}
|
|
for _, subject := range cisupplychainconformance.RequiredImages {
|
|
gaps = append(gaps, subject+":0/1")
|
|
}
|
|
for _, subject := range cisupplychainconformance.RequiredSBOMs {
|
|
gaps = append(gaps, "SBOM_"+subject+":0/1")
|
|
}
|
|
summary := RemoteCISupplyChainValidationSummary{Status: "PENDING", CompletedChecks: []string{}, ImageDigests: map[string]string{}, SBOMDigests: map[string]string{}, Gaps: gaps}
|
|
var item EvidenceItem
|
|
found := false
|
|
for _, candidate := range index.Items {
|
|
if candidate.Code == remoteCISupplyChainEvidenceCode {
|
|
item, found = candidate, true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
return summary, errors.New("required evidence code is missing: " + remoteCISupplyChainEvidenceCode)
|
|
}
|
|
summary.Status = item.Status
|
|
if len(item.Files) == 0 {
|
|
if item.Status == "VERIFIED" {
|
|
return summary, errors.New("REMOTE_CI_SUPPLY_CHAIN is incomplete and must remain PENDING: REMOTE_CI_REPORT:0/1")
|
|
}
|
|
return summary, nil
|
|
}
|
|
reportPath := item.Files[0]
|
|
if !filepath.IsAbs(reportPath) {
|
|
reportPath = filepath.Join(indexDirectory, reportPath)
|
|
}
|
|
content, err := readRegularFile(reportPath)
|
|
if err != nil {
|
|
return summary, fmt.Errorf("read remote CI supply-chain report: %w", err)
|
|
}
|
|
report, err := cisupplychainconformance.ParseReport(content)
|
|
if err != nil {
|
|
return summary, err
|
|
}
|
|
if err = validateRemoteCISupplyChainReport(report, candidateVersion, buildDigest, sourceCommit); err != nil {
|
|
return summary, fmt.Errorf("remote CI supply-chain report %s is not eligible: %w", filepath.Base(reportPath), err)
|
|
}
|
|
summary.ReportPresent = true
|
|
summary.Provider = report.Provider
|
|
summary.RunID = report.RunID
|
|
summary.RunURL = report.RunURL
|
|
summary.CompletedChecks = slices.Clone(report.CompletedChecks)
|
|
summary.ImageDigests = mapsCloneString(report.ImageDigests)
|
|
summary.SBOMDigests = mapsCloneString(report.SBOMDigests)
|
|
summary.CoverageComplete = true
|
|
summary.Gaps = []string{}
|
|
return summary, nil
|
|
}
|
|
|
|
func validateRemoteCISupplyChainReport(report cisupplychainconformance.Report, candidateVersion, buildDigest, sourceCommit string) error {
|
|
if report.SchemaVersion != cisupplychainconformance.ReportSchema || report.Result != "PASS" || report.EvidenceScope != "REMOTE_CI_SUPPLY_CHAIN" || report.ExternalValidationRequired {
|
|
return errors.New("report must be a PASS REMOTE_CI_SUPPLY_CHAIN report with external validation completed")
|
|
}
|
|
if report.CandidateVersion != candidateVersion || strings.ToLower(report.BuildDigest) != strings.ToLower(buildDigest) || strings.ToLower(report.SourceCommit) != strings.ToLower(sourceCommit) {
|
|
return errors.New("candidate version, build digest, or source commit does not match the release bundle")
|
|
}
|
|
if report.GeneratedAt.IsZero() || report.Provider == "" || report.RunID == "" || report.RunURL == "" || strings.TrimSpace(report.Disclaimer) == "" {
|
|
return errors.New("report is missing provider, run, time, or disclaimer identity")
|
|
}
|
|
for _, code := range cisupplychainconformance.RequiredChecks {
|
|
if !slices.Contains(report.CompletedChecks, code) {
|
|
return errors.New("required CI check is missing: " + code)
|
|
}
|
|
}
|
|
for _, subject := range cisupplychainconformance.RequiredImages {
|
|
if !digestPattern.MatchString(strings.ToLower(report.ImageDigests[subject])) {
|
|
return errors.New("required image digest is missing: " + subject)
|
|
}
|
|
}
|
|
for _, subject := range cisupplychainconformance.RequiredSBOMs {
|
|
if !digestPattern.MatchString(strings.ToLower(report.SBOMDigests[subject])) {
|
|
return errors.New("required SBOM digest is missing: " + subject)
|
|
}
|
|
}
|
|
if len(report.InputSHA256) != 1 || !digestPattern.MatchString("sha256:"+report.InputSHA256["ci_capture"]) {
|
|
return errors.New("report capture digest is missing or invalid")
|
|
}
|
|
checks := map[string]bool{}
|
|
for _, check := range report.Checks {
|
|
if check.Status != "PASS" || strings.TrimSpace(check.Detail) == "" || checks[check.Code] {
|
|
return errors.New("report contains failed, duplicated, or incomplete checks")
|
|
}
|
|
checks[check.Code] = true
|
|
}
|
|
for _, code := range requiredRemoteCISupplyChainChecks {
|
|
if !checks[code] {
|
|
return fmt.Errorf("required report check is missing: %s", code)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func validateRealAgentModelsEvidence(index EvidenceIndex, indexDirectory, candidateVersion, buildDigest string) (RealAgentModelsValidationSummary, error) {
|
|
gaps := []string{"REAL_AGENT_MODELS_REPORT:0/1", "REAL_GENERATION:0/1", "REAL_EMBEDDING:0/1"}
|
|
for _, category := range agentmodelsconformance.RequiredEmbeddingCategories {
|
|
gaps = append(gaps, category+":0/1")
|
|
}
|
|
gaps = append(gaps, "REAL_CONTENT_SAFETY:0/1")
|
|
for _, category := range agentmodelsconformance.RequiredSafetyCategories {
|
|
gaps = append(gaps, category+":0/1")
|
|
}
|
|
summary := RealAgentModelsValidationSummary{Status: "PENDING", EmbeddingCategories: []string{}, ContentSafetyCategories: []string{}, Gaps: gaps}
|
|
var item EvidenceItem
|
|
found := false
|
|
for _, candidate := range index.Items {
|
|
if candidate.Code == realAgentModelsEvidenceCode {
|
|
item, found = candidate, true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
return summary, errors.New("required evidence code is missing: " + realAgentModelsEvidenceCode)
|
|
}
|
|
summary.Status = item.Status
|
|
if len(item.Files) == 0 {
|
|
if item.Status == "VERIFIED" {
|
|
return summary, errors.New("REAL_AGENT_MODELS is incomplete and must remain PENDING: REAL_AGENT_MODELS_REPORT:0/1")
|
|
}
|
|
return summary, nil
|
|
}
|
|
reportPath := item.Files[0]
|
|
if !filepath.IsAbs(reportPath) {
|
|
reportPath = filepath.Join(indexDirectory, reportPath)
|
|
}
|
|
content, err := readRegularFile(reportPath)
|
|
if err != nil {
|
|
return summary, fmt.Errorf("read real Agent models report: %w", err)
|
|
}
|
|
report, err := agentmodelsconformance.ParseReport(content)
|
|
if err != nil {
|
|
return summary, err
|
|
}
|
|
if err = validateRealAgentModelsReport(report, candidateVersion, buildDigest); err != nil {
|
|
return summary, fmt.Errorf("real Agent models report %s is not eligible: %w", filepath.Base(reportPath), err)
|
|
}
|
|
summary.ReportPresent = true
|
|
summary.Environment = report.Environment
|
|
summary.GenerationProviderRef = report.GenerationProviderRef
|
|
summary.AgentVersion = report.AgentVersion
|
|
summary.ModelConfigRef = report.ModelConfigRef
|
|
summary.SuiteVersion = report.SuiteVersion
|
|
summary.GenerationCaseCount = report.GenerationCaseCount
|
|
summary.EmbeddingProviderRef = report.EmbeddingProviderRef
|
|
summary.EmbeddingModelRef = report.EmbeddingModelRef
|
|
summary.EmbeddingQueryCount = report.EmbeddingQueryCount
|
|
summary.EmbeddingCategories = slices.Clone(report.EmbeddingCategories)
|
|
summary.ContentSafetyProviderRef = report.ContentSafetyProviderRef
|
|
summary.ContentSafetyPolicyRef = report.ContentSafetyPolicyRef
|
|
summary.ContentSafetyCaseCount = report.ContentSafetyCaseCount
|
|
summary.ContentSafetyCategories = slices.Clone(report.ContentSafetyCategories)
|
|
summary.CoverageComplete = true
|
|
summary.Gaps = []string{}
|
|
return summary, nil
|
|
}
|
|
|
|
func validateRealAgentModelsReport(report agentmodelsconformance.Report, candidateVersion, buildDigest string) error {
|
|
if report.SchemaVersion != agentmodelsconformance.ReportSchema || report.Result != "PASS" || report.EvidenceScope != "REAL_AGENT_MODELS" || report.ExternalValidationRequired {
|
|
return errors.New("report must be a PASS REAL_AGENT_MODELS report with external validation completed")
|
|
}
|
|
if report.CandidateVersion != candidateVersion || strings.ToLower(report.BuildDigest) != strings.ToLower(buildDigest) {
|
|
return errors.New("candidate version or build digest does not match the release bundle")
|
|
}
|
|
if report.GeneratedAt.IsZero() || strings.TrimSpace(report.Disclaimer) == "" || !slices.Contains([]string{"STAGING", "PRODUCTION"}, report.Environment) || report.GenerationProviderRef == "" || report.AgentVersion == "" || report.ModelConfigRef == "" || report.SuiteVersion == "" || report.GenerationCaseCount < 10 || report.EmbeddingProviderRef == "" || report.EmbeddingModelRef == "" || report.EmbeddingQueryCount < len(agentmodelsconformance.RequiredEmbeddingCategories) || report.ContentSafetyProviderRef == "" || report.ContentSafetyPolicyRef == "" || report.ContentSafetyCaseCount < len(agentmodelsconformance.RequiredSafetyCategories) {
|
|
return errors.New("report is missing environment, provider, model, suite, or minimum evaluation counts")
|
|
}
|
|
for _, category := range agentmodelsconformance.RequiredEmbeddingCategories {
|
|
if !slices.Contains(report.EmbeddingCategories, category) {
|
|
return errors.New("required embedding category is missing: " + category)
|
|
}
|
|
}
|
|
for _, category := range agentmodelsconformance.RequiredSafetyCategories {
|
|
if !slices.Contains(report.ContentSafetyCategories, category) {
|
|
return errors.New("required content safety category is missing: " + category)
|
|
}
|
|
}
|
|
for _, key := range []string{"models_capture", "agent_suite", "agent_observations", "agent_report"} {
|
|
if !digestPattern.MatchString("sha256:" + report.InputSHA256[key]) {
|
|
return errors.New("report input digest is missing or invalid: " + key)
|
|
}
|
|
}
|
|
if len(report.InputSHA256) != 4 {
|
|
return errors.New("report must contain exactly four input digests")
|
|
}
|
|
checks := map[string]bool{}
|
|
for _, check := range report.Checks {
|
|
if check.Status != "PASS" || strings.TrimSpace(check.Detail) == "" || checks[check.Code] {
|
|
return errors.New("report contains failed, duplicated, or incomplete checks")
|
|
}
|
|
checks[check.Code] = true
|
|
}
|
|
for _, code := range requiredRealAgentModelsChecks {
|
|
if !checks[code] {
|
|
return fmt.Errorf("required report check is missing: %s", code)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func validateRealDeviceRTCEvidence(index EvidenceIndex, indexDirectory, candidateVersion, buildDigest string) (RealDeviceRTCValidationSummary, error) {
|
|
gaps := []string{"REAL_DEVICE_RTC_PROVIDER_REPORT:0/1", fmt.Sprintf("DEVICES:0/%d", rtcconformance.MinimumDevices), fmt.Sprintf("SESSIONS:0/%d", rtcconformance.MinimumSessions)}
|
|
for _, mode := range rtcconformance.RequiredEngineModes {
|
|
gaps = append(gaps, mode+":0/1")
|
|
}
|
|
for _, scenario := range rtcconformance.RequiredScenarios {
|
|
gaps = append(gaps, scenario+":0/1")
|
|
}
|
|
summary := RealDeviceRTCValidationSummary{Status: "PENDING", CompletedEngineModes: []string{}, CompletedScenarios: []string{}, ModeSessionCounts: map[string]int{}, ScenarioSessionCounts: map[string]int{}, Gaps: gaps}
|
|
var item EvidenceItem
|
|
found := false
|
|
for _, candidate := range index.Items {
|
|
if candidate.Code == realDeviceRTCEvidenceCode {
|
|
item, found = candidate, true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
return summary, errors.New("required evidence code is missing: " + realDeviceRTCEvidenceCode)
|
|
}
|
|
summary.Status = item.Status
|
|
if len(item.Files) == 0 {
|
|
if item.Status == "VERIFIED" {
|
|
return summary, errors.New("REAL_DEVICE_RTC is incomplete and must remain PENDING: REAL_DEVICE_RTC_PROVIDER_REPORT:0/1")
|
|
}
|
|
return summary, nil
|
|
}
|
|
reportPath := item.Files[0]
|
|
if !filepath.IsAbs(reportPath) {
|
|
reportPath = filepath.Join(indexDirectory, reportPath)
|
|
}
|
|
content, err := readRegularFile(reportPath)
|
|
if err != nil {
|
|
return summary, fmt.Errorf("read real device RTC report: %w", err)
|
|
}
|
|
report, err := rtcconformance.ParseReport(content)
|
|
if err != nil {
|
|
return summary, err
|
|
}
|
|
if err = validateRealDeviceRTCReport(report, candidateVersion, buildDigest); err != nil {
|
|
return summary, fmt.Errorf("real device RTC report %s is not eligible: %w", filepath.Base(reportPath), err)
|
|
}
|
|
summary.ReportPresent = true
|
|
summary.Environment = report.Environment
|
|
summary.RTCProviderRef = report.RTCProviderRef
|
|
summary.VoiceProviderRef = report.VoiceProviderRef
|
|
summary.DeviceCount = report.DeviceCount
|
|
summary.SessionCount = report.SessionCount
|
|
summary.CompletedEngineModes = slices.Clone(report.CompletedEngineModes)
|
|
summary.CompletedScenarios = slices.Clone(report.CompletedScenarios)
|
|
summary.ModeSessionCounts = mapsClone(report.ModeSessionCounts)
|
|
summary.ScenarioSessionCounts = mapsClone(report.ScenarioSessionCounts)
|
|
summary.CoverageComplete = true
|
|
summary.Gaps = []string{}
|
|
return summary, nil
|
|
}
|
|
|
|
func validateRealDeviceRTCReport(report rtcconformance.Report, candidateVersion, buildDigest string) error {
|
|
if report.SchemaVersion != rtcconformance.ReportSchema || report.Result != "PASS" || report.EvidenceScope != "REAL_DEVICE_RTC_PROVIDER" || report.ExternalValidationRequired {
|
|
return errors.New("report must be a PASS REAL_DEVICE_RTC_PROVIDER report with external validation completed")
|
|
}
|
|
if report.CandidateVersion != candidateVersion || strings.ToLower(report.BuildDigest) != strings.ToLower(buildDigest) {
|
|
return errors.New("candidate version or build digest does not match the release bundle")
|
|
}
|
|
if report.GeneratedAt.IsZero() || strings.TrimSpace(report.Disclaimer) == "" || !slices.Contains([]string{"STAGING", "PRODUCTION"}, report.Environment) || report.RTCProviderRef == "" || report.VoiceProviderRef == "" || report.DeviceCount < rtcconformance.MinimumDevices || report.SessionCount < rtcconformance.MinimumSessions {
|
|
return errors.New("report is missing environment, provider, device, or session coverage")
|
|
}
|
|
for _, mode := range rtcconformance.RequiredEngineModes {
|
|
if !slices.Contains(report.CompletedEngineModes, mode) || report.ModeSessionCounts[mode] == 0 {
|
|
return errors.New("required engine mode is missing: " + mode)
|
|
}
|
|
}
|
|
for _, scenario := range rtcconformance.RequiredScenarios {
|
|
if !slices.Contains(report.CompletedScenarios, scenario) || report.ScenarioSessionCounts[scenario] == 0 {
|
|
return errors.New("required RTC scenario is missing: " + scenario)
|
|
}
|
|
}
|
|
if len(report.InputSHA256) != 1 || !digestPattern.MatchString("sha256:"+report.InputSHA256["rtc_capture"]) {
|
|
return errors.New("report capture digest is missing or invalid")
|
|
}
|
|
checks := map[string]bool{}
|
|
for _, check := range report.Checks {
|
|
if check.Status != "PASS" || strings.TrimSpace(check.Detail) == "" || checks[check.Code] {
|
|
return errors.New("report contains failed, duplicated, or incomplete checks")
|
|
}
|
|
checks[check.Code] = true
|
|
}
|
|
for _, code := range requiredRealDeviceRTCChecks {
|
|
if !checks[code] {
|
|
return fmt.Errorf("required report check is missing: %s", code)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func validateSafariCompatibilityEvidence(index EvidenceIndex, indexDirectory, candidateVersion, buildDigest string) (SafariCompatibilityValidationSummary, error) {
|
|
gaps := []string{"REAL_SAFARI_REPORT:0/1"}
|
|
for _, rank := range safariconformance.RequiredReleaseRanks {
|
|
gaps = append(gaps, rank+":0/1")
|
|
}
|
|
summary := SafariCompatibilityValidationSummary{Status: "PENDING", BrowserVersions: map[string]string{}, CompletedReleaseRanks: []string{}, Gaps: gaps}
|
|
var item EvidenceItem
|
|
found := false
|
|
for _, candidate := range index.Items {
|
|
if candidate.Code == safariCompatibilityEvidenceCode {
|
|
item, found = candidate, true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
return summary, errors.New("required evidence code is missing: " + safariCompatibilityEvidenceCode)
|
|
}
|
|
summary.Status = item.Status
|
|
if len(item.Files) == 0 {
|
|
if item.Status == "VERIFIED" {
|
|
return summary, errors.New("SAFARI_COMPATIBILITY is incomplete and must remain PENDING: REAL_SAFARI_REPORT:0/1")
|
|
}
|
|
return summary, nil
|
|
}
|
|
reportPath := item.Files[0]
|
|
if !filepath.IsAbs(reportPath) {
|
|
reportPath = filepath.Join(indexDirectory, reportPath)
|
|
}
|
|
content, err := readRegularFile(reportPath)
|
|
if err != nil {
|
|
return summary, fmt.Errorf("read Safari compatibility report: %w", err)
|
|
}
|
|
report, err := safariconformance.ParseReport(content)
|
|
if err != nil {
|
|
return summary, err
|
|
}
|
|
if err = validateSafariCompatibilityReport(report, candidateVersion, buildDigest); err != nil {
|
|
return summary, fmt.Errorf("Safari compatibility report %s is not eligible: %w", filepath.Base(reportPath), err)
|
|
}
|
|
summary.ReportPresent = true
|
|
summary.Environment = report.Environment
|
|
summary.BrowserVersions = mapsCloneString(report.BrowserVersions)
|
|
summary.RunCount = report.RunCount
|
|
summary.ResultCount = report.ResultCount
|
|
summary.CompletedReleaseRanks = slices.Clone(report.CompletedReleaseRanks)
|
|
summary.CoverageComplete = true
|
|
summary.Gaps = []string{}
|
|
return summary, nil
|
|
}
|
|
|
|
func validateSafariCompatibilityReport(report safariconformance.Report, candidateVersion, buildDigest string) error {
|
|
if report.SchemaVersion != safariconformance.ReportSchema || report.Result != "PASS" || report.EvidenceScope != "REAL_SAFARI" || report.ExternalValidationRequired {
|
|
return errors.New("report must be a PASS REAL_SAFARI report with external validation completed")
|
|
}
|
|
if report.CandidateVersion != candidateVersion || strings.ToLower(report.BuildDigest) != strings.ToLower(buildDigest) {
|
|
return errors.New("candidate version or build digest does not match the release bundle")
|
|
}
|
|
expectedResults := len(safariconformance.RequiredReleaseRanks) * len(safariconformance.RequiredWorkflows) * len(safariconformance.RequiredViewports)
|
|
if report.GeneratedAt.IsZero() || strings.TrimSpace(report.Disclaimer) == "" || !slices.Contains([]string{"STAGING", "PRODUCTION"}, report.Environment) || report.RunCount != len(safariconformance.RequiredReleaseRanks) || report.ResultCount != expectedResults {
|
|
return errors.New("report is missing target environment or required Safari matrix coverage")
|
|
}
|
|
versions := map[string]bool{}
|
|
for _, rank := range safariconformance.RequiredReleaseRanks {
|
|
version := strings.TrimSpace(report.BrowserVersions[rank])
|
|
if version == "" || versions[version] || !slices.Contains(report.CompletedReleaseRanks, rank) {
|
|
return errors.New("required distinct Safari release is missing: " + rank)
|
|
}
|
|
versions[version] = true
|
|
}
|
|
for _, workflow := range safariconformance.RequiredWorkflows {
|
|
if report.CompletedWorkflows[workflow] != len(safariconformance.RequiredReleaseRanks)*len(safariconformance.RequiredViewports) {
|
|
return errors.New("required Safari workflow coverage is missing: " + workflow)
|
|
}
|
|
}
|
|
for _, viewport := range safariconformance.RequiredViewports {
|
|
if report.CompletedViewports[viewport] != len(safariconformance.RequiredReleaseRanks)*len(safariconformance.RequiredWorkflows) {
|
|
return errors.New("required Safari viewport coverage is missing: " + viewport)
|
|
}
|
|
}
|
|
if len(report.InputSHA256) != 1 || !digestPattern.MatchString("sha256:"+report.InputSHA256["safari_capture"]) {
|
|
return errors.New("report capture digest is missing or invalid")
|
|
}
|
|
checks := map[string]bool{}
|
|
for _, check := range report.Checks {
|
|
if check.Status != "PASS" || strings.TrimSpace(check.Detail) == "" || checks[check.Code] {
|
|
return errors.New("report contains failed, duplicated, or incomplete checks")
|
|
}
|
|
checks[check.Code] = true
|
|
}
|
|
for _, code := range requiredSafariCompatibilityChecks {
|
|
if !checks[code] {
|
|
return fmt.Errorf("required report check is missing: %s", code)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func validateProductionInfrastructureEvidence(index EvidenceIndex, indexDirectory, candidateVersion, buildDigest string) (ProductionInfrastructureValidationSummary, error) {
|
|
gaps := []string{"PRODUCTION_INFRASTRUCTURE_REPORT:0/1"}
|
|
for _, component := range infrastructureconformance.RequiredComponents {
|
|
gaps = append(gaps, component+":0/1")
|
|
}
|
|
summary := ProductionInfrastructureValidationSummary{Status: "PENDING", VerifiedComponents: []string{}, ControlCounts: map[string]int{}, Gaps: gaps}
|
|
var item EvidenceItem
|
|
found := false
|
|
for _, candidate := range index.Items {
|
|
if candidate.Code == productionInfrastructureEvidenceCode {
|
|
item, found = candidate, true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
return summary, errors.New("required evidence code is missing: " + productionInfrastructureEvidenceCode)
|
|
}
|
|
summary.Status = item.Status
|
|
if len(item.Files) == 0 {
|
|
if item.Status == "VERIFIED" {
|
|
return summary, errors.New("PRODUCTION_INFRASTRUCTURE is incomplete and must remain PENDING: PRODUCTION_INFRASTRUCTURE_REPORT:0/1")
|
|
}
|
|
return summary, nil
|
|
}
|
|
reportPath := item.Files[0]
|
|
if !filepath.IsAbs(reportPath) {
|
|
reportPath = filepath.Join(indexDirectory, reportPath)
|
|
}
|
|
content, err := readRegularFile(reportPath)
|
|
if err != nil {
|
|
return summary, fmt.Errorf("read production infrastructure report: %w", err)
|
|
}
|
|
report, err := infrastructureconformance.ParseReport(content)
|
|
if err != nil {
|
|
return summary, err
|
|
}
|
|
if err = validateProductionInfrastructureReport(report, candidateVersion, buildDigest); err != nil {
|
|
return summary, fmt.Errorf("production infrastructure report %s is not eligible: %w", filepath.Base(reportPath), err)
|
|
}
|
|
summary.ReportPresent = true
|
|
summary.Environment = report.Environment
|
|
summary.RegionRef = report.RegionRef
|
|
summary.ClusterRef = report.ClusterRef
|
|
summary.ComponentCount = report.ComponentCount
|
|
summary.VerifiedComponents = slices.Clone(report.VerifiedComponents)
|
|
summary.ControlCounts = mapsClone(report.ControlCounts)
|
|
summary.CoverageComplete = true
|
|
summary.Gaps = []string{}
|
|
return summary, nil
|
|
}
|
|
|
|
func validateProductionInfrastructureReport(report infrastructureconformance.Report, candidateVersion, buildDigest string) error {
|
|
if report.SchemaVersion != infrastructureconformance.ReportSchema || report.Result != "PASS" || report.EvidenceScope != "PRODUCTION_INFRASTRUCTURE" || report.ExternalValidationRequired {
|
|
return errors.New("report must be a PASS PRODUCTION_INFRASTRUCTURE report with external validation completed")
|
|
}
|
|
if report.CandidateVersion != candidateVersion || strings.ToLower(report.BuildDigest) != strings.ToLower(buildDigest) {
|
|
return errors.New("candidate version or build digest does not match the release bundle")
|
|
}
|
|
if report.GeneratedAt.IsZero() || strings.TrimSpace(report.Disclaimer) == "" || !slices.Contains([]string{"STAGING", "PRODUCTION"}, report.Environment) || strings.TrimSpace(report.RegionRef) == "" || strings.TrimSpace(report.ClusterRef) == "" || report.ComponentCount != len(infrastructureconformance.RequiredComponents) {
|
|
return errors.New("report is missing environment, region, cluster, or required component coverage")
|
|
}
|
|
for _, component := range infrastructureconformance.RequiredComponents {
|
|
if !slices.Contains(report.VerifiedComponents, component) || report.ControlCounts[component] < len(infrastructureconformance.RequiredControls(component)) {
|
|
return errors.New("required infrastructure component or controls are missing: " + component)
|
|
}
|
|
}
|
|
if len(report.InputSHA256) != 1 || !digestPattern.MatchString("sha256:"+report.InputSHA256["infrastructure_capture"]) {
|
|
return errors.New("report capture digest is missing or invalid")
|
|
}
|
|
checks := map[string]bool{}
|
|
for _, check := range report.Checks {
|
|
if check.Status != "PASS" || strings.TrimSpace(check.Detail) == "" || checks[check.Code] {
|
|
return errors.New("report contains failed, duplicated, or incomplete checks")
|
|
}
|
|
checks[check.Code] = true
|
|
}
|
|
for _, code := range requiredProductionInfrastructureChecks {
|
|
if !checks[code] {
|
|
return fmt.Errorf("required report check is missing: %s", code)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func validateProductionRecoveryEvidence(index EvidenceIndex, indexDirectory, candidateVersion, buildDigest string) (ProductionRecoveryValidationSummary, error) {
|
|
gaps := make([]string, 0, len(recoveryconformance.RequiredDrillTypes)+1)
|
|
gaps = append(gaps, "PRODUCTION_EQUIVALENT_REPORT:0/1")
|
|
for _, drillType := range recoveryconformance.RequiredDrillTypes {
|
|
gaps = append(gaps, drillType+":0/1")
|
|
}
|
|
summary := ProductionRecoveryValidationSummary{Status: "PENDING", CompletedDrillTypes: []string{}, RecoverySeconds: map[string]int{}, Gaps: gaps}
|
|
var item EvidenceItem
|
|
found := false
|
|
for _, candidate := range index.Items {
|
|
if candidate.Code == productionRecoveryEvidenceCode {
|
|
item, found = candidate, true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
return summary, errors.New("required evidence code is missing: " + productionRecoveryEvidenceCode)
|
|
}
|
|
summary.Status = item.Status
|
|
if len(item.Files) == 0 {
|
|
if item.Status == "VERIFIED" {
|
|
return summary, errors.New("PRODUCTION_RECOVERY is incomplete and must remain PENDING: PRODUCTION_EQUIVALENT_REPORT:0/1")
|
|
}
|
|
return summary, nil
|
|
}
|
|
reportPath := item.Files[0]
|
|
if !filepath.IsAbs(reportPath) {
|
|
reportPath = filepath.Join(indexDirectory, reportPath)
|
|
}
|
|
content, err := readRegularFile(reportPath)
|
|
if err != nil {
|
|
return summary, fmt.Errorf("read production recovery report: %w", err)
|
|
}
|
|
report, err := recoveryconformance.ParseReport(content)
|
|
if err != nil {
|
|
return summary, err
|
|
}
|
|
if err = validateProductionRecoveryReport(report, candidateVersion, buildDigest); err != nil {
|
|
return summary, fmt.Errorf("production recovery report %s is not eligible: %w", filepath.Base(reportPath), err)
|
|
}
|
|
summary.ReportPresent = true
|
|
summary.InfrastructureRef = report.InfrastructureRef
|
|
summary.DrillCount = report.DrillCount
|
|
summary.CompletedDrillTypes = slices.Clone(report.CompletedDrillTypes)
|
|
summary.RecoverySeconds = mapsClone(report.ObservedRecoverySeconds)
|
|
summary.CoverageComplete = true
|
|
summary.Gaps = []string{}
|
|
return summary, nil
|
|
}
|
|
|
|
func validateProductionRecoveryReport(report recoveryconformance.Report, candidateVersion, buildDigest string) error {
|
|
if report.SchemaVersion != recoveryconformance.ReportSchema || report.Result != "PASS" || report.EvidenceScope != "PRODUCTION_EQUIVALENT" || report.ExternalValidationRequired {
|
|
return errors.New("report must be a PASS PRODUCTION_EQUIVALENT report with external validation completed")
|
|
}
|
|
if report.CandidateVersion != candidateVersion || strings.ToLower(report.BuildDigest) != strings.ToLower(buildDigest) {
|
|
return errors.New("candidate version or build digest does not match the release bundle")
|
|
}
|
|
if report.GeneratedAt.IsZero() || strings.TrimSpace(report.Disclaimer) == "" || strings.TrimSpace(report.InfrastructureRef) == "" || !slices.Contains([]string{"STAGING", "PRODUCTION"}, report.Environment) || report.DrillCount != len(recoveryconformance.RequiredDrillTypes) {
|
|
return errors.New("report is missing environment, infrastructure, or required drill coverage")
|
|
}
|
|
for _, drillType := range recoveryconformance.RequiredDrillTypes {
|
|
if !slices.Contains(report.CompletedDrillTypes, drillType) {
|
|
return errors.New("required recovery drill is missing: " + drillType)
|
|
}
|
|
if _, ok := report.ObservedRecoverySeconds[drillType]; !ok {
|
|
return errors.New("observed recovery duration is missing: " + drillType)
|
|
}
|
|
}
|
|
if len(report.InputSHA256) != 1 || !digestPattern.MatchString("sha256:"+report.InputSHA256["recovery_capture"]) {
|
|
return errors.New("report capture digest is missing or invalid")
|
|
}
|
|
checks := map[string]bool{}
|
|
for _, check := range report.Checks {
|
|
if check.Status != "PASS" || strings.TrimSpace(check.Detail) == "" || checks[check.Code] {
|
|
return errors.New("report contains failed, duplicated, or incomplete checks")
|
|
}
|
|
checks[check.Code] = true
|
|
}
|
|
for _, code := range requiredProductionRecoveryChecks {
|
|
if !checks[code] {
|
|
return fmt.Errorf("required report check is missing: %s", code)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func mapsClone(source map[string]int) map[string]int {
|
|
result := make(map[string]int, len(source))
|
|
for key, value := range source {
|
|
result[key] = value
|
|
}
|
|
return result
|
|
}
|
|
|
|
func mapsCloneString(source map[string]string) map[string]string {
|
|
result := make(map[string]string, len(source))
|
|
for key, value := range source {
|
|
result[key] = value
|
|
}
|
|
return result
|
|
}
|
|
|
|
func validateKMSRotationEvidence(index EvidenceIndex, indexDirectory, candidateVersion, buildDigest string) (KMSRotationValidationSummary, error) {
|
|
summary := KMSRotationValidationSummary{Status: "PENDING", ConsumptionModes: []string{}, Gaps: []string{"REAL_KMS_HSM_REPORT:0/1", "DYNAMIC_READ:0/1", "ROLLING_RESTART:0/1"}}
|
|
var item EvidenceItem
|
|
found := false
|
|
for _, candidate := range index.Items {
|
|
if candidate.Code == kmsRotationEvidenceCode {
|
|
item, found = candidate, true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
return summary, errors.New("required evidence code is missing: " + kmsRotationEvidenceCode)
|
|
}
|
|
summary.Status = item.Status
|
|
if len(item.Files) == 0 {
|
|
if item.Status == "VERIFIED" {
|
|
return summary, errors.New("KMS_ROTATION is incomplete and must remain PENDING: REAL_KMS_HSM_REPORT:0/1")
|
|
}
|
|
return summary, nil
|
|
}
|
|
reportPath := item.Files[0]
|
|
if !filepath.IsAbs(reportPath) {
|
|
reportPath = filepath.Join(indexDirectory, reportPath)
|
|
}
|
|
content, err := readRegularFile(reportPath)
|
|
if err != nil {
|
|
return summary, fmt.Errorf("read KMS rotation conformance report: %w", err)
|
|
}
|
|
report, err := kmsrotationconformance.ParseReport(content)
|
|
if err != nil {
|
|
return summary, err
|
|
}
|
|
if err = validateKMSRotationReport(report, candidateVersion, buildDigest); err != nil {
|
|
return summary, fmt.Errorf("KMS rotation report %s is not eligible: %w", filepath.Base(reportPath), err)
|
|
}
|
|
summary.ReportPresent = true
|
|
summary.ProviderRef = report.ProviderRef
|
|
summary.RotationCount = report.RotationCount
|
|
summary.ConsumptionModes = slices.Clone(report.ConsumptionModes)
|
|
summary.CoverageComplete = true
|
|
summary.Gaps = []string{}
|
|
return summary, nil
|
|
}
|
|
|
|
func validateKMSRotationReport(report kmsrotationconformance.Report, candidateVersion, buildDigest string) error {
|
|
if report.SchemaVersion != kmsrotationconformance.ReportSchema || report.Result != "PASS" || report.EvidenceScope != "REAL_KMS_HSM" || report.ExternalValidationRequired {
|
|
return errors.New("report must be a PASS REAL_KMS_HSM report with external validation completed")
|
|
}
|
|
if report.CandidateVersion != candidateVersion || strings.ToLower(report.BuildDigest) != strings.ToLower(buildDigest) {
|
|
return errors.New("candidate version or build digest does not match the release bundle")
|
|
}
|
|
if report.GeneratedAt.IsZero() || strings.TrimSpace(report.Disclaimer) == "" || strings.TrimSpace(report.ProviderRef) == "" || !slices.Contains([]string{"STAGING", "PRODUCTION"}, report.Environment) || report.RotationCount < 2 || !slices.Contains(report.ConsumptionModes, "DYNAMIC_READ") || !slices.Contains(report.ConsumptionModes, "ROLLING_RESTART") {
|
|
return errors.New("report is missing provider, environment, or required rotation mode coverage")
|
|
}
|
|
if len(report.InputSHA256) != 1 || !digestPattern.MatchString("sha256:"+report.InputSHA256["rotation_capture"]) {
|
|
return errors.New("report capture digest is missing or invalid")
|
|
}
|
|
checks := map[string]bool{}
|
|
for _, check := range report.Checks {
|
|
if check.Status != "PASS" || strings.TrimSpace(check.Detail) == "" || checks[check.Code] {
|
|
return errors.New("report contains failed, duplicated, or incomplete checks")
|
|
}
|
|
checks[check.Code] = true
|
|
}
|
|
for _, code := range requiredKMSRotationChecks {
|
|
if !checks[code] {
|
|
return fmt.Errorf("required report check is missing: %s", code)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func validateAlertReceiverEvidence(index EvidenceIndex, indexDirectory, candidateVersion, buildDigest string) (AlertReceiverValidationSummary, error) {
|
|
summary := AlertReceiverValidationSummary{Status: "PENDING", Gaps: []string{"REAL_RECEIVER_REPORT:0/1", "FORMAL_ALERT_DRILL:0/1"}}
|
|
var item EvidenceItem
|
|
found := false
|
|
for _, candidate := range index.Items {
|
|
if candidate.Code == alertReceiverEvidenceCode {
|
|
item, found = candidate, true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
return summary, errors.New("required evidence code is missing: " + alertReceiverEvidenceCode)
|
|
}
|
|
summary.Status = item.Status
|
|
if len(item.Files) == 0 {
|
|
if item.Status == "VERIFIED" {
|
|
return summary, errors.New("REAL_ALERT_RECEIVER is incomplete and must remain PENDING: REAL_RECEIVER_REPORT:0/1")
|
|
}
|
|
return summary, nil
|
|
}
|
|
reportPath := item.Files[0]
|
|
if !filepath.IsAbs(reportPath) {
|
|
reportPath = filepath.Join(indexDirectory, reportPath)
|
|
}
|
|
content, err := readRegularFile(reportPath)
|
|
if err != nil {
|
|
return summary, fmt.Errorf("read alert receiver conformance report: %w", err)
|
|
}
|
|
report, err := alertreceiverconformance.ParseReport(content)
|
|
if err != nil {
|
|
return summary, err
|
|
}
|
|
if err = validateAlertReceiverReport(report, candidateVersion, buildDigest); err != nil {
|
|
return summary, fmt.Errorf("alert receiver report %s is not eligible: %w", filepath.Base(reportPath), err)
|
|
}
|
|
summary.ReportPresent = true
|
|
summary.ActiveRouteCount = report.ActiveRouteCount
|
|
summary.MappedGroupCount = report.MappedGroupCount
|
|
summary.TestReceiptCount = report.TestReceiptCount
|
|
summary.FormalDrillVerified = report.FormalDrillVerified
|
|
summary.CoverageComplete = true
|
|
summary.Gaps = []string{}
|
|
return summary, nil
|
|
}
|
|
|
|
func validateAlertReceiverReport(report alertreceiverconformance.Report, candidateVersion, buildDigest string) error {
|
|
if report.SchemaVersion != alertreceiverconformance.ReportSchema || report.Result != "PASS" || report.EvidenceScope != "REAL_RECEIVER" || report.ExternalValidationRequired {
|
|
return errors.New("report must be a PASS REAL_RECEIVER report with external validation completed")
|
|
}
|
|
if report.CandidateVersion != candidateVersion || strings.ToLower(report.BuildDigest) != strings.ToLower(buildDigest) {
|
|
return errors.New("candidate version or build digest does not match the release bundle")
|
|
}
|
|
if report.GeneratedAt.IsZero() || strings.TrimSpace(report.Disclaimer) == "" || strings.TrimSpace(report.ReceiverSystemRef) == "" || !report.FormalDrillVerified || report.ActiveRouteCount < 1 || report.MappedGroupCount < 1 || report.TestReceiptCount != report.ActiveRouteCount || len(report.RouteCoverage) != report.ActiveRouteCount {
|
|
return errors.New("report is missing route coverage, mapping, receipt, drill, or identity evidence")
|
|
}
|
|
if _, err := uuid.Parse(report.TenantID); err != nil {
|
|
return errors.New("report contains an invalid tenant identifier")
|
|
}
|
|
for _, coverage := range report.RouteCoverage {
|
|
if _, err := uuid.Parse(coverage.RoutingPolicyID); err != nil || coverage.Status != "PASS" || strings.TrimSpace(coverage.ReceiverGroup) == "" {
|
|
return errors.New("report contains invalid route coverage")
|
|
}
|
|
if _, err := uuid.Parse(coverage.TestRef); err != nil {
|
|
return errors.New("report contains an invalid delivery test reference")
|
|
}
|
|
}
|
|
inputNames := []string{"cloud_routes", "cloud_tests", "cloud_incident", "receiver_capture"}
|
|
if len(report.InputSHA256) != len(inputNames) {
|
|
return errors.New("report input digest inventory is incomplete")
|
|
}
|
|
for _, name := range inputNames {
|
|
if !digestPattern.MatchString("sha256:" + report.InputSHA256[name]) {
|
|
return errors.New("report input digest is missing or invalid: " + name)
|
|
}
|
|
}
|
|
checks := map[string]bool{}
|
|
for _, check := range report.Checks {
|
|
if check.Status != "PASS" || strings.TrimSpace(check.Detail) == "" || checks[check.Code] {
|
|
return errors.New("report contains failed, duplicated, or incomplete checks")
|
|
}
|
|
checks[check.Code] = true
|
|
}
|
|
for _, code := range requiredAlertReceiverChecks {
|
|
if !checks[code] {
|
|
return fmt.Errorf("required report check is missing: %s", code)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func validateFirmwareEvidence(index EvidenceIndex, indexDirectory, candidateVersion, buildDigest string) (FirmwareValidationSummary, error) {
|
|
summary := FirmwareValidationSummary{
|
|
Status: "PENDING", RequiredDeviceCount: firmwareDeviceMinimum,
|
|
RequiredScenarios: slices.Clone(requiredFirmwareScenarios), ScenarioDeviceCounts: map[string]int{}, Gaps: []string{},
|
|
}
|
|
var item EvidenceItem
|
|
found := false
|
|
for _, candidate := range index.Items {
|
|
if candidate.Code == firmwareEvidenceCode {
|
|
item, found = candidate, true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
return summary, errors.New("required evidence code is missing: " + firmwareEvidenceCode)
|
|
}
|
|
summary.Status = item.Status
|
|
deviceScenarios := map[string]map[string]bool{}
|
|
seenReport := map[string]bool{}
|
|
for _, reportPath := range item.Files {
|
|
resolved := reportPath
|
|
if !filepath.IsAbs(resolved) {
|
|
resolved = filepath.Join(indexDirectory, resolved)
|
|
}
|
|
content, err := readRegularFile(resolved)
|
|
if err != nil {
|
|
return summary, fmt.Errorf("read firmware conformance report: %w", err)
|
|
}
|
|
report, err := firmwareconformance.ParseReport(content)
|
|
if err != nil {
|
|
return summary, err
|
|
}
|
|
if err = validateFirmwareReport(report, candidateVersion, buildDigest); err != nil {
|
|
return summary, fmt.Errorf("firmware report %s is not eligible: %w", filepath.Base(resolved), err)
|
|
}
|
|
identity := report.DeviceID + "|" + report.Scenario
|
|
if seenReport[identity] {
|
|
return summary, fmt.Errorf("duplicate firmware report for device %s and scenario %s", report.DeviceID, report.Scenario)
|
|
}
|
|
seenReport[identity] = true
|
|
if deviceScenarios[report.DeviceID] == nil {
|
|
deviceScenarios[report.DeviceID] = map[string]bool{}
|
|
}
|
|
deviceScenarios[report.DeviceID][report.Scenario] = true
|
|
summary.ScenarioDeviceCounts[report.Scenario]++
|
|
summary.ReportCount++
|
|
}
|
|
summary.ObservedDeviceCount = len(deviceScenarios)
|
|
for _, scenarios := range deviceScenarios {
|
|
qualified := true
|
|
for _, scenario := range requiredFirmwareScenarios {
|
|
if !scenarios[scenario] {
|
|
qualified = false
|
|
break
|
|
}
|
|
}
|
|
if qualified {
|
|
summary.QualifiedDeviceCount++
|
|
}
|
|
}
|
|
if summary.QualifiedDeviceCount < firmwareDeviceMinimum {
|
|
summary.Gaps = append(summary.Gaps, fmt.Sprintf("QUALIFIED_DEVICE_COUNT:%d/%d", summary.QualifiedDeviceCount, firmwareDeviceMinimum))
|
|
}
|
|
for _, scenario := range requiredFirmwareScenarios {
|
|
count := summary.ScenarioDeviceCounts[scenario]
|
|
if count < firmwareDeviceMinimum {
|
|
summary.Gaps = append(summary.Gaps, fmt.Sprintf("%s:%d/%d", scenario, count, firmwareDeviceMinimum))
|
|
}
|
|
}
|
|
summary.CoverageComplete = summary.QualifiedDeviceCount >= firmwareDeviceMinimum && len(summary.Gaps) == 0
|
|
if item.Status == "VERIFIED" && !summary.CoverageComplete {
|
|
return summary, fmt.Errorf("REAL_DEVICE_FIRMWARE is incomplete and must remain PENDING: %s", strings.Join(summary.Gaps, ", "))
|
|
}
|
|
return summary, nil
|
|
}
|
|
|
|
func validateFirmwareReport(report firmwareconformance.Report, candidateVersion, buildDigest string) error {
|
|
if report.SchemaVersion != firmwareconformance.ReportSchema || report.Result != "PASS" || report.EvidenceScope != "HARDWARE_CAPTURE" || !report.ExternalValidationRequired {
|
|
return errors.New("report must be a PASS HARDWARE_CAPTURE report with external validation retained")
|
|
}
|
|
if report.CandidateVersion != candidateVersion || strings.ToLower(report.BuildDigest) != strings.ToLower(buildDigest) {
|
|
return errors.New("candidate version or build digest does not match the release bundle")
|
|
}
|
|
if report.GeneratedAt.IsZero() || strings.TrimSpace(report.Disclaimer) == "" || report.CloudEventCount == 0 || report.DeviceEventCount == 0 {
|
|
return errors.New("report is missing time, disclaimer, or event counts")
|
|
}
|
|
for _, value := range []string{report.DeviceID, report.ReleaseID, report.RolloutID, report.CommandID} {
|
|
if _, err := uuid.Parse(value); err != nil {
|
|
return errors.New("report contains an invalid public identifier")
|
|
}
|
|
}
|
|
if !slices.Contains(requiredFirmwareScenarios, report.Scenario) {
|
|
return errors.New("report scenario is not part of the required hardware suite")
|
|
}
|
|
if len(report.InputSHA256) != 2 || !digestPattern.MatchString("sha256:"+report.InputSHA256["cloud_rollout"]) || !digestPattern.MatchString("sha256:"+report.InputSHA256["device_evidence"]) {
|
|
return errors.New("report input digests are missing or invalid")
|
|
}
|
|
checks := map[string]bool{}
|
|
for _, check := range report.Checks {
|
|
if check.Status != "PASS" || strings.TrimSpace(check.Detail) == "" || checks[check.Code] {
|
|
return errors.New("report contains failed, duplicated, or incomplete checks")
|
|
}
|
|
checks[check.Code] = true
|
|
}
|
|
for _, code := range requiredFirmwareChecks {
|
|
if !checks[code] {
|
|
return fmt.Errorf("required report check is missing: %s", code)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func decodeAcceptance(data []byte) (acceptanceSnapshot, error) {
|
|
var snapshot acceptanceSnapshot
|
|
if err := json.Unmarshal(data, &snapshot); err != nil {
|
|
return acceptanceSnapshot{}, fmt.Errorf("decode acceptance snapshot: %w", err)
|
|
}
|
|
if snapshot.CandidateVersion == "" {
|
|
var envelope struct {
|
|
Data acceptanceSnapshot `json:"data"`
|
|
}
|
|
if err := json.Unmarshal(data, &envelope); err != nil {
|
|
return acceptanceSnapshot{}, fmt.Errorf("decode acceptance snapshot envelope: %w", err)
|
|
}
|
|
snapshot = envelope.Data
|
|
}
|
|
return snapshot, nil
|
|
}
|
|
|
|
func collectPilotEvidence(options Options, acceptance acceptanceSnapshot) (pilotBundleEvidence, error) {
|
|
hasPilotInput := len(options.PilotDailySnapshotPaths) > 0 || strings.TrimSpace(options.PilotWeeklySnapshotPath) != ""
|
|
if acceptance.RunType != "PILOT" && !hasPilotInput {
|
|
return pilotBundleEvidence{}, nil
|
|
}
|
|
if len(options.PilotDailySnapshotPaths) != 7 || strings.TrimSpace(options.PilotWeeklySnapshotPath) == "" {
|
|
return pilotBundleEvidence{}, errors.New("PILOT evidence requires exactly seven daily snapshots and one weekly snapshot")
|
|
}
|
|
if strings.TrimSpace(acceptance.TenantID) == "" {
|
|
return pilotBundleEvidence{}, errors.New("PILOT acceptance snapshot is missing tenant identity")
|
|
}
|
|
|
|
weeklyBytes, err := readRegularFile(options.PilotWeeklySnapshotPath)
|
|
if err != nil {
|
|
return pilotBundleEvidence{}, fmt.Errorf("read PILOT weekly snapshot: %w", err)
|
|
}
|
|
var weekly pilotWeeklySnapshot
|
|
if err = decodeArtifact(weeklyBytes, &weekly); err != nil {
|
|
return pilotBundleEvidence{}, fmt.Errorf("decode PILOT weekly snapshot: %w", err)
|
|
}
|
|
location, err := time.LoadLocation(weekly.Timezone)
|
|
if err != nil {
|
|
return pilotBundleEvidence{}, errors.New("PILOT weekly snapshot timezone is invalid")
|
|
}
|
|
weekStart, err := time.ParseInLocation("2006-01-02", weekly.WeekStart, location)
|
|
if err != nil || weekStart.Weekday() != time.Monday || weekly.ID == "" || weekly.TenantID != acceptance.TenantID || weekly.CandidateVersion != acceptance.CandidateVersion || strings.ToLower(weekly.BuildDigest) != strings.ToLower(acceptance.BuildDigest) || weekly.RedactionPolicy != "ops-pilot-weekly-minimal-v1" {
|
|
return pilotBundleEvidence{}, errors.New("PILOT weekly snapshot identity, tenant, candidate, week, or redaction policy is invalid")
|
|
}
|
|
expectedWeekStart := weekStart.UTC()
|
|
expectedWeekEnd := weekStart.AddDate(0, 0, 7).UTC()
|
|
if !weekly.IsComplete || weekly.GeneratedAt.IsZero() || weekly.GeneratedAt.Before(expectedWeekEnd) || !weekly.WindowStart.Equal(expectedWeekStart) || !weekly.WindowEnd.Equal(expectedWeekEnd) {
|
|
return pilotBundleEvidence{}, errors.New("PILOT weekly snapshot is incomplete or has inconsistent calendar boundaries")
|
|
}
|
|
complaintsRequired := false
|
|
for _, item := range weekly.ExternalEvidence {
|
|
if item.Code == "complaints" && item.Status == "REQUIRED" {
|
|
complaintsRequired = true
|
|
}
|
|
}
|
|
if !complaintsRequired {
|
|
return pilotBundleEvidence{}, errors.New("PILOT weekly snapshot must preserve the required external complaints evidence gap")
|
|
}
|
|
|
|
result := pilotBundleEvidence{
|
|
WeekStart: weekly.WeekStart,
|
|
Timezone: weekly.Timezone,
|
|
Daily: []bundleFile{},
|
|
Weekly: bundleFile{archivePath: "pilot/weekly/" + weekly.WeekStart + ".json", data: weeklyBytes},
|
|
}
|
|
seenDates := map[string]bool{}
|
|
for _, path := range options.PilotDailySnapshotPaths {
|
|
dailyBytes, readErr := readRegularFile(path)
|
|
if readErr != nil {
|
|
return pilotBundleEvidence{}, fmt.Errorf("read PILOT daily snapshot: %w", readErr)
|
|
}
|
|
var daily pilotDailySnapshot
|
|
if decodeErr := decodeArtifact(dailyBytes, &daily); decodeErr != nil {
|
|
return pilotBundleEvidence{}, fmt.Errorf("decode PILOT daily snapshot: %w", decodeErr)
|
|
}
|
|
localDate, parseErr := time.ParseInLocation("2006-01-02", daily.LocalDate, location)
|
|
if parseErr != nil || daily.ID == "" || daily.GeneratedAt.IsZero() || daily.TenantID != acceptance.TenantID || daily.CandidateVersion != acceptance.CandidateVersion || strings.ToLower(daily.BuildDigest) != strings.ToLower(acceptance.BuildDigest) || daily.Timezone != weekly.Timezone || daily.RedactionPolicy != "ops-pilot-daily-minimal-v1" {
|
|
return pilotBundleEvidence{}, fmt.Errorf("PILOT daily snapshot identity, tenant, candidate, date, timezone, or redaction policy is invalid: %s", daily.LocalDate)
|
|
}
|
|
if localDate.Before(weekStart) || !localDate.Before(weekStart.AddDate(0, 0, 7)) || seenDates[daily.LocalDate] {
|
|
return pilotBundleEvidence{}, fmt.Errorf("PILOT daily snapshot is outside the weekly window or duplicated: %s", daily.LocalDate)
|
|
}
|
|
if !daily.WindowStart.Equal(localDate.UTC()) || !daily.WindowEnd.Equal(localDate.AddDate(0, 0, 1).UTC()) {
|
|
return pilotBundleEvidence{}, fmt.Errorf("PILOT daily snapshot has inconsistent calendar boundaries: %s", daily.LocalDate)
|
|
}
|
|
seenDates[daily.LocalDate] = true
|
|
result.Daily = append(result.Daily, bundleFile{archivePath: "pilot/daily/" + daily.LocalDate + ".json", data: dailyBytes})
|
|
}
|
|
for day := 0; day < 7; day++ {
|
|
expected := weekStart.AddDate(0, 0, day).Format("2006-01-02")
|
|
if !seenDates[expected] {
|
|
return pilotBundleEvidence{}, fmt.Errorf("PILOT daily snapshot is missing: %s", expected)
|
|
}
|
|
}
|
|
sort.Slice(result.Daily, func(i, j int) bool { return result.Daily[i].archivePath < result.Daily[j].archivePath })
|
|
return result, nil
|
|
}
|
|
|
|
func decodeArtifact(data []byte, target any) error {
|
|
var envelope struct {
|
|
Data json.RawMessage `json:"data"`
|
|
}
|
|
if err := json.Unmarshal(data, &envelope); err != nil {
|
|
return err
|
|
}
|
|
if len(envelope.Data) > 0 && string(envelope.Data) != "null" {
|
|
return json.Unmarshal(envelope.Data, target)
|
|
}
|
|
return json.Unmarshal(data, target)
|
|
}
|
|
|
|
func collectBaseline(root string) ([]bundleFile, int, []string, error) {
|
|
required := []string{
|
|
"openapi/openapi.yaml", "compose.yaml", ".github/workflows/ci.yml", "go.mod", "go.sum",
|
|
"web/package.json", "web/package-lock.json", "docs/development-plan.md", "docs/development-status.md",
|
|
"docs/requirements-traceability.md", "docs/external-acceptance-checklist.md", "docs/kubernetes-deployment.md",
|
|
"docs/alert-operations.md", "docs/integration-diagnostics.md", "docs/content-safety-provider.md", "docs/remote-ci-supply-chain-evidence.md", "docs/real-device-rtc-evidence.md", "docs/real-agent-models-evidence.md", "docs/production-infrastructure-evidence.md", "docs/safari-compatibility-evidence.md", "docs/pilot-daily-operations.md", "docs/pilot-weekly-review.md",
|
|
}
|
|
paths := slices.Clone(required)
|
|
for _, directory := range []string{"openapi", "migrations", "deploy/kubernetes"} {
|
|
err := filepath.WalkDir(filepath.Join(root, directory), func(path string, entry os.DirEntry, walkErr error) error {
|
|
if walkErr != nil {
|
|
return walkErr
|
|
}
|
|
if entry.Type().IsRegular() {
|
|
relative, _ := filepath.Rel(root, path)
|
|
paths = append(paths, filepath.ToSlash(relative))
|
|
}
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
return nil, 0, nil, fmt.Errorf("inventory %s: %w", directory, err)
|
|
}
|
|
}
|
|
sbomMatches, _ := filepath.Glob(filepath.Join(root, "artifacts", "*.cdx.json"))
|
|
for _, match := range sbomMatches {
|
|
relative, _ := filepath.Rel(root, match)
|
|
paths = append(paths, filepath.ToSlash(relative))
|
|
}
|
|
sort.Strings(paths)
|
|
paths = slices.Compact(paths)
|
|
files := make([]bundleFile, 0, len(paths))
|
|
for _, relative := range paths {
|
|
data, err := readRegularFile(filepath.Join(root, filepath.FromSlash(relative)))
|
|
if err != nil {
|
|
return nil, 0, nil, fmt.Errorf("read baseline file %s: %w", relative, err)
|
|
}
|
|
files = append(files, bundleFile{archivePath: "baseline/" + relative, data: data})
|
|
}
|
|
warnings := []string{}
|
|
if len(sbomMatches) == 0 {
|
|
warnings = append(warnings, "SBOM_NOT_PRESENT_IN_WORKTREE: use the CI-generated CycloneDX artifacts before release review")
|
|
}
|
|
return files, len(sbomMatches), warnings, nil
|
|
}
|
|
|
|
func readRegularFile(path string) ([]byte, error) {
|
|
path = strings.TrimSpace(path)
|
|
if path == "" {
|
|
return nil, errors.New("file path is required")
|
|
}
|
|
info, err := os.Lstat(path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !info.Mode().IsRegular() || info.Size() > maxEvidenceFileBytes {
|
|
return nil, errors.New("file must be regular and no larger than 100 MiB")
|
|
}
|
|
return os.ReadFile(path)
|
|
}
|
|
|
|
func safeBaseName(value string) string {
|
|
value = regexp.MustCompile(`[^A-Za-z0-9._-]+`).ReplaceAllString(value, "-")
|
|
value = strings.Trim(value, ".-")
|
|
if value == "" {
|
|
return "evidence.bin"
|
|
}
|
|
return value
|
|
}
|
|
|
|
func writeZIP(output *os.File, files []bundleFile, generatedAt time.Time) error {
|
|
writer := zip.NewWriter(output)
|
|
for _, file := range files {
|
|
header := &zip.FileHeader{Name: filepath.ToSlash(file.archivePath), Method: zip.Deflate}
|
|
header.SetMode(0o644)
|
|
header.SetModTime(generatedAt)
|
|
entry, err := writer.CreateHeader(header)
|
|
if err != nil {
|
|
writer.Close()
|
|
return fmt.Errorf("create bundle entry %s: %w", file.archivePath, err)
|
|
}
|
|
if _, err = entry.Write(file.data); err != nil {
|
|
writer.Close()
|
|
return fmt.Errorf("write bundle entry %s: %w", file.archivePath, err)
|
|
}
|
|
}
|
|
if err := writer.Close(); err != nil {
|
|
return fmt.Errorf("close bundle zip: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func hashFile(path string) (string, error) {
|
|
file, err := os.Open(path)
|
|
if err != nil {
|
|
return "", fmt.Errorf("open evidence bundle for hashing: %w", err)
|
|
}
|
|
defer file.Close()
|
|
hash := sha256.New()
|
|
if _, err = io.Copy(hash, file); err != nil {
|
|
return "", fmt.Errorf("hash evidence bundle: %w", err)
|
|
}
|
|
return "sha256:" + hex.EncodeToString(hash.Sum(nil)), nil
|
|
}
|