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 .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -60,4 +60,4 @@ project-settings.user.json
*.lock

modelsource
rules
/rules
6 changes: 5 additions & 1 deletion lint/lint_rego.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ func evalTestcase_Rego(rulePath string, queryString string, inputFilePath string
regoFile, _ := os.ReadFile(rulePath)
log.Debugf("rego file: \n%s", regoFile)

// Pre-process rego content to quote rulenumber in metadata
// This prevents YAML 1.1 octal interpretation of values like "002_0002"
regoContent := quoteRegoMetadataRulenumber(string(regoFile))

yamlFile, err := os.ReadFile(inputFilePath)
if err != nil {
log.Errorf("Error reading YAML file: %s\n", err)
Expand Down Expand Up @@ -51,7 +55,7 @@ func evalTestcase_Rego(rulePath string, queryString string, inputFilePath string
startTime := time.Now()
r := rego.New(
rego.Query(queryString),
rego.Load([]string{rulePath}, nil),
rego.Module(rulePath, regoContent),
rego.Input(data),
rego.Trace(true),
)
Expand Down
11 changes: 10 additions & 1 deletion lint/rules.go
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,15 @@ func runRegoTestCases(rule Rule) error {
return err
}

// Read and pre-process rego content once for all test cases
regoFile, err := os.ReadFile(rule.Path)
if err != nil {
return err
}
// Pre-process rego content to quote rulenumber in metadata
// This prevents YAML 1.1 octal interpretation of values like "002_0002"
regoContent := quoteRegoMetadataRulenumber(string(regoFile))

for _, testCase := range testCases {
var input map[string]interface{}
var allow bool
Expand Down Expand Up @@ -201,7 +210,7 @@ func runRegoTestCases(rule Rule) error {

r := rego.New(
rego.Query(queryString),
rego.Load([]string{rule.Path}, nil),
rego.Module(rule.Path, regoContent),
rego.Input(input),
rego.Trace(true),
)
Expand Down
21 changes: 21 additions & 0 deletions lint/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,27 @@ func shouldSkipRule(documentation string, ruleNumber string, ignoreNoqa bool) (b
return false, ""
}

// quoteRegoMetadataRulenumber pre-processes rego content to ensure the rulenumber
// value in YAML metadata is quoted as a string. This prevents YAML 1.1 parsers
// (like OPA's metadata parser) from interpreting values like "002_0002" as octal numbers.
func quoteRegoMetadataRulenumber(content string) string {
// Pattern matches "rulenumber: <value>" in metadata comments
// and ensures the value is quoted if it isn't already
re := regexp.MustCompile(`(#\s*rulenumber:\s*)([^"'\s][^\n]*)`)
return re.ReplaceAllStringFunc(content, func(match string) string {
parts := re.FindStringSubmatch(match)
if len(parts) == 3 {
prefix := parts[1]
value := strings.TrimSpace(parts[2])
// Only quote if not already quoted
if !strings.HasPrefix(value, `"`) && !strings.HasPrefix(value, `'`) {
return prefix + `"` + value + `"`
}
}
return match
})
}

func expandPaths(pattern string, workingDirectory string) ([]string, error) {
// backwards compatible with old filepath.glob(...)
if !strings.HasPrefix(pattern, ".*") {
Expand Down
137 changes: 101 additions & 36 deletions lint/utils_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,67 +6,67 @@ import (

func TestParseNoqaDirective(t *testing.T) {
tests := []struct {
name string
line string
name string
line string
expectedSkipAll bool
expectedRules []string
expectedReason string
expectedRules []string
expectedReason string
}{
{
name: "Skip all rules with #noqa",
line: "#noqa",
name: "Skip all rules with #noqa",
line: "#noqa",
expectedSkipAll: true,
expectedRules: nil,
expectedReason: "#noqa",
expectedRules: nil,
expectedReason: "#noqa",
},
{
name: "Skip all rules with # noqa",
line: "# noqa",
name: "Skip all rules with # noqa",
line: "# noqa",
expectedSkipAll: true,
expectedRules: nil,
expectedReason: "# noqa",
expectedRules: nil,
expectedReason: "# noqa",
},
{
name: "Skip all rules with message",
line: "#noqa This is a reason",
name: "Skip all rules with message",
line: "#noqa This is a reason",
expectedSkipAll: true,
expectedRules: nil,
expectedReason: "#noqa This is a reason",
expectedRules: nil,
expectedReason: "#noqa This is a reason",
},
{
name: "Skip specific rule",
line: "#noqa:001_0002",
name: "Skip specific rule",
line: "#noqa:001_0002",
expectedSkipAll: false,
expectedRules: []string{"001_0002"},
expectedReason: "#noqa:001_0002",
expectedRules: []string{"001_0002"},
expectedReason: "#noqa:001_0002",
},
{
name: "Skip multiple rules",
line: "#noqa:001_0002,001_0003",
name: "Skip multiple rules",
line: "#noqa:001_0002,001_0003",
expectedSkipAll: false,
expectedRules: []string{"001_0002", "001_0003"},
expectedReason: "#noqa:001_0002,001_0003",
expectedRules: []string{"001_0002", "001_0003"},
expectedReason: "#noqa:001_0002,001_0003",
},
{
name: "Skip multiple rules with reason",
line: "#noqa:001_0002,001_0003 some reason here",
name: "Skip multiple rules with reason",
line: "#noqa:001_0002,001_0003 some reason here",
expectedSkipAll: false,
expectedRules: []string{"001_0002", "001_0003"},
expectedReason: "#noqa:001_0002,001_0003 some reason here",
expectedRules: []string{"001_0002", "001_0003"},
expectedReason: "#noqa:001_0002,001_0003 some reason here",
},
{
name: "Case insensitive",
line: "#NOQA:001_0002",
name: "Case insensitive",
line: "#NOQA:001_0002",
expectedSkipAll: false,
expectedRules: []string{"001_0002"},
expectedReason: "#NOQA:001_0002",
expectedRules: []string{"001_0002"},
expectedReason: "#NOQA:001_0002",
},
{
name: "Not a noqa directive",
line: "This is not a noqa",
name: "Not a noqa directive",
line: "This is not a noqa",
expectedSkipAll: false,
expectedRules: nil,
expectedReason: "",
expectedRules: nil,
expectedReason: "",
},
}

Expand Down Expand Up @@ -193,3 +193,68 @@ func TestShouldSkipRuleWithIgnoreNoqa(t *testing.T) {
}
}

func TestQuoteRegoMetadataRulenumber(t *testing.T) {
tests := []struct {
name string
input string
expected string
}{
{
name: "Quote unquoted rulenumber with leading zeros",
input: "# rulenumber: 002_0002",
expected: `# rulenumber: "002_0002"`,
},
{
name: "Quote unquoted rulenumber without leading zeros",
input: "# rulenumber: 001_0001",
expected: `# rulenumber: "001_0001"`,
},
{
name: "Preserve already double-quoted rulenumber",
input: `# rulenumber: "002_0002"`,
expected: `# rulenumber: "002_0002"`,
},
{
name: "Preserve already single-quoted rulenumber",
input: `# rulenumber: '002_0002'`,
expected: `# rulenumber: '002_0002'`,
},
{
name: "Full metadata block with rulenumber",
input: `# METADATA
# scope: package
# title: Test Rule
# custom:
# category: Maintainability
# rulename: TestRule
# severity: MEDIUM
# rulenumber: 002_0002
# remediation: Fix it
package test`,
expected: `# METADATA
# scope: package
# title: Test Rule
# custom:
# category: Maintainability
# rulename: TestRule
# severity: MEDIUM
# rulenumber: "002_0002"
# remediation: Fix it
package test`,
},
{
name: "No rulenumber in content",
input: "# METADATA\npackage test",
expected: "# METADATA\npackage test",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := quoteRegoMetadataRulenumber(tt.input)
if result != tt.expected {
t.Errorf("Expected:\n%s\n\nGot:\n%s", tt.expected, result)
}
})
}
}
46 changes: 46 additions & 0 deletions resources/rules/001_0004_readfile_feature.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
const metadata = {
scope: "package",
title: "Project settings must have valid configuration",
description: "Validates project settings by reading related configuration files",
authors: ["Test <test@example.com>"],
custom: {
category: "Configuration",
rulename: "ProjectSettingsValidation",
severity: "LOW",
rulenumber: "001_0004",
remediation: "Ensure Settings$ProjectSettings.yaml exists and contains valid configuration",
input: ".*Security\\$ProjectSecurity\\.yaml"
}
};


function rule(input = {}) {
const errors = [];

// Use mxlint.readfile to read the Settings$ProjectSettings.yaml file
// which should be in the same directory as the Security$ProjectSecurity.yaml input file
try {
const settingsContent = mxlint.io.readfile("Settings$ProjectSettings.yaml");

// Check if the settings file contains expected content
if (!settingsContent.includes("$Type:")) {
errors.push("Settings file does not contain expected $Type field");
}

// Verify we can read the content correctly
if (!settingsContent.includes("Settings$ProjectSettings")) {
errors.push("Settings file does not contain Settings$ProjectSettings type");
}
} catch (e) {
errors.push("Failed to read Settings$ProjectSettings.yaml: " + e.message);
}

// Determine final authorization decision
const allow = errors.length === 0;

return {
allow,
errors
};
}

6 changes: 6 additions & 0 deletions resources/rules/001_0004_readfile_feature_test.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
TestCases:
- name: validates_project_settings_exist
input:
EnableDemoUsers: false
allow: true

46 changes: 46 additions & 0 deletions resources/rules/001_0004_readfile_test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
const metadata = {
scope: "package",
title: "Test mxlint.readfile function",
description: "Validates that mxlint.readfile can read files relative to the input file",
authors: ["Test <test@example.com>"],
custom: {
category: "Testing",
rulename: "ReadFileTest",
severity: "LOW",
rulenumber: "001_0004",
remediation: "N/A",
input: ".*Security\\$ProjectSecurity\\.yaml"
}
};


function rule(input = {}) {
const errors = [];

// Use mxlint.readfile to read the Settings$ProjectSettings.yaml file
// which should be in the same directory as the input file
try {
const settingsContent = mxlint.readfile("Settings$ProjectSettings.yaml");

// Check if the settings file contains expected content
if (!settingsContent.includes("$Type:")) {
errors.push("Settings file does not contain expected $Type field");
}

// Verify we can parse and check content
if (!settingsContent.includes("Settings$ProjectSettings")) {
errors.push("Settings file does not contain Settings$ProjectSettings type");
}
} catch (e) {
errors.push("Failed to read Settings$ProjectSettings.yaml: " + e.message);
}

// Determine final authorization decision
const allow = errors.length === 0;

return {
allow,
errors
};
}

6 changes: 6 additions & 0 deletions resources/rules/001_0004_readfile_test_test.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
TestCases:
- name: can_read_sibling_file
input:
EnableDemoUsers: false
allow: true

Loading
Loading