Skip to content

Commit 4b8bb36

Browse files
committed
Fix ci and improve rulesets download
1 parent 16d2b33 commit 4b8bb36

7 files changed

Lines changed: 160 additions & 10 deletions

File tree

.github/workflows/ci.yml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,22 @@ jobs:
165165
modelsource: resources/modelsource-v1
166166
lint:
167167
xunitReport: report.xml
168+
skip:
169+
MyFirstModule/DomainModels$DomainModel:
170+
- rule: "002_0009"
171+
reason: CI fixture intentionally keeps a default value for rule coverage
172+
MyFirstModule/Home_Web.Forms$Page:
173+
- rule: "004_0001"
174+
reason: CI fixture intentionally uses inline style for rule coverage
175+
- rule: "004_0004"
176+
reason: CI fixture intentionally has heading order issue for rule coverage
177+
MyFirstModule/BadPage.Forms$Page:
178+
- rule: "004_0002"
179+
reason: CI fixture intentionally omits image alt text for rule coverage
180+
- rule: "004_0003"
181+
reason: CI fixture intentionally uses multiple h1 tags for rule coverage
182+
- rule: "004_0004"
183+
reason: CI fixture intentionally has heading order issue for rule coverage
168184
EOF
169185
./bin/mxlint --config .ci/lint.yaml lint
170186

