Skip to content

Commit 07d2538

Browse files
authored
Merge pull request #100 from mxlint/bugfix/mxlint-io-rootdir-check
Bugfix/mxlint io rootdir check
2 parents 9f2b5a4 + ca109a0 commit 07d2538

14 files changed

Lines changed: 1941 additions & 116 deletions

README.md

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,123 @@ You will see a summary of the policy evaluations in the terminal and a report in
4343

4444
Do you want to create your own policies? Please refer to our guide [Create new policy](./docs/create-new-policy.md)
4545

46+
## Subcommands Reference
47+
48+
### export-model
49+
50+
Export Mendix model to yaml files. The output is a text representation of the model. It is a one-way conversion that aims to keep the semantics yet readable for humans and computers.
51+
52+
**Usage:**
53+
```bash
54+
mxlint-cli export-model [flags]
55+
```
56+
57+
**Flags:**
58+
| Flag | Short | Default | Description |
59+
|------|-------|---------|-------------|
60+
| `--input` | `-i` | `.` | Path to directory or mpr file to export. If it's a directory, all mpr files will be exported |
61+
| `--output` | `-o` | `modelsource` | Path to directory to write the yaml files. If it doesn't exist, it will be created |
62+
| `--mode` | `-m` | `basic` | Export mode. Valid options: `basic`, `advanced` |
63+
| `--filter` | `-f` | | Regex pattern to filter units by name. Only units with names matching the pattern will be exported |
64+
| `--raw` | | `false` | If set, the output yaml will include all attributes as they are in the model |
65+
| `--appstore` | | `false` | If set, appstore modules will be included in the output |
66+
| `--verbose` | | `false` | Turn on for debug logs |
67+
68+
---
69+
70+
### lint
71+
72+
Evaluate Mendix model against rules. Requires the model to be exported first. The model is evaluated against a set of rules defined in OPA Rego files or JavaScript. The output is a list of checked rules and their outcome.
73+
74+
**Usage:**
75+
```bash
76+
mxlint-cli lint [flags]
77+
```
78+
79+
**Flags:**
80+
| Flag | Short | Default | Description |
81+
|------|-------|---------|-------------|
82+
| `--rules` | `-r` | `.mendix-cache/rules` | Path to directory with rules |
83+
| `--modelsource` | `-m` | `modelsource` | Path to directory with exported model |
84+
| `--xunit-report` | `-x` | | Path to output file for xunit report. If not provided, no xunit report will be generated |
85+
| `--json-file` | `-j` | | Path to output file for JSON report. If not provided, no JSON file will be generated |
86+
| `--ignore-noqa` | | `false` | Ignore noqa directives in documents |
87+
| `--no-cache` | | `false` | Disable caching of lint results. By default, results are cached and reused if rules and model files haven't changed |
88+
| `--verbose` | | `false` | Turn on for debug logs |
89+
90+
---
91+
92+
### serve
93+
94+
Run a server that exports model and lints whenever the input MPR file changes. Works in standalone mode and via integration with the Mendix Studio Pro extension.
95+
96+
**Usage:**
97+
```bash
98+
mxlint-cli serve [flags]
99+
```
100+
101+
**Flags:**
102+
| Flag | Short | Default | Description |
103+
|------|-------|---------|-------------|
104+
| `--input` | `-i` | `.` | Path to directory or mpr file to export. If it's a directory, all mpr files will be exported |
105+
| `--output` | `-o` | `modelsource` | Path to directory to write the yaml files. If it doesn't exist, it will be created |
106+
| `--mode` | `-m` | `basic` | Export mode. Valid options: `basic`, `advanced` |
107+
| `--rules` | `-r` | `.mendix-cache/rules` | Path to directory with rules |
108+
| `--port` | `-p` | `8082` | Port to run the server on |
109+
| `--debounce` | `-d` | `500` | Debounce time in milliseconds for file change events |
110+
| `--verbose` | | `false` | Turn on for debug logs |
111+
112+
---
113+
114+
### test-rules
115+
116+
Ensure rules are working as expected against predefined test cases. When you are developing a new rule, you can use this command to ensure it works as expected.
117+
118+
**Usage:**
119+
```bash
120+
mxlint-cli test-rules [flags]
121+
```
122+
123+
**Flags:**
124+
| Flag | Short | Default | Description |
125+
|------|-------|---------|-------------|
126+
| `--rules` | `-r` | `.mendix-cache/rules` | Path to directory with rules |
127+
| `--verbose` | | `false` | Turn on for debug logs |
128+
129+
---
130+
131+
### cache-clear
132+
133+
Clear the lint results cache. Removes all cached lint results. The cache is used to speed up repeated linting operations when rules and model files haven't changed.
134+
135+
**Usage:**
136+
```bash
137+
mxlint-cli cache-clear [flags]
138+
```
139+
140+
**Flags:**
141+
| Flag | Short | Default | Description |
142+
|------|-------|---------|-------------|
143+
| `--verbose` | | `false` | Turn on for debug logs |
144+
145+
---
146+
147+
### cache-stats
148+
149+
Show cache statistics. Displays information about the cached lint results, including number of entries and total size.
150+
151+
**Usage:**
152+
```bash
153+
mxlint-cli cache-stats [flags]
154+
```
155+
156+
**Flags:**
157+
| Flag | Short | Default | Description |
158+
|------|-------|---------|-------------|
159+
| `--verbose` | | `false` | Turn on for debug logs |
160+
161+
---
162+
46163
## export-model
47164

