From c0b6060e389c27fc82182f0d762b5e554e9c3ebb Mon Sep 17 00:00:00 2001 From: Hannah Braswell Date: Thu, 18 Jun 2026 11:24:15 -0400 Subject: [PATCH 1/2] fix(bug): updates support for oci refs Signed-off-by: Hannah Braswell --- CHANGELOG.md | 6 + cmd/complyctl/cli/generate.go | 5 +- cmd/complyctl/cli/get.go | 10 +- cmd/complyctl/cli/list.go | 5 +- cmd/complyctl/cli/scan.go | 5 +- internal/cache/complypack_sync.go | 5 +- internal/cache/sync.go | 20 ++- internal/cache/sync_test.go | 33 +++++ internal/complytime/config.go | 102 +++++++++++--- internal/complytime/config_test.go | 131 +++++++++++++++++- internal/doctor/doctor.go | 27 +++- .../fix-complypack-oci-ref/.openspec.yaml | 2 + .../changes/fix-complypack-oci-ref/design.md | 55 ++++++++ .../fix-complypack-oci-ref/proposal.md | 31 +++++ .../specs/oci-ref-parsing/spec.md | 88 ++++++++++++ .../changes/fix-complypack-oci-ref/tasks.md | 27 ++++ 16 files changed, 512 insertions(+), 40 deletions(-) create mode 100644 openspec/changes/fix-complypack-oci-ref/.openspec.yaml create mode 100644 openspec/changes/fix-complypack-oci-ref/design.md create mode 100644 openspec/changes/fix-complypack-oci-ref/proposal.md create mode 100644 openspec/changes/fix-complypack-oci-ref/specs/oci-ref-parsing/spec.md create mode 100644 openspec/changes/fix-complypack-oci-ref/tasks.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 702247ab..3930067d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -51,6 +51,12 @@ ### Fixed +- OCI reference parsing now supports standard `:tag` syntax (e.g., + `registry.com/org/image:v0.4.0`) in addition to the existing `@version` + notation for both policies and complypacks. Digest references + (`@sha256:...`) are also supported. Invalid OCI references in + `complytime.yaml` are now detected at config load time with clear + error messages. (#594) - Scan reports now resolve assessment plan IDs to requirement IDs, ensuring output displays meaningful identifiers instead of internal plan references. Affects EvaluationLog, OSCAL, SARIF, and Markdown diff --git a/cmd/complyctl/cli/generate.go b/cmd/complyctl/cli/generate.go index 1bfcf967..aafb7d6b 100644 --- a/cmd/complyctl/cli/generate.go +++ b/cmd/complyctl/cli/generate.go @@ -93,7 +93,10 @@ func (o *generateOptions) run(ctx context.Context) error { } func (o *generateOptions) generatePolicy(ctx context.Context, cfg *complytime.WorkspaceConfig, entry complytime.PolicyEntry, baseDir string) error { - ref := complytime.ParsePolicyRef(entry.URL) + ref, err := complytime.ParsePolicyRef(entry.URL) + if err != nil { + return fmt.Errorf("invalid policy reference %q: %w", entry.URL, err) + } eid := entry.EffectiveID() version, graph, err := resolveVersionAndGraph(o.cacheDir, ref) diff --git a/cmd/complyctl/cli/get.go b/cmd/complyctl/cli/get.go index 61f283ff..2dcc8d03 100644 --- a/cmd/complyctl/cli/get.go +++ b/cmd/complyctl/cli/get.go @@ -151,7 +151,10 @@ func syncAllPolicies(ctx context.Context, cacheMgr *cache.Cache, state *cache.St } func syncSinglePolicy(ctx context.Context, cacheMgr *cache.Cache, state *cache.State, credFunc auth.CredentialFunc, entry complytime.PolicyEntry, index, total int) error { - ref := complytime.ParsePolicyRef(entry.URL) + ref, err := complytime.ParsePolicyRef(entry.URL) + if err != nil { + return fmt.Errorf("invalid policy reference %q: %w", entry.URL, err) + } version := ref.Version client := registry.NewClient(ref.Registry, credFunc) @@ -202,7 +205,10 @@ func syncAllComplypacks(ctx context.Context, state *cache.State, credFunc auth.C } func syncSingleComplypack(ctx context.Context, state *cache.State, credFunc auth.CredentialFunc, entry complytime.PolicyEntry, index, total int, cacheDir string) error { - ref := complytime.ParsePolicyRef(entry.URL) + ref, err := complytime.ParsePolicyRef(entry.URL) + if err != nil { + return fmt.Errorf("invalid complypack reference %q: %w", entry.URL, err) + } version := ref.Version client := registry.NewClient(ref.Registry, credFunc) diff --git a/cmd/complyctl/cli/list.go b/cmd/complyctl/cli/list.go index 162cb543..ef2e3717 100644 --- a/cmd/complyctl/cli/list.go +++ b/cmd/complyctl/cli/list.go @@ -88,7 +88,10 @@ func (o *listOptions) run(_ context.Context) error { continue } - ref := complytime.ParsePolicyRef(p.URL) + ref, err := complytime.ParsePolicyRef(p.URL) + if err != nil { + return fmt.Errorf("invalid policy reference %q: %w", p.URL, err) + } versions, _ := loader.GetCachedVersions(ref.Repository) var versionStr string diff --git a/cmd/complyctl/cli/scan.go b/cmd/complyctl/cli/scan.go index 2019d499..0329eb7b 100644 --- a/cmd/complyctl/cli/scan.go +++ b/cmd/complyctl/cli/scan.go @@ -252,7 +252,10 @@ func loadWorkspaceConfig(baseDir string) (*complytime.WorkspaceConfig, error) { } func (o *scanOptions) scanPolicy(ctx context.Context, cfg *complytime.WorkspaceConfig, entry complytime.PolicyEntry, targetID, baseDir string) error { - ref := complytime.ParsePolicyRef(entry.URL) + ref, err := complytime.ParsePolicyRef(entry.URL) + if err != nil { + return fmt.Errorf("invalid policy reference %q: %w", entry.URL, err) + } eid := entry.EffectiveID() version, graph, err := resolveVersionAndGraph(o.cacheDir, ref) diff --git a/internal/cache/complypack_sync.go b/internal/cache/complypack_sync.go index f6a694c9..8dd8e01e 100644 --- a/internal/cache/complypack_sync.go +++ b/internal/cache/complypack_sync.go @@ -44,10 +44,7 @@ func (s *ComplypackSync) SyncComplypack(ctx context.Context, repository, version return false, fmt.Errorf("complypack repository cannot be empty") } - lookupRef := repository - if version != "" && version != "latest" { - lookupRef = repository + ":" + version - } + lookupRef := BuildLookupRef(repository, version) remoteDigest, remoteVersion, err := s.source.DefinitionVersion(ctx, lookupRef) if err != nil { diff --git a/internal/cache/sync.go b/internal/cache/sync.go index e988526e..8af7148e 100644 --- a/internal/cache/sync.go +++ b/internal/cache/sync.go @@ -6,10 +6,25 @@ import ( "context" "errors" "fmt" + "strings" "github.com/complytime/complyctl/internal/registry" ) +// BuildLookupRef constructs an OCI lookup reference from a repository and +// version string. For digest versions (sha256:, sha512:) it uses "@" as +// the separator; for tag versions it uses ":". If the version is empty or +// "latest", the bare repository is returned so oras resolves the default tag. +func BuildLookupRef(repository, version string) string { + if version == "" || version == "latest" { + return repository + } + if strings.HasPrefix(version, "sha256:") || strings.HasPrefix(version, "sha512:") { + return repository + "@" + version + } + return repository + ":" + version +} + // Sync provides incremental sync using oras.Copy() for remote-to-local transfer. type Sync struct { cache *Cache @@ -33,10 +48,7 @@ func (s *Sync) SyncPolicy(ctx context.Context, policyID, version string) error { return fmt.Errorf("policy ID cannot be empty") } - lookupRef := policyID - if version != "" && version != "latest" { - lookupRef = policyID + ":" + version - } + lookupRef := BuildLookupRef(policyID, version) remoteDigest, remoteVersion, err := s.source.DefinitionVersion(ctx, lookupRef) if err != nil { diff --git a/internal/cache/sync_test.go b/internal/cache/sync_test.go index ea2fde35..7e353456 100644 --- a/internal/cache/sync_test.go +++ b/internal/cache/sync_test.go @@ -241,3 +241,36 @@ func TestSync_StressConcurrentFailures(t *testing.T) { "OCI layout marker must exist after successful syncs") } } + +func TestBuildLookupRef(t *testing.T) { + tests := []struct { + name string + repository string + version string + want string + }{ + {"tag version", "org/policy", "v1.0", "org/policy:v1.0"}, + {"sha256 digest", "org/policy", "sha256:9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08", "org/policy@sha256:9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08"}, + {"sha512 digest", "org/policy", "sha512:def456", "org/policy@sha512:def456"}, + {"empty version", "org/policy", "", "org/policy"}, + {"latest version", "org/policy", "latest", "org/policy"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := cache.BuildLookupRef(tt.repository, tt.version) + assert.Equal(t, tt.want, got) + }) + } +} + +// TestBuildLookupRef_Regression_NoDoubleTag verifies that the original +// bug (issue #594) is prevented: a :tag in the repository must not +// produce a double-tagged reference like "repo:v0.4.0:v0.4.0". +func TestBuildLookupRef_Regression_NoDoubleTag(t *testing.T) { + // When ParsePolicyRef correctly extracts the tag, Repository + // will be "complytime/complypack-ampel-bp" and Version "v0.4.0". + // BuildLookupRef should produce a single-tagged reference. + lookupRef := cache.BuildLookupRef("complytime/complypack-ampel-bp", "v0.4.0") + assert.Equal(t, "complytime/complypack-ampel-bp:v0.4.0", lookupRef) + assert.NotContains(t, lookupRef, ":v0.4.0:v0.4.0") +} diff --git a/internal/complytime/config.go b/internal/complytime/config.go index e28eba2d..23c262ce 100644 --- a/internal/complytime/config.go +++ b/internal/complytime/config.go @@ -9,6 +9,7 @@ import ( "strings" "github.com/goccy/go-yaml" + orasreg "oras.land/oras-go/v2/registry" ) var envVarPattern = regexp.MustCompile(`\$\{([^}]+)\}`) @@ -85,7 +86,9 @@ func (p PolicyEntry) EffectiveID() string { if p.ID != "" { return p.ID } - ref := ParsePolicyRef(p.URL) + // Error ignored: LoadFrom validates all URLs via validatePolicyRefs + // at config load time, so ParsePolicyRef will not fail for loaded entries. + ref, _ := ParsePolicyRef(p.URL) segments := strings.Split(ref.Repository, "/") return segments[len(segments)-1] } @@ -98,21 +101,45 @@ type TargetConfig struct { Variables map[string]string `yaml:"variables,omitempty"` } -// PolicyRef represents a parsed OCI policy reference. +// PolicyRef represents a parsed OCI policy reference with its components +// separated for downstream use (registry client construction, cache lookup, +// version resolution). type PolicyRef struct { - Raw string - Registry string + // Raw is the original unparsed input string. + Raw string + // Registry is the registry host, optionally prefixed with http:// or + // https://. Empty for bare policy IDs (no slash in input). + Registry string + // Repository is the repository path within the registry (e.g., + // "policies/nist-800-53-r5"). For bare policy IDs, this is the + // identifier itself. Repository string - Version string + // Version is the tag, digest, or empty string. For :tag and @version + // inputs it holds the version string (e.g., "v1.0"). For digest inputs + // it holds the full algorithm:hex string (e.g., "sha256:9f86d..."). + // Empty when no version or tag was specified. + Version string } // ParsePolicyRef parses a full OCI reference into its components. // Handles optional scheme (http://, https://), registry host detection, -// and @version suffix. -func ParsePolicyRef(raw string) PolicyRef { +// :tag, @version, @digest, and bare policy IDs (no slash). Delegates to +// oras-go's registry.ParseReference for standard OCI references. +// +// The @version notation (e.g., "registry.com/repo@v1.0") is a complytime +// convention that predates standard OCI tag syntax. ParsePolicyRef converts +// @version to :tag before delegating to oras-go, preserving backwards +// compatibility. Actual digests (e.g., "@sha256:...") are passed through +// to oras-go directly. +func ParsePolicyRef(raw string) (PolicyRef, error) { ref := PolicyRef{Raw: raw} s := strings.TrimSpace(raw) + if s == "" { + return ref, fmt.Errorf("policy reference cannot be empty") + } + + // Strip URL scheme prefix; oras-go does not accept schemes. var scheme string if strings.HasPrefix(s, "http://") { scheme = "http://" @@ -122,20 +149,40 @@ func ParsePolicyRef(raw string) PolicyRef { s = strings.TrimPrefix(s, "https://") } + // Bare policy IDs (no slash) are convention-based identifiers, not OCI + // references. Handle them directly: extract an optional @version suffix + // and treat the rest as the repository. + if !strings.Contains(s, "/") { + if idx := strings.LastIndex(s, "@"); idx > 0 && idx < len(s)-1 { + ref.Version = s[idx+1:] + s = s[:idx] + } + ref.Repository = s + return ref, nil + } + + // Convert complytime's @version notation to standard :tag syntax before + // delegating to oras-go. Actual digests (sha256:, sha512:) keep the @ + // separator so oras-go parses them as digests. if idx := strings.LastIndex(s, "@"); idx > 0 && idx < len(s)-1 { - ref.Version = s[idx+1:] - s = s[:idx] + suffix := s[idx+1:] + if !strings.HasPrefix(suffix, "sha256:") && !strings.HasPrefix(suffix, "sha512:") { + // Non-digest @version — convert to :tag for oras-go. + s = s[:idx] + ":" + suffix + } } - parts := strings.SplitN(s, "/", 2) - if len(parts) == 2 && (strings.Contains(parts[0], ".") || strings.Contains(parts[0], ":")) { - ref.Registry = scheme + parts[0] - ref.Repository = parts[1] - } else { - ref.Repository = s + // Delegate to oras-go for standard OCI references. + orasRef, err := orasreg.ParseReference(s) + if err != nil { + return ref, fmt.Errorf("invalid OCI reference %q: %w", raw, err) } - return ref + ref.Registry = scheme + orasRef.Registry + ref.Repository = orasRef.Repository + ref.Version = orasRef.Reference + + return ref, nil } // FindPolicy matches a policy identifier against the policies list by effective ID. @@ -186,9 +233,32 @@ func LoadFrom(configPath string) (*WorkspaceConfig, error) { return nil, err } + if err := validatePolicyRefs(&config); err != nil { + return nil, fmt.Errorf("invalid config %s: %w", configPath, err) + } + return &config, nil } +// validatePolicyRefs checks that all policy and complypack URLs are +// parseable OCI references. Called by LoadFrom at load time; if any +// entry fails parsing, LoadFrom returns an error and the config is not +// returned. Downstream code can therefore assume ParsePolicyRef will +// succeed for any URL in a loaded config. +func validatePolicyRefs(config *WorkspaceConfig) error { + for _, entry := range config.Policies { + if _, err := ParsePolicyRef(entry.URL); err != nil { + return fmt.Errorf("policies[].url %q: %w", entry.URL, err) + } + } + for _, entry := range config.Complypacks { + if _, err := ParsePolicyRef(entry.URL); err != nil { + return fmt.Errorf("complypacks[].url %q: %w", entry.URL, err) + } + } + return nil +} + // resolveEnvVars expands ${VAR} references in target variable values // and collector auth fields from the process environment. Returns an // error if a referenced variable is not set. diff --git a/internal/complytime/config_test.go b/internal/complytime/config_test.go index 0c0a8e00..0d8d1b75 100644 --- a/internal/complytime/config_test.go +++ b/internal/complytime/config_test.go @@ -14,54 +14,106 @@ import ( ) func TestParsePolicyRef_FullReference(t *testing.T) { - ref := complytime.ParsePolicyRef("registry.com/policies/nist-800-53-r5@v1.2.3") + ref, err := complytime.ParsePolicyRef("registry.com/policies/nist-800-53-r5@v1.2.3") + require.NoError(t, err) assert.Equal(t, "registry.com", ref.Registry) assert.Equal(t, "policies/nist-800-53-r5", ref.Repository) assert.Equal(t, "v1.2.3", ref.Version) } func TestParsePolicyRef_NoVersion(t *testing.T) { - ref := complytime.ParsePolicyRef("registry.com/policies/nist-800-53-r5") + ref, err := complytime.ParsePolicyRef("registry.com/policies/nist-800-53-r5") + require.NoError(t, err) assert.Equal(t, "registry.com", ref.Registry) assert.Equal(t, "policies/nist-800-53-r5", ref.Repository) assert.Empty(t, ref.Version) } func TestParsePolicyRef_WithHTTPScheme(t *testing.T) { - ref := complytime.ParsePolicyRef("http://localhost:5000/policies/test@v1.0") + ref, err := complytime.ParsePolicyRef("http://localhost:5000/policies/test@v1.0") + require.NoError(t, err) assert.Equal(t, "http://localhost:5000", ref.Registry) assert.Equal(t, "policies/test", ref.Repository) assert.Equal(t, "v1.0", ref.Version) } func TestParsePolicyRef_WithHTTPSScheme(t *testing.T) { - ref := complytime.ParsePolicyRef("https://ghcr.io/org/policy@latest") + ref, err := complytime.ParsePolicyRef("https://ghcr.io/org/policy@latest") + require.NoError(t, err) assert.Equal(t, "https://ghcr.io", ref.Registry) assert.Equal(t, "org/policy", ref.Repository) assert.Equal(t, "latest", ref.Version) } func TestParsePolicyRef_NoRegistry(t *testing.T) { - ref := complytime.ParsePolicyRef("nist-800-53-r5@v1.0") + ref, err := complytime.ParsePolicyRef("nist-800-53-r5@v1.0") + require.NoError(t, err) assert.Empty(t, ref.Registry) assert.Equal(t, "nist-800-53-r5", ref.Repository) assert.Equal(t, "v1.0", ref.Version) } func TestParsePolicyRef_BareID(t *testing.T) { - ref := complytime.ParsePolicyRef("nist-800-53-r5") + ref, err := complytime.ParsePolicyRef("nist-800-53-r5") + require.NoError(t, err) assert.Empty(t, ref.Registry) assert.Equal(t, "nist-800-53-r5", ref.Repository) assert.Empty(t, ref.Version) } func TestParsePolicyRef_PortInRegistry(t *testing.T) { - ref := complytime.ParsePolicyRef("localhost:5000/policy@v2") + ref, err := complytime.ParsePolicyRef("localhost:5000/policy@v2") + require.NoError(t, err) assert.Equal(t, "localhost:5000", ref.Registry) assert.Equal(t, "policy", ref.Repository) assert.Equal(t, "v2", ref.Version) } +func TestParsePolicyRef_ColonTag(t *testing.T) { + ref, err := complytime.ParsePolicyRef("quay.io/complytime/complypack-ampel-bp:v0.4.0") + require.NoError(t, err) + assert.Equal(t, "quay.io", ref.Registry) + assert.Equal(t, "complytime/complypack-ampel-bp", ref.Repository) + assert.Equal(t, "v0.4.0", ref.Version) +} + +func TestParsePolicyRef_ColonLatest(t *testing.T) { + ref, err := complytime.ParsePolicyRef("quay.io/complytime/complypack-ampel-bp:latest") + require.NoError(t, err) + assert.Equal(t, "quay.io", ref.Registry) + assert.Equal(t, "complytime/complypack-ampel-bp", ref.Repository) + assert.Equal(t, "latest", ref.Version) +} + +func TestParsePolicyRef_Digest(t *testing.T) { + digest := "sha256:9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08" + ref, err := complytime.ParsePolicyRef("quay.io/complytime/complypack-ampel-bp@" + digest) + require.NoError(t, err) + assert.Equal(t, "quay.io", ref.Registry) + assert.Equal(t, "complytime/complypack-ampel-bp", ref.Repository) + assert.Equal(t, digest, ref.Version) +} + +func TestParsePolicyRef_HTTPSchemeWithColonTag(t *testing.T) { + ref, err := complytime.ParsePolicyRef("http://localhost:5000/policies/test:v1.0") + require.NoError(t, err) + assert.Equal(t, "http://localhost:5000", ref.Registry) + assert.Equal(t, "policies/test", ref.Repository) + assert.Equal(t, "v1.0", ref.Version) +} + +func TestParsePolicyRef_ErrorOnEmpty(t *testing.T) { + _, err := complytime.ParsePolicyRef("") + require.Error(t, err) + assert.Contains(t, err.Error(), "cannot be empty") +} + +func TestParsePolicyRef_ErrorOnWhitespace(t *testing.T) { + _, err := complytime.ParsePolicyRef(" ") + require.Error(t, err) + assert.Contains(t, err.Error(), "cannot be empty") +} + func TestPolicyEntry_EffectiveID_ExplicitID(t *testing.T) { p := complytime.PolicyEntry{URL: "registry.com/policies/nist-800-53-r5@v1.0", ID: "nist"} assert.Equal(t, "nist", p.EffectiveID()) @@ -694,6 +746,71 @@ targets: assert.Equal(t, "complypack-ubuntu", cfg.Complypacks[1].EffectiveID()) } +func TestLoadFrom_InvalidPolicyURL(t *testing.T) { + dir := t.TempDir() + configPath := filepath.Join(dir, "complytime.yml") + + yamlContent := `policies: + - url: "" +targets: + - id: local + policies: + - nist +` + require.NoError(t, os.WriteFile(configPath, []byte(yamlContent), 0600)) + + _, err := complytime.LoadFrom(configPath) + require.Error(t, err) + assert.Contains(t, err.Error(), "invalid config") +} + +func TestLoadFrom_InvalidComplypackURL(t *testing.T) { + dir := t.TempDir() + configPath := filepath.Join(dir, "complytime.yml") + + yamlContent := `policies: + - url: registry.com/policies/nist@v1.0 + id: nist +complypacks: + - url: " " +targets: + - id: local + policies: + - nist +` + require.NoError(t, os.WriteFile(configPath, []byte(yamlContent), 0600)) + + _, err := complytime.LoadFrom(configPath) + require.Error(t, err) + assert.Contains(t, err.Error(), "invalid config") +} + +// TestLoadFrom_ColonTagComplypack verifies the fix for issue #594: +// complypack URLs using :tag syntax must load and parse correctly. +func TestLoadFrom_ColonTagComplypack(t *testing.T) { + dir := t.TempDir() + configPath := filepath.Join(dir, "complytime.yml") + + yamlContent := `policies: + - url: quay.io/complytime/policies-ampel-bp:latest + id: ampel-bp +complypacks: + - url: quay.io/complytime/complypack-ampel-bp:v0.4.0 + id: ampel-bp-pack +targets: + - id: local + policies: + - ampel-bp +` + require.NoError(t, os.WriteFile(configPath, []byte(yamlContent), 0600)) + + cfg, err := complytime.LoadFrom(configPath) + require.NoError(t, err) + require.NotNil(t, cfg) + require.Len(t, cfg.Complypacks, 1) + assert.Equal(t, "quay.io/complytime/complypack-ampel-bp:v0.4.0", cfg.Complypacks[0].URL) +} + func TestLoadFrom_WithoutComplypacks(t *testing.T) { dir := t.TempDir() configPath := filepath.Join(dir, "complytime.yml") diff --git a/internal/doctor/doctor.go b/internal/doctor/doctor.go index 59b0b5a6..035174b4 100644 --- a/internal/doctor/doctor.go +++ b/internal/doctor/doctor.go @@ -241,7 +241,16 @@ func CheckPolicyVersions(cfg *complytime.WorkspaceConfig, cacheDir string, versi var results []CheckResult for _, p := range cfg.Policies { - ref := complytime.ParsePolicyRef(p.URL) + ref, err := complytime.ParsePolicyRef(p.URL) + if err != nil { + results = append(results, CheckResult{ + Name: fmt.Sprintf("policy/%s", p.EffectiveID()), + Status: StatusFail, + Message: fmt.Sprintf("invalid policy reference: %v", err), + Blocking: true, + }) + continue + } eid := p.EffectiveID() if unreachable[ref.Registry] { @@ -435,7 +444,11 @@ func CheckVariables(cfg *complytime.WorkspaceConfig, healthData []ProviderHealth resolveFailures++ continue } - ref := complytime.ParsePolicyRef(entry.URL) + ref, refErr := complytime.ParsePolicyRef(entry.URL) + if refErr != nil { + resolveFailures++ + continue + } version, err := resolver.ResolveVersion(ref.Repository, ref.Version) if err != nil { resolveFailures++ @@ -588,7 +601,10 @@ func CheckPolicyActivePeriod(cfg *complytime.WorkspaceConfig, resolver PolicyGra var results []CheckResult for _, p := range cfg.Policies { - ref := complytime.ParsePolicyRef(p.URL) + ref, refErr := complytime.ParsePolicyRef(p.URL) + if refErr != nil { + continue + } eid := p.EffectiveID() version, err := resolver.ResolveVersion(ref.Repository, ref.Version) @@ -770,7 +786,10 @@ func CheckComplypacks(cfg *complytime.WorkspaceConfig, cacheDir string, resolver // following the same resolution pattern as CheckVariables. evaluatorIDs := make(map[string]bool) for _, p := range cfg.Policies { - ref := complytime.ParsePolicyRef(p.URL) + ref, refErr := complytime.ParsePolicyRef(p.URL) + if refErr != nil { + continue + } version, err := resolver.ResolveVersion(ref.Repository, ref.Version) if err != nil { continue diff --git a/openspec/changes/fix-complypack-oci-ref/.openspec.yaml b/openspec/changes/fix-complypack-oci-ref/.openspec.yaml new file mode 100644 index 00000000..95ae5a2c --- /dev/null +++ b/openspec/changes/fix-complypack-oci-ref/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-06-18 diff --git a/openspec/changes/fix-complypack-oci-ref/design.md b/openspec/changes/fix-complypack-oci-ref/design.md new file mode 100644 index 00000000..b6a0dc27 --- /dev/null +++ b/openspec/changes/fix-complypack-oci-ref/design.md @@ -0,0 +1,55 @@ +## Context + +`ParsePolicyRef` is a hand-rolled OCI reference parser in `internal/complytime/config.go` used by 10 production call sites across `cmd/` and `internal/`. It only recognizes `@` as a version separator, missing the standard `:tag` syntax. The codebase already vendors `oras.land/oras-go/v2` (v2.6.1) which includes `registry.ParseReference` — a robust parser that handles all four OCI reference forms (`:tag`, `@digest`, `:tag@digest`, and bare). + +The downstream sync functions (`SyncPolicy`, `SyncComplypack`) construct `lookupRef` by concatenating `repository + ":" + version`, which produces invalid double-tagged references when the tag was not stripped from the repository during parsing. + +## Goals / Non-Goals + +**Goals:** +- Support `:tag`, `@digest`, and bare OCI references in both `policies` and `complypacks` config entries +- Return validation errors for malformed OCI references +- Fail fast at config load time with clear error messages +- Maintain backwards compatibility with existing `@version` notation and bare policy IDs + +**Non-Goals:** +- Distinguishing digest from tag at the `PolicyRef` struct level (keep single `Version` field) +- Changing `EffectiveID()` signature +- Refactoring the sync functions beyond the `lookupRef` construction fix +- Supporting `registry/repo:tag@digest` form (oras-go silently drops the tag in this case, which is acceptable) + +## Decisions + +### 1. Delegate to oras-go `registry.ParseReference` for OCI refs + +**Decision:** Strip any URL scheme prefix, then delegate to `registry.ParseReference` for inputs containing a `/`. For bare IDs (no `/`), handle directly as repository-only refs. + +**Rationale:** oras-go's parser is battle-tested, handles all four OCI forms correctly, and is already vendored. Reimplementing this logic is the source of the current bug. + +**Alternatives considered:** +- Fix the hand-rolled parser to also handle `:tag` — viable but fragile, duplicates logic that oras-go already provides. +- Replace `PolicyRef` with oras-go's `Reference` directly — too invasive, loses the scheme handling and bare-ID support that `ParsePolicyRef` provides. + +### 2. Keep `Version` field as-is (Option 3 from exploration) + +**Decision:** Map oras-go's `Reference.Reference` field directly to `PolicyRef.Version`. Callers that construct OCI references check `strings.HasPrefix(version, "sha256:")` to decide between `@` and `:` separators. + +**Rationale:** Minimal struct change, no new fields. The digest-vs-tag distinction only matters in two places (the sync `lookupRef` construction), and a prefix check is clear and sufficient. + +### 3. Validate at `LoadFrom` time + +**Decision:** After unmarshaling `complytime.yaml`, iterate all `Policies` and `Complypacks` entries and call `ParsePolicyRef` on each URL. Return an error if any are invalid. + +**Rationale:** Fail fast with a clear error message. Downstream code (including `EffectiveID()`) can then assume valid refs without propagating errors. This avoids changing `EffectiveID()` to return `(string, error)`, which would cascade through many callers. + +### 4. Extract `lookupRef` construction into a helper + +**Decision:** Add a small helper (e.g., `buildLookupRef(repository, version string) string`) that encapsulates the tag-vs-digest logic, used by both `SyncPolicy` and `SyncComplypack`. + +**Rationale:** Both sync functions have identical `lookupRef` construction logic. Centralizing it prevents the digest handling from being duplicated. + +## Risks / Trade-offs + +- **[Bare IDs bypass oras-go validation]** → Bare IDs like `nist-800-53-r5` are intentionally passed through without oras-go validation. These are convention-based identifiers, not OCI refs. If stricter validation is needed later, it can be added separately. +- **[Scheme stripping before oras-go]** → `ParsePolicyRef` strips `http://`/`https://` before passing to oras-go, then reattaches the scheme to `Registry`. If oras-go ever changes how it validates the registry component, this could interact poorly. Mitigation: the scheme handling is straightforward string manipulation with existing test coverage. +- **[`EffectiveID()` trusts pre-validation]** → If `ParsePolicyRef` is called on an unvalidated string outside of config loading, `EffectiveID()` could silently produce wrong IDs. Mitigation: `ParsePolicyRef` returns an error, so any new call site will be prompted by the compiler to handle it. diff --git a/openspec/changes/fix-complypack-oci-ref/proposal.md b/openspec/changes/fix-complypack-oci-ref/proposal.md new file mode 100644 index 00000000..41e06405 --- /dev/null +++ b/openspec/changes/fix-complypack-oci-ref/proposal.md @@ -0,0 +1,31 @@ +## Why + +`ParsePolicyRef` in `internal/complytime/config.go` only recognizes `@` as a version separator, not the standard OCI `:tag` syntax. When a complypack (or policy) URL uses `:v0.4.0`, the tag is left embedded in the `Repository` field. Downstream sync functions then append `":" + version`, producing a double-tagged reference like `org/pack:v0.4.0:v0.4.0` which fails as an invalid OCI reference. The `:latest` tag works only by accident due to a guard that skips appending when the version is `"latest"`. This is tracked in [issue #594](https://github.com/complytime/complyctl/issues/594). + +## What Changes + +- Rewrite `ParsePolicyRef` to delegate OCI reference parsing to the vendored `oras.land/oras-go/v2/registry.ParseReference`, which correctly handles `:tag`, `@digest`, and bare references. +- Change `ParsePolicyRef` signature from `ParsePolicyRef(raw string) PolicyRef` to `ParsePolicyRef(raw string) (PolicyRef, error)`, returning validation errors for malformed OCI references. +- Add digest-aware reference construction in `SyncPolicy` and `SyncComplypack` so digest versions use `@` instead of `:` when building `lookupRef`. +- Add config-time validation in `LoadFrom` to fail fast on invalid policy/complypack URLs with clear error messages. +- Update all 10 production callers of `ParsePolicyRef` to handle the new error return. + +## Capabilities + +### New Capabilities +- `oci-ref-parsing`: Standard OCI reference parsing for policy and complypack URLs, supporting `:tag`, `@digest`, and bare repository forms with validation. + +### Modified Capabilities + +## Impact + +- `internal/complytime/config.go` — `ParsePolicyRef` signature and implementation change; `LoadFrom` gains validation loop +- `internal/complytime/config_test.go` — existing tests updated for error return; new test cases for `:tag` and `@digest` +- `internal/cache/sync.go` — `lookupRef` construction handles digest references +- `internal/cache/complypack_sync.go` — `lookupRef` construction handles digest references +- `cmd/complyctl/cli/get.go` — 2 call sites handle `ParsePolicyRef` error +- `cmd/complyctl/cli/scan.go` — 1 call site handles `ParsePolicyRef` error +- `cmd/complyctl/cli/list.go` — 1 call site handles `ParsePolicyRef` error +- `cmd/complyctl/cli/generate.go` — 1 call site handles `ParsePolicyRef` error +- `internal/doctor/doctor.go` — 4 call sites handle `ParsePolicyRef` error +- `EffectiveID()` is unchanged — config validation at load time ensures it never receives invalid refs diff --git a/openspec/changes/fix-complypack-oci-ref/specs/oci-ref-parsing/spec.md b/openspec/changes/fix-complypack-oci-ref/specs/oci-ref-parsing/spec.md new file mode 100644 index 00000000..2074fabc --- /dev/null +++ b/openspec/changes/fix-complypack-oci-ref/specs/oci-ref-parsing/spec.md @@ -0,0 +1,88 @@ +## ADDED Requirements + +### Requirement: ParsePolicyRef supports colon-tag syntax +`ParsePolicyRef` SHALL parse OCI references using `:tag` syntax (e.g., `registry.com/org/image:v0.4.0`) and populate `PolicyRef.Version` with the tag value and `PolicyRef.Repository` with the repository path excluding the tag. + +#### Scenario: Reference with colon tag +- **WHEN** `ParsePolicyRef` is called with `"quay.io/complytime/complypack-ampel-bp:v0.4.0"` +- **THEN** `PolicyRef.Registry` SHALL be `"quay.io"`, `PolicyRef.Repository` SHALL be `"complytime/complypack-ampel-bp"`, and `PolicyRef.Version` SHALL be `"v0.4.0"` + +#### Scenario: Reference with latest tag +- **WHEN** `ParsePolicyRef` is called with `"quay.io/complytime/complypack-ampel-bp:latest"` +- **THEN** `PolicyRef.Registry` SHALL be `"quay.io"`, `PolicyRef.Repository` SHALL be `"complytime/complypack-ampel-bp"`, and `PolicyRef.Version` SHALL be `"latest"` + +### Requirement: ParsePolicyRef supports digest syntax +`ParsePolicyRef` SHALL parse OCI references using `@digest` syntax (e.g., `registry.com/org/image@sha256:abc123`) and populate `PolicyRef.Version` with the digest value. + +#### Scenario: Reference with SHA256 digest +- **WHEN** `ParsePolicyRef` is called with `"quay.io/complytime/complypack-ampel-bp@sha256:abc123def456"` +- **THEN** `PolicyRef.Registry` SHALL be `"quay.io"`, `PolicyRef.Repository` SHALL be `"complytime/complypack-ampel-bp"`, and `PolicyRef.Version` SHALL be `"sha256:abc123def456"` + +### Requirement: ParsePolicyRef supports bare references +`ParsePolicyRef` SHALL parse bare OCI references with no tag or digest and leave `PolicyRef.Version` empty. + +#### Scenario: Reference without tag or digest +- **WHEN** `ParsePolicyRef` is called with `"quay.io/complytime/complypack-ampel-bp"` +- **THEN** `PolicyRef.Registry` SHALL be `"quay.io"`, `PolicyRef.Repository` SHALL be `"complytime/complypack-ampel-bp"`, and `PolicyRef.Version` SHALL be `""` + +### Requirement: ParsePolicyRef supports bare policy IDs +`ParsePolicyRef` SHALL accept bare policy identifiers with no registry or path separator and populate only `PolicyRef.Repository`. + +#### Scenario: Bare policy ID without version +- **WHEN** `ParsePolicyRef` is called with `"nist-800-53-r5"` +- **THEN** `PolicyRef.Registry` SHALL be `""`, `PolicyRef.Repository` SHALL be `"nist-800-53-r5"`, and `PolicyRef.Version` SHALL be `""` + +#### Scenario: Bare policy ID with at-version +- **WHEN** `ParsePolicyRef` is called with `"nist-800-53-r5@v1.0"` +- **THEN** `PolicyRef.Registry` SHALL be `""`, `PolicyRef.Repository` SHALL be `"nist-800-53-r5"`, and `PolicyRef.Version` SHALL be `"v1.0"` + +### Requirement: ParsePolicyRef supports URL scheme prefixes +`ParsePolicyRef` SHALL strip `http://` and `https://` scheme prefixes and include them in `PolicyRef.Registry`. + +#### Scenario: HTTP scheme with port +- **WHEN** `ParsePolicyRef` is called with `"http://localhost:5000/policies/test:v1.0"` +- **THEN** `PolicyRef.Registry` SHALL be `"http://localhost:5000"`, `PolicyRef.Repository` SHALL be `"policies/test"`, and `PolicyRef.Version` SHALL be `"v1.0"` + +#### Scenario: HTTPS scheme with tag +- **WHEN** `ParsePolicyRef` is called with `"https://ghcr.io/org/policy:v2.0"` +- **THEN** `PolicyRef.Registry` SHALL be `"https://ghcr.io"`, `PolicyRef.Repository` SHALL be `"org/policy"`, and `PolicyRef.Version` SHALL be `"v2.0"` + +### Requirement: ParsePolicyRef returns error for invalid references +`ParsePolicyRef` SHALL return an error for OCI references that are structurally invalid. + +#### Scenario: Empty input +- **WHEN** `ParsePolicyRef` is called with `""` +- **THEN** it SHALL return a non-nil error + +#### Scenario: Whitespace-only input +- **WHEN** `ParsePolicyRef` is called with `" "` +- **THEN** it SHALL return a non-nil error + +### Requirement: ParsePolicyRef preserves at-version backwards compatibility +`ParsePolicyRef` SHALL continue to support the existing `@version` notation used in policy references. + +#### Scenario: Full reference with at-version +- **WHEN** `ParsePolicyRef` is called with `"registry.com/policies/nist-800-53-r5@v1.2.3"` +- **THEN** `PolicyRef.Registry` SHALL be `"registry.com"`, `PolicyRef.Repository` SHALL be `"policies/nist-800-53-r5"`, and `PolicyRef.Version` SHALL be `"v1.2.3"` + +### Requirement: Sync functions construct valid lookup references for digests +`SyncPolicy` and `SyncComplypack` SHALL use `@` as the separator when constructing lookup references for digest versions and `:` for tag versions. + +#### Scenario: Lookup reference with tag version +- **WHEN** `SyncPolicy` or `SyncComplypack` is called with `repository="org/policy"` and `version="v1.0"` +- **THEN** the lookup reference SHALL be `"org/policy:v1.0"` + +#### Scenario: Lookup reference with digest version +- **WHEN** `SyncPolicy` or `SyncComplypack` is called with `repository="org/policy"` and `version="sha256:abc123"` +- **THEN** the lookup reference SHALL be `"org/policy@sha256:abc123"` + +### Requirement: Config loading validates OCI references +`LoadFrom` SHALL validate all policy and complypack URLs after loading `complytime.yaml` and return an error if any are structurally invalid. + +#### Scenario: Invalid policy URL in config +- **WHEN** `LoadFrom` loads a `complytime.yaml` containing a policy with an invalid OCI reference +- **THEN** it SHALL return an error identifying the invalid entry + +#### Scenario: Valid config with mixed reference styles +- **WHEN** `LoadFrom` loads a `complytime.yaml` containing policies using `:tag`, `@version`, and bare references +- **THEN** it SHALL succeed without error diff --git a/openspec/changes/fix-complypack-oci-ref/tasks.md b/openspec/changes/fix-complypack-oci-ref/tasks.md new file mode 100644 index 00000000..f9cc2f7f --- /dev/null +++ b/openspec/changes/fix-complypack-oci-ref/tasks.md @@ -0,0 +1,27 @@ +## 1. ParsePolicyRef Rewrite + +- [x] 1.1 Rewrite `ParsePolicyRef` in `internal/complytime/config.go` to delegate to `oras.land/oras-go/v2/registry.ParseReference` for inputs containing `/`, handling scheme stripping and bare IDs separately. Change signature to `(PolicyRef, error)`. +- [x] 1.2 Update `internal/complytime/config_test.go`: update existing 7 test cases for `(PolicyRef, error)` return and add new cases for `:tag`, `@sha256:digest`, empty input, and whitespace-only input. + +## 2. Config Load Validation + +- [x] 2.1 Add a validation loop in `LoadFrom` (`internal/complytime/config.go`) after YAML unmarshal to call `ParsePolicyRef` on all `Policies` and `Complypacks` entry URLs, returning an error on the first invalid reference. + +## 3. Sync Lookup Reference Fix + +- [x] 3.1 Extract a `buildLookupRef(repository, version string) string` helper (in `internal/cache/` or alongside the sync functions) that uses `@` for digest versions and `:` for tag versions. +- [x] 3.2 Update `SyncPolicy` in `internal/cache/sync.go` to use `buildLookupRef`. +- [x] 3.3 Update `SyncComplypack` in `internal/cache/complypack_sync.go` to use `buildLookupRef`. + +## 4. Caller Updates + +- [x] 4.1 Update `syncSinglePolicy` and `syncSingleComplypack` in `cmd/complyctl/cli/get.go` to handle `ParsePolicyRef` error return. +- [x] 4.2 Update `scanOptions.scanPolicy` in `cmd/complyctl/cli/scan.go` to handle `ParsePolicyRef` error return. +- [x] 4.3 Update list command in `cmd/complyctl/cli/list.go` to handle `ParsePolicyRef` error return. +- [x] 4.4 Update `generateOptions.generatePolicy` in `cmd/complyctl/cli/generate.go` to handle `ParsePolicyRef` error return. +- [x] 4.5 Update 4 call sites in `internal/doctor/doctor.go` (`CheckPolicyStaleness`, `CheckVariables`, `CheckPolicyExpiry`, `CheckComplypackCache`) to handle `ParsePolicyRef` error return. + +## 5. Verification + +- [x] 5.1 Run `make test-unit` and confirm all tests pass. +- [x] 5.2 Run `make lint` and confirm no lint errors. From daf86fd651fbb6ca12c0d8ec03c04af781c7cf08 Mon Sep 17 00:00:00 2001 From: Hannah Braswell Date: Thu, 18 Jun 2026 12:16:38 -0400 Subject: [PATCH 2/2] fix(ci): regenerate CRAP baseline and normalize recommended_actions paths Regenerate .gaze/baseline.json to reflect current CRAP scores after OCI ref parsing changes. The previous baseline was stale, causing 11 false regressions in CI. Also add .summary.recommended_actions[]? to the jq path-normalization filter in the crapload-baseline and crapload-check Makefile targets, preventing absolute paths from leaking into the committed baseline. --- .gaze/baseline.json | 553 ++++++++++++++++++++++---------------------- Makefile | 4 +- 2 files changed, 284 insertions(+), 273 deletions(-) diff --git a/.gaze/baseline.json b/.gaze/baseline.json index 0c8e25eb..0a5e6771 100644 --- a/.gaze/baseline.json +++ b/.gaze/baseline.json @@ -154,15 +154,15 @@ "function": "(*generateOptions).generatePolicy", "file": "cmd/complyctl/cli/generate.go", "line": 95, - "complexity": 4, - "line_coverage": 29.41176470588235, - "crap": 9.627518827600243 + "complexity": 5, + "line_coverage": 31.57894736842105, + "crap": 13.007727073917481 }, { "package": "cli", "function": "invokeGenerate", "file": "cmd/complyctl/cli/generate.go", - "line": 123, + "line": 126, "complexity": 2, "line_coverage": 0, "crap": 6 @@ -171,7 +171,7 @@ "package": "cli", "function": "buildExecutionPlan", "file": "cmd/complyctl/cli/generate.go", - "line": 137, + "line": 140, "complexity": 3, "line_coverage": 0, "crap": 12 @@ -180,7 +180,7 @@ "package": "cli", "function": "providerStatus", "file": "cmd/complyctl/cli/generate.go", - "line": 155, + "line": 158, "complexity": 2, "line_coverage": 0, "crap": 6 @@ -189,7 +189,7 @@ "package": "cli", "function": "saveGenerationAndPrint", "file": "cmd/complyctl/cli/generate.go", - "line": 162, + "line": 165, "complexity": 3, "line_coverage": 0, "crap": 12 @@ -272,15 +272,15 @@ "function": "syncSinglePolicy", "file": "cmd/complyctl/cli/get.go", "line": 153, - "complexity": 3, - "line_coverage": 75, - "crap": 3.140625 + "complexity": 4, + "line_coverage": 72.22222222222223, + "crap": 4.342935528120713 }, { "package": "cli", "function": "resolveLatestVersion", "file": "cmd/complyctl/cli/get.go", - "line": 177, + "line": 180, "complexity": 2, "line_coverage": 0, "crap": 6 @@ -289,7 +289,7 @@ "package": "cli", "function": "syncAllComplypacks", "file": "cmd/complyctl/cli/get.go", - "line": 189, + "line": 192, "complexity": 3, "line_coverage": 0, "crap": 12 @@ -298,10 +298,10 @@ "package": "cli", "function": "syncSingleComplypack", "file": "cmd/complyctl/cli/get.go", - "line": 204, - "complexity": 4, + "line": 207, + "complexity": 5, "line_coverage": 0, - "crap": 20, + "crap": 30, "fix_strategy": "add_tests" }, { @@ -383,15 +383,15 @@ "function": "(*listOptions).run", "file": "cmd/complyctl/cli/list.go", "line": 69, - "complexity": 7, - "line_coverage": 86.95652173913044, - "crap": 7.10873674693844 + "complexity": 8, + "line_coverage": 84, + "crap": 8.262144000000001 }, { "package": "cli", "function": "printGemaraPolicyTable", "file": "cmd/complyctl/cli/list.go", - "line": 108, + "line": 111, "complexity": 1, "line_coverage": 80, "crap": 1.008 @@ -554,7 +554,7 @@ "package": "cli", "function": "scanCmd", "file": "cmd/complyctl/cli/scan.go", - "line": 45, + "line": 44, "complexity": 6, "line_coverage": 42.10526315789474, "crap": 12.985857996792536 @@ -563,7 +563,7 @@ "package": "cli", "function": "completeTargetIDs", "file": "cmd/complyctl/cli/scan.go", - "line": 111, + "line": 110, "complexity": 5, "line_coverage": 91.66666666666667, "crap": 5.014467592592593 @@ -572,7 +572,7 @@ "package": "cli", "function": "(*scanOptions).validate", "file": "cmd/complyctl/cli/scan.go", - "line": 131, + "line": 130, "complexity": 3, "line_coverage": 100, "crap": 3 @@ -581,7 +581,7 @@ "package": "cli", "function": "(*scanOptions).complete", "file": "cmd/complyctl/cli/scan.go", - "line": 143, + "line": 142, "complexity": 3, "line_coverage": 0, "crap": 12 @@ -590,7 +590,7 @@ "package": "cli", "function": "(*scanOptions).run", "file": "cmd/complyctl/cli/scan.go", - "line": 156, + "line": 155, "complexity": 8, "line_coverage": 100, "crap": 8 @@ -599,7 +599,7 @@ "package": "cli", "function": "resolveTarget", "file": "cmd/complyctl/cli/scan.go", - "line": 198, + "line": 197, "complexity": 4, "line_coverage": 100, "crap": 4 @@ -608,7 +608,7 @@ "package": "cli", "function": "resolvePolicy", "file": "cmd/complyctl/cli/scan.go", - "line": 217, + "line": 216, "complexity": 9, "line_coverage": 100, "crap": 9 @@ -617,7 +617,7 @@ "package": "cli", "function": "loadWorkspaceConfig", "file": "cmd/complyctl/cli/scan.go", - "line": 247, + "line": 246, "complexity": 2, "line_coverage": 100, "crap": 2 @@ -626,17 +626,17 @@ "package": "cli", "function": "(*scanOptions).scanPolicy", "file": "cmd/complyctl/cli/scan.go", - "line": 255, - "complexity": 5, - "line_coverage": 23.80952380952381, - "crap": 16.05712126120289, + "line": 254, + "complexity": 6, + "line_coverage": 26.08695652173913, + "crap": 20.536697624722606, "fix_strategy": "add_tests" }, { "package": "cli", "function": "(*scanOptions).executeScanPhase", "file": "cmd/complyctl/cli/scan.go", - "line": 295, + "line": 297, "complexity": 2, "line_coverage": 0, "crap": 6 @@ -645,7 +645,7 @@ "package": "cli", "function": "(*scanOptions).maybeExport", "file": "cmd/complyctl/cli/scan.go", - "line": 303, + "line": 305, "complexity": 3, "line_coverage": 100, "crap": 3 @@ -654,7 +654,7 @@ "package": "cli", "function": "resolveVersionAndGraph", "file": "cmd/complyctl/cli/scan.go", - "line": 316, + "line": 318, "complexity": 3, "line_coverage": 60, "crap": 3.576 @@ -663,7 +663,7 @@ "package": "cli", "function": "loadProviders", "file": "cmd/complyctl/cli/scan.go", - "line": 333, + "line": 335, "complexity": 4, "line_coverage": 0, "crap": 20, @@ -673,7 +673,7 @@ "package": "cli", "function": "evaluatorIDList", "file": "cmd/complyctl/cli/scan.go", - "line": 349, + "line": 351, "complexity": 2, "line_coverage": 0, "crap": 6 @@ -682,7 +682,7 @@ "package": "cli", "function": "targetIDList", "file": "cmd/complyctl/cli/scan.go", - "line": 357, + "line": 359, "complexity": 2, "line_coverage": 0, "crap": 6 @@ -691,7 +691,7 @@ "package": "cli", "function": "ensureGenerated", "file": "cmd/complyctl/cli/scan.go", - "line": 365, + "line": 367, "complexity": 3, "line_coverage": 0, "crap": 12 @@ -700,7 +700,7 @@ "package": "cli", "function": "runScanAndReport", "file": "cmd/complyctl/cli/scan.go", - "line": 380, + "line": 382, "complexity": 2, "line_coverage": 0, "crap": 6 @@ -709,7 +709,7 @@ "package": "cli", "function": "processScanOutput", "file": "cmd/complyctl/cli/scan.go", - "line": 402, + "line": 403, "complexity": 3, "line_coverage": 87.5, "crap": 3.017578125 @@ -718,7 +718,7 @@ "package": "cli", "function": "reportOperationalWarnings", "file": "cmd/complyctl/cli/scan.go", - "line": 420, + "line": 421, "complexity": 2, "line_coverage": 100, "crap": 2 @@ -727,7 +727,7 @@ "package": "cli", "function": "checkOperationalErrors", "file": "cmd/complyctl/cli/scan.go", - "line": 429, + "line": 430, "complexity": 3, "line_coverage": 100, "crap": 3 @@ -736,7 +736,7 @@ "package": "cli", "function": "buildEvaluators", "file": "cmd/complyctl/cli/scan.go", - "line": 440, + "line": 441, "complexity": 4, "line_coverage": 100, "crap": 4 @@ -745,7 +745,7 @@ "package": "cli", "function": "filterTargetByID", "file": "cmd/complyctl/cli/scan.go", - "line": 459, + "line": 460, "complexity": 3, "line_coverage": 100, "crap": 3 @@ -754,7 +754,7 @@ "package": "cli", "function": "filterTargetsForPolicy", "file": "cmd/complyctl/cli/scan.go", - "line": 468, + "line": 469, "complexity": 3, "line_coverage": 100, "crap": 3 @@ -763,7 +763,7 @@ "package": "cli", "function": "checkGenerationFreshness", "file": "cmd/complyctl/cli/scan.go", - "line": 478, + "line": 479, "complexity": 3, "line_coverage": 0, "crap": 12 @@ -772,7 +772,7 @@ "package": "cli", "function": "needsRegeneration", "file": "cmd/complyctl/cli/scan.go", - "line": 493, + "line": 494, "complexity": 4, "line_coverage": 100, "crap": 4 @@ -781,7 +781,7 @@ "package": "cli", "function": "evaluatorArtifactsExist", "file": "cmd/complyctl/cli/scan.go", - "line": 510, + "line": 511, "complexity": 6, "line_coverage": 100, "crap": 6 @@ -790,7 +790,7 @@ "package": "cli", "function": "runGeneration", "file": "cmd/complyctl/cli/scan.go", - "line": 525, + "line": 526, "complexity": 3, "line_coverage": 0, "crap": 12 @@ -799,7 +799,7 @@ "package": "cli", "function": "generateForAllTargets", "file": "cmd/complyctl/cli/scan.go", - "line": 541, + "line": 542, "complexity": 5, "line_coverage": 36.36363636363637, "crap": 11.442524417731029 @@ -808,7 +808,7 @@ "package": "cli", "function": "executeScan", "file": "cmd/complyctl/cli/scan.go", - "line": 561, + "line": 562, "complexity": 1, "line_coverage": 0, "crap": 2 @@ -817,7 +817,7 @@ "package": "cli", "function": "scanAllTargets", "file": "cmd/complyctl/cli/scan.go", - "line": 569, + "line": 570, "complexity": 4, "line_coverage": 0, "crap": 20, @@ -827,7 +827,7 @@ "package": "cli", "function": "scanSingleTarget", "file": "cmd/complyctl/cli/scan.go", - "line": 595, + "line": 596, "complexity": 3, "line_coverage": 0, "crap": 12 @@ -836,7 +836,7 @@ "package": "cli", "function": "writeScanReports", "file": "cmd/complyctl/cli/scan.go", - "line": 614, + "line": 615, "complexity": 3, "line_coverage": 71.42857142857143, "crap": 3.2099125364431487 @@ -845,7 +845,7 @@ "package": "cli", "function": "writeFormatReport", "file": "cmd/complyctl/cli/scan.go", - "line": 628, + "line": 629, "complexity": 4, "line_coverage": 40, "crap": 7.4559999999999995 @@ -854,7 +854,7 @@ "package": "cli", "function": "writePrettyReport", "file": "cmd/complyctl/cli/scan.go", - "line": 640, + "line": 641, "complexity": 2, "line_coverage": 0, "crap": 6 @@ -863,7 +863,7 @@ "package": "cli", "function": "writeSARIFReport", "file": "cmd/complyctl/cli/scan.go", - "line": 651, + "line": 652, "complexity": 2, "line_coverage": 0, "crap": 6 @@ -872,7 +872,7 @@ "package": "cli", "function": "writeOSCALReport", "file": "cmd/complyctl/cli/scan.go", - "line": 660, + "line": 661, "complexity": 2, "line_coverage": 0, "crap": 6 @@ -881,7 +881,7 @@ "package": "cli", "function": "(*scanOptions).runExport", "file": "cmd/complyctl/cli/scan.go", - "line": 671, + "line": 672, "complexity": 5, "line_coverage": 16.666666666666668, "crap": 19.467592592592588, @@ -891,7 +891,7 @@ "package": "cli", "function": "authRequired", "file": "cmd/complyctl/cli/scan.go", - "line": 697, + "line": 698, "complexity": 2, "line_coverage": 100, "crap": 2 @@ -900,7 +900,7 @@ "package": "cli", "function": "validateAuthCredentials", "file": "cmd/complyctl/cli/scan.go", - "line": 701, + "line": 702, "complexity": 3, "line_coverage": 100, "crap": 3 @@ -909,7 +909,7 @@ "package": "cli", "function": "resolveCollectorAuth", "file": "cmd/complyctl/cli/scan.go", - "line": 708, + "line": 709, "complexity": 4, "line_coverage": 0, "crap": 20, @@ -919,7 +919,7 @@ "package": "cli", "function": "exportToProviders", "file": "cmd/complyctl/cli/scan.go", - "line": 723, + "line": 724, "complexity": 2, "line_coverage": 0, "crap": 6 @@ -928,7 +928,7 @@ "package": "cli", "function": "exportSingleProvider", "file": "cmd/complyctl/cli/scan.go", - "line": 731, + "line": 732, "complexity": 3, "line_coverage": 0, "crap": 12 @@ -937,7 +937,7 @@ "package": "cli", "function": "resolveOIDCToken", "file": "cmd/complyctl/cli/scan.go", - "line": 748, + "line": 749, "complexity": 2, "line_coverage": 0, "crap": 6 @@ -946,7 +946,7 @@ "package": "cli", "function": "formatExportSummary", "file": "cmd/complyctl/cli/scan.go", - "line": 761, + "line": 762, "complexity": 4, "line_coverage": 100, "crap": 4 @@ -955,7 +955,7 @@ "package": "cli", "function": "appendExportRow", "file": "cmd/complyctl/cli/scan.go", - "line": 781, + "line": 782, "complexity": 3, "line_coverage": 100, "crap": 3 @@ -964,7 +964,7 @@ "package": "cli", "function": "appendResponseRow", "file": "cmd/complyctl/cli/scan.go", - "line": 795, + "line": 796, "complexity": 3, "line_coverage": 85.71428571428571, "crap": 3.0262390670553936 @@ -973,7 +973,7 @@ "package": "cli", "function": "exportResponseStatus", "file": "cmd/complyctl/cli/scan.go", - "line": 808, + "line": 809, "complexity": 4, "line_coverage": 100, "crap": 4 @@ -982,7 +982,7 @@ "package": "cli", "function": "countExportFailures", "file": "cmd/complyctl/cli/scan.go", - "line": 830, + "line": 831, "complexity": 7, "line_coverage": 100, "crap": 7 @@ -991,7 +991,7 @@ "package": "cli", "function": "injectWorkspaceIntoTargets", "file": "cmd/complyctl/cli/scan.go", - "line": 851, + "line": 852, "complexity": 2, "line_coverage": 100, "crap": 2 @@ -1000,7 +1000,7 @@ "package": "cli", "function": "extractReqToControlMap", "file": "cmd/complyctl/cli/scan.go", - "line": 862, + "line": 863, "complexity": 6, "line_coverage": 90, "crap": 6.036 @@ -1009,7 +1009,7 @@ "package": "cli", "function": "extractPlanToReqMap", "file": "cmd/complyctl/cli/scan.go", - "line": 883, + "line": 884, "complexity": 4, "line_coverage": 100, "crap": 4 @@ -1018,7 +1018,7 @@ "package": "cli", "function": "resolveAssessmentIDs", "file": "cmd/complyctl/cli/scan.go", - "line": 898, + "line": 899, "complexity": 3, "line_coverage": 100, "crap": 3 @@ -1027,28 +1027,19 @@ "package": "cli", "function": "reverseMap", "file": "cmd/complyctl/cli/scan.go", - "line": 908, + "line": 909, "complexity": 3, "line_coverage": 83.33333333333333, "crap": 3.0416666666666665 }, { "package": "cli", - "function": "buildComplypackRefs", + "function": "buildReqToComplypackRef", "file": "cmd/complyctl/cli/scan.go", - "line": 923, - "complexity": 6, - "line_coverage": 63.63636363636363, - "crap": 7.7310293012772355 - }, - { - "package": "cli", - "function": "extractReqToEvaluator", - "file": "cmd/complyctl/cli/scan.go", - "line": 944, - "complexity": 3, - "line_coverage": 100, - "crap": 3 + "line": 930, + "complexity": 11, + "line_coverage": 82.6086956521739, + "crap": 11.636475712994166 }, { "package": "cli", @@ -1596,13 +1587,12 @@ "function": "(*ComplypackSync).SyncComplypack", "file": "internal/cache/complypack_sync.go", "line": 42, - "complexity": 15, - "line_coverage": 85.29411764705883, - "crap": 15.715576022796661, + "complexity": 13, + "line_coverage": 84.375, + "crap": 13.644683837890625, "contract_coverage": 100, - "gaze_crap": 15, - "quadrant": "Q4_Dangerous", - "fix_strategy": "decompose" + "gaze_crap": 13, + "quadrant": "Q1_Safe" }, { "package": "cache", @@ -1712,11 +1702,23 @@ "gaze_crap": 2, "quadrant": "Q1_Safe" }, + { + "package": "cache", + "function": "BuildLookupRef", + "file": "internal/cache/sync.go", + "line": 18, + "complexity": 5, + "line_coverage": 100, + "crap": 5, + "contract_coverage": 100, + "gaze_crap": 5, + "quadrant": "Q1_Safe" + }, { "package": "cache", "function": "NewSync", "file": "internal/cache/sync.go", - "line": 20, + "line": 35, "complexity": 1, "line_coverage": 100, "crap": 1, @@ -1728,19 +1730,19 @@ "package": "cache", "function": "(*Sync).SyncPolicy", "file": "internal/cache/sync.go", - "line": 31, - "complexity": 14, - "line_coverage": 84, - "crap": 14.802816, + "line": 46, + "complexity": 12, + "line_coverage": 82.6086956521739, + "crap": 12.757458699761651, "contract_coverage": 100, - "gaze_crap": 14, + "gaze_crap": 12, "quadrant": "Q1_Safe" }, { "package": "complytime", "function": "ValidateOCIRef", "file": "internal/complytime/config.go", - "line": 23, + "line": 24, "complexity": 6, "line_coverage": 100, "crap": 6 @@ -1749,7 +1751,7 @@ "package": "complytime", "function": "(PolicyEntry).EffectiveID", "file": "internal/complytime/config.go", - "line": 84, + "line": 85, "complexity": 2, "line_coverage": 100, "crap": 2 @@ -1758,16 +1760,16 @@ "package": "complytime", "function": "ParsePolicyRef", "file": "internal/complytime/config.go", - "line": 112, - "complexity": 8, - "line_coverage": 100, - "crap": 8 + "line": 134, + "complexity": 12, + "line_coverage": 96.42857142857143, + "crap": 12.006559766763848 }, { "package": "complytime", "function": "FindPolicy", "file": "internal/complytime/config.go", - "line": 142, + "line": 189, "complexity": 3, "line_coverage": 100, "crap": 3 @@ -1776,7 +1778,7 @@ "package": "complytime", "function": "PolicyIDs", "file": "internal/complytime/config.go", - "line": 155, + "line": 202, "complexity": 2, "line_coverage": 100, "crap": 2 @@ -1785,19 +1787,28 @@ "package": "complytime", "function": "LoadFrom", "file": "internal/complytime/config.go", - "line": 165, - "complexity": 5, - "line_coverage": 63.63636363636363, - "crap": 6.202103681442525, + "line": 212, + "complexity": 6, + "line_coverage": 69.23076923076923, + "crap": 7.048702776513427, "contract_coverage": 50, - "gaze_crap": 8.125, + "gaze_crap": 10.5, "quadrant": "Q1_Safe" }, + { + "package": "complytime", + "function": "validatePolicyRefs", + "file": "internal/complytime/config.go", + "line": 248, + "complexity": 5, + "line_coverage": 100, + "crap": 5 + }, { "package": "complytime", "function": "resolveEnvVars", "file": "internal/complytime/config.go", - "line": 195, + "line": 265, "complexity": 4, "line_coverage": 100, "crap": 4 @@ -1806,7 +1817,7 @@ "package": "complytime", "function": "resolveCollectorEnvVars", "file": "internal/complytime/config.go", - "line": 208, + "line": 278, "complexity": 5, "line_coverage": 100, "crap": 5 @@ -1815,7 +1826,7 @@ "package": "complytime", "function": "expandEnvRef", "file": "internal/complytime/config.go", - "line": 230, + "line": 300, "complexity": 3, "line_coverage": 100, "crap": 3 @@ -1824,7 +1835,7 @@ "package": "complytime", "function": "SaveTo", "file": "internal/complytime/config.go", - "line": 247, + "line": 317, "complexity": 3, "line_coverage": 0, "crap": 12, @@ -1836,7 +1847,7 @@ "package": "complytime", "function": "Validate", "file": "internal/complytime/config.go", - "line": 261, + "line": 331, "complexity": 13, "line_coverage": 92.3076923076923, "crap": 13.076923076923077 @@ -1845,7 +1856,7 @@ "package": "complytime", "function": "validateEntries", "file": "internal/complytime/config.go", - "line": 313, + "line": 383, "complexity": 6, "line_coverage": 100, "crap": 6 @@ -2095,11 +2106,11 @@ "function": "CheckPolicyVersions", "file": "internal/doctor/doctor.go", "line": 221, - "complexity": 15, - "line_coverage": 100, - "crap": 15, + "complexity": 16, + "line_coverage": 95, + "crap": 16.032, "contract_coverage": 100, - "gaze_crap": 15, + "gaze_crap": 16, "quadrant": "Q4_Dangerous", "fix_strategy": "decompose" }, @@ -2107,7 +2118,7 @@ "package": "doctor", "function": "resolvePinnedFallback", "file": "internal/doctor/doctor.go", - "line": 317, + "line": 326, "complexity": 5, "line_coverage": 100, "crap": 5 @@ -2116,7 +2127,7 @@ "package": "doctor", "function": "CheckCache", "file": "internal/doctor/doctor.go", - "line": 359, + "line": 368, "complexity": 5, "line_coverage": 90.9090909090909, "crap": 5.018782870022539, @@ -2128,12 +2139,12 @@ "package": "doctor", "function": "CheckVariables", "file": "internal/doctor/doctor.go", - "line": 414, - "complexity": 34, - "line_coverage": 87.8048780487805, - "crap": 36.09660335746724, + "line": 423, + "complexity": 35, + "line_coverage": 85.88235294117646, + "crap": 38.44685528190515, "contract_coverage": 100, - "gaze_crap": 34, + "gaze_crap": 35, "quadrant": "Q4_Dangerous", "fix_strategy": "decompose" }, @@ -2141,19 +2152,19 @@ "package": "doctor", "function": "CheckPolicyActivePeriod", "file": "internal/doctor/doctor.go", - "line": 582, - "complexity": 11, - "line_coverage": 92.85714285714286, - "crap": 11.044096209912537, + "line": 595, + "complexity": 12, + "line_coverage": 90, + "crap": 12.144, "contract_coverage": 100, - "gaze_crap": 11, + "gaze_crap": 12, "quadrant": "Q1_Safe" }, { "package": "doctor", "function": "parseDatetime", "file": "internal/doctor/doctor.go", - "line": 655, + "line": 671, "complexity": 3, "line_coverage": 100, "crap": 3 @@ -2162,7 +2173,7 @@ "package": "doctor", "function": "evaluateTimeline", "file": "internal/doctor/doctor.go", - "line": 664, + "line": 680, "complexity": 7, "line_coverage": 86.66666666666667, "crap": 7.116148148148148 @@ -2171,7 +2182,7 @@ "package": "doctor", "function": "countResolved", "file": "internal/doctor/doctor.go", - "line": 694, + "line": 710, "complexity": 3, "line_coverage": 100, "crap": 3 @@ -2180,7 +2191,7 @@ "package": "doctor", "function": "unmappedReason", "file": "internal/doctor/doctor.go", - "line": 704, + "line": 720, "complexity": 3, "line_coverage": 80, "crap": 3.072 @@ -2189,7 +2200,7 @@ "package": "doctor", "function": "CheckCollector", "file": "internal/doctor/doctor.go", - "line": 717, + "line": 733, "complexity": 6, "line_coverage": 100, "crap": 6, @@ -2201,19 +2212,19 @@ "package": "doctor", "function": "CheckComplypacks", "file": "internal/doctor/doctor.go", - "line": 758, - "complexity": 12, - "line_coverage": 81.25, - "crap": 12.94921875, + "line": 774, + "complexity": 13, + "line_coverage": 79.41176470588235, + "crap": 14.474837166700592, "contract_coverage": 100, - "gaze_crap": 12, + "gaze_crap": 13, "quadrant": "Q1_Safe" }, { "package": "doctor", "function": "checkExportEnabled", "file": "internal/doctor/doctor.go", - "line": 829, + "line": 848, "complexity": 4, "line_coverage": 100, "crap": 4 @@ -2222,7 +2233,7 @@ "package": "doctor", "function": "checkCollectorAuth", "file": "internal/doctor/doctor.go", - "line": 855, + "line": 874, "complexity": 4, "line_coverage": 100, "crap": 4 @@ -2231,7 +2242,7 @@ "package": "doctor", "function": "effectiveGlobals", "file": "internal/doctor/doctor.go", - "line": 880, + "line": 899, "complexity": 2, "line_coverage": 100, "crap": 2 @@ -2240,7 +2251,7 @@ "package": "doctor", "function": "joinNames", "file": "internal/doctor/doctor.go", - "line": 889, + "line": 908, "complexity": 4, "line_coverage": 100, "crap": 4 @@ -2249,7 +2260,7 @@ "package": "output", "function": "defaultMap", "file": "internal/output/evaluator.go", - "line": 36, + "line": 35, "complexity": 2, "line_coverage": 100, "crap": 2 @@ -2258,7 +2269,7 @@ "package": "output", "function": "NewEvaluator", "file": "internal/output/evaluator.go", - "line": 51, + "line": 48, "complexity": 1, "line_coverage": 100, "crap": 1, @@ -2270,7 +2281,7 @@ "package": "output", "function": "(*Evaluator).TargetID", "file": "internal/output/evaluator.go", - "line": 65, + "line": 61, "complexity": 1, "line_coverage": 0, "crap": 2 @@ -2279,7 +2290,7 @@ "package": "output", "function": "(*Evaluator).AddTarget", "file": "internal/output/evaluator.go", - "line": 71, + "line": 67, "complexity": 3, "line_coverage": 100, "crap": 3 @@ -2288,7 +2299,7 @@ "package": "output", "function": "(*Evaluator).GemaraLog", "file": "internal/output/evaluator.go", - "line": 101, + "line": 97, "complexity": 3, "line_coverage": 100, "crap": 3 @@ -2297,7 +2308,7 @@ "package": "output", "function": "(*Evaluator).Write", "file": "internal/output/evaluator.go", - "line": 138, + "line": 134, "complexity": 4, "line_coverage": 75, "crap": 4.25 @@ -2306,7 +2317,7 @@ "package": "output", "function": "(*Evaluator).resolveControl", "file": "internal/output/evaluator.go", - "line": 161, + "line": 157, "complexity": 2, "line_coverage": 100, "crap": 2 @@ -2315,7 +2326,7 @@ "package": "output", "function": "(*Evaluator).providerToGemaraAssessment", "file": "internal/output/evaluator.go", - "line": 168, + "line": 164, "complexity": 8, "line_coverage": 100, "crap": 8 @@ -2324,7 +2335,7 @@ "package": "output", "function": "providerStepToGemara", "file": "internal/output/evaluator.go", - "line": 222, + "line": 218, "complexity": 1, "line_coverage": 100, "crap": 1 @@ -2333,7 +2344,7 @@ "package": "output", "function": "providerStepsToGemara", "file": "internal/output/evaluator.go", - "line": 232, + "line": 228, "complexity": 3, "line_coverage": 100, "crap": 3 @@ -2342,7 +2353,7 @@ "package": "output", "function": "providerConfidenceToGemara", "file": "internal/output/evaluator.go", - "line": 243, + "line": 239, "complexity": 5, "line_coverage": 83.33333333333333, "crap": 5.1157407407407405 @@ -2351,7 +2362,7 @@ "package": "output", "function": "formatStepIdentity", "file": "internal/output/evaluator.go", - "line": 295, + "line": 291, "complexity": 3, "line_coverage": 100, "crap": 3 @@ -2360,16 +2371,16 @@ "package": "output", "function": "(*Evaluator).resolveComplypackRef", "file": "internal/output/evaluator.go", - "line": 306, - "complexity": 2, + "line": 300, + "complexity": 1, "line_coverage": 100, - "crap": 2 + "crap": 1 }, { "package": "output", "function": "(*Evaluator).toSerializable", "file": "internal/output/evaluator.go", - "line": 316, + "line": 306, "complexity": 5, "line_coverage": 100, "crap": 5 @@ -3756,25 +3767,25 @@ } ], "summary": { - "total_functions": 387, - "avg_complexity": 3.503875968992248, - "avg_line_coverage": 48.44754991621372, - "avg_crap": 9.967246916782646, - "crapload": 55, + "total_functions": 388, + "avg_complexity": 3.5489690721649483, + "avg_line_coverage": 48.59530700313812, + "avg_crap": 10.036572652333856, + "crapload": 54, "crap_threshold": 15, - "gaze_crapload": 4, + "gaze_crapload": 3, "gaze_crap_threshold": 15, - "avg_gaze_crap": 5.520833333333333, - "avg_contract_coverage": 74.52380952380953, + "avg_gaze_crap": 5.546948356807511, + "avg_contract_coverage": 74.88262910798123, "quadrant_counts": { - "Q1_Safe": 65, + "Q1_Safe": 67, "Q2_ComplexButTested": 1, "Q3_SimpleButUnderspecified": 1, - "Q4_Dangerous": 3 + "Q4_Dangerous": 2 }, "fix_strategy_counts": { "add_tests": 51, - "decompose": 3, + "decompose": 2, "decompose_and_test": 1 }, "worst_crap": [ @@ -3788,16 +3799,6 @@ "crap": 380, "fix_strategy": "decompose_and_test" }, - { - "package": "cli", - "function": "promptTargets", - "file": "cmd/complyctl/cli/init.go", - "line": 153, - "complexity": 10, - "line_coverage": 0, - "crap": 110, - "fix_strategy": "add_tests" - }, { "package": "terminal", "function": "ShowPlainTable", @@ -3818,6 +3819,16 @@ "crap": 110, "fix_strategy": "add_tests" }, + { + "package": "cli", + "function": "promptTargets", + "file": "cmd/complyctl/cli/init.go", + "line": 153, + "complexity": 10, + "line_coverage": 0, + "crap": 110, + "fix_strategy": "add_tests" + }, { "package": "main", "function": "main", @@ -3834,12 +3845,12 @@ "package": "doctor", "function": "CheckVariables", "file": "internal/doctor/doctor.go", - "line": 414, - "complexity": 34, - "line_coverage": 87.8048780487805, - "crap": 36.09660335746724, + "line": 423, + "complexity": 35, + "line_coverage": 85.88235294117646, + "crap": 38.44685528190515, "contract_coverage": 100, - "gaze_crap": 34, + "gaze_crap": 35, "quadrant": "Q4_Dangerous", "fix_strategy": "decompose" }, @@ -3860,45 +3871,54 @@ "function": "CheckPolicyVersions", "file": "internal/doctor/doctor.go", "line": 221, - "complexity": 15, - "line_coverage": 100, - "crap": 15, + "complexity": 16, + "line_coverage": 95, + "crap": 16.032, "contract_coverage": 100, - "gaze_crap": 15, + "gaze_crap": 16, "quadrant": "Q4_Dangerous", "fix_strategy": "decompose" }, { "package": "cache", - "function": "(*ComplypackSync).SyncComplypack", - "file": "internal/cache/complypack_sync.go", - "line": 42, - "complexity": 15, - "line_coverage": 85.29411764705883, - "crap": 15.715576022796661, + "function": "(*ComplypackCache).Store", + "file": "internal/cache/complypack.go", + "line": 77, + "complexity": 13, + "line_coverage": 66.66666666666667, + "crap": 19.259259259259256, "contract_coverage": 100, - "gaze_crap": 15, - "quadrant": "Q4_Dangerous", - "fix_strategy": "decompose" + "gaze_crap": 13, + "quadrant": "Q2_ComplexButTested", + "fix_strategy": "add_tests" }, { - "package": "cache", - "function": "(*Sync).SyncPolicy", - "file": "internal/cache/sync.go", - "line": 31, - "complexity": 14, - "line_coverage": 84, - "crap": 14.802816, + "package": "complytime", + "function": "ResolveWorkspaceDir", + "file": "internal/complytime/workspace.go", + "line": 108, + "complexity": 13, + "line_coverage": 87.5, + "crap": 13.330078125, "contract_coverage": 100, - "gaze_crap": 14, + "gaze_crap": 13, "quadrant": "Q1_Safe" } ], "recommended_actions": [ + { + "function": "(*MockPolicySource).CopyPolicy", + "package": "cachetest", + "file": "internal/cache/cachetest/mock_source.go", + "line": 73, + "fix_strategy": "add_tests", + "crap": 110, + "complexity": 10 + }, { "function": "ShowPlainTable", "package": "terminal", - "file": "/Users/hbraswel/github/complytime/complyctl/internal/terminal/table.go", + "file": "internal/terminal/table.go", "line": 19, "fix_strategy": "add_tests", "crap": 110, @@ -3907,62 +3927,44 @@ { "function": "promptTargets", "package": "cli", - "file": "/Users/hbraswel/github/complytime/complyctl/cmd/complyctl/cli/init.go", + "file": "cmd/complyctl/cli/init.go", "line": 153, "fix_strategy": "add_tests", "crap": 110, "complexity": 10 }, - { - "function": "(*MockPolicySource).CopyPolicy", - "package": "cachetest", - "file": "/Users/hbraswel/github/complytime/complyctl/internal/cache/cachetest/mock_source.go", - "line": 73, - "fix_strategy": "add_tests", - "crap": 110, - "complexity": 10 - }, { "function": "main", "package": "main", - "file": "/Users/hbraswel/github/complytime/complyctl/cmd/behavioral-report/main.go", + "file": "cmd/behavioral-report/main.go", "line": 28, "fix_strategy": "add_tests", "crap": 90, "complexity": 9 }, - { - "function": "DigestRecordedInState", - "package": "behavioral", - "file": "/Users/hbraswel/github/complytime/complyctl/tests/behavioral/supply_chain.go", - "line": 50, - "fix_strategy": "add_tests", - "crap": 72, - "complexity": 8 - }, { "function": "CheckProviders", "package": "doctor", - "file": "/Users/hbraswel/github/complytime/complyctl/internal/doctor/doctor.go", + "file": "internal/doctor/doctor.go", "line": 136, "fix_strategy": "add_tests", "crap": 72, "complexity": 8 }, { - "function": "(*Cache).ListPolicies", - "package": "cache", - "file": "/Users/hbraswel/github/complytime/complyctl/internal/cache/cache.go", + "function": "DigestRecordedInState", + "package": "behavioral", + "file": "tests/behavioral/supply_chain.go", "line": 50, "fix_strategy": "add_tests", - "crap": 56, - "complexity": 7 + "crap": 72, + "complexity": 8 }, { - "function": "SignatureVerified", + "function": "PluginBinaryIntegrityCheck", "package": "behavioral", - "file": "/Users/hbraswel/github/complytime/complyctl/tests/behavioral/supply_chain.go", - "line": 19, + "file": "tests/behavioral/plugin_security.go", + "line": 67, "fix_strategy": "add_tests", "crap": 56, "complexity": 7 @@ -3970,43 +3972,43 @@ { "function": "InstallTestPlugin", "package": "behavioral", - "file": "/Users/hbraswel/github/complytime/complyctl/tests/behavioral/reusable_steps.go", + "file": "tests/behavioral/reusable_steps.go", "line": 57, "fix_strategy": "add_tests", "crap": 56, "complexity": 7 }, { - "function": "PluginBinaryIntegrityCheck", + "function": "OSCALResultProduced", "package": "behavioral", - "file": "/Users/hbraswel/github/complytime/complyctl/tests/behavioral/plugin_security.go", - "line": 67, + "file": "tests/behavioral/audit.go", + "line": 48, "fix_strategy": "add_tests", "crap": 56, "complexity": 7 }, { - "function": "OSCALResultProduced", + "function": "SignatureVerified", "package": "behavioral", - "file": "/Users/hbraswel/github/complytime/complyctl/tests/behavioral/audit.go", - "line": 48, + "file": "tests/behavioral/supply_chain.go", + "line": 19, "fix_strategy": "add_tests", "crap": 56, "complexity": 7 }, { - "function": "EvaluationLogProduced", - "package": "behavioral", - "file": "/Users/hbraswel/github/complytime/complyctl/tests/behavioral/audit.go", - "line": 17, + "function": "(*Cache).ListPolicies", + "package": "cache", + "file": "internal/cache/cache.go", + "line": 50, "fix_strategy": "add_tests", - "crap": 42, - "complexity": 6 + "crap": 56, + "complexity": 7 }, { - "function": "HTTPSchemeRejected", + "function": "EvaluationLogProduced", "package": "behavioral", - "file": "/Users/hbraswel/github/complytime/complyctl/tests/behavioral/transport_security.go", + "file": "tests/behavioral/audit.go", "line": 17, "fix_strategy": "add_tests", "crap": 42, @@ -4015,16 +4017,25 @@ { "function": "(*Client).fetchVersion", "package": "registry", - "file": "/Users/hbraswel/github/complytime/complyctl/internal/registry/client.go", + "file": "internal/registry/client.go", "line": 118, "fix_strategy": "add_tests", "crap": 42, "complexity": 6 }, + { + "function": "HTTPSchemeRejected", + "package": "behavioral", + "file": "tests/behavioral/transport_security.go", + "line": 17, + "fix_strategy": "add_tests", + "crap": 42, + "complexity": 6 + }, { "function": "registerOCIRoutes", "package": "main", - "file": "/Users/hbraswel/github/complytime/complyctl/cmd/mock-oci-registry/main.go", + "file": "cmd/mock-oci-registry/main.go", "line": 83, "fix_strategy": "add_tests", "crap": 42, @@ -4033,17 +4044,17 @@ { "function": "serveManifest", "package": "main", - "file": "/Users/hbraswel/github/complytime/complyctl/cmd/mock-oci-registry/main.go", + "file": "cmd/mock-oci-registry/main.go", "line": 161, "fix_strategy": "add_tests", "crap": 42, "complexity": 6 }, { - "function": "UnsetEnvVarFails", + "function": "findFile", "package": "behavioral", - "file": "/Users/hbraswel/github/complytime/complyctl/tests/behavioral/credential_protection.go", - "line": 17, + "file": "tests/behavioral/audit.go", + "line": 76, "fix_strategy": "add_tests", "crap": 30, "complexity": 5 @@ -4051,26 +4062,26 @@ { "function": "EvaluatorMismatchRejected", "package": "behavioral", - "file": "/Users/hbraswel/github/complytime/complyctl/tests/behavioral/plugin_security.go", + "file": "tests/behavioral/plugin_security.go", "line": 36, "fix_strategy": "add_tests", "crap": 30, "complexity": 5 }, { - "function": "LogCredentialRedaction", + "function": "HTTPSSchemeNoPlainHTTP", "package": "behavioral", - "file": "/Users/hbraswel/github/complytime/complyctl/tests/behavioral/log_security.go", - "line": 16, + "file": "tests/behavioral/transport_security.go", + "line": 48, "fix_strategy": "add_tests", "crap": 30, "complexity": 5 }, { - "function": "EnvVarResolution", + "function": "LogCredentialRedaction", "package": "behavioral", - "file": "/Users/hbraswel/github/complytime/complyctl/tests/behavioral/credential_protection.go", - "line": 54, + "file": "tests/behavioral/log_security.go", + "line": 16, "fix_strategy": "add_tests", "crap": 30, "complexity": 5 diff --git a/Makefile b/Makefile index bce565c6..9fd33f10 100644 --- a/Makefile +++ b/Makefile @@ -152,7 +152,7 @@ crapload-baseline: ensure-gaze test-unit ## generate baseline thresholds in .gaz @mkdir -p .gaze @REPO_ROOT=$$(pwd); \ gaze crap --format=json --coverprofile=$(GAZE_COVERPROFILE) ./... | \ - jq --arg root "$$REPO_ROOT/" '(.scores[],.summary.worst_crap[]?,.summary.worst_gaze_crap[]?) |= (.file |= ltrimstr($$root))' > $(GAZE_BASELINE) + jq --arg root "$$REPO_ROOT/" '(.scores[],.summary.worst_crap[]?,.summary.worst_gaze_crap[]?,.summary.recommended_actions[]?) |= (.file |= ltrimstr($$root))' > $(GAZE_BASELINE) @echo "Baseline written to $(GAZE_BASELINE)" .PHONY: crapload-baseline @@ -163,7 +163,7 @@ crapload-check: ensure-gaze test-unit ## check for CRAP regressions against base fi @REPO_ROOT=$$(pwd); \ gaze crap --format=json --coverprofile=$(GAZE_COVERPROFILE) ./... | \ - jq --arg root "$$REPO_ROOT/" '(.scores[],.summary.worst_crap[]?,.summary.worst_gaze_crap[]?) |= (.file |= ltrimstr($$root))' > /tmp/crapload-current.json + jq --arg root "$$REPO_ROOT/" '(.scores[],.summary.worst_crap[]?,.summary.worst_gaze_crap[]?,.summary.recommended_actions[]?) |= (.file |= ltrimstr($$root))' > /tmp/crapload-current.json @echo "Comparing against baseline..." @jq -r '.scores[] | "\(.file):\(.function) \(.crap) \(.gaze_crap // 0)"' $(GAZE_BASELINE) | sort > /tmp/crapload-baseline.txt @jq -r '.scores[] | "\(.file):\(.function) \(.crap) \(.gaze_crap // 0)"' /tmp/crapload-current.json | sort > /tmp/crapload-current.txt