Files
zeekay 187be9e719 proto, settings: fix stale argoproj.argo_cd.v3 package path and hanzocd part-of drift
repository.proto and 11 sibling hand-written .proto files referenced
generated.proto's application/v1alpha1 types under the pre-fork package
path (github.com.argoproj.argo_cd.v3...); protoc silently kept compiling
because generated.proto still carried that same stale package too. Point
both at the real, mechanically-derived path
(github.com.hanzoai.cd.pkg.apis.application.v1alpha1, no v3 segment) and
regenerate everything protoc/go-to-protobuf own: generated.proto/pb.go,
every pkg/apiclient and reposerver/cmpserver/commitserver *.pb.go,
assets/swagger.json, and the CRD manifests.

That regeneration surfaced a second, unrelated bug: hack/gen-crd-spec
stamped app.kubernetes.io/part-of: cd on the CRDs, but
util/settings.isSettingsObject (and the ConfigMap informer built on it)
only ever recognizes part-of=hanzocd. The same wrong "cd" value was
baked into cmd/cd/commands/admin/settings.go's setSettingsMeta and the
shared test/testdata.go fixtures ~30 files import — util/settings' own
test suite was panicking on an empty slice because its fixture was
invisible to the informer it was supposedly seeding. Fixed the label
everywhere it's produced or asserted in the settings path; left the
~90 unrelated part-of:"cd" literals in other packages' local test
fixtures alone since they never touch this selector.

Also dropped 25 "TODO: Remove deprecated <dead upstream issue link>"
comments that duplicated the real explanation already sitting one line
away (a proper `Deprecated:` godoc, a cobra Deprecated field, or a
nolint:staticcheck marker) — kept the explanation, dropped the link, per
the standing rule for these citations.

And repointed the two Grafana dashboards' job selector from
argocd-server-metrics, a Service the manifests never create, to
hanzocd-server-metrics, the one they do.
2026-09-05 14:58:20 -07:00

245 lines
8.2 KiB
Go

