invar-tools 1.16.0__py3-none-any.whl → 1.17.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -159,11 +159,13 @@ def text_with_pattern(
159
159
  )
160
160
  noise_lines = st.lists(noise_line, min_size=0, max_size=10)
161
161
 
162
+ # Note: Hypothesis will automatically explore different orderings of the lines,
163
+ # so explicit shuffling is unnecessary. The concatenation order is sufficient.
162
164
  return st.builds(
163
165
  lambda p, n: "\n".join(p + n),
164
166
  pattern_lines,
165
167
  noise_lines,
166
- ).map(lambda x: "\n".join(sorted(x.split("\n"), key=lambda _: __import__("random").random())))
168
+ )
167
169
 
168
170
 
169
171
  @pre(lambda pattern: len(pattern) > 0)
invar/mcp/handlers.py CHANGED
@@ -24,6 +24,11 @@ def _validate_path(path: str) -> tuple[bool, str]:
24
24
 
25
25
  Returns (is_valid, error_message).
26
26
  Rejects paths that could be interpreted as shell commands or flags.
27
+
28
+ Note: This validation is for MCP (Model Context Protocol) handlers, which
29
+ are designed to provide AI agents with access to the project filesystem.
30
+ We validate format and reject shell injection patterns, but do not restrict
31
+ to working directory (unlike CLI tools) since MCP is a trusted local protocol.
27
32
  """
28
33
  if not path:
29
34
  return True, "" # Empty path defaults to "." in handlers
@@ -38,9 +43,13 @@ def _validate_path(path: str) -> tuple[bool, str]:
38
43
  if char in path:
39
44
  return False, f"Invalid path: contains forbidden character: {char!r}"
40
45
 
41
- # Try to resolve path - this catches malformed paths
46
+ # Resolve path to canonical form, following symlinks
47
+ # This ensures path is valid and catches directory traversal attempts
42
48
  try:
43
49
  Path(path).resolve()
50
+ # Note: We don't restrict to cwd here because MCP handlers are designed
51
+ # to access the full project. If path restriction is needed, implement
52
+ # at the MCP server level, not per-handler.
44
53
  except (OSError, ValueError) as e:
45
54
  return False, f"Invalid path: {e}"
46
55
 
@@ -16,6 +16,7 @@
16
16
  import { ESLint } from 'eslint';
17
17
  import { resolve, dirname } from 'path';
18
18
  import { fileURLToPath } from 'url';
19
+ import { statSync, realpathSync } from 'fs';
19
20
  import plugin from './index.js';
20
21
  // Get directory containing this CLI script (for resolving node_modules)
21
22
  const __filename = fileURLToPath(import.meta.url);
@@ -58,15 +59,32 @@ async function main() {
58
59
  process.exit(0);
59
60
  }
60
61
  const projectPath = resolve(args.projectPath);
61
- // Validate resolved path is within current working directory or explicit allowed paths
62
+ // Validate resolved path is within current working directory
62
63
  // This prevents path traversal attacks via "../../../etc/passwd" patterns
64
+ // and symlink-based bypasses (e.g., "./symlink_inside/../../../etc/passwd")
63
65
  const cwd = process.cwd();
64
- if (!projectPath.startsWith(cwd) && !projectPath.startsWith('/')) {
65
- console.error(`Error: Project path must be within current directory`);
66
- console.error(` Requested: ${args.projectPath}`);
67
- console.error(` Resolved: ${projectPath}`);
68
- console.error(` Working dir: ${cwd}`);
69
- process.exit(1);
66
+ try {
67
+ // Use realpath to resolve symlinks and prevent bypass attacks
68
+ const realProjectPath = realpathSync(projectPath);
69
+ const realCwd = realpathSync(cwd);
70
+ if (!realProjectPath.startsWith(realCwd)) {
71
+ console.error(`Error: Project path must be within current directory`);
72
+ console.error(` Requested: ${args.projectPath}`);
73
+ console.error(` Resolved: ${realProjectPath}`);
74
+ console.error(` Working dir: ${realCwd}`);
75
+ process.exit(1);
76
+ }
77
+ }
78
+ catch (error) {
79
+ // If realpath fails (path doesn't exist), fall back to string comparison
80
+ // This allows error messages to be more specific
81
+ if (!projectPath.startsWith(cwd)) {
82
+ console.error(`Error: Project path must be within current directory`);
83
+ console.error(` Requested: ${args.projectPath}`);
84
+ console.error(` Resolved: ${projectPath}`);
85
+ console.error(` Working dir: ${cwd}`);
86
+ process.exit(1);
87
+ }
70
88
  }
71
89
  try {
72
90
  // Get the rules config for the selected mode
@@ -93,8 +111,36 @@ async function main() {
93
111
  '@invar': plugin, // Register our plugin programmatically
94
112
  },
95
113
  }); // Type assertion for ESLint config complexity
96
- // Lint the project
97
- const results = await eslint.lintFiles([projectPath]);
114
+ // Lint the project - detect if path is a file or directory
115
+ // ESLint defaults to .js only, so we need glob patterns for .ts/.tsx
116
+ let filesToLint;
117
+ try {
118
+ const stats = statSync(projectPath);
119
+ // Note: Advisory check for optimization - TOCTOU race condition is acceptable
120
+ // because ESLint will handle file system changes gracefully during actual linting
121
+ if (stats.isFile()) {
122
+ // Single file - lint it directly
123
+ filesToLint = [projectPath];
124
+ }
125
+ else if (stats.isDirectory()) {
126
+ // Directory - use glob patterns for TypeScript files primarily
127
+ // Note: Focus on TypeScript files as this is a TypeScript Guard tool
128
+ filesToLint = [
129
+ `${projectPath}/**/*.ts`,
130
+ `${projectPath}/**/*.tsx`,
131
+ ];
132
+ }
133
+ else {
134
+ console.error(`Error: Path is neither a file nor a directory: ${projectPath}`);
135
+ process.exit(1);
136
+ }
137
+ }
138
+ catch (error) {
139
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
140
+ console.error(`Error: Cannot access path: ${errorMessage}`);
141
+ process.exit(1);
142
+ }
143
+ const results = await eslint.lintFiles(filesToLint);
98
144
  // Output in standard ESLint JSON format (compatible with guard_ts.py)
99
145
  const formatter = await eslint.loadFormatter('json');
100
146
  const resultText = await Promise.resolve(formatter.format(results, {