lint/lint.go

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -370,32 +370,35 @@ func evalTestcaseWithCaching(rule Rule, queryString string, inputFile string, ca
370370

371371
func ReadRulesMetadata(rulesPath string) ([]Rule, error) {
372372
rules := make([]Rule, 0)
373-
filepath.Walk(rulesPath, func(path string, info os.FileInfo, err error) error {
373+
walkErr := filepath.Walk(rulesPath, func(path string, info os.FileInfo, err error) error {
374374
if err != nil {
375375
return err
376376
}
377377
if !info.IsDir() && !strings.HasSuffix(info.Name(), "_test.rego") && strings.HasSuffix(info.Name(), ".rego") {
378378
rule, err := parseRuleMetadata_Rego(path)
379379
if err != nil {
380-
return err
380+
return fmt.Errorf("failed to parse rego rule metadata for %s: %w", path, err)
381381
}
382382
rules = append(rules, *rule)
383383
}
384384
if !info.IsDir() && !strings.HasSuffix(info.Name(), "_test.js") && strings.HasSuffix(info.Name(), ".js") {
385385
rule, err := parseRuleMetadata_Javascript(path)
386386
if err != nil {
387-
return err
387+
return fmt.Errorf("failed to parse javascript rule metadata for %s: %w", path, err)
388388
}
389389
rules = append(rules, *rule)
390390
}
391391
if !info.IsDir() && !strings.HasSuffix(info.Name(), "_test.ts") && strings.HasSuffix(info.Name(), ".ts") {
392392
rule, err := parseRuleMetadata_Typescript(path)
393393
if err != nil {
394-
return err
394+
return fmt.Errorf("failed to parse typescript rule metadata for %s: %w", path, err)
395395
}
396396
rules = append(rules, *rule)
397397
}
398398
return nil
399399
})
400+
if walkErr != nil {
401+
return nil, walkErr
402+
}
400403
return rules, nil
401404
}

lint/lint_javascript.go

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -242,24 +242,43 @@ func parseRuleMetadata_Javascript(rulePath string) (*Rule, error) {
242242
vm := sobek.New()
243243
_, err = vm.RunString(string(ruleContent))
244244
if err != nil {
245-
panic(err)
245+
return nil, fmt.Errorf("failed to evaluate javascript rule: %w", err)
246246
}
247-
// FIXME: handle the case where metadata is not defined correctly
247+
248248
metadata := vm.Get("metadata")
249+
if metadata == nil || sobek.IsUndefined(metadata) || sobek.IsNull(metadata) {
250+
return nil, fmt.Errorf("metadata object not defined")
251+
}
249252
metadataMap := metadata.ToObject(vm)
253+
if metadataMap == nil {
254+
return nil, fmt.Errorf("metadata must be an object")
255+
}
250256

251257
var packageName string = rulePath
252258
var title string = metadataMap.Get("title").String()
253259
var description string = metadataMap.Get("description").String()
260+
if strings.TrimSpace(title) == "" || strings.TrimSpace(description) == "" {
261+
return nil, fmt.Errorf("metadata.title and metadata.description are required")
262+
}
254263

255264
// custom metadata
256-
custom := metadataMap.Get("custom").ToObject(vm)
265+
customValue := metadataMap.Get("custom")
266+
if customValue == nil || sobek.IsUndefined(customValue) || sobek.IsNull(customValue) {
267+
return nil, fmt.Errorf("metadata.custom object is required")
268+
}
269+
custom := customValue.ToObject(vm)
270+
if custom == nil {
271+
return nil, fmt.Errorf("metadata.custom must be an object")
272+
}
257273
var category string = custom.Get("category").String()
258274
var severity string = custom.Get("severity").String()
259275
var ruleNumber string = custom.Get("rulenumber").String()
260276
var remediation string = custom.Get("remediation").String()
261277
var ruleName string = custom.Get("rulename").String()
262278
var pattern string = custom.Get("input").String()
279+
if strings.TrimSpace(ruleNumber) == "" {
280+
return nil, fmt.Errorf("metadata.custom.rulenumber is required")
281+
}
263282

264283
rule := &Rule{
265284
Title: title,

lint/lint_test.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package lint
33
import (
44
"os"
55
"path/filepath"
6+
"strings"
67
"testing"
78
)
89

@@ -426,6 +427,21 @@ function rule(input) {
426427
t.Errorf("Expected 1 rule (test file should be ignored), got %d", len(rules))
427428
}
428429
})
430+
431+
t.Run("returns error for javascript rule missing metadata", func(t *testing.T) {
432+
tempDir := t.TempDir()
433+
if err := os.WriteFile(filepath.Join(tempDir, "broken.js"), []byte("function rule(input) { return { allow: true, errors: [] }; }"), 0644); err != nil {
434+
t.Fatalf("Failed to write broken js file: %v", err)
435+
}
436+
437+
_, err := ReadRulesMetadata(tempDir)
438+
if err == nil {
439+
t.Fatal("Expected ReadRulesMetadata to fail for javascript rule without metadata")
440+
}
441+
if !strings.Contains(err.Error(), "metadata object not defined") {
442+
t.Fatalf("Expected metadata error, got: %v", err)
443+
}
444+
})
429445
}
430446

431447
func TestEvalAll(t *testing.T) {

lint/lint_typscript.go

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -179,24 +179,43 @@ func parseRuleMetadata_Typescript(rulePath string) (*Rule, error) {
179179
vm := sobek.New()
180180
_, err = vm.RunString(ruleContent)
181181
if err != nil {
182-
panic(err)
182+
return nil, fmt.Errorf("failed to evaluate typescript rule: %w", err)
183183
}
184-
// FIXME: handle the case where metadata is not defined correctly
184+
185185
metadata := vm.Get("metadata")
186+
if metadata == nil || sobek.IsUndefined(metadata) || sobek.IsNull(metadata) {
187+
return nil, fmt.Errorf("metadata object not defined")
188+
}
186189
metadataMap := metadata.ToObject(vm)
190+
if metadataMap == nil {
191+
return nil, fmt.Errorf("metadata must be an object")
192+
}
187193

188194
var packageName string = rulePath
189195
var title string = metadataMap.Get("title").String()
190196
var description string = metadataMap.Get("description").String()
197+
if strings.TrimSpace(title) == "" || strings.TrimSpace(description) == "" {
198+
return nil, fmt.Errorf("metadata.title and metadata.description are required")
199+
}
191200

192201
// custom metadata
193-
custom := metadataMap.Get("custom").ToObject(vm)
202+
customValue := metadataMap.Get("custom")
203+
if customValue == nil || sobek.IsUndefined(customValue) || sobek.IsNull(customValue) {
204+
return nil, fmt.Errorf("metadata.custom object is required")
205+
}
206+
custom := customValue.ToObject(vm)
207+
if custom == nil {
208+
return nil, fmt.Errorf("metadata.custom must be an object")
209+
}
194210
var category string = custom.Get("category").String()
195211
var severity string = custom.Get("severity").String()
196212
var ruleNumber string = custom.Get("rulenumber").String()
197213
var remediation string = custom.Get("remediation").String()
198214
var ruleName string = custom.Get("rulename").String()
199215
var pattern string = custom.Get("input").String()
216+
if strings.TrimSpace(ruleNumber) == "" {
217+
return nil, fmt.Errorf("metadata.custom.rulenumber is required")
218+
}
200219

201220
rule := &Rule{
202221
Title: title,

lint/rulesets.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,18 @@ func selectRulesRoot(root string) string {
168168
}
169169

170170
func copyRulesFromPath(sourcePath string, targetPath string) error {
171+
absSourcePath, err := filepath.Abs(sourcePath)
172+
if err != nil {
173+
return fmt.Errorf("failed to resolve source path %s: %w", sourcePath, err)
174+
}
175+
absTargetPath, err := filepath.Abs(targetPath)
176+
if err != nil {
177+
return fmt.Errorf("failed to resolve target path %s: %w", targetPath, err)
178+
}
179+
if filepath.Clean(absSourcePath) == filepath.Clean(absTargetPath) {
180+
return nil
181+
}
182+
171183
info, err := os.Stat(sourcePath)
172184
if err != nil {
173185
return fmt.Errorf("ruleset path %s not found: %w", sourcePath, err)
@@ -196,6 +208,18 @@ func copyRulesFromPath(sourcePath string, targetPath string) error {
196208
}
197209

198210
func copyFile(src string, dst string) error {
211+
absSrc, err := filepath.Abs(src)
212+
if err != nil {
213+
return err
214+
}
215+
absDst, err := filepath.Abs(dst)
216+
if err != nil {
217+
return err
218+
}
219+
if filepath.Clean(absSrc) == filepath.Clean(absDst) {
220+
return nil
221+
}
222+
199223
srcFile, err := os.Open(src)
200224
if err != nil {
201225
return err

lint/rulesets_test.go

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package lint
2+
3+
import (
4+
"os"
5+
"path/filepath"
6+
"testing"
7+
)
8+
9+
func TestCopyRulesFromPath_SameSourceAndTarget_NoOp(t *testing.T) {
10+
tempDir := t.TempDir()
11+
rulesDir := filepath.Join(tempDir, "rules")
12+
if err := os.MkdirAll(filepath.Join(rulesDir, "sample"), 0755); err != nil {
13+
t.Fatalf("failed to create rules directory: %v", err)
14+
}
15+
rulePath := filepath.Join(rulesDir, "sample", "rule.js")
16+
original := []byte("const metadata = { title: 'a', description: 'b', custom: { rulenumber: '001_0001' } };")
17+
if err := os.WriteFile(rulePath, original, 0644); err != nil {
18+
t.Fatalf("failed to write sample rule: %v", err)
19+
}
20+
21+
if err := copyRulesFromPath(rulesDir, rulesDir); err != nil {
22+
t.Fatalf("copyRulesFromPath should no-op for identical source/target: %v", err)
23+
}
24+
25+
got, err := os.ReadFile(rulePath)
26+
if err != nil {
27+
t.Fatalf("failed to read sample rule after copy: %v", err)
28+
}
29+
if string(got) != string(original) {
30+
t.Fatalf("expected rule content unchanged, got %q", string(got))
31+
}
32+
}
33+
34+
func TestCopyFile_SameSourceAndDestination_NoOp(t *testing.T) {
35+
tempDir := t.TempDir()
36+
filePath := filepath.Join(tempDir, "rule.rego")
37+
original := []byte("# METADATA\n# title: x\n")
38+
if err := os.WriteFile(filePath, original, 0644); err != nil {
39+
t.Fatalf("failed to write source file: %v", err)
40+
}
41+
42+
if err := copyFile(filePath, filePath); err != nil {
43+
t.Fatalf("copyFile should no-op for identical source/destination: %v", err)
44+
}
45+
46+
got, err := os.ReadFile(filePath)
47+
if err != nil {
48+
t.Fatalf("failed to read file after copy: %v", err)
49+
}
50+
if string(got) != string(original) {
51+
t.Fatalf("expected file content unchanged, got %q", string(got))
52+
}
53+
}

0 commit comments

Comments
 (0)