Files
story/internal/cisupplychainconformance/conformance.go
T

234 lines
10 KiB
Go

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
}