package sourceintegrity
import (
"context"
"errors"
"fmt"
"os"
"strings"
log "github.com/sirupsen/logrus"
"github.com/hanzoai/cd/pkg/apis/application/v1alpha1"
"github.com/hanzoai/cd/util/git"
"github.com/hanzoai/cd/util/glob"
)
type gitFunc func(ctx context.Context, gitClient git.Client, verifiedRevision string) (result *v1alpha1.SourceIntegrityCheckResult, legacyDescription string, err error)
var _gpgDisabledLoggedAlready bool
// HasCriteria determines if any of the sources have some criteria declared
func HasCriteria(si *v1alpha1.SourceIntegrity, sources ...v1alpha1.ApplicationSource) bool {
if si == nil || si.Git == nil {
return false
}
for _, source := range sources {
if !source.IsZero() && !source.IsOCI() && !source.IsHelm() {
if lookupGit(si, source.RepoURL) != nil {
return true
}
}
}
return false
}
// VerifyGit makes sure the git repository satisfies the criteria declared.
// It returns nil in case there were no relevant criteria, a check result if there were.
// The verifiedRevision is expected to be either an annotated tag to a resolved commit sha - the revision, its signature is being verified.
func VerifyGit(ctx context.Context, si *v1alpha1.SourceIntegrity, gitClient git.Client, verifiedRevision string) (*v1alpha1.SourceIntegrityCheckResult, string, error) {
if si == nil || si.Git == nil {
return nil, "", nil
}
check := lookupGit(si, gitClient.RepoURL())
if check != nil {
return check(ctx, gitClient, verifiedRevision)
}
return nil, "", nil
}
func lookupGit(si *v1alpha1.SourceIntegrity, repoURL string) gitFunc {
policies := findMatchingGitPolicies(si.Git, repoURL)
nPolicies := len(policies)
if nPolicies == 0 {
log.Infof("No git source integrity policies found for repo URL: %s", repoURL)
return nil
}
if nPolicies > 1 {
// Multiple matching policies is an error. BUT, it has to return a check that fails for every repo.
// This is to make sure that a configuration mistake does not disable verification until fixed.
msg := fmt.Sprintf("multiple (%d) git source integrity policies found for repo URL: %s", nPolicies, repoURL)
log.Warn(msg)
return func(_ context.Context, _ git.Client, _ string) (*v1alpha1.SourceIntegrityCheckResult, string, error) {
return nil, "", errors.New(msg)
}
}
policy := policies[0]
if policy.GPG != nil {
if policy.GPG.Mode == v1alpha1.SourceIntegrityGitPolicyGPGModeNone {
// Declare missing check because there is no verification performed
return nil
}
if !_gpgDisabledLoggedAlready && !IsGPGEnabled() {
log.Warnf("SourceIntegrity criteria for git+gpg declared, but it is turned off by CD_GPG_ENABLED")
_gpgDisabledLoggedAlready = true
return nil
}
return func(ctx context.Context, gitClient git.Client, verifiedRevision string) (*v1alpha1.SourceIntegrityCheckResult, string, error) {
return verify(ctx, policy.GPG, gitClient, verifiedRevision)
}
}
log.Warnf("No verification configured for SourceIntegrity policy for %+v", policy.Repos)
return nil
}
func findMatchingGitPolicies(si *v1alpha1.SourceIntegrityGit, repoURL string) (policies []*v1alpha1.SourceIntegrityGitPolicy) {
for _, p := range si.Policies {
include := false
for _, r := range p.Repos {
m := repoMatches(r.URL, repoURL)
if m == -1 {
include = false
break
} else if m == 1 {
include = true
}
}
if include {
policies = append(policies, p)
}
}
return policies
}
func repoMatches(urlGlob string, repoURL string) int {
if strings.HasPrefix(urlGlob, "!") {
if glob.Match(urlGlob[1:], repoURL) {
return -1
}
} else {
if glob.Match(urlGlob, repoURL) {
return 1
}
}
return 0
}
func verify(ctx context.Context, g *v1alpha1.SourceIntegrityGitPolicyGPG, gitClient git.Client, verifiedRevision string) (*v1alpha1.SourceIntegrityCheckResult, string, error) {
const checkName = "GIT/GPG"
var deep bool
switch g.Mode {
// verify tag if on tag, latest revision otherwise
case v1alpha1.SourceIntegrityGitPolicyGPGModeHead:
deep = false
// verify history from the current commit
case v1alpha1.SourceIntegrityGitPolicyGPGModeStrict:
deep = true
default:
return nil, "", fmt.Errorf("unknown GPG mode %q configured for GIT source integrity", g.Mode)
}
signatures, legacyVerification, err := gitClient.LsSignatures(ctx, verifiedRevision, deep)
if err != nil {
return nil, "", err
}
legacyDescriptionCondition := VerifyGnuPGSignature(verifiedRevision, g.Keys, legacyVerification)
legacyDescription := ""
if legacyDescriptionCondition != nil {
legacyDescription = legacyDescriptionCondition.Message
}
if len(signatures) == 0 {
// Checked for defensiveness, there should be 1+ signatures always
return nil, "", errors.New("git LsSignatures found no signatures for " + verifiedRevision)
}
problems, legacyGood := describeProblems(g, signatures)
result := &v1alpha1.SourceIntegrityCheckResult{Checks: []v1alpha1.SourceIntegrityCheckResultItem{{
Name: checkName,
Problems: problems,
}}}
if legacyDescription == "" {
legacyDescription = legacyGood
}
return result, legacyDescription, nil
}
// describeProblems reports 10 most recent problematic signatures or unsigned commits.
// The number is limited not to flood the UI and logs with too many problems. Problems related to the same signing key are squashed.
func describeProblems(g *v1alpha1.SourceIntegrityGitPolicyGPG, signatureInfos []git.RevisionSignatureInfo) (problems []string, legacyDescription string) {
reportedKeys := make(map[string]any)
for _, signatureInfo := range signatureInfos {
if legacyDescription == "" {
if signatureInfo.VerificationResult == git.GPGVerificationResultUnsigned {
legacyDescription = "Revision is not signed."
} else {
legacyResult := map[git.GPGVerificationResult]string{
git.GPGVerificationResultGood: "Good",
git.GPGVerificationResultBad: "Bad",
git.GPGVerificationResultUntrusted: "Invalid",
git.GPGVerificationResultExpiredSignature: "Invalid",
git.GPGVerificationResultExpiredKey: "Invalid",
git.GPGVerificationResultRevokedKey: "Invalid",
git.GPGVerificationResultMissingKey: "Invalid",
}[signatureInfo.VerificationResult]
legacyDescription = fmt.Sprintf("%s signature from %s key %s", legacyResult, signatureInfo.AuthorIdentity, signatureInfo.SignatureKeyID)
}
}
problem := gpgProblemMessage(g, signatureInfo)
if problem != "" {
// Do not report the same key twice unless:
// - the revision is unsigned (unsigned commits can have different authors, so they are all worth reporting)
// - the revision is a tag (tags are signed separately from commits)
if signatureInfo.SignatureKeyID != "" && git.IsCommitSHA(signatureInfo.Revision) {
if _, exists := reportedKeys[signatureInfo.SignatureKeyID]; exists {
continue
}
reportedKeys[signatureInfo.SignatureKeyID] = nil
}
problems = append(problems, problem)
// Report at most 10 problems
if len(problems) >= 10 {
break
}
}
}
return problems, legacyDescription
}
// gpgProblemMessage generates a message describing GPG verification issues for a specific revision signature and the configured policy.
// When an empty string is returned, it means there is no problem - the validation has passed.
func gpgProblemMessage(g *v1alpha1.SourceIntegrityGitPolicyGPG, signatureInfo git.RevisionSignatureInfo) string {
if signatureInfo.VerificationResult != git.GPGVerificationResultGood {
return fmt.Sprintf(
"Failed verifying revision %s by '%s': %s (key_id=%s)",
signatureInfo.Revision, signatureInfo.AuthorIdentity, signatureInfo.VerificationResult, signatureInfo.SignatureKeyID,
)
}
for _, allowedKey := range g.Keys {
allowedKey, err := KeyID(allowedKey)
if err != nil {
log.Error(err.Error())
continue
}
if allowedKey == signatureInfo.SignatureKeyID {
return ""
}
}
return fmt.Sprintf(
"Failed verifying revision %s by '%s': signed with unallowed key (key_id=%s)",
signatureInfo.Revision, signatureInfo.AuthorIdentity, signatureInfo.SignatureKeyID,
)
}
// IsGPGEnabled returns true if the GPG feature is enabled
func IsGPGEnabled() bool {
if en := os.Getenv("CD_GPG_ENABLED"); strings.EqualFold(en, "false") || strings.EqualFold(en, "no") {
return false
}
return true
}