package cisupplychainconformance import ( "bytes" "crypto/sha256" "encoding/hex" "encoding/json" "errors" "fmt" "io" "net/url" "regexp" "slices" "strings" "time" ) const ( CaptureSchema = "remote-ci-supply-chain-capture-v1" ReportSchema = "remote-ci-supply-chain-conformance-report-v1" ) var digestPattern = regexp.MustCompile(`^sha256:[0-9a-f]{64}$`) var commitPattern = regexp.MustCompile(`^[0-9a-f]{40}([0-9a-f]{24})?$`) var RequiredChecks = []string{ "GO_FORMAT", "GO_VET_TEST", "OPENAPI_GENERATION", "WEB_TYPECHECK_BUILD", "COMPOSE_VALIDATION", "BACKEND_IMAGE_BUILD", "WEB_IMAGE_BUILD", "GO_VULNERABILITY_LICENSE", "FRONTEND_VULNERABILITY_LICENSE", "REPOSITORY_SECRET_SCAN", "INFRASTRUCTURE_CONFIG_SCAN", "BACKEND_IMAGE_SCAN", "WEB_IMAGE_SCAN", "CYCLONEDX_SBOM", } var RequiredImages = []string{"BACKEND_IMAGE", "WEB_IMAGE"} var RequiredSBOMs = []string{"SOURCE", "BACKEND_IMAGE", "WEB_IMAGE"} type Capture struct { SchemaVersion string `json:"schema_version"` EvidenceSource string `json:"evidence_source"` CandidateVersion string `json:"candidate_version"` BuildDigest string `json:"build_digest"` SourceCommit string `json:"source_commit"` Provider string `json:"provider"` RepositoryRef string `json:"repository_ref"` WorkflowRef string `json:"workflow_ref"` RunID string `json:"run_id"` RunURL string `json:"run_url"` Trigger string `json:"trigger"` Ref string `json:"ref"` Attempt int `json:"attempt"` Conclusion string `json:"conclusion"` StartedAt time.Time `json:"started_at"` CompletedAt time.Time `json:"completed_at"` CollectedAt time.Time `json:"collected_at"` ExecutedByRef string `json:"executed_by_ref"` ReviewedByRef string `json:"reviewed_by_ref"` ReviewRef string `json:"review_ref"` Checks []CheckResult `json:"checks"` Images []ImageArtifact `json:"images"` SBOMs []SBOMArtifact `json:"sboms"` } type CheckResult struct { Code string `json:"code"` Status string `json:"status"` EvidenceRef string `json:"evidence_ref"` } type ImageArtifact struct { Subject string `json:"subject"` Reference string `json:"reference"` Digest string `json:"digest"` EvidenceRef string `json:"evidence_ref"` } type SBOMArtifact struct { Subject string `json:"subject"` Format string `json:"format"` Digest string `json:"digest"` EvidenceRef string `json:"evidence_ref"` } type Check struct { Code string `json:"code"` Status string `json:"status"` Detail string `json:"detail"` } type Report struct { SchemaVersion string `json:"schema_version"` Result string `json:"result"` GeneratedAt time.Time `json:"generated_at"` CandidateVersion string `json:"candidate_version"` BuildDigest string `json:"build_digest"` SourceCommit string `json:"source_commit"` EvidenceScope string `json:"evidence_scope"` ExternalValidationRequired bool `json:"external_validation_required"` Provider string `json:"provider"` RunID string `json:"run_id"` RunURL string `json:"run_url"` CompletedChecks []string `json:"completed_checks"` ImageDigests map[string]string `json:"image_digests"` SBOMDigests map[string]string `json:"sbom_digests"` InputSHA256 map[string]string `json:"input_sha256"` Checks []Check `json:"checks"` Disclaimer string `json:"disclaimer"` } func ParseCapture(content []byte) (Capture, error) { var capture Capture if err := decodeStrict(content, &capture); err != nil { return Capture{}, fmt.Errorf("decode remote CI supply-chain capture: %w", err) } return capture, nil } func ParseReport(content []byte) (Report, error) { var report Report if err := decodeStrict(content, &report); err != nil { return Report{}, fmt.Errorf("decode remote CI supply-chain report: %w", err) } return report, nil } func Evaluate(capture Capture, candidateVersion, buildDigest, sourceCommit string, generatedAt time.Time) Report { candidateVersion = strings.TrimSpace(candidateVersion) buildDigest = strings.ToLower(strings.TrimSpace(buildDigest)) sourceCommit = strings.ToLower(strings.TrimSpace(sourceCommit)) report := Report{ SchemaVersion: ReportSchema, Result: "PASS", GeneratedAt: generatedAt.UTC(), CandidateVersion: candidateVersion, BuildDigest: buildDigest, SourceCommit: sourceCommit, EvidenceScope: "LOCAL_CI_TOOLCHAIN", ExternalValidationRequired: true, Provider: capture.Provider, RunID: capture.RunID, RunURL: capture.RunURL, CompletedChecks: []string{}, ImageDigests: map[string]string{}, SBOMDigests: map[string]string{}, InputSHA256: map[string]string{}, Checks: []Check{}, Disclaimer: "该报告只核对固定提交、远程CI检查、镜像摘要和SBOM证据的一致性;本地执行不能替代远程CI,也不等于业务发布批准。", } if capture.EvidenceSource == "REMOTE_MANAGED_CI" { report.EvidenceScope = "REMOTE_CI_SUPPLY_CHAIN" } add := func(code string, ok bool, detail string) { status := "PASS" if !ok { status, report.Result = "FAIL", "FAIL" } report.Checks = append(report.Checks, Check{Code: code, Status: status, Detail: detail}) } add("CAPTURE_SCHEMA", capture.SchemaVersion == CaptureSchema, "采集必须使用"+CaptureSchema) add("EVIDENCE_SOURCE", capture.EvidenceSource == "REMOTE_MANAGED_CI" || capture.EvidenceSource == "LOCAL_TOOLCHAIN", "证据来源必须明确为远程托管CI或本地工具链") identityOK := candidateVersion != "" && digestPattern.MatchString(buildDigest) && commitPattern.MatchString(sourceCommit) add("CANDIDATE_IDENTITY", identityOK, "候选版本、sha256构建摘要和完整源提交必须有效") add("CAPTURE_CANDIDATE_BINDING", capture.CandidateVersion == candidateVersion && strings.ToLower(capture.BuildDigest) == buildDigest && strings.ToLower(capture.SourceCommit) == sourceCommit, "采集必须绑定同一候选、构建摘要和源提交") remote := capture.EvidenceSource == "REMOTE_MANAGED_CI" providerOK := capture.Provider != "" && capture.RepositoryRef != "" && capture.WorkflowRef != "" && capture.RunID != "" && validHTTPS(capture.RunURL) if remote { providerOK = providerOK && !strings.Contains(strings.ToLower(capture.Provider), "local") } add("REMOTE_RUN_IDENTITY", providerOK, "远程运行必须绑定提供器、仓库、工作流、运行编号和HTTPS运行地址") triggerOK := slices.Contains([]string{"PUSH", "WORKFLOW_DISPATCH", "RELEASE"}, capture.Trigger) && capture.Ref != "" && capture.Attempt >= 1 && capture.Conclusion == "SUCCESS" add("REMOTE_RUN_OUTCOME", triggerOK, "候选运行必须由受控触发、成功结束并记录ref和尝试次数") timelineOK := !capture.StartedAt.IsZero() && !capture.CompletedAt.Before(capture.StartedAt) && !capture.CollectedAt.Before(capture.CompletedAt) add("RUN_TIMELINE", timelineOK, "开始、完成和采集时间必须单调") reviewOK := capture.ExecutedByRef != "" && capture.ReviewedByRef != "" && capture.ReviewRef != "" if remote { reviewOK = reviewOK && capture.ExecutedByRef != capture.ReviewedByRef } add("INDEPENDENT_REVIEW", reviewOK, "真实远程证据必须由不同人员执行和复核并附复核引用") seenChecks, checksOK := map[string]bool{}, true for _, result := range capture.Checks { if !slices.Contains(RequiredChecks, result.Code) || seenChecks[result.Code] || result.Status != "PASS" || strings.TrimSpace(result.EvidenceRef) == "" { checksOK = false } seenChecks[result.Code] = true } for _, code := range RequiredChecks { if !seenChecks[code] { checksOK = false } else { report.CompletedChecks = append(report.CompletedChecks, code) } } add("REQUIRED_CHECKS", checksOK, "14项构建、测试、许可证、漏洞、密钥、配置、镜像和SBOM检查必须唯一通过") seenImages, imagesOK := map[string]bool{}, true for _, artifact := range capture.Images { if !slices.Contains(RequiredImages, artifact.Subject) || seenImages[artifact.Subject] || artifact.Reference == "" || !digestPattern.MatchString(strings.ToLower(artifact.Digest)) || artifact.EvidenceRef == "" { imagesOK = false } seenImages[artifact.Subject] = true report.ImageDigests[artifact.Subject] = strings.ToLower(artifact.Digest) } for _, subject := range RequiredImages { imagesOK = imagesOK && seenImages[subject] } add("IMAGE_DIGESTS", imagesOK, "后端和Web镜像都必须有不可变sha256摘要及制品引用") seenSBOMs, sbomsOK := map[string]bool{}, true for _, artifact := range capture.SBOMs { if !slices.Contains(RequiredSBOMs, artifact.Subject) || seenSBOMs[artifact.Subject] || artifact.Format != "CYCLONEDX_JSON" || !digestPattern.MatchString(strings.ToLower(artifact.Digest)) || artifact.EvidenceRef == "" { sbomsOK = false } seenSBOMs[artifact.Subject] = true report.SBOMDigests[artifact.Subject] = strings.ToLower(artifact.Digest) } for _, subject := range RequiredSBOMs { sbomsOK = sbomsOK && seenSBOMs[subject] } add("CYCLONEDX_SBOMS", sbomsOK, "源码、后端镜像和Web镜像必须各有CycloneDX JSON及文件摘要") if remote && providerOK && triggerOK && timelineOK && reviewOK && checksOK && imagesOK && sbomsOK && report.Result == "PASS" { report.ExternalValidationRequired = false } return report } func SHA256(content []byte) string { digest := sha256.Sum256(content) return hex.EncodeToString(digest[:]) } func validHTTPS(value string) bool { parsed, err := url.Parse(strings.TrimSpace(value)) return err == nil && parsed.Scheme == "https" && parsed.Host != "" } func decodeStrict(content []byte, target any) error { decoder := json.NewDecoder(bytes.NewReader(content)) decoder.DisallowUnknownFields() if err := decoder.Decode(target); err != nil { return err } var trailing any if err := decoder.Decode(&trailing); !errors.Is(err, io.EOF) { return errors.New("trailing JSON data") } return nil }