48165
Mendix models are stored in a binary file with `.mpr` extension. This project exports Mendix model to a human readable format, such as Yaml. This enables developers to use traditional code analysis tools on Mendix models. Think of quality checks like linting, code formatting, etc.

lint/lint.go

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ func printTestsuite(ts Testsuite) {
3030

3131
// EvalAllWithResults evaluates all rules and returns the results
3232
// This is similar to EvalAll but returns the results instead of just printing them
33-
func EvalAllWithResults(rulesPath string, modelSourcePath string, xunitReport string, jsonFile string, ignoreNoqa bool) (interface{}, error) {
33+
func EvalAllWithResults(rulesPath string, modelSourcePath string, xunitReport string, jsonFile string, ignoreNoqa bool, useCache bool) (interface{}, error) {
3434
rules, err := ReadRulesMetadata(rulesPath)
3535
if err != nil {
3636
return nil, err
@@ -54,7 +54,7 @@ func EvalAllWithResults(rulesPath string, modelSourcePath string, xunitReport st
5454
go func(index int, r Rule) {
5555
defer wg.Done()
5656

57-
testsuite, err := evalTestsuite(r, modelSourcePath, ignoreNoqa)
57+
testsuite, err := evalTestsuite(r, modelSourcePath, ignoreNoqa, useCache)
5858
if err != nil {
5959
errChan <- err
6060
return
@@ -138,7 +138,7 @@ func EvalAllWithResults(rulesPath string, modelSourcePath string, xunitReport st
138138
return testsuitesContainer, nil
139139
}
140140

141-
func EvalAll(rulesPath string, modelSourcePath string, xunitReport string, jsonFile string, ignoreNoqa bool) error {
141+
func EvalAll(rulesPath string, modelSourcePath string, xunitReport string, jsonFile string, ignoreNoqa bool, useCache bool) error {
142142
rules, err := ReadRulesMetadata(rulesPath)
143143
if err != nil {
144144
return err
@@ -162,7 +162,7 @@ func EvalAll(rulesPath string, modelSourcePath string, xunitReport string, jsonF
162162
go func(index int, r Rule) {
163163
defer wg.Done()
164164

165-
testsuite, err := evalTestsuite(r, modelSourcePath, ignoreNoqa)
165+
testsuite, err := evalTestsuite(r, modelSourcePath, ignoreNoqa, useCache)
166166
if err != nil {
167167
errChan <- err
168168
return
@@ -259,7 +259,7 @@ func countTotalTestcases(testsuites []Testsuite) int {
259259
return count
260260
}
261261

262-
func evalTestsuite(rule Rule, modelSourcePath string, ignoreNoqa bool) (*Testsuite, error) {
262+
func evalTestsuite(rule Rule, modelSourcePath string, ignoreNoqa bool, useCache bool) (*Testsuite, error) {
263263

264264
log.Debugf("evaluating rule %s", rule.Path)
265265

@@ -276,25 +276,25 @@ func evalTestsuite(rule Rule, modelSourcePath string, ignoreNoqa bool) (*Testsui
276276

277277
for _, inputFile := range inputFiles {
278278

279-
// Try to load from cache first (but skip cache if ignoreNoqa is true)
279+
// Try to load from cache first (but skip cache if ignoreNoqa is true or useCache is false)
280280
cacheKey, err := createCacheKey(rule.Path, inputFile)
281281
if err != nil {
282282
log.Debugf("Error creating cache key: %v", err)
283-
} else if !ignoreNoqa {
283+
} else if useCache && !ignoreNoqa {
284284
cachedTestcase, found := loadCachedTestcase(*cacheKey)
285285
if found {
286286
testcase = cachedTestcase
287287
log.Debugf("Using cached result for %s", inputFile)
288288
} else {
289289
// Cache miss - evaluate and save to cache
290-
testcase, err = evalTestcaseWithCaching(rule, queryString, inputFile, cacheKey, ignoreNoqa)
290+
testcase, err = evalTestcaseWithCaching(rule, queryString, inputFile, cacheKey, ignoreNoqa, modelSourcePath, useCache)
291291
if err != nil {
292292
return nil, err
293293
}
294294
}
295295
} else {
296-
// ignoreNoqa is true, skip cache and evaluate directly
297-
testcase, err = evalTestcaseWithCaching(rule, queryString, inputFile, cacheKey, ignoreNoqa)
296+
// useCache is false or ignoreNoqa is true, skip cache and evaluate directly
297+
testcase, err = evalTestcaseWithCaching(rule, queryString, inputFile, cacheKey, ignoreNoqa, modelSourcePath, useCache)
298298
if err != nil {
299299
return nil, err
300300
}
@@ -305,9 +305,9 @@ func evalTestsuite(rule Rule, modelSourcePath string, ignoreNoqa bool) (*Testsui
305305
if rule.Language == LanguageRego {
306306
testcase, err = evalTestcase_Rego(rule.Path, queryString, inputFile, rule.RuleNumber, ignoreNoqa)
307307
} else if rule.Language == LanguageJavascript {
308-
testcase, err = evalTestcase_Javascript(rule.Path, inputFile, rule.RuleNumber, ignoreNoqa)
308+
testcase, err = evalTestcase_Javascript(rule.Path, inputFile, rule.RuleNumber, ignoreNoqa, modelSourcePath)
309309
} else if rule.Language == LanguageTypescript {
310-
testcase, err = evalTestcase_Typescript(rule.Path, inputFile, rule.RuleNumber, ignoreNoqa)
310+
testcase, err = evalTestcase_Typescript(rule.Path, inputFile, rule.RuleNumber, ignoreNoqa, modelSourcePath)
311311
}
312312
if err != nil {
313313
return nil, err
@@ -340,25 +340,25 @@ func evalTestsuite(rule Rule, modelSourcePath string, ignoreNoqa bool) (*Testsui
340340
}
341341

342342
// evalTestcaseWithCaching evaluates a testcase and saves the result to cache
343-
func evalTestcaseWithCaching(rule Rule, queryString string, inputFile string, cacheKey *CacheKey, ignoreNoqa bool) (*Testcase, error) {
343+
func evalTestcaseWithCaching(rule Rule, queryString string, inputFile string, cacheKey *CacheKey, ignoreNoqa bool, modelSourcePath string, useCache bool) (*Testcase, error) {
344344
var testcase *Testcase
345345
var err error
346346

347347
if rule.Language == LanguageRego {
348348
testcase, err = evalTestcase_Rego(rule.Path, queryString, inputFile, rule.RuleNumber, ignoreNoqa)
349349
} else if rule.Language == LanguageJavascript {
350-
testcase, err = evalTestcase_Javascript(rule.Path, inputFile, rule.RuleNumber, ignoreNoqa)
350+
testcase, err = evalTestcase_Javascript(rule.Path, inputFile, rule.RuleNumber, ignoreNoqa, modelSourcePath)
351351
} else if rule.Language == LanguageTypescript {
352-
testcase, err = evalTestcase_Typescript(rule.Path, inputFile, rule.RuleNumber, ignoreNoqa)
352+
testcase, err = evalTestcase_Typescript(rule.Path, inputFile, rule.RuleNumber, ignoreNoqa, modelSourcePath)
353353
}
354354

355355
if err != nil {
356356
return nil, err
357357
}
358358

359-
// Only save to cache when ignoreNoqa is false
359+
// Only save to cache when useCache is true and ignoreNoqa is false
360360
// When ignoreNoqa is true, the result might differ from the normal behavior
361-
if !ignoreNoqa {
361+
if useCache && !ignoreNoqa {
362362
if cacheErr := saveCachedTestcase(*cacheKey, testcase); cacheErr != nil {
363363
log.Debugf("Error saving to cache: %v", cacheErr)
364364
// Don't fail the evaluation if cache save fails

lint/lint_javascript.go

Lines changed: 26 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,11 @@ import (
1212
)
1313

1414
// resolvePath resolves the given path relative to the working directory and validates
15-
// that it stays within the working directory. Returns the absolute path or an error.
16-
func resolvePath(pathArg string, workingDirectory string) (string, error) {
15+
// that it stays within the allowed root. Returns the absolute path or an error.
16+
func resolvePath(pathArg string, workingDirectory string, allowedRoot string) (string, error) {
17+
if allowedRoot == "" {
18+
allowedRoot = workingDirectory
19+
}
1720
// Resolve the path relative to working directory
1821
var fullPath string
1922
if filepath.IsAbs(pathArg) {
@@ -29,15 +32,19 @@ func resolvePath(pathArg string, workingDirectory string) (string, error) {
2932
}
3033
absFullPath = filepath.Clean(absFullPath)
3134

32-
absWorkingDir, err := filepath.Abs(workingDirectory)
35+
absAllowedRoot, err := filepath.Abs(allowedRoot)
3336
if err != nil {
3437
return "", fmt.Errorf("failed to resolve working directory: %w", err)
3538
}
36-
absWorkingDir = filepath.Clean(absWorkingDir)
39+
absAllowedRoot = filepath.Clean(absAllowedRoot)
3740

38-
// Check that the resolved path is within the working directory
39-
if !strings.HasPrefix(absFullPath, absWorkingDir+string(filepath.Separator)) && absFullPath != absWorkingDir {
40-
return "", fmt.Errorf("path %q is outside working directory %q", pathArg, workingDirectory)
41+
// Check that the resolved path is within the allowed root
42+
relPath, err := filepath.Rel(absAllowedRoot, absFullPath)
43+
if err != nil {
44+
return "", fmt.Errorf("failed to resolve path: %w", err)
45+
}
46+
if relPath == ".." || strings.HasPrefix(relPath, ".."+string(filepath.Separator)) {
47+
return "", fmt.Errorf("path %q is outside modelsource root %q", absFullPath, absAllowedRoot)
4148
}
4249

4350
return absFullPath, nil
@@ -51,7 +58,7 @@ func resolvePath(pathArg string, workingDirectory string) (string, error) {
5158
// The path is resolved relative to the workingDirectory.
5259
// - mxlint.io.isdir(path): Returns true if the path is a directory, false otherwise.
5360
// The path is resolved relative to the workingDirectory.
54-
func setupJavascriptVM(workingDirectory string) *sobek.Runtime {
61+
func setupJavascriptVM(workingDirectory string, allowedRoot string) *sobek.Runtime {
5562
vm := sobek.New()
5663

5764
// Create the mxlint object
@@ -69,7 +76,7 @@ func setupJavascriptVM(workingDirectory string) *sobek.Runtime {
6976
}
7077
filepathArg := call.Argument(0).String()
7178

72-
absPath, err := resolvePath(filepathArg, workingDirectory)
79+
absPath, err := resolvePath(filepathArg, workingDirectory, allowedRoot)
7380
if err != nil {
7481
panic(vm.NewGoError(fmt.Errorf("mxlint.io.readfile: %w", err)))
7582
}
@@ -88,7 +95,7 @@ func setupJavascriptVM(workingDirectory string) *sobek.Runtime {
8895
}
8996
dirpathArg := call.Argument(0).String()
9097

91-
absPath, err := resolvePath(dirpathArg, workingDirectory)
98+
absPath, err := resolvePath(dirpathArg, workingDirectory, allowedRoot)
9299
if err != nil {
93100
panic(vm.NewGoError(fmt.Errorf("mxlint.io.listdir: %w", err)))
94101
}
@@ -114,7 +121,7 @@ func setupJavascriptVM(workingDirectory string) *sobek.Runtime {
114121
}
115122
pathArg := call.Argument(0).String()
116123

117-
absPath, err := resolvePath(pathArg, workingDirectory)
124+
absPath, err := resolvePath(pathArg, workingDirectory, allowedRoot)
118125
if err != nil {
119126
panic(vm.NewGoError(fmt.Errorf("mxlint.io.isdir: %w", err)))
120127
}
@@ -133,7 +140,7 @@ func setupJavascriptVM(workingDirectory string) *sobek.Runtime {
133140
return vm
134141
}
135142

136-
func evalTestcase_Javascript(rulePath string, inputFilePath string, ruleNumber string, ignoreNoqa bool) (*Testcase, error) {
143+
func evalTestcase_Javascript(rulePath string, inputFilePath string, ruleNumber string, ignoreNoqa bool, modelSourcePath string) (*Testcase, error) {
137144
ruleContent, _ := os.ReadFile(rulePath)
138145
log.Debugf("js file: \n%s", ruleContent)
139146

@@ -171,9 +178,13 @@ func evalTestcase_Javascript(rulePath string, inputFilePath string, ruleNumber s
171178

172179
startTime := time.Now()
173180

174-
// Use the directory containing the input file as the working directory
175-
workingDirectory := filepath.Dir(inputFilePath)
176-
vm := setupJavascriptVM(workingDirectory)
181+
// Use the modelsource path as the working directory, falling back to input file's directory
182+
workingDirectory := modelSourcePath
183+
if workingDirectory == "" {
184+
workingDirectory = filepath.Dir(inputFilePath)
185+
}
186+
allowedRoot := resolveAllowedRoot(modelSourcePath)
187+
vm := setupJavascriptVM(workingDirectory, allowedRoot)
177188
_, err = vm.RunString(string(ruleContent))
178189
if err != nil {
179190
panic(err)

0 commit comments

Comments
 (0)