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
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -432,7 +432,7 @@ jobs:
go-version-file: go.mod

- name: Run golangci-lint
uses: golangci/golangci-lint-action@v6
uses: golangci/golangci-lint-action@v9
with:
version: latest
args: --timeout=5m
Expand Down
4 changes: 4 additions & 0 deletions cmd/mpe/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,10 @@ func main() {
Name: "no-opa-flags",
Usage: "Disable all OPA flags (overrides --opa-flags and MPE_CLI_OPA_FLAGS).",
},
&cli.BoolFlag{
Name: "regal",
Usage: "Run Regal linting instead of standard validation. Uses the bundled Regal library to check embedded Rego code against Regal's rule set.",
},
},
Action: lint.Execute,
},
Expand Down
31 changes: 24 additions & 7 deletions cmd/mpe/subcommands/build/core.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ func File(inputFile, outputFile string) Result {
return result
}

if err := processYAMLNode(&rootNode); err != nil {
if err := processYAMLNode(&rootNode, ""); err != nil {
result.Error = err
return result
}
Expand Down Expand Up @@ -160,27 +160,34 @@ func IsPolicyDomainReference(filePath string) (bool, error) {
return doc.Kind == "PolicyDomainReference", nil
}

func processYAMLNode(node *yaml.Node) error {
// regoRequiredParents defines the YAML keys whose child items must have 'rego' or 'rego_filename'.
var regoRequiredParents = map[string]bool{
"policies": true,
"policy-libraries": true,
"mappers": true,
}

func processYAMLNode(node *yaml.Node, parentKey string) error {
if node == nil {
return nil
}

if node.Kind == yaml.DocumentNode {
for _, child := range node.Content {
if err := processYAMLNode(child); err != nil {
if err := processYAMLNode(child, parentKey); err != nil {
return err
}
}
return nil
}

if node.Kind == yaml.MappingNode {
return processMappingNode(node)
return processMappingNode(node, parentKey)
}

if node.Kind == yaml.SequenceNode {
for _, item := range node.Content {
if err := processYAMLNode(item); err != nil {
if err := processYAMLNode(item, parentKey); err != nil {
return err
}
}
Expand All @@ -190,7 +197,7 @@ func processYAMLNode(node *yaml.Node) error {
return nil
}

func processMappingNode(node *yaml.Node) error {
func processMappingNode(node *yaml.Node, parentKey string) error {
if len(node.Content)%2 != 0 {
return fmt.Errorf("invalid YAML mapping node")
}
Expand All @@ -217,7 +224,12 @@ func processMappingNode(node *yaml.Node) error {
}
}

if err := processYAMLNode(valueNode); err != nil {
// Pass the current key as parentKey so children know their context
currentKey := ""
if keyNode.Kind == yaml.ScalarNode {
currentKey = keyNode.Value
}
if err := processYAMLNode(valueNode, currentKey); err != nil {
return err
}
}
Expand All @@ -226,6 +238,11 @@ func processMappingNode(node *yaml.Node) error {
return fmt.Errorf("cannot specify both 'rego' and 'rego_filename' in the same block")
}

// If this node is inside a rego-bearing section, it must have rego or rego_filename
if regoRequiredParents[parentKey] && !hasRego && !hasRegoFilename {
return fmt.Errorf("missing 'rego' or 'rego_filename' in '%s' entry", parentKey)
}

if hasRegoFilename {
if regoFilenameValue == "" {
return fmt.Errorf("rego_filename cannot be empty")
Expand Down
14 changes: 14 additions & 0 deletions cmd/mpe/subcommands/build/core_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,20 @@ func TestBuildFile_MissingRegoErrorCase(t *testing.T) {
// Error encountered should be "failed to read rego file '/nonexistent/path/missing.rego'"
assert.Contains(t, result.Error.Error(), "failed to read rego file '/nonexistent/path/missing.rego'")
}
func TestBuildFile_NoRegoInPolicyErrorCase(t *testing.T) {
inputFile := createTempFileFromTestData(t, "error-no-rego-in-policy.yml")

result := File(inputFile, "")

// Build should fail
assert.False(t, result.Success, "Build should fail")
assert.NotNil(t, result.Error, "Should have error")

// Error encountered should indicate missing rego
assert.Contains(t, result.Error.Error(), "missing 'rego' or 'rego_filename'")
assert.Contains(t, result.Error.Error(), "policies")
}

func TestBuildFile_EmptyRegoErrorCase(t *testing.T) {
inputFile := createTempFileFromTestData(t, "error-empty-rego.yml")

Expand Down
10 changes: 10 additions & 0 deletions cmd/mpe/subcommands/build/test/error-no-rego-in-policy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
apiVersion: iamlite.manetu.io/v1alpha3
kind: PolicyDomainReference
metadata:
name: error-test
spec:
policies:
- mrn: "mrn:iam:policy:missing-rego"
name: missing-rego
description: "This policy has neither rego nor rego_filename - should error"

17 changes: 17 additions & 0 deletions cmd/mpe/subcommands/lint/core.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,23 @@ func Execute(ctx context.Context, cmd *cli.Command) error {
}
files = processedFiles

// If --regal is supplied, run only Regal linting
if cmd.Bool("regal") {
fmt.Println("Running Regal linting...")
fmt.Println()

regalErrors := performRegalLinting(ctx, files)

fmt.Println("---")
if regalErrors > 0 {
fmt.Printf("Regal linting completed: %d violation(s)\n", regalErrors)
return fmt.Errorf("regal linting failed: %d violation(s)", regalErrors)
}

fmt.Printf("Regal linting passed: %d file(s) validated successfully\n", len(files))
return nil
}

// Get OPA flags from command line, environment variable, or use default
noOpaFlags := cmd.Bool("no-opa-flags")
opaFlags := cmd.String("opa-flags")
Expand Down
142 changes: 142 additions & 0 deletions cmd/mpe/subcommands/lint/core_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
package lint

import (
"context"
"os"
"path/filepath"
"testing"
Expand Down Expand Up @@ -235,3 +236,144 @@ func TestLintFile_FailOpaCheck(t *testing.T) {
errorCount := lintRegoUsingExistingValidation([]string{validFile}, "--v0-compatible")
assert.Greater(t, errorCount, 0, "Should have OPA check errors (undefined function)")
}

// =============================================================================
// Regal linting tests
// =============================================================================

// TestSyntheticFileName tests the syntheticFileName helper function
func TestSyntheticFileName(t *testing.T) {
testCases := []struct {
name string
sourceFile string
entityType string
entityID string
expected string
}{
{
name: "Simple names",
sourceFile: "test.yml",
entityType: "policy",
entityID: "my-policy",
expected: "test.yml_policy_my-policy.rego",
},
{
name: "Entity ID with colons",
sourceFile: "domain.yml",
entityType: "library",
entityID: "mrn:iam:library:utils",
expected: "domain.yml_library_mrn_iam_library_utils.rego",
},
{
name: "Entity ID with slashes",
sourceFile: "test.yml",
entityType: "mapper",
entityID: "path/to/mapper",
expected: "test.yml_mapper_path_to_mapper.rego",
},
{
name: "Mixed special characters",
sourceFile: "input.yml",
entityType: "policy",
entityID: "mrn:ns/policy:test",
expected: "input.yml_policy_mrn_ns_policy_test.rego",
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
result := syntheticFileName(tc.sourceFile, tc.entityType, tc.entityID)
assert.Equal(t, tc.expected, result)
})
}
}

// TestPerformRegalLinting_ValidFiles tests performRegalLinting with valid test data files
func TestPerformRegalLinting_ValidFiles(t *testing.T) {
ctx := context.Background()

testCases := []struct {
name string
filename string
}{
{"Lint valid simple", "lint-valid-simple.yml"},
{"Alpha domain", "alpha.yml"},
{"Consolidated domain", "consolidated.yml"},
{"Valid alpha", "valid-alpha.yml"},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
validFile := createTempFileFromTestData(t, tc.filename)

// performRegalLinting should not panic or return a parse-failure error (1)
// It may return 0 (no violations) or >0 (Regal rule violations)
result := performRegalLinting(ctx, []string{validFile})
assert.GreaterOrEqual(t, result, 0, "performRegalLinting should succeed for %s", tc.filename)
})
}
}

// TestPerformRegalLinting_InvalidRego tests performRegalLinting with bad Rego
func TestPerformRegalLinting_InvalidRego(t *testing.T) {
ctx := context.Background()

badRegoFile := createTempFileFromTestData(t, "bad-rego.yml")

result := performRegalLinting(ctx, []string{badRegoFile})
assert.Equal(t, 1, result, "performRegalLinting should return 1 for unparseable Rego")
}

// TestPerformRegalLinting_NoRegoContent tests performRegalLinting with a file that has no Rego
func TestPerformRegalLinting_NoRegoContent(t *testing.T) {
ctx := context.Background()

noRegoFile := createTempFileFromTestData(t, "lint-no-rego.yml")

result := performRegalLinting(ctx, []string{noRegoFile})
assert.Equal(t, 0, result, "performRegalLinting should return 0 when no Rego code is found")
}

// TestPerformRegalLinting_FileNotFound tests performRegalLinting with a non-existent file
func TestPerformRegalLinting_FileNotFound(t *testing.T) {
ctx := context.Background()

// Non-existent files are silently skipped (parsers.Load fails, we continue)
result := performRegalLinting(ctx, []string{"/nonexistent/file.yml"})
assert.Equal(t, 0, result, "performRegalLinting should return 0 for non-existent files (skipped)")
}

// TestPerformRegalLinting_MultipleFiles tests performRegalLinting with multiple files
func TestPerformRegalLinting_MultipleFiles(t *testing.T) {
ctx := context.Background()

file1 := createTempFileFromTestData(t, "lint-valid-simple.yml")
file2 := createTempFileFromTestData(t, "valid-alpha.yml")

result := performRegalLinting(ctx, []string{file1, file2})
assert.GreaterOrEqual(t, result, 0, "performRegalLinting should succeed for multiple valid files")
}

// TestPerformRegalLinting_FailOpaCheck tests performRegalLinting with a file that fails OPA check
func TestPerformRegalLinting_FailOpaCheck(t *testing.T) {
ctx := context.Background()

// fail-opa-check.yml has valid Rego syntax but uses undefined functions;
// Regal should still be able to parse and lint it (violations expected)
opaCheckFile := createTempFileFromTestData(t, "fail-opa-check.yml")

result := performRegalLinting(ctx, []string{opaCheckFile})
assert.GreaterOrEqual(t, result, 0, "performRegalLinting should succeed for file with OPA check errors")
}

// TestPerformRegalLinting_MixedValidInvalid tests performRegalLinting with a mix of valid and invalid files
func TestPerformRegalLinting_MixedValidInvalid(t *testing.T) {
ctx := context.Background()

validFile := createTempFileFromTestData(t, "valid-alpha.yml")
invalidFile := createTempFileFromTestData(t, "lint-invalid-syntax.yml")

// Invalid YAML files are skipped by parsers.Load; valid files are linted
result := performRegalLinting(ctx, []string{validFile, invalidFile})
assert.GreaterOrEqual(t, result, 0, "performRegalLinting should handle mix of valid and invalid files")
}
Loading
Loading