chore: establish local platform baseline
This commit is contained in:
@@ -0,0 +1,233 @@
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package cisupplychainconformance
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestRemoteManagedCICompleteEvidencePasses(t *testing.T) {
|
||||
t.Parallel()
|
||||
capture := ciFixture("REMOTE_MANAGED_CI")
|
||||
report := Evaluate(capture, capture.CandidateVersion, capture.BuildDigest, capture.SourceCommit, time.Now())
|
||||
if report.Result != "PASS" || report.EvidenceScope != "REMOTE_CI_SUPPLY_CHAIN" || report.ExternalValidationRequired || len(report.CompletedChecks) != len(RequiredChecks) {
|
||||
t.Fatalf("unexpected report: %+v", report)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMissingRequiredArtifactFails(t *testing.T) {
|
||||
t.Parallel()
|
||||
capture := ciFixture("REMOTE_MANAGED_CI")
|
||||
capture.SBOMs = capture.SBOMs[:2]
|
||||
report := Evaluate(capture, capture.CandidateVersion, capture.BuildDigest, capture.SourceCommit, time.Now())
|
||||
if report.Result != "FAIL" || !report.ExternalValidationRequired {
|
||||
t.Fatalf("expected failed report: %+v", report)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLocalToolchainCannotCompleteExternalValidation(t *testing.T) {
|
||||
t.Parallel()
|
||||
capture := ciFixture("LOCAL_TOOLCHAIN")
|
||||
capture.Provider = "LOCAL_GO_TEST"
|
||||
report := Evaluate(capture, capture.CandidateVersion, capture.BuildDigest, capture.SourceCommit, time.Now())
|
||||
if report.Result != "PASS" || report.EvidenceScope != "LOCAL_CI_TOOLCHAIN" || !report.ExternalValidationRequired {
|
||||
t.Fatalf("unexpected local report: %+v", report)
|
||||
}
|
||||
}
|
||||
|
||||
func ciFixture(source string) Capture {
|
||||
started := time.Date(2026, 8, 25, 9, 0, 0, 0, time.UTC)
|
||||
digest := "sha256:" + strings.Repeat("a", 64)
|
||||
checks := make([]CheckResult, 0, len(RequiredChecks))
|
||||
for _, code := range RequiredChecks {
|
||||
checks = append(checks, CheckResult{Code: code, Status: "PASS", EvidenceRef: "artifact:checks/" + strings.ToLower(code)})
|
||||
}
|
||||
images := make([]ImageArtifact, 0, len(RequiredImages))
|
||||
for position, subject := range RequiredImages {
|
||||
images = append(images, ImageArtifact{Subject: subject, Reference: "registry.example.com/voice/" + strings.ToLower(subject), Digest: "sha256:" + strings.Repeat(string(rune('b'+position)), 64), EvidenceRef: "artifact:images/" + strings.ToLower(subject)})
|
||||
}
|
||||
sboms := make([]SBOMArtifact, 0, len(RequiredSBOMs))
|
||||
for position, subject := range RequiredSBOMs {
|
||||
sboms = append(sboms, SBOMArtifact{Subject: subject, Format: "CYCLONEDX_JSON", Digest: "sha256:" + strings.Repeat(string(rune('d'+position)), 64), EvidenceRef: "artifact:sbom/" + strings.ToLower(subject)})
|
||||
}
|
||||
return Capture{
|
||||
SchemaVersion: CaptureSchema, EvidenceSource: source, CandidateVersion: "0.64.0-rc.1", BuildDigest: digest,
|
||||
SourceCommit: strings.Repeat("1", 40), Provider: "GITHUB_ACTIONS", RepositoryRef: "github:example/voice-platform",
|
||||
WorkflowRef: ".github/workflows/ci.yml@refs/tags/v0.64.0-rc.1", RunID: "123456789", RunURL: "https://github.com/example/voice-platform/actions/runs/123456789",
|
||||
Trigger: "RELEASE", Ref: "refs/tags/v0.64.0-rc.1", Attempt: 1, Conclusion: "SUCCESS", StartedAt: started, CompletedAt: started.Add(20 * time.Minute),
|
||||
CollectedAt: started.Add(25 * time.Minute), ExecutedByRef: "release-engineer", ReviewedByRef: "security-reviewer", ReviewRef: "approval:SEC-2026-081",
|
||||
Checks: checks, Images: images, SBOMs: sboms,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user