From e0a3896f526f76b84f164cbfe74e3ee92eb6a7f1 Mon Sep 17 00:00:00 2001 From: Gustavo Carvalho Date: Fri, 8 May 2026 07:25:12 -0300 Subject: [PATCH 1/8] fix: adds config hash label as unique seeding label Signed-off-by: Gustavo Carvalho --- README.md | 4 + cmd/agent.go | 147 +++++++++++++++++--- cmd/agent_test.go | 314 +++++++++++++++++++++++++++++++++++++++++- docs/configuration.md | 14 +- runner/result.go | 5 + 5 files changed, 452 insertions(+), 32 deletions(-) diff --git a/README.md b/README.md index 25c287e..04f6b2f 100644 --- a/README.md +++ b/README.md @@ -63,6 +63,10 @@ agent_evidence: See [configuration](./docs/configuration.md) for more information. +The agent adds `_agent_config_hash` to plugin and agent evidence labels. The value is a deterministic SHA-256 hash of +the runtime plugin and agent evidence configuration, which prevents multiple unauthenticated agents using the fallback +`_agent=ccf` identity from writing to the same evidence seed when their configurations differ. + ### Environment variables The agent can load specific configruation values from environment variables, which are prefixed with `CCF_` and the path diff --git a/cmd/agent.go b/cmd/agent.go index 6176550..e629e84 100644 --- a/cmd/agent.go +++ b/cmd/agent.go @@ -3,6 +3,7 @@ package cmd import ( "bytes" "context" + "crypto/sha256" "encoding/base64" "encoding/json" "fmt" @@ -199,6 +200,7 @@ const RunnerV2ProtocolVersion int32 = 2 const AnnotationProtocolVersionKey = "org.ccf.plugin.protocol.version" const daemonCronStopTimeout = 30 * time.Second const agentEvidenceErrorArtifactMaxBytes = 1024 * 1024 +const agentConfigHashLabel = "_agent_config_hash" type pluginRunStatus string @@ -839,12 +841,129 @@ func agentIdentityLabel(config *agentConfig) string { func agentFoundationalLabels(config *agentConfig) map[string]string { return map[string]string{ - "_agent": agentIdentityLabel(config), - "tool": "ccf", - "type": "operations", + "_agent": agentIdentityLabel(config), + agentConfigHashLabel: agentConfigurationHash(config), + "tool": "ccf", + "type": "operations", } } +type normalizedAgentConfigForHash struct { + AgentEvidence normalizedAgentEvidenceConfigForHash `json:"agent_evidence"` + Plugins []normalizedAgentPluginForHash `json:"plugins"` +} + +type normalizedAgentEvidenceConfigForHash struct { + Enabled bool `json:"enabled"` + EmitOnRunCompletion bool `json:"emit_on_run_completion"` + Interval string `json:"interval"` +} + +type normalizedAgentPluginForHash struct { + Name string `json:"name"` + ProtocolVersion int32 `json:"protocol_version"` + Schedule string `json:"schedule"` + Source string `json:"source"` + Policies []string `json:"policies"` + Config map[string]string `json:"config,omitempty"` + Labels map[string]string `json:"labels,omitempty"` +} + +func agentConfigurationHash(config *agentConfig) string { + normalized := normalizedAgentConfigForHash{ + AgentEvidence: normalizedAgentEvidenceConfigForHash{ + Enabled: config.agentEvidenceEnabled(), + EmitOnRunCompletion: config.agentEvidenceEmitOnRunCompletion(), + Interval: normalizedAgentEvidenceInterval(config), + }, + } + + if config != nil { + pluginNames := make([]string, 0, len(config.Plugins)) + for pluginName := range config.Plugins { + pluginNames = append(pluginNames, pluginName) + } + sort.Strings(pluginNames) + + normalized.Plugins = make([]normalizedAgentPluginForHash, 0, len(pluginNames)) + for _, pluginName := range pluginNames { + pluginConfig := config.Plugins[pluginName] + normalizedPlugin := normalizedAgentPluginForHash{ + Name: pluginName, + Schedule: "* * * * *", + } + if pluginConfig != nil { + normalizedPlugin.ProtocolVersion = effectivePluginProtocolVersion(pluginConfig) + normalizedPlugin.Source = pluginConfig.Source + if pluginConfig.Schedule != nil { + normalizedPlugin.Schedule = *pluginConfig.Schedule + } + normalizedPlugin.Policies = make([]string, 0, len(pluginConfig.Policies)) + for _, policy := range pluginConfig.Policies { + normalizedPlugin.Policies = append(normalizedPlugin.Policies, string(policy)) + } + normalizedPlugin.Config = copyStringMap(pluginConfig.Config) + normalizedPlugin.Labels = copyStringMap(pluginConfig.Labels) + } + normalized.Plugins = append(normalized.Plugins, normalizedPlugin) + } + } + + payload, err := json.Marshal(normalized) + if err != nil { + return "" + } + sum := sha256.Sum256(payload) + return fmt.Sprintf("%x", sum[:]) +} + +func normalizedAgentEvidenceInterval(config *agentConfig) string { + interval, err := config.agentEvidenceInterval() + if err != nil { + if config == nil || config.AgentEvidence == nil { + return "" + } + return strings.TrimSpace(config.AgentEvidence.Interval) + } + return interval.String() +} + +func copyStringMap(input map[string]string) map[string]string { + if len(input) == 0 { + return nil + } + + output := make(map[string]string, len(input)) + for key, value := range input { + output[key] = value + } + return output +} + +func pluginEvidenceLabels(config *agentConfig, pluginName string, pluginConfig *agentPlugin) map[string]string { + labels := map[string]string{ + "_agent": agentIdentityLabel(config), + "_plugin": pluginName, + } + if pluginConfig != nil { + for k, v := range pluginConfig.Labels { + labels[k] = v + } + } + labels[agentConfigHashLabel] = agentConfigurationHash(config) + return labels +} + +func effectivePluginProtocolVersion(pluginConfig *agentPlugin) int32 { + if pluginConfig == nil { + return 0 + } + if pluginConfig.ProtocolVersion == 0 && !pluginConfig.protocolSet { + return DefaultProtocolVersion + } + return pluginConfig.ProtocolVersion +} + func maskClientID(clientID string) string { trimmed := strings.TrimSpace(clientID) if trimmed == "" { @@ -1196,13 +1315,7 @@ func (ar *AgentRunner) runAllPlugins(ctx context.Context) error { Level: hclog.Level(config.logVerbosity()), }) - labels := map[string]string{ - "_agent": agentIdentityLabel(config), - "_plugin": pluginName, - } - for k, v := range pluginConfig.Labels { - labels[k] = v - } + labels := pluginEvidenceLabels(config, pluginName, pluginConfig) source := ar.pluginLocations[pluginConfig.Source] @@ -1362,13 +1475,7 @@ func (ar *AgentRunner) runPlugin(ctx context.Context, name string, plugin *agent Level: hclog.Level(config.logVerbosity()), }) - labels := map[string]string{ - "_agent": agentIdentityLabel(config), - "_plugin": name, - } - for k, v := range plugin.Labels { - labels[k] = v - } + labels := pluginEvidenceLabels(config, name, plugin) pluginLogger.Debug("Running plugin", "source", pluginExecutable, "protocol_version", plugin.ProtocolVersion) @@ -1516,11 +1623,7 @@ func (ar *AgentRunner) buildAgentRunEvidence(now time.Time) (*agentEvidenceCreat description := formatAgentEvidenceDescription(snapshot) remarks := formatAgentEvidenceRemarks(snapshot) labels := agentFoundationalLabels(config) - evidenceUUID, err := sdk.SeededUUID(map[string]string{ - "type": labels["type"], - "_agent": labels["_agent"], - "tool": labels["tool"], - }) + evidenceUUID, err := sdk.SeededUUID(labels) if err != nil { return nil, err } diff --git a/cmd/agent_test.go b/cmd/agent_test.go index 01ab4e1..447479c 100644 --- a/cmd/agent_test.go +++ b/cmd/agent_test.go @@ -16,6 +16,7 @@ import ( "github.com/compliance-framework/agent/runner" "github.com/compliance-framework/agent/runner/proto" + "github.com/compliance-framework/api/sdk" "github.com/google/go-containerregistry/pkg/v1/remote" "github.com/google/uuid" "github.com/hashicorp/go-hclog" @@ -1301,9 +1302,10 @@ func TestAgentRunEvidenceIncludesPluginRunSummaryAndErrorArtifacts(t *testing.T) t.Fatalf("expected readable failure reason, got %q", evidence.Status.Reason) } expectedLabels := map[string]string{ - "_agent": clientID, - "tool": "ccf", - "type": "operations", + "_agent": clientID, + agentConfigHashLabel: agentConfigurationHash(agentRunner.getConfig()), + "tool": "ccf", + "type": "operations", } for key, expected := range expectedLabels { if evidence.Labels[key] != expected { @@ -1656,8 +1658,11 @@ func TestSendAgentRunEvidenceAllowsNoPlugins(t *testing.T) { if labels["_agent"] != "ccf" || labels["tool"] != "ccf" || labels["type"] != "operations" { t.Fatalf("unexpected foundational labels: %#v", labels) } - if len(labels) != 3 { - t.Fatalf("expected only three foundational labels, got %#v", labels) + if labels[agentConfigHashLabel] != agentConfigurationHash(agentRunner.getConfig()) { + t.Fatalf("expected agent config hash label, got %#v", labels) + } + if len(labels) != 4 { + t.Fatalf("expected only four foundational labels, got %#v", labels) } if _, ok := submitted["back-matter"]; ok { t.Fatalf("expected no backmatter for passing no-plugin evidence, got %#v", submitted["back-matter"]) @@ -1720,6 +1725,255 @@ func TestAgentIdentityLabelFallsBackToKubernetesPodName(t *testing.T) { } } +func TestAgentConfigurationHashUsesRuntimeConfigOnly(t *testing.T) { + base := newRuntimeHashTestConfig() + baseHash := agentConfigurationHash(base) + if len(baseHash) != 64 { + t.Fatalf("expected sha256 hex hash, got %q", baseHash) + } + + reordered := newRuntimeHashTestConfigWithReorderedPlugins() + reordered.ApiConfig = &apiConfig{ + Url: "http://different.example.test", + Auth: &apiAuthConfig{ + ClientID: "123e4567-e89b-12d3-a456-426614174000", + ClientSecret: "different-secret", + }, + } + reordered.Verbosity = 3 + reordered.Daemon = !base.Daemon + if got := agentConfigurationHash(reordered); got != baseHash { + t.Fatalf("expected reordered plugins and excluded fields to keep hash stable, got %q want %q", got, baseHash) + } + + tests := []struct { + name string + mutate func(*agentConfig) + }{ + { + name: "plugin source", + mutate: func(config *agentConfig) { + config.Plugins["plugin-a"].Source = "ghcr.io/example/plugin-a:v2" + }, + }, + { + name: "plugin schedule", + mutate: func(config *agentConfig) { + schedule := "0 * * * *" + config.Plugins["plugin-a"].Schedule = &schedule + }, + }, + { + name: "plugin policy", + mutate: func(config *agentConfig) { + config.Plugins["plugin-a"].Policies = append(config.Plugins["plugin-a"].Policies, "policy-c") + }, + }, + { + name: "plugin config", + mutate: func(config *agentConfig) { + config.Plugins["plugin-a"].Config["region"] = "eu-west-1" + }, + }, + { + name: "plugin label", + mutate: func(config *agentConfig) { + config.Plugins["plugin-a"].Labels["environment"] = "prod" + }, + }, + { + name: "protocol version", + mutate: func(config *agentConfig) { + config.Plugins["plugin-a"].ProtocolVersion = RunnerV2ProtocolVersion + }, + }, + { + name: "agent evidence interval", + mutate: func(config *agentConfig) { + config.AgentEvidence.Interval = "2h" + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + config := newRuntimeHashTestConfig() + tt.mutate(config) + if got := agentConfigurationHash(config); got == baseHash { + t.Fatalf("expected %s change to alter hash %q", tt.name, got) + } + }) + } +} + +func TestAgentFoundationalLabelsIncludeAgentConfigurationHash(t *testing.T) { + config := newRuntimeHashTestConfig() + + labels := agentFoundationalLabels(config) + + if labels[agentConfigHashLabel] != agentConfigurationHash(config) { + t.Fatalf("expected foundational labels to include config hash, got %#v", labels) + } +} + +func TestAgentRunEvidenceUUIDUsesAgentConfigurationHash(t *testing.T) { + now := time.Date(2026, 5, 8, 12, 0, 0, 0, time.UTC) + firstRunner := NewAgentRunner() + firstRunner.UpdateConfig(newRuntimeHashTestConfig()) + firstEvidence, err := firstRunner.buildAgentRunEvidence(now) + if err != nil { + t.Fatalf("build first agent run evidence: %v", err) + } + + secondConfig := newRuntimeHashTestConfig() + secondConfig.Plugins["plugin-a"].Source = "ghcr.io/example/plugin-a:v2" + secondRunner := NewAgentRunner() + secondRunner.UpdateConfig(secondConfig) + secondEvidence, err := secondRunner.buildAgentRunEvidence(now) + if err != nil { + t.Fatalf("build second agent run evidence: %v", err) + } + + if firstEvidence.Labels[agentConfigHashLabel] == secondEvidence.Labels[agentConfigHashLabel] { + t.Fatalf("expected different config hash labels, got %q", firstEvidence.Labels[agentConfigHashLabel]) + } + if firstEvidence.UUID == secondEvidence.UUID { + t.Fatalf("expected config hash change to alter agent evidence UUID %s", firstEvidence.UUID) + } +} + +func TestPluginEvidenceLabelsIncludeAgentConfigurationHash(t *testing.T) { + config := newRuntimeHashTestConfig() + + labels := pluginEvidenceLabels(config, "plugin-a", config.Plugins["plugin-a"]) + + if labels[agentConfigHashLabel] != agentConfigurationHash(config) { + t.Fatalf("expected plugin labels to include config hash, got %#v", labels) + } + if labels["_plugin"] != "plugin-a" { + t.Fatalf("expected plugin label, got %#v", labels) + } + if labels["team"] != "security" { + t.Fatalf("expected configured plugin labels to be preserved, got %#v", labels) + } +} + +func TestPluginEvidenceSubmissionIncludesAgentConfigurationHash(t *testing.T) { + config := newRuntimeHashTestConfig() + config.ApiConfig.Auth = nil + var submittedLabels map[string]string + var submittedUUID uuid.UUID + client := newTestHTTPClient(func(r *http.Request) (*http.Response, error) { + if r.URL.Path != "/api/evidence" { + t.Fatalf("unexpected path %q", r.URL.Path) + return nil, nil + } + var submitted struct { + Labels map[string]string `json:"labels"` + UUID uuid.UUID `json:"uuid"` + } + if err := json.NewDecoder(r.Body).Decode(&submitted); err != nil { + t.Fatalf("decode evidence request: %v", err) + } + submittedLabels = submitted.Labels + submittedUUID = submitted.UUID + return jsonResponse(http.StatusCreated, ""), nil + }) + + agentRunner := NewAgentRunner() + agentRunner.httpClient = client + agentRunner.UpdateConfig(config) + apiHelper := runner.NewApiHelper( + hclog.NewNullLogger(), + agentRunner.getAPIClient(), + pluginEvidenceLabels(config, "plugin-a", config.Plugins["plugin-a"]), + "plugin-a", + ) + + now := time.Now().UTC() + if err := apiHelper.CreateEvidence(context.Background(), []*proto.Evidence{ + { + UUID: uuid.NewString(), + Title: "Evidence", + Start: timestamppb.New(now.Add(-time.Minute)), + End: timestamppb.New(now), + Status: &proto.EvidenceStatus{ + Reason: "pass", + State: proto.EvidenceStatusState_EVIDENCE_STATUS_STATE_SATISFIED, + }, + }, + }); err != nil { + t.Fatalf("create evidence: %v", err) + } + + if submittedLabels[agentConfigHashLabel] != agentConfigurationHash(config) { + t.Fatalf("expected submitted plugin evidence to include config hash, got %#v", submittedLabels) + } + if submittedLabels["_plugin"] != "plugin-a" || submittedLabels["team"] != "security" { + t.Fatalf("expected submitted plugin evidence to include plugin labels, got %#v", submittedLabels) + } + expectedUUID, err := sdk.SeededUUID(submittedLabels) + if err != nil { + t.Fatalf("seed expected UUID: %v", err) + } + if submittedUUID != expectedUUID { + t.Fatalf("expected submitted plugin evidence UUID to be seeded from merged labels, got %s want %s", submittedUUID, expectedUUID) + } +} + +func TestPluginProvidedEvidenceLabelsCanOverrideAgentConfigurationHash(t *testing.T) { + config := newRuntimeHashTestConfig() + config.ApiConfig.Auth = nil + var submittedLabels map[string]string + client := newTestHTTPClient(func(r *http.Request) (*http.Response, error) { + if r.URL.Path != "/api/evidence" { + t.Fatalf("unexpected path %q", r.URL.Path) + return nil, nil + } + var submitted struct { + Labels map[string]string `json:"labels"` + } + if err := json.NewDecoder(r.Body).Decode(&submitted); err != nil { + t.Fatalf("decode evidence request: %v", err) + } + submittedLabels = submitted.Labels + return jsonResponse(http.StatusCreated, ""), nil + }) + + agentRunner := NewAgentRunner() + agentRunner.httpClient = client + agentRunner.UpdateConfig(config) + apiHelper := runner.NewApiHelper( + hclog.NewNullLogger(), + agentRunner.getAPIClient(), + pluginEvidenceLabels(config, "plugin-a", config.Plugins["plugin-a"]), + "plugin-a", + ) + + now := time.Now().UTC() + if err := apiHelper.CreateEvidence(context.Background(), []*proto.Evidence{ + { + UUID: uuid.NewString(), + Title: "Evidence", + Labels: map[string]string{ + agentConfigHashLabel: "plugin-provided-hash", + }, + Start: timestamppb.New(now.Add(-time.Minute)), + End: timestamppb.New(now), + Status: &proto.EvidenceStatus{ + Reason: "pass", + State: proto.EvidenceStatusState_EVIDENCE_STATUS_STATE_SATISFIED, + }, + }, + }); err != nil { + t.Fatalf("create evidence: %v", err) + } + + if submittedLabels[agentConfigHashLabel] != "plugin-provided-hash" { + t.Fatalf("expected plugin evidence labels to override config hash, got %#v", submittedLabels) + } +} + func TestAgentEvidenceConfigDefaultsAndValidation(t *testing.T) { config := &agentConfig{ ApiConfig: &apiConfig{Url: "http://localhost:8080"}, @@ -1761,6 +2015,56 @@ func newTestAgentConfig(baseURL string, auth *apiAuthConfig) *agentConfig { } } +func newRuntimeHashTestConfig() *agentConfig { + schedule := "*/5 * * * *" + emitOnRunCompletion := true + enabled := true + return &agentConfig{ + Daemon: true, + Verbosity: 1, + ApiConfig: &apiConfig{ + Url: "http://example.test", + Auth: &apiAuthConfig{ + ClientID: "00000000-0000-0000-0000-000000000001", + ClientSecret: "client-secret", + }, + }, + AgentEvidence: &agentEvidenceConfig{ + Enabled: &enabled, + EmitOnRunCompletion: &emitOnRunCompletion, + Interval: "1h", + }, + Plugins: map[string]*agentPlugin{ + "plugin-a": { + ProtocolVersion: DefaultProtocolVersion, + Schedule: &schedule, + Source: "ghcr.io/example/plugin-a:v1", + Policies: []agentPolicy{"policy-a", "policy-b"}, + Config: agentPluginConfig{ + "region": "us-east-1", + }, + Labels: map[string]string{ + "team": "security", + }, + }, + "plugin-b": { + ProtocolVersion: DefaultProtocolVersion, + Source: "ghcr.io/example/plugin-b:v1", + }, + }, + } +} + +func newRuntimeHashTestConfigWithReorderedPlugins() *agentConfig { + config := newRuntimeHashTestConfig() + plugins := config.Plugins + config.Plugins = map[string]*agentPlugin{ + "plugin-b": plugins["plugin-b"], + "plugin-a": plugins["plugin-a"], + } + return config +} + type roundTripFunc func(*http.Request) (*http.Response, error) func (f roundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) { diff --git a/docs/configuration.md b/docs/configuration.md index 6bd7481..4324cd3 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -40,8 +40,10 @@ The `plugin_identifier` is a unique identifier for the plugin, and is used to id name this whatever you like but it must be unique. The `labels` should uniquely identify this agent instance. The agent also sets the `_agent` label on plugin evidence -using `api.auth.client_id` when available, then `KUBERNETES_POD_NAME` or `KUBERNETES_POD`, and finally `ccf`. Because -evidence UUIDs are seeded from labels, changing that identity changes the evidence stream for plugin evidence. +using `api.auth.client_id` when available, then `KUBERNETES_POD_NAME` or `KUBERNETES_POD`, and finally `ccf`. The agent +also sets `_agent_config_hash`, a deterministic hash of the runtime plugin and agent evidence configuration. Because +evidence UUIDs are seeded from labels, changing either the agent identity or runtime configuration changes the evidence +stream for plugin evidence. The `plugin_source` is the path to the plugin binary that the agent will run. This can be a relative or absolute path or even a URL to a remote plugin. @@ -62,9 +64,11 @@ is `satisfied`. A plugin remains in the `Plugins with errors` summary until it f Plugins that have never run are listed as pending. Failed plugin errors are attached as back-matter resources and linked from the evidence so they can be downloaded. -Agent evidence uses only these labels: `_agent`, `tool`, and `type`. The `_agent` label uses `api.auth.client_id` when -available, then `KUBERNETES_POD_NAME` or `KUBERNETES_POD`, and finally defaults to `ccf`. The `tool` label is `ccf`; -the `type` label is `operations`. +Agent evidence uses these labels: `_agent`, `_agent_config_hash`, `tool`, and `type`. The `_agent` label uses +`api.auth.client_id` when available, then `KUBERNETES_POD_NAME` or `KUBERNETES_POD`, and finally defaults to `ccf`. The +`_agent_config_hash` label is a SHA-256 hash of plugin names, sources, protocol versions, schedules, policies, plugin +config, plugin labels, and `agent_evidence` settings. It does not include API URL, API auth, or verbosity. The `tool` +label is `ccf`; the `type` label is `operations`. If no plugins are configured, ccf-agent still emits passing agent evidence on the configured interval when running in daemon mode. In non-daemon mode, ccf-agent can emit agent evidence only once per invocation. diff --git a/runner/result.go b/runner/result.go index aaecb02..1630b26 100644 --- a/runner/result.go +++ b/runner/result.go @@ -40,6 +40,11 @@ func (h *apiHelper) CreateEvidence(ctx context.Context, evidence []*proto.Eviden labels[k] = v } evid.Labels = labels + evidenceUUID, err := sdk.SeededUUID(labels) + if err != nil { + return err + } + evid.UUID = evidenceUUID labelled = append(labelled, *evid) } From 0d436054dbe50ac3926dad099a999d5cfed86ec6 Mon Sep 17 00:00:00 2001 From: Gustavo Carvalho Date: Fri, 8 May 2026 07:37:09 -0300 Subject: [PATCH 2/8] fix: copilot issues Signed-off-by: Gustavo Carvalho --- README.md | 4 +++- cmd/agent.go | 37 +++++++++++++++++++++++++++++++++---- cmd/agent_test.go | 34 ++++++++++++++++++++++++++++------ docs/configuration.md | 6 ++++-- runner/result.go | 16 ++++++++++++++++ 5 files changed, 84 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 04f6b2f..9c6f03c 100644 --- a/README.md +++ b/README.md @@ -65,7 +65,9 @@ See [configuration](./docs/configuration.md) for more information. The agent adds `_agent_config_hash` to plugin and agent evidence labels. The value is a deterministic SHA-256 hash of the runtime plugin and agent evidence configuration, which prevents multiple unauthenticated agents using the fallback -`_agent=ccf` identity from writing to the same evidence seed when their configurations differ. +`_agent=ccf` identity from writing to the same evidence seed when their configurations differ. The hash includes plugin +config keys, but not config values, so secrets such as tokens or passwords are not included in the digest. The agent +reserves `_agent`, `_plugin`, and `_agent_config_hash` when merging plugin evidence labels. ### Environment variables diff --git a/cmd/agent.go b/cmd/agent.go index e629e84..83e1790 100644 --- a/cmd/agent.go +++ b/cmd/agent.go @@ -865,7 +865,7 @@ type normalizedAgentPluginForHash struct { Schedule string `json:"schedule"` Source string `json:"source"` Policies []string `json:"policies"` - Config map[string]string `json:"config,omitempty"` + ConfigKeys []string `json:"config_keys,omitempty"` Labels map[string]string `json:"labels,omitempty"` } @@ -898,11 +898,12 @@ func agentConfigurationHash(config *agentConfig) string { if pluginConfig.Schedule != nil { normalizedPlugin.Schedule = *pluginConfig.Schedule } - normalizedPlugin.Policies = make([]string, 0, len(pluginConfig.Policies)) + policies := make([]string, 0, len(pluginConfig.Policies)) for _, policy := range pluginConfig.Policies { - normalizedPlugin.Policies = append(normalizedPlugin.Policies, string(policy)) + policies = append(policies, string(policy)) } - normalizedPlugin.Config = copyStringMap(pluginConfig.Config) + normalizedPlugin.Policies = sortedUniqueStrings(policies) + normalizedPlugin.ConfigKeys = sortedMapKeys(pluginConfig.Config) normalizedPlugin.Labels = copyStringMap(pluginConfig.Labels) } normalized.Plugins = append(normalized.Plugins, normalizedPlugin) @@ -940,6 +941,34 @@ func copyStringMap(input map[string]string) map[string]string { return output } +func sortedMapKeys(input map[string]string) []string { + if len(input) == 0 { + return nil + } + + keys := make([]string, 0, len(input)) + for key := range input { + keys = append(keys, key) + } + sort.Strings(keys) + return keys +} + +func sortedUniqueStrings(input []string) []string { + if len(input) == 0 { + return nil + } + + sort.Strings(input) + output := input[:0] + for _, value := range input { + if len(output) == 0 || output[len(output)-1] != value { + output = append(output, value) + } + } + return output +} + func pluginEvidenceLabels(config *agentConfig, pluginName string, pluginConfig *agentPlugin) map[string]string { labels := map[string]string{ "_agent": agentIdentityLabel(config), diff --git a/cmd/agent_test.go b/cmd/agent_test.go index 447479c..a57a1da 100644 --- a/cmd/agent_test.go +++ b/cmd/agent_test.go @@ -1742,8 +1742,13 @@ func TestAgentConfigurationHashUsesRuntimeConfigOnly(t *testing.T) { } reordered.Verbosity = 3 reordered.Daemon = !base.Daemon + reordered.Plugins["plugin-a"].Policies = []agentPolicy{"policy-b", "policy-a", "policy-b"} + reordered.Plugins["plugin-a"].Config = agentPluginConfig{ + "token": "different-secret-token", + "region": "different-region", + } if got := agentConfigurationHash(reordered); got != baseHash { - t.Fatalf("expected reordered plugins and excluded fields to keep hash stable, got %q want %q", got, baseHash) + t.Fatalf("expected reordered plugins, reordered policies, and excluded fields to keep hash stable, got %q want %q", got, baseHash) } tests := []struct { @@ -1770,9 +1775,9 @@ func TestAgentConfigurationHashUsesRuntimeConfigOnly(t *testing.T) { }, }, { - name: "plugin config", + name: "plugin config key", mutate: func(config *agentConfig) { - config.Plugins["plugin-a"].Config["region"] = "eu-west-1" + config.Plugins["plugin-a"].Config["account_id"] = "123456789012" }, }, { @@ -1806,6 +1811,17 @@ func TestAgentConfigurationHashUsesRuntimeConfigOnly(t *testing.T) { } } +func TestAgentConfigurationHashExcludesPluginConfigValues(t *testing.T) { + base := newRuntimeHashTestConfig() + changedSecret := newRuntimeHashTestConfig() + changedSecret.Plugins["plugin-a"].Config["token"] = "different-secret-token" + changedSecret.Plugins["plugin-a"].Config["region"] = "eu-west-1" + + if got, want := agentConfigurationHash(changedSecret), agentConfigurationHash(base); got != want { + t.Fatalf("expected plugin config value changes to be excluded from hash, got %q want %q", got, want) + } +} + func TestAgentFoundationalLabelsIncludeAgentConfigurationHash(t *testing.T) { config := newRuntimeHashTestConfig() @@ -1921,7 +1937,7 @@ func TestPluginEvidenceSubmissionIncludesAgentConfigurationHash(t *testing.T) { } } -func TestPluginProvidedEvidenceLabelsCanOverrideAgentConfigurationHash(t *testing.T) { +func TestPluginProvidedEvidenceLabelsCannotOverrideReservedAgentLabels(t *testing.T) { config := newRuntimeHashTestConfig() config.ApiConfig.Auth = nil var submittedLabels map[string]string @@ -1957,6 +1973,8 @@ func TestPluginProvidedEvidenceLabelsCanOverrideAgentConfigurationHash(t *testin Title: "Evidence", Labels: map[string]string{ agentConfigHashLabel: "plugin-provided-hash", + "_agent": "plugin-provided-agent", + "_plugin": "plugin-provided-plugin", }, Start: timestamppb.New(now.Add(-time.Minute)), End: timestamppb.New(now), @@ -1969,8 +1987,11 @@ func TestPluginProvidedEvidenceLabelsCanOverrideAgentConfigurationHash(t *testin t.Fatalf("create evidence: %v", err) } - if submittedLabels[agentConfigHashLabel] != "plugin-provided-hash" { - t.Fatalf("expected plugin evidence labels to override config hash, got %#v", submittedLabels) + if submittedLabels[agentConfigHashLabel] != agentConfigurationHash(config) { + t.Fatalf("expected agent config hash label to be reserved, got %#v", submittedLabels) + } + if submittedLabels["_agent"] != "ccf" || submittedLabels["_plugin"] != "plugin-a" { + t.Fatalf("expected reserved agent labels to be preserved, got %#v", submittedLabels) } } @@ -2042,6 +2063,7 @@ func newRuntimeHashTestConfig() *agentConfig { Policies: []agentPolicy{"policy-a", "policy-b"}, Config: agentPluginConfig{ "region": "us-east-1", + "token": "secret-token", }, Labels: map[string]string{ "team": "security", diff --git a/docs/configuration.md b/docs/configuration.md index 4324cd3..628599f 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -67,8 +67,10 @@ from the evidence so they can be downloaded. Agent evidence uses these labels: `_agent`, `_agent_config_hash`, `tool`, and `type`. The `_agent` label uses `api.auth.client_id` when available, then `KUBERNETES_POD_NAME` or `KUBERNETES_POD`, and finally defaults to `ccf`. The `_agent_config_hash` label is a SHA-256 hash of plugin names, sources, protocol versions, schedules, policies, plugin -config, plugin labels, and `agent_evidence` settings. It does not include API URL, API auth, or verbosity. The `tool` -label is `ccf`; the `type` label is `operations`. +config keys, plugin labels, and `agent_evidence` settings. It does not include plugin config values, API URL, API auth, +or verbosity. The agent reserves `_agent`, `_plugin`, and `_agent_config_hash` when merging plugin evidence labels, so +plugins cannot override those values in submitted evidence. The `tool` label is `ccf`; the `type` label is +`operations`. If no plugins are configured, ccf-agent still emits passing agent evidence on the configured interval when running in daemon mode. In non-daemon mode, ccf-agent can emit agent evidence only once per invocation. diff --git a/runner/result.go b/runner/result.go index 1630b26..aabe030 100644 --- a/runner/result.go +++ b/runner/result.go @@ -16,6 +16,8 @@ type apiHelper struct { pluginName string } +const agentConfigHashLabel = "_agent_config_hash" + func NewApiHelper(logger hclog.Logger, client *sdk.Client, agentLabels map[string]string, pluginName string) *apiHelper { logger = logger.Named("api-helper") return &apiHelper{ @@ -37,6 +39,11 @@ func (h *apiHelper) CreateEvidence(ctx context.Context, evidence []*proto.Eviden labels[k] = v } for k, v := range evid.Labels { + if isReservedEvidenceLabel(k) { + if _, ok := h.agentLabels[k]; ok { + continue + } + } labels[k] = v } evid.Labels = labels @@ -52,6 +59,15 @@ func (h *apiHelper) CreateEvidence(ctx context.Context, evidence []*proto.Eviden return h.client.Evidence.Create(ctx, labelled...) } +func isReservedEvidenceLabel(key string) bool { + switch key { + case "_agent", "_plugin", agentConfigHashLabel: + return true + default: + return false + } +} + func (h *apiHelper) UpsertRiskTemplates(ctx context.Context, packageName string, riskTemplates []*proto.RiskTemplate) error { templates := ProtoToSdk(riskTemplates, RiskTemplateProtoToSdk) From 184b05e2eb8cbda6661ff1de9534525de8794bbc Mon Sep 17 00:00:00 2001 From: Gustavo Carvalho Date: Fri, 8 May 2026 07:48:03 -0300 Subject: [PATCH 3/8] fix: copilot issues Signed-off-by: Gustavo Carvalho --- cmd/agent.go | 11 +++++--- cmd/agent_test.go | 66 +++++++++++++++++++++++++++++++++++++++++++++++ runner/result.go | 7 ++--- 3 files changed, 78 insertions(+), 6 deletions(-) diff --git a/cmd/agent.go b/cmd/agent.go index 83e1790..6bc7153 100644 --- a/cmd/agent.go +++ b/cmd/agent.go @@ -970,6 +970,10 @@ func sortedUniqueStrings(input []string) []string { } func pluginEvidenceLabels(config *agentConfig, pluginName string, pluginConfig *agentPlugin) map[string]string { + return pluginEvidenceLabelsWithHash(config, pluginName, pluginConfig, agentConfigurationHash(config)) +} + +func pluginEvidenceLabelsWithHash(config *agentConfig, pluginName string, pluginConfig *agentPlugin, configHash string) map[string]string { labels := map[string]string{ "_agent": agentIdentityLabel(config), "_plugin": pluginName, @@ -979,7 +983,7 @@ func pluginEvidenceLabels(config *agentConfig, pluginName string, pluginConfig * labels[k] = v } } - labels[agentConfigHashLabel] = agentConfigurationHash(config) + labels[agentConfigHashLabel] = configHash return labels } @@ -1334,6 +1338,7 @@ func (ar *AgentRunner) runAllPlugins(ctx context.Context) error { pluginNames = append(pluginNames, pluginName) } sort.Strings(pluginNames) + configHash := agentConfigurationHash(config) for _, pluginName := range pluginNames { pluginConfig := config.Plugins[pluginName] @@ -1344,7 +1349,7 @@ func (ar *AgentRunner) runAllPlugins(ctx context.Context) error { Level: hclog.Level(config.logVerbosity()), }) - labels := pluginEvidenceLabels(config, pluginName, pluginConfig) + labels := pluginEvidenceLabelsWithHash(config, pluginName, pluginConfig, configHash) source := ar.pluginLocations[pluginConfig.Source] @@ -1504,7 +1509,7 @@ func (ar *AgentRunner) runPlugin(ctx context.Context, name string, plugin *agent Level: hclog.Level(config.logVerbosity()), }) - labels := pluginEvidenceLabels(config, name, plugin) + labels := pluginEvidenceLabelsWithHash(config, name, plugin, agentConfigurationHash(config)) pluginLogger.Debug("Running plugin", "source", pluginExecutable, "protocol_version", plugin.ProtocolVersion) diff --git a/cmd/agent_test.go b/cmd/agent_test.go index a57a1da..7e5bcb5 100644 --- a/cmd/agent_test.go +++ b/cmd/agent_test.go @@ -1995,6 +1995,72 @@ func TestPluginProvidedEvidenceLabelsCannotOverrideReservedAgentLabels(t *testin } } +func TestPluginProvidedReservedEvidenceLabelsIgnoredWhenAgentLabelsOmitThem(t *testing.T) { + var submittedLabels map[string]string + client := newTestHTTPClient(func(r *http.Request) (*http.Response, error) { + if r.URL.Path != "/api/evidence" { + t.Fatalf("unexpected path %q", r.URL.Path) + return nil, nil + } + var submitted struct { + Labels map[string]string `json:"labels"` + } + if err := json.NewDecoder(r.Body).Decode(&submitted); err != nil { + t.Fatalf("decode evidence request: %v", err) + } + submittedLabels = submitted.Labels + return jsonResponse(http.StatusCreated, ""), nil + }) + + agentRunner := NewAgentRunner() + agentRunner.httpClient = client + agentRunner.UpdateConfig(&agentConfig{ + ApiConfig: &apiConfig{Url: "http://example.test"}, + Plugins: map[string]*agentPlugin{}, + }) + apiHelper := runner.NewApiHelper( + hclog.NewNullLogger(), + agentRunner.getAPIClient(), + map[string]string{}, + "plugin-a", + ) + + now := time.Now().UTC() + if err := apiHelper.CreateEvidence(context.Background(), []*proto.Evidence{ + { + UUID: uuid.NewString(), + Title: "Evidence", + Labels: map[string]string{ + agentConfigHashLabel: "plugin-provided-hash", + "_agent": "plugin-provided-agent", + "_plugin": "plugin-provided-plugin", + "finding": "preserved", + }, + Start: timestamppb.New(now.Add(-time.Minute)), + End: timestamppb.New(now), + Status: &proto.EvidenceStatus{ + Reason: "pass", + State: proto.EvidenceStatusState_EVIDENCE_STATUS_STATE_SATISFIED, + }, + }, + }); err != nil { + t.Fatalf("create evidence: %v", err) + } + + if _, ok := submittedLabels[agentConfigHashLabel]; ok { + t.Fatalf("expected plugin-provided config hash to be ignored, got %#v", submittedLabels) + } + if _, ok := submittedLabels["_agent"]; ok { + t.Fatalf("expected plugin-provided agent label to be ignored, got %#v", submittedLabels) + } + if submittedLabels["_plugin"] != "plugin-a" { + t.Fatalf("expected plugin label to come from helper plugin name, got %#v", submittedLabels) + } + if submittedLabels["finding"] != "preserved" { + t.Fatalf("expected non-reserved evidence label to be preserved, got %#v", submittedLabels) + } +} + func TestAgentEvidenceConfigDefaultsAndValidation(t *testing.T) { config := &agentConfig{ ApiConfig: &apiConfig{Url: "http://localhost:8080"}, diff --git a/runner/result.go b/runner/result.go index aabe030..da0f184 100644 --- a/runner/result.go +++ b/runner/result.go @@ -38,11 +38,12 @@ func (h *apiHelper) CreateEvidence(ctx context.Context, evidence []*proto.Eviden for k, v := range h.agentLabels { labels[k] = v } + if _, ok := labels["_plugin"]; !ok && h.pluginName != "" { + labels["_plugin"] = h.pluginName + } for k, v := range evid.Labels { if isReservedEvidenceLabel(k) { - if _, ok := h.agentLabels[k]; ok { - continue - } + continue } labels[k] = v } From 181e738a5f03a71274346d5136b996a4add7fb8b Mon Sep 17 00:00:00 2001 From: Gustavo Carvalho Date: Fri, 8 May 2026 08:02:24 -0300 Subject: [PATCH 4/8] fix: copilot issues Signed-off-by: Gustavo Carvalho --- README.md | 2 +- cmd/agent.go | 25 +++++++++++++++++++++++-- cmd/agent_test.go | 16 ++++++++++++++-- docs/configuration.md | 6 +++--- runner/result.go | 22 ++++++++++++---------- 5 files changed, 53 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 9c6f03c..67d0c91 100644 --- a/README.md +++ b/README.md @@ -67,7 +67,7 @@ The agent adds `_agent_config_hash` to plugin and agent evidence labels. The val the runtime plugin and agent evidence configuration, which prevents multiple unauthenticated agents using the fallback `_agent=ccf` identity from writing to the same evidence seed when their configurations differ. The hash includes plugin config keys, but not config values, so secrets such as tokens or passwords are not included in the digest. The agent -reserves `_agent`, `_plugin`, and `_agent_config_hash` when merging plugin evidence labels. +reserves `_agent`, `_plugin`, and `_agent_config_hash` when merging plugin labels from configuration or evidence. ### Environment variables diff --git a/cmd/agent.go b/cmd/agent.go index 6bc7153..e77c86c 100644 --- a/cmd/agent.go +++ b/cmd/agent.go @@ -200,7 +200,7 @@ const RunnerV2ProtocolVersion int32 = 2 const AnnotationProtocolVersionKey = "org.ccf.plugin.protocol.version" const daemonCronStopTimeout = 30 * time.Second const agentEvidenceErrorArtifactMaxBytes = 1024 * 1024 -const agentConfigHashLabel = "_agent_config_hash" +const agentConfigHashLabel = internal.AgentConfigHashLabel type pluginRunStatus string @@ -904,7 +904,7 @@ func agentConfigurationHash(config *agentConfig) string { } normalizedPlugin.Policies = sortedUniqueStrings(policies) normalizedPlugin.ConfigKeys = sortedMapKeys(pluginConfig.Config) - normalizedPlugin.Labels = copyStringMap(pluginConfig.Labels) + normalizedPlugin.Labels = copyNonReservedStringMap(pluginConfig.Labels) } normalized.Plugins = append(normalized.Plugins, normalizedPlugin) } @@ -941,6 +941,24 @@ func copyStringMap(input map[string]string) map[string]string { return output } +func copyNonReservedStringMap(input map[string]string) map[string]string { + if len(input) == 0 { + return nil + } + + output := make(map[string]string, len(input)) + for key, value := range input { + if internal.IsReservedEvidenceLabel(key) { + continue + } + output[key] = value + } + if len(output) == 0 { + return nil + } + return output +} + func sortedMapKeys(input map[string]string) []string { if len(input) == 0 { return nil @@ -980,6 +998,9 @@ func pluginEvidenceLabelsWithHash(config *agentConfig, pluginName string, plugin } if pluginConfig != nil { for k, v := range pluginConfig.Labels { + if internal.IsReservedEvidenceLabel(k) { + continue + } labels[k] = v } } diff --git a/cmd/agent_test.go b/cmd/agent_test.go index 7e5bcb5..36fb3bc 100644 --- a/cmd/agent_test.go +++ b/cmd/agent_test.go @@ -1860,6 +1860,9 @@ func TestAgentRunEvidenceUUIDUsesAgentConfigurationHash(t *testing.T) { func TestPluginEvidenceLabelsIncludeAgentConfigurationHash(t *testing.T) { config := newRuntimeHashTestConfig() + config.Plugins["plugin-a"].Labels["_agent"] = "configured-agent" + config.Plugins["plugin-a"].Labels["_plugin"] = "configured-plugin" + config.Plugins["plugin-a"].Labels[agentConfigHashLabel] = "configured-hash" labels := pluginEvidenceLabels(config, "plugin-a", config.Plugins["plugin-a"]) @@ -1869,6 +1872,9 @@ func TestPluginEvidenceLabelsIncludeAgentConfigurationHash(t *testing.T) { if labels["_plugin"] != "plugin-a" { t.Fatalf("expected plugin label, got %#v", labels) } + if labels["_agent"] != "00000000-0000-0000-0000-000000000001" { + t.Fatalf("expected configured reserved labels to be ignored, got %#v", labels) + } if labels["team"] != "security" { t.Fatalf("expected configured plugin labels to be preserved, got %#v", labels) } @@ -1907,9 +1913,10 @@ func TestPluginEvidenceSubmissionIncludesAgentConfigurationHash(t *testing.T) { ) now := time.Now().UTC() + originalUUID := uuid.New() if err := apiHelper.CreateEvidence(context.Background(), []*proto.Evidence{ { - UUID: uuid.NewString(), + UUID: originalUUID.String(), Title: "Evidence", Start: timestamppb.New(now.Add(-time.Minute)), End: timestamppb.New(now), @@ -1928,7 +1935,12 @@ func TestPluginEvidenceSubmissionIncludesAgentConfigurationHash(t *testing.T) { if submittedLabels["_plugin"] != "plugin-a" || submittedLabels["team"] != "security" { t.Fatalf("expected submitted plugin evidence to include plugin labels, got %#v", submittedLabels) } - expectedUUID, err := sdk.SeededUUID(submittedLabels) + uuidSeedLabels := make(map[string]string, len(submittedLabels)+1) + for key, value := range submittedLabels { + uuidSeedLabels[key] = value + } + uuidSeedLabels["_evidence_uuid"] = originalUUID.String() + expectedUUID, err := sdk.SeededUUID(uuidSeedLabels) if err != nil { t.Fatalf("seed expected UUID: %v", err) } diff --git a/docs/configuration.md b/docs/configuration.md index 628599f..077a6c3 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -68,9 +68,9 @@ Agent evidence uses these labels: `_agent`, `_agent_config_hash`, `tool`, and `t `api.auth.client_id` when available, then `KUBERNETES_POD_NAME` or `KUBERNETES_POD`, and finally defaults to `ccf`. The `_agent_config_hash` label is a SHA-256 hash of plugin names, sources, protocol versions, schedules, policies, plugin config keys, plugin labels, and `agent_evidence` settings. It does not include plugin config values, API URL, API auth, -or verbosity. The agent reserves `_agent`, `_plugin`, and `_agent_config_hash` when merging plugin evidence labels, so -plugins cannot override those values in submitted evidence. The `tool` label is `ccf`; the `type` label is -`operations`. +or verbosity. The agent reserves `_agent`, `_plugin`, and `_agent_config_hash` when merging plugin labels from +configuration or plugin-submitted evidence, so plugins cannot override those values. The `tool` label is `ccf`; the +`type` label is `operations`. If no plugins are configured, ccf-agent still emits passing agent evidence on the configured interval when running in daemon mode. In non-daemon mode, ccf-agent can emit agent evidence only once per invocation. diff --git a/runner/result.go b/runner/result.go index da0f184..70defd7 100644 --- a/runner/result.go +++ b/runner/result.go @@ -3,9 +3,11 @@ package runner import ( "context" + "github.com/compliance-framework/agent/internal" "github.com/compliance-framework/agent/runner/proto" "github.com/compliance-framework/api/sdk" "github.com/compliance-framework/api/sdk/types" + "github.com/google/uuid" "github.com/hashicorp/go-hclog" ) @@ -16,8 +18,6 @@ type apiHelper struct { pluginName string } -const agentConfigHashLabel = "_agent_config_hash" - func NewApiHelper(logger hclog.Logger, client *sdk.Client, agentLabels map[string]string, pluginName string) *apiHelper { logger = logger.Named("api-helper") return &apiHelper{ @@ -42,13 +42,13 @@ func (h *apiHelper) CreateEvidence(ctx context.Context, evidence []*proto.Eviden labels["_plugin"] = h.pluginName } for k, v := range evid.Labels { - if isReservedEvidenceLabel(k) { + if internal.IsReservedEvidenceLabel(k) { continue } labels[k] = v } evid.Labels = labels - evidenceUUID, err := sdk.SeededUUID(labels) + evidenceUUID, err := seededEvidenceUUID(labels, evid.UUID) if err != nil { return err } @@ -60,13 +60,15 @@ func (h *apiHelper) CreateEvidence(ctx context.Context, evidence []*proto.Eviden return h.client.Evidence.Create(ctx, labelled...) } -func isReservedEvidenceLabel(key string) bool { - switch key { - case "_agent", "_plugin", agentConfigHashLabel: - return true - default: - return false +func seededEvidenceUUID(labels map[string]string, originalUUID uuid.UUID) (uuid.UUID, error) { + seedLabels := make(map[string]string, len(labels)+1) + for key, value := range labels { + seedLabels[key] = value + } + if originalUUID != uuid.Nil { + seedLabels["_evidence_uuid"] = originalUUID.String() } + return sdk.SeededUUID(seedLabels) } func (h *apiHelper) UpsertRiskTemplates(ctx context.Context, packageName string, riskTemplates []*proto.RiskTemplate) error { From 993bfec467302d2d1862af15a9af23d2d78ecd12 Mon Sep 17 00:00:00 2001 From: Gustavo Carvalho Date: Fri, 8 May 2026 08:03:49 -0300 Subject: [PATCH 5/8] fix: missing file Signed-off-by: Gustavo Carvalho --- internal/labels.go | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 internal/labels.go diff --git a/internal/labels.go b/internal/labels.go new file mode 100644 index 0000000..51b23ec --- /dev/null +++ b/internal/labels.go @@ -0,0 +1,12 @@ +package internal + +const AgentConfigHashLabel = "_agent_config_hash" + +func IsReservedEvidenceLabel(key string) bool { + switch key { + case "_agent", "_plugin", AgentConfigHashLabel: + return true + default: + return false + } +} From b9a8c120bcb4acd51193897420d81c2462259d71 Mon Sep 17 00:00:00 2001 From: Gustavo Carvalho Date: Fri, 8 May 2026 08:25:54 -0300 Subject: [PATCH 6/8] fix: copilot issues Signed-off-by: Gustavo Carvalho --- cmd/agent.go | 21 +++++++++++++++------ cmd/agent_test.go | 26 +++++++++++++++++++++----- internal/labels.go | 3 ++- runner/result.go | 8 +++++--- 4 files changed, 43 insertions(+), 15 deletions(-) diff --git a/cmd/agent.go b/cmd/agent.go index e77c86c..e2aa5ec 100644 --- a/cmd/agent.go +++ b/cmd/agent.go @@ -200,6 +200,7 @@ const RunnerV2ProtocolVersion int32 = 2 const AnnotationProtocolVersionKey = "org.ccf.plugin.protocol.version" const daemonCronStopTimeout = 30 * time.Second const agentEvidenceErrorArtifactMaxBytes = 1024 * 1024 +const defaultPluginSchedule = "* * * * *" const agentConfigHashLabel = internal.AgentConfigHashLabel type pluginRunStatus string @@ -872,13 +873,16 @@ type normalizedAgentPluginForHash struct { func agentConfigurationHash(config *agentConfig) string { normalized := normalizedAgentConfigForHash{ AgentEvidence: normalizedAgentEvidenceConfigForHash{ - Enabled: config.agentEvidenceEnabled(), - EmitOnRunCompletion: config.agentEvidenceEmitOnRunCompletion(), + Enabled: true, + EmitOnRunCompletion: true, Interval: normalizedAgentEvidenceInterval(config), }, } if config != nil { + normalized.AgentEvidence.Enabled = config.agentEvidenceEnabled() + normalized.AgentEvidence.EmitOnRunCompletion = config.agentEvidenceEmitOnRunCompletion() + pluginNames := make([]string, 0, len(config.Plugins)) for pluginName := range config.Plugins { pluginNames = append(pluginNames, pluginName) @@ -890,7 +894,7 @@ func agentConfigurationHash(config *agentConfig) string { pluginConfig := config.Plugins[pluginName] normalizedPlugin := normalizedAgentPluginForHash{ Name: pluginName, - Schedule: "* * * * *", + Schedule: defaultPluginSchedule, } if pluginConfig != nil { normalizedPlugin.ProtocolVersion = effectivePluginProtocolVersion(pluginConfig) @@ -912,16 +916,21 @@ func agentConfigurationHash(config *agentConfig) string { payload, err := json.Marshal(normalized) if err != nil { - return "" + sum := sha256.Sum256([]byte(err.Error())) + return fmt.Sprintf("%x", sum[:]) } sum := sha256.Sum256(payload) return fmt.Sprintf("%x", sum[:]) } func normalizedAgentEvidenceInterval(config *agentConfig) string { + if config == nil { + return time.Hour.String() + } + interval, err := config.agentEvidenceInterval() if err != nil { - if config == nil || config.AgentEvidence == nil { + if config.AgentEvidence == nil { return "" } return strings.TrimSpace(config.AgentEvidence.Interval) @@ -1306,7 +1315,7 @@ func (ar *AgentRunner) setupCron(ctx context.Context) (*cron.Cron, error) { currentPluginConfig := pluginConfig var schedule string if currentPluginConfig.Schedule == nil { - schedule = "* * * * *" + schedule = defaultPluginSchedule } else { schedule = *currentPluginConfig.Schedule } diff --git a/cmd/agent_test.go b/cmd/agent_test.go index 36fb3bc..bee7825 100644 --- a/cmd/agent_test.go +++ b/cmd/agent_test.go @@ -14,6 +14,7 @@ import ( "testing" "time" + "github.com/compliance-framework/agent/internal" "github.com/compliance-framework/agent/runner" "github.com/compliance-framework/agent/runner/proto" "github.com/compliance-framework/api/sdk" @@ -1822,6 +1823,17 @@ func TestAgentConfigurationHashExcludesPluginConfigValues(t *testing.T) { } } +func TestAgentConfigurationHashHandlesNilConfig(t *testing.T) { + hash := agentConfigurationHash(nil) + if len(hash) != 64 { + t.Fatalf("expected nil config hash to be deterministic sha256 hex, got %q", hash) + } + + if got := normalizedAgentEvidenceInterval(nil); got != time.Hour.String() { + t.Fatalf("expected nil config interval to use default interval, got %q", got) + } +} + func TestAgentFoundationalLabelsIncludeAgentConfigurationHash(t *testing.T) { config := newRuntimeHashTestConfig() @@ -1939,7 +1951,7 @@ func TestPluginEvidenceSubmissionIncludesAgentConfigurationHash(t *testing.T) { for key, value := range submittedLabels { uuidSeedLabels[key] = value } - uuidSeedLabels["_evidence_uuid"] = originalUUID.String() + uuidSeedLabels[internal.EvidenceUUIDSeedLabel] = originalUUID.String() expectedUUID, err := sdk.SeededUUID(uuidSeedLabels) if err != nil { t.Fatalf("seed expected UUID: %v", err) @@ -2043,10 +2055,11 @@ func TestPluginProvidedReservedEvidenceLabelsIgnoredWhenAgentLabelsOmitThem(t *t UUID: uuid.NewString(), Title: "Evidence", Labels: map[string]string{ - agentConfigHashLabel: "plugin-provided-hash", - "_agent": "plugin-provided-agent", - "_plugin": "plugin-provided-plugin", - "finding": "preserved", + agentConfigHashLabel: "plugin-provided-hash", + internal.EvidenceUUIDSeedLabel: "plugin-provided-uuid-seed", + "_agent": "plugin-provided-agent", + "_plugin": "plugin-provided-plugin", + "finding": "preserved", }, Start: timestamppb.New(now.Add(-time.Minute)), End: timestamppb.New(now), @@ -2065,6 +2078,9 @@ func TestPluginProvidedReservedEvidenceLabelsIgnoredWhenAgentLabelsOmitThem(t *t if _, ok := submittedLabels["_agent"]; ok { t.Fatalf("expected plugin-provided agent label to be ignored, got %#v", submittedLabels) } + if _, ok := submittedLabels[internal.EvidenceUUIDSeedLabel]; ok { + t.Fatalf("expected plugin-provided UUID seed label to be ignored, got %#v", submittedLabels) + } if submittedLabels["_plugin"] != "plugin-a" { t.Fatalf("expected plugin label to come from helper plugin name, got %#v", submittedLabels) } diff --git a/internal/labels.go b/internal/labels.go index 51b23ec..c549566 100644 --- a/internal/labels.go +++ b/internal/labels.go @@ -1,10 +1,11 @@ package internal const AgentConfigHashLabel = "_agent_config_hash" +const EvidenceUUIDSeedLabel = "_evidence_uuid" func IsReservedEvidenceLabel(key string) bool { switch key { - case "_agent", "_plugin", AgentConfigHashLabel: + case "_agent", "_plugin", AgentConfigHashLabel, EvidenceUUIDSeedLabel: return true default: return false diff --git a/runner/result.go b/runner/result.go index 70defd7..a8f7ffd 100644 --- a/runner/result.go +++ b/runner/result.go @@ -61,13 +61,15 @@ func (h *apiHelper) CreateEvidence(ctx context.Context, evidence []*proto.Eviden } func seededEvidenceUUID(labels map[string]string, originalUUID uuid.UUID) (uuid.UUID, error) { + if originalUUID == uuid.Nil { + return sdk.SeededUUID(labels) + } + seedLabels := make(map[string]string, len(labels)+1) for key, value := range labels { seedLabels[key] = value } - if originalUUID != uuid.Nil { - seedLabels["_evidence_uuid"] = originalUUID.String() - } + seedLabels[internal.EvidenceUUIDSeedLabel] = originalUUID.String() return sdk.SeededUUID(seedLabels) } From 3b0adef59d4e2dc7bb51219ad0ba7c1eab6f535a Mon Sep 17 00:00:00 2001 From: Gustavo Carvalho Date: Fri, 8 May 2026 08:40:11 -0300 Subject: [PATCH 7/8] fix: revert ALL changes copilot wrongfully made us do Signed-off-by: Gustavo Carvalho --- README.md | 4 +- cmd/agent.go | 67 ++------------- cmd/agent_test.go | 187 +----------------------------------------- docs/configuration.md | 6 +- internal/labels.go | 13 --- runner/result.go | 26 ------ 6 files changed, 15 insertions(+), 288 deletions(-) delete mode 100644 internal/labels.go diff --git a/README.md b/README.md index 67d0c91..04f6b2f 100644 --- a/README.md +++ b/README.md @@ -65,9 +65,7 @@ See [configuration](./docs/configuration.md) for more information. The agent adds `_agent_config_hash` to plugin and agent evidence labels. The value is a deterministic SHA-256 hash of the runtime plugin and agent evidence configuration, which prevents multiple unauthenticated agents using the fallback -`_agent=ccf` identity from writing to the same evidence seed when their configurations differ. The hash includes plugin -config keys, but not config values, so secrets such as tokens or passwords are not included in the digest. The agent -reserves `_agent`, `_plugin`, and `_agent_config_hash` when merging plugin labels from configuration or evidence. +`_agent=ccf` identity from writing to the same evidence seed when their configurations differ. ### Environment variables diff --git a/cmd/agent.go b/cmd/agent.go index e2aa5ec..7b8eefe 100644 --- a/cmd/agent.go +++ b/cmd/agent.go @@ -200,8 +200,7 @@ const RunnerV2ProtocolVersion int32 = 2 const AnnotationProtocolVersionKey = "org.ccf.plugin.protocol.version" const daemonCronStopTimeout = 30 * time.Second const agentEvidenceErrorArtifactMaxBytes = 1024 * 1024 -const defaultPluginSchedule = "* * * * *" -const agentConfigHashLabel = internal.AgentConfigHashLabel +const agentConfigHashLabel = "_agent_config_hash" type pluginRunStatus string @@ -866,7 +865,7 @@ type normalizedAgentPluginForHash struct { Schedule string `json:"schedule"` Source string `json:"source"` Policies []string `json:"policies"` - ConfigKeys []string `json:"config_keys,omitempty"` + Config map[string]string `json:"config,omitempty"` Labels map[string]string `json:"labels,omitempty"` } @@ -894,7 +893,7 @@ func agentConfigurationHash(config *agentConfig) string { pluginConfig := config.Plugins[pluginName] normalizedPlugin := normalizedAgentPluginForHash{ Name: pluginName, - Schedule: defaultPluginSchedule, + Schedule: "* * * * *", } if pluginConfig != nil { normalizedPlugin.ProtocolVersion = effectivePluginProtocolVersion(pluginConfig) @@ -902,13 +901,12 @@ func agentConfigurationHash(config *agentConfig) string { if pluginConfig.Schedule != nil { normalizedPlugin.Schedule = *pluginConfig.Schedule } - policies := make([]string, 0, len(pluginConfig.Policies)) + normalizedPlugin.Policies = make([]string, 0, len(pluginConfig.Policies)) for _, policy := range pluginConfig.Policies { - policies = append(policies, string(policy)) + normalizedPlugin.Policies = append(normalizedPlugin.Policies, string(policy)) } - normalizedPlugin.Policies = sortedUniqueStrings(policies) - normalizedPlugin.ConfigKeys = sortedMapKeys(pluginConfig.Config) - normalizedPlugin.Labels = copyNonReservedStringMap(pluginConfig.Labels) + normalizedPlugin.Config = copyStringMap(pluginConfig.Config) + normalizedPlugin.Labels = copyStringMap(pluginConfig.Labels) } normalized.Plugins = append(normalized.Plugins, normalizedPlugin) } @@ -950,52 +948,6 @@ func copyStringMap(input map[string]string) map[string]string { return output } -func copyNonReservedStringMap(input map[string]string) map[string]string { - if len(input) == 0 { - return nil - } - - output := make(map[string]string, len(input)) - for key, value := range input { - if internal.IsReservedEvidenceLabel(key) { - continue - } - output[key] = value - } - if len(output) == 0 { - return nil - } - return output -} - -func sortedMapKeys(input map[string]string) []string { - if len(input) == 0 { - return nil - } - - keys := make([]string, 0, len(input)) - for key := range input { - keys = append(keys, key) - } - sort.Strings(keys) - return keys -} - -func sortedUniqueStrings(input []string) []string { - if len(input) == 0 { - return nil - } - - sort.Strings(input) - output := input[:0] - for _, value := range input { - if len(output) == 0 || output[len(output)-1] != value { - output = append(output, value) - } - } - return output -} - func pluginEvidenceLabels(config *agentConfig, pluginName string, pluginConfig *agentPlugin) map[string]string { return pluginEvidenceLabelsWithHash(config, pluginName, pluginConfig, agentConfigurationHash(config)) } @@ -1007,9 +959,6 @@ func pluginEvidenceLabelsWithHash(config *agentConfig, pluginName string, plugin } if pluginConfig != nil { for k, v := range pluginConfig.Labels { - if internal.IsReservedEvidenceLabel(k) { - continue - } labels[k] = v } } @@ -1315,7 +1264,7 @@ func (ar *AgentRunner) setupCron(ctx context.Context) (*cron.Cron, error) { currentPluginConfig := pluginConfig var schedule string if currentPluginConfig.Schedule == nil { - schedule = defaultPluginSchedule + schedule = "* * * * *" } else { schedule = *currentPluginConfig.Schedule } diff --git a/cmd/agent_test.go b/cmd/agent_test.go index bee7825..164043c 100644 --- a/cmd/agent_test.go +++ b/cmd/agent_test.go @@ -14,10 +14,8 @@ import ( "testing" "time" - "github.com/compliance-framework/agent/internal" "github.com/compliance-framework/agent/runner" "github.com/compliance-framework/agent/runner/proto" - "github.com/compliance-framework/api/sdk" "github.com/google/go-containerregistry/pkg/v1/remote" "github.com/google/uuid" "github.com/hashicorp/go-hclog" @@ -1743,13 +1741,8 @@ func TestAgentConfigurationHashUsesRuntimeConfigOnly(t *testing.T) { } reordered.Verbosity = 3 reordered.Daemon = !base.Daemon - reordered.Plugins["plugin-a"].Policies = []agentPolicy{"policy-b", "policy-a", "policy-b"} - reordered.Plugins["plugin-a"].Config = agentPluginConfig{ - "token": "different-secret-token", - "region": "different-region", - } if got := agentConfigurationHash(reordered); got != baseHash { - t.Fatalf("expected reordered plugins, reordered policies, and excluded fields to keep hash stable, got %q want %q", got, baseHash) + t.Fatalf("expected reordered plugins and excluded fields to keep hash stable, got %q want %q", got, baseHash) } tests := []struct { @@ -1776,9 +1769,9 @@ func TestAgentConfigurationHashUsesRuntimeConfigOnly(t *testing.T) { }, }, { - name: "plugin config key", + name: "plugin config", mutate: func(config *agentConfig) { - config.Plugins["plugin-a"].Config["account_id"] = "123456789012" + config.Plugins["plugin-a"].Config["region"] = "eu-west-1" }, }, { @@ -1812,28 +1805,6 @@ func TestAgentConfigurationHashUsesRuntimeConfigOnly(t *testing.T) { } } -func TestAgentConfigurationHashExcludesPluginConfigValues(t *testing.T) { - base := newRuntimeHashTestConfig() - changedSecret := newRuntimeHashTestConfig() - changedSecret.Plugins["plugin-a"].Config["token"] = "different-secret-token" - changedSecret.Plugins["plugin-a"].Config["region"] = "eu-west-1" - - if got, want := agentConfigurationHash(changedSecret), agentConfigurationHash(base); got != want { - t.Fatalf("expected plugin config value changes to be excluded from hash, got %q want %q", got, want) - } -} - -func TestAgentConfigurationHashHandlesNilConfig(t *testing.T) { - hash := agentConfigurationHash(nil) - if len(hash) != 64 { - t.Fatalf("expected nil config hash to be deterministic sha256 hex, got %q", hash) - } - - if got := normalizedAgentEvidenceInterval(nil); got != time.Hour.String() { - t.Fatalf("expected nil config interval to use default interval, got %q", got) - } -} - func TestAgentFoundationalLabelsIncludeAgentConfigurationHash(t *testing.T) { config := newRuntimeHashTestConfig() @@ -1872,9 +1843,6 @@ func TestAgentRunEvidenceUUIDUsesAgentConfigurationHash(t *testing.T) { func TestPluginEvidenceLabelsIncludeAgentConfigurationHash(t *testing.T) { config := newRuntimeHashTestConfig() - config.Plugins["plugin-a"].Labels["_agent"] = "configured-agent" - config.Plugins["plugin-a"].Labels["_plugin"] = "configured-plugin" - config.Plugins["plugin-a"].Labels[agentConfigHashLabel] = "configured-hash" labels := pluginEvidenceLabels(config, "plugin-a", config.Plugins["plugin-a"]) @@ -1884,9 +1852,6 @@ func TestPluginEvidenceLabelsIncludeAgentConfigurationHash(t *testing.T) { if labels["_plugin"] != "plugin-a" { t.Fatalf("expected plugin label, got %#v", labels) } - if labels["_agent"] != "00000000-0000-0000-0000-000000000001" { - t.Fatalf("expected configured reserved labels to be ignored, got %#v", labels) - } if labels["team"] != "security" { t.Fatalf("expected configured plugin labels to be preserved, got %#v", labels) } @@ -1896,7 +1861,6 @@ func TestPluginEvidenceSubmissionIncludesAgentConfigurationHash(t *testing.T) { config := newRuntimeHashTestConfig() config.ApiConfig.Auth = nil var submittedLabels map[string]string - var submittedUUID uuid.UUID client := newTestHTTPClient(func(r *http.Request) (*http.Response, error) { if r.URL.Path != "/api/evidence" { t.Fatalf("unexpected path %q", r.URL.Path) @@ -1904,13 +1868,11 @@ func TestPluginEvidenceSubmissionIncludesAgentConfigurationHash(t *testing.T) { } var submitted struct { Labels map[string]string `json:"labels"` - UUID uuid.UUID `json:"uuid"` } if err := json.NewDecoder(r.Body).Decode(&submitted); err != nil { t.Fatalf("decode evidence request: %v", err) } submittedLabels = submitted.Labels - submittedUUID = submitted.UUID return jsonResponse(http.StatusCreated, ""), nil }) @@ -1925,10 +1887,9 @@ func TestPluginEvidenceSubmissionIncludesAgentConfigurationHash(t *testing.T) { ) now := time.Now().UTC() - originalUUID := uuid.New() if err := apiHelper.CreateEvidence(context.Background(), []*proto.Evidence{ { - UUID: originalUUID.String(), + UUID: uuid.NewString(), Title: "Evidence", Start: timestamppb.New(now.Add(-time.Minute)), End: timestamppb.New(now), @@ -1947,146 +1908,6 @@ func TestPluginEvidenceSubmissionIncludesAgentConfigurationHash(t *testing.T) { if submittedLabels["_plugin"] != "plugin-a" || submittedLabels["team"] != "security" { t.Fatalf("expected submitted plugin evidence to include plugin labels, got %#v", submittedLabels) } - uuidSeedLabels := make(map[string]string, len(submittedLabels)+1) - for key, value := range submittedLabels { - uuidSeedLabels[key] = value - } - uuidSeedLabels[internal.EvidenceUUIDSeedLabel] = originalUUID.String() - expectedUUID, err := sdk.SeededUUID(uuidSeedLabels) - if err != nil { - t.Fatalf("seed expected UUID: %v", err) - } - if submittedUUID != expectedUUID { - t.Fatalf("expected submitted plugin evidence UUID to be seeded from merged labels, got %s want %s", submittedUUID, expectedUUID) - } -} - -func TestPluginProvidedEvidenceLabelsCannotOverrideReservedAgentLabels(t *testing.T) { - config := newRuntimeHashTestConfig() - config.ApiConfig.Auth = nil - var submittedLabels map[string]string - client := newTestHTTPClient(func(r *http.Request) (*http.Response, error) { - if r.URL.Path != "/api/evidence" { - t.Fatalf("unexpected path %q", r.URL.Path) - return nil, nil - } - var submitted struct { - Labels map[string]string `json:"labels"` - } - if err := json.NewDecoder(r.Body).Decode(&submitted); err != nil { - t.Fatalf("decode evidence request: %v", err) - } - submittedLabels = submitted.Labels - return jsonResponse(http.StatusCreated, ""), nil - }) - - agentRunner := NewAgentRunner() - agentRunner.httpClient = client - agentRunner.UpdateConfig(config) - apiHelper := runner.NewApiHelper( - hclog.NewNullLogger(), - agentRunner.getAPIClient(), - pluginEvidenceLabels(config, "plugin-a", config.Plugins["plugin-a"]), - "plugin-a", - ) - - now := time.Now().UTC() - if err := apiHelper.CreateEvidence(context.Background(), []*proto.Evidence{ - { - UUID: uuid.NewString(), - Title: "Evidence", - Labels: map[string]string{ - agentConfigHashLabel: "plugin-provided-hash", - "_agent": "plugin-provided-agent", - "_plugin": "plugin-provided-plugin", - }, - Start: timestamppb.New(now.Add(-time.Minute)), - End: timestamppb.New(now), - Status: &proto.EvidenceStatus{ - Reason: "pass", - State: proto.EvidenceStatusState_EVIDENCE_STATUS_STATE_SATISFIED, - }, - }, - }); err != nil { - t.Fatalf("create evidence: %v", err) - } - - if submittedLabels[agentConfigHashLabel] != agentConfigurationHash(config) { - t.Fatalf("expected agent config hash label to be reserved, got %#v", submittedLabels) - } - if submittedLabels["_agent"] != "ccf" || submittedLabels["_plugin"] != "plugin-a" { - t.Fatalf("expected reserved agent labels to be preserved, got %#v", submittedLabels) - } -} - -func TestPluginProvidedReservedEvidenceLabelsIgnoredWhenAgentLabelsOmitThem(t *testing.T) { - var submittedLabels map[string]string - client := newTestHTTPClient(func(r *http.Request) (*http.Response, error) { - if r.URL.Path != "/api/evidence" { - t.Fatalf("unexpected path %q", r.URL.Path) - return nil, nil - } - var submitted struct { - Labels map[string]string `json:"labels"` - } - if err := json.NewDecoder(r.Body).Decode(&submitted); err != nil { - t.Fatalf("decode evidence request: %v", err) - } - submittedLabels = submitted.Labels - return jsonResponse(http.StatusCreated, ""), nil - }) - - agentRunner := NewAgentRunner() - agentRunner.httpClient = client - agentRunner.UpdateConfig(&agentConfig{ - ApiConfig: &apiConfig{Url: "http://example.test"}, - Plugins: map[string]*agentPlugin{}, - }) - apiHelper := runner.NewApiHelper( - hclog.NewNullLogger(), - agentRunner.getAPIClient(), - map[string]string{}, - "plugin-a", - ) - - now := time.Now().UTC() - if err := apiHelper.CreateEvidence(context.Background(), []*proto.Evidence{ - { - UUID: uuid.NewString(), - Title: "Evidence", - Labels: map[string]string{ - agentConfigHashLabel: "plugin-provided-hash", - internal.EvidenceUUIDSeedLabel: "plugin-provided-uuid-seed", - "_agent": "plugin-provided-agent", - "_plugin": "plugin-provided-plugin", - "finding": "preserved", - }, - Start: timestamppb.New(now.Add(-time.Minute)), - End: timestamppb.New(now), - Status: &proto.EvidenceStatus{ - Reason: "pass", - State: proto.EvidenceStatusState_EVIDENCE_STATUS_STATE_SATISFIED, - }, - }, - }); err != nil { - t.Fatalf("create evidence: %v", err) - } - - if _, ok := submittedLabels[agentConfigHashLabel]; ok { - t.Fatalf("expected plugin-provided config hash to be ignored, got %#v", submittedLabels) - } - if _, ok := submittedLabels["_agent"]; ok { - t.Fatalf("expected plugin-provided agent label to be ignored, got %#v", submittedLabels) - } - if _, ok := submittedLabels[internal.EvidenceUUIDSeedLabel]; ok { - t.Fatalf("expected plugin-provided UUID seed label to be ignored, got %#v", submittedLabels) - } - if submittedLabels["_plugin"] != "plugin-a" { - t.Fatalf("expected plugin label to come from helper plugin name, got %#v", submittedLabels) - } - if submittedLabels["finding"] != "preserved" { - t.Fatalf("expected non-reserved evidence label to be preserved, got %#v", submittedLabels) - } } func TestAgentEvidenceConfigDefaultsAndValidation(t *testing.T) { diff --git a/docs/configuration.md b/docs/configuration.md index 077a6c3..4324cd3 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -67,10 +67,8 @@ from the evidence so they can be downloaded. Agent evidence uses these labels: `_agent`, `_agent_config_hash`, `tool`, and `type`. The `_agent` label uses `api.auth.client_id` when available, then `KUBERNETES_POD_NAME` or `KUBERNETES_POD`, and finally defaults to `ccf`. The `_agent_config_hash` label is a SHA-256 hash of plugin names, sources, protocol versions, schedules, policies, plugin -config keys, plugin labels, and `agent_evidence` settings. It does not include plugin config values, API URL, API auth, -or verbosity. The agent reserves `_agent`, `_plugin`, and `_agent_config_hash` when merging plugin labels from -configuration or plugin-submitted evidence, so plugins cannot override those values. The `tool` label is `ccf`; the -`type` label is `operations`. +config, plugin labels, and `agent_evidence` settings. It does not include API URL, API auth, or verbosity. The `tool` +label is `ccf`; the `type` label is `operations`. If no plugins are configured, ccf-agent still emits passing agent evidence on the configured interval when running in daemon mode. In non-daemon mode, ccf-agent can emit agent evidence only once per invocation. diff --git a/internal/labels.go b/internal/labels.go deleted file mode 100644 index c549566..0000000 --- a/internal/labels.go +++ /dev/null @@ -1,13 +0,0 @@ -package internal - -const AgentConfigHashLabel = "_agent_config_hash" -const EvidenceUUIDSeedLabel = "_evidence_uuid" - -func IsReservedEvidenceLabel(key string) bool { - switch key { - case "_agent", "_plugin", AgentConfigHashLabel, EvidenceUUIDSeedLabel: - return true - default: - return false - } -} diff --git a/runner/result.go b/runner/result.go index a8f7ffd..aaecb02 100644 --- a/runner/result.go +++ b/runner/result.go @@ -3,11 +3,9 @@ package runner import ( "context" - "github.com/compliance-framework/agent/internal" "github.com/compliance-framework/agent/runner/proto" "github.com/compliance-framework/api/sdk" "github.com/compliance-framework/api/sdk/types" - "github.com/google/uuid" "github.com/hashicorp/go-hclog" ) @@ -38,21 +36,10 @@ func (h *apiHelper) CreateEvidence(ctx context.Context, evidence []*proto.Eviden for k, v := range h.agentLabels { labels[k] = v } - if _, ok := labels["_plugin"]; !ok && h.pluginName != "" { - labels["_plugin"] = h.pluginName - } for k, v := range evid.Labels { - if internal.IsReservedEvidenceLabel(k) { - continue - } labels[k] = v } evid.Labels = labels - evidenceUUID, err := seededEvidenceUUID(labels, evid.UUID) - if err != nil { - return err - } - evid.UUID = evidenceUUID labelled = append(labelled, *evid) } @@ -60,19 +47,6 @@ func (h *apiHelper) CreateEvidence(ctx context.Context, evidence []*proto.Eviden return h.client.Evidence.Create(ctx, labelled...) } -func seededEvidenceUUID(labels map[string]string, originalUUID uuid.UUID) (uuid.UUID, error) { - if originalUUID == uuid.Nil { - return sdk.SeededUUID(labels) - } - - seedLabels := make(map[string]string, len(labels)+1) - for key, value := range labels { - seedLabels[key] = value - } - seedLabels[internal.EvidenceUUIDSeedLabel] = originalUUID.String() - return sdk.SeededUUID(seedLabels) -} - func (h *apiHelper) UpsertRiskTemplates(ctx context.Context, packageName string, riskTemplates []*proto.RiskTemplate) error { templates := ProtoToSdk(riskTemplates, RiskTemplateProtoToSdk) From b0cb2f15a05eb8ed12d0128fa38c54042ba7a514 Mon Sep 17 00:00:00 2001 From: Gustavo Fernandes de Carvalho <17139678+gusfcarvalho@users.noreply.github.com> Date: Fri, 8 May 2026 08:47:28 -0300 Subject: [PATCH 8/8] fix: typo Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 04f6b2f..8eba50d 100644 --- a/README.md +++ b/README.md @@ -69,7 +69,7 @@ the runtime plugin and agent evidence configuration, which prevents multiple una ### Environment variables -The agent can load specific configruation values from environment variables, which are prefixed with `CCF_` and the path +The agent can load specific configuration values from environment variables, which are prefixed with `CCF_` and the path in the config is specified using underscore-separated key. For example, to specify the `token` value in the GitHub config, you may set an environment variable `CCF_PLUGINS_GITHUB_CONFIG_TOKEN`