Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 13 additions & 13 deletions internal/eval.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,10 +117,6 @@ func (pe *PolicyEvaluator) Eval(ctx context.Context, cert CertificateContext, po
}

for _, policyPath := range policyPaths {
rootData, err := loadBundleRootData(policyPath, policyData)
if err != nil {
return nil, fmt.Errorf("loading bundle data for %s: %w", policyPath, err)
}
processor := policyManager.NewPolicyProcessor(
pe.logger,
labels,
Expand All @@ -129,10 +125,13 @@ func (pe *PolicyEvaluator) Eval(ctx context.Context, cert CertificateContext, po
inventory,
actors,
pe.stepActivities,
rootData,
policyData,
)

evidence, perr := processor.GenerateResults(ctx, policyPath, input)
for _, ev := range evidence {
ev.Title = fmt.Sprintf("%s [%s]", ev.GetTitle(), cert.DomainName)
}
evidences = append(evidences, evidence...)
if perr != nil {
accumulatedErrors = errors.Join(accumulatedErrors, perr)
Expand All @@ -153,15 +152,16 @@ func certificateBaseLabels() map[string]string {
}
}

// loadBundleRootData reads data.json from the OPA bundle root and merges it
// with base. When the agent downloads a policy OCI artifact it returns the
// LoadBundleRootData reads data.json from the OPA bundle root and merges it
// with overrides. When the agent downloads a policy OCI artifact it returns the
// policies/ subdirectory as policyPath; the bundle's data.json lives one level
// up in the bundle root. For local source trees the data.json lives inside the
// policies/ directory itself, so we check both locations.
func loadBundleRootData(policyPath string, base map[string]interface{}) (map[string]interface{}, error) {
// policies/ directory itself, so both locations are checked. overrides win on
// conflict, so operator-supplied policy_data takes precedence over bundle defaults.
func LoadBundleRootData(policyPath string, overrides map[string]interface{}) (map[string]interface{}, error) {
candidates := []string{
filepath.Join(filepath.Dir(policyPath), "data.json"),
filepath.Join(policyPath, "data.json"),
filepath.Join(filepath.Dir(policyPath), "data.json"),
}
for _, p := range candidates {
raw, err := os.ReadFile(p)
Expand All @@ -175,16 +175,16 @@ func loadBundleRootData(policyPath string, base map[string]interface{}) (map[str
if err := json.Unmarshal(raw, &bundleData); err != nil {
return nil, fmt.Errorf("parsing bundle data %s: %w", p, err)
}
merged := make(map[string]interface{}, len(bundleData)+len(base))
merged := make(map[string]interface{}, len(bundleData)+len(overrides))
for k, v := range bundleData {
merged[k] = v
}
for k, v := range base {
for k, v := range overrides {
merged[k] = v
}
return merged, nil
}
return base, nil
return overrides, nil
}

// arnCertID extracts the certificate UUID from an ACM ARN.
Expand Down
102 changes: 102 additions & 0 deletions internal/eval_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
package internal

import (
"os"
"path/filepath"
"testing"
)

func TestLoadBundleRootData_OverridesWinOverBundleDefaults(t *testing.T) {
dir := t.TempDir()
bundleJSON := `{"expiry_warning_days":30,"required_certificate_tags":["Environment"]}`
if err := os.WriteFile(filepath.Join(dir, "data.json"), []byte(bundleJSON), 0644); err != nil {
t.Fatal(err)
}

overrides := map[string]interface{}{
"expiry_warning_days": float64(90),
}

result, err := LoadBundleRootData(dir, overrides)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}

// Operator override (90) must win over bundle default (30).
if got := result["expiry_warning_days"]; got != float64(90) {
t.Errorf("expiry_warning_days: got %v, want 90", got)
}

// Bundle key absent from overrides must still be present.
if _, ok := result["required_certificate_tags"]; !ok {
t.Error("required_certificate_tags from bundle missing from merged result")
}
}

func TestLoadBundleRootData_NoBundleDataJsonReturnsOverrides(t *testing.T) {
dir := t.TempDir() // no data.json written

overrides := map[string]interface{}{
"expiry_warning_days": float64(60),
}

result, err := LoadBundleRootData(dir, overrides)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}

if got := result["expiry_warning_days"]; got != float64(60) {
t.Errorf("expiry_warning_days: got %v, want 60", got)
}
}

func TestLoadBundleRootData_PolicyPathDataJsonWinsOverParent(t *testing.T) {
root := t.TempDir()
parentJSON := `{"expiry_warning_days":30,"source":"parent"}`
if err := os.WriteFile(filepath.Join(root, "data.json"), []byte(parentJSON), 0644); err != nil {
t.Fatal(err)
}
policiesDir := filepath.Join(root, "policies")
if err := os.Mkdir(policiesDir, 0755); err != nil {
t.Fatal(err)
}
policiesJSON := `{"expiry_warning_days":60,"source":"policyPath"}`
if err := os.WriteFile(filepath.Join(policiesDir, "data.json"), []byte(policiesJSON), 0644); err != nil {
t.Fatal(err)
}

// When both data.json locations exist, policyPath/data.json must win.
result, err := LoadBundleRootData(policiesDir, nil)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}

if got := result["source"]; got != "policyPath" {
t.Errorf("source: got %v, want policyPath", got)
}
if got := result["expiry_warning_days"]; got != float64(60) {
t.Errorf("expiry_warning_days: got %v, want 60", got)
}
}

func TestLoadBundleRootData_FindsDataJsonOneDirectoryUp(t *testing.T) {
root := t.TempDir()
bundleJSON := `{"expiry_warning_days":30}`
if err := os.WriteFile(filepath.Join(root, "data.json"), []byte(bundleJSON), 0644); err != nil {
t.Fatal(err)
}
policiesDir := filepath.Join(root, "policies")
if err := os.Mkdir(policiesDir, 0755); err != nil {
t.Fatal(err)
}

// policyPath is the policies/ subdirectory; data.json is one level up.
result, err := LoadBundleRootData(policiesDir, nil)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}

if got := result["expiry_warning_days"]; got != float64(30) {
t.Errorf("expiry_warning_days: got %v, want 30", got)
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
13 changes: 12 additions & 1 deletion main.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,12 +72,23 @@ func (l *CompliancePlugin) Eval(request *proto.EvalRequest, apiHelper runner.Api
}, fmt.Errorf("failed to fetch data: %w", err)
}

// Load bundle data.json defaults and merge with operator overrides once per
// evaluation cycle. All policy paths share the same bundle so one load suffices.
policyData := l.policyData
if paths := request.GetPolicyPaths(); len(paths) > 0 {
merged, err := internal.LoadBundleRootData(paths[0], l.policyData)
if err != nil {
return &proto.EvalResponse{Status: proto.ExecutionStatus_FAILURE}, fmt.Errorf("loading bundle data for %s: %w", paths[0], err)
}
policyData = merged
}

policyEvaluator := internal.NewPolicyEvaluator(ctx, l.logger, activities)

var allEvidences []*proto.Evidence
var evalErrors error
for _, cert := range certs {
certEvidences, err := policyEvaluator.Eval(ctx, cert, request.GetPolicyPaths(), l.policyData, l.config.PolicyLabels)
certEvidences, err := policyEvaluator.Eval(ctx, cert, request.GetPolicyPaths(), policyData, l.config.PolicyLabels)
allEvidences = append(allEvidences, certEvidences...)
if err != nil {
evalErrors = errors.Join(evalErrors, fmt.Errorf("evaluating cert %s: %w", cert.CertificateArn, err))
Expand Down
Loading