catalyst-core 0.3.0-beta.2 → 0.3.0-beta.3

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.
@@ -209,13 +209,15 @@ async function checkEmulator(ADB_PATH) {
209
209
  }
210
210
  async function startEmulator(EMULATOR_PATH, androidConfig) {
211
211
  progress.log(`Starting emulator: ${androidConfig.emulatorName}...`);
212
- // nosemgrep: javascript.lang.security.detect-child-process.detect-child-process - EMULATOR_PATH is derived from SDK configuration and emulatorName is local native setup config.
213
- // eslint-disable-next-line security/detect-child-process
214
- (0, _child_process.exec)(`${EMULATOR_PATH} -avd ${androidConfig.emulatorName} -read-only > /dev/null &`, error => {
215
- if (error) {
216
- progress.log(`Error starting emulator: ${error}`, "error");
217
- }
212
+ // nosemgrep: javascript.lang.security.detect-child-process.detect-child-process - spawn with an argv array (no shell), so androidConfig.emulatorName can't be interpreted as shell syntax even though it comes from a locally-editable config file.
213
+ const child = (0, _child_process.spawn)(EMULATOR_PATH, ["-avd", androidConfig.emulatorName, "-read-only"], {
214
+ detached: true,
215
+ stdio: "ignore"
216
+ });
217
+ child.on("error", error => {
218
+ progress.log(`Error starting emulator: ${error}`, "error");
218
219
  });
220
+ child.unref();
219
221
  }
220
222
  async function updateLocalProperties(sdkPath) {
221
223
  progress.start("properties");
@@ -99,6 +99,7 @@ function isOfflineRoute(url, manifest) {
99
99
  const { pathname } = new URL(url)
100
100
  return (manifest.routes || []).some((route) => {
101
101
  try {
102
+ // nosemgrep: javascript.lang.security.audit.detect-non-literal-regexp.detect-non-literal-regexp - route.regex is generated at build time by generateOfflineManifest.js's routeRegex(), which escapes every regex metacharacter in literal path segments and only ever emits flat (non-nested, non-repeating) patterns for :param and * segments — it cannot produce a catastrophic-backtracking shape. The manifest itself is same-origin build output, not user-submitted at runtime.
102
103
  return new RegExp(route.regex).test(pathname)
103
104
  } catch (_) {
104
105
  return false
@@ -4,6 +4,7 @@ import { arrayToObject } from "./scriptUtils.js"
4
4
  import { fileURLToPath } from "url"
5
5
  import { dirname } from "path"
6
6
  import { readFileSync, existsSync, rmSync } from "fs"
7
+ import { createRequire } from "module"
7
8
 
8
9
  const __filename = fileURLToPath(import.meta.url)
9
10
  const __dirname = dirname(__filename)
@@ -11,16 +12,19 @@ const loaderPath = path.resolve(__dirname, "../../dist/vite/node-loader.mjs")
11
12
  const configPath = path.join(process.env.PWD, "config/config.json")
12
13
  const configJSON = JSON.parse(readFileSync(configPath), "utf-8")
13
14
 
15
+ // Resolve vite's actual JS entry point rather than spawning the "vite" command name,
16
+ // which on Windows only resolves via the npm-installed .cmd shim (i.e. requires a shell).
17
+ // Invoking process.execPath + this path directly works cross-platform with no shell.
18
+ const require = createRequire(import.meta.url)
19
+ const viteBinPath = path.join(path.dirname(require.resolve("vite/package.json")), "bin", "vite.js")
20
+
14
21
  /**
15
- * @param {string} command
22
+ * @param {string[]} args
16
23
  * @param {import('child_process').SpawnOptions} options
17
24
  */
18
- function runBuildStep(command, options) {
25
+ function runBuildStep(args, options) {
19
26
  return new Promise((resolve, reject) => {
20
- const child = spawn(command, [], {
21
- ...options,
22
- shell: true,
23
- })
27
+ const child = spawn(process.execPath, args, options)
24
28
  child.on("close", (code) => {
25
29
  if (code === 0) {
26
30
  resolve()
@@ -69,8 +73,8 @@ async function build() {
69
73
  ]),
70
74
  }
71
75
 
72
- const serverBuildCommand = `vite build --config ./dist/vite/vite.config.server.js --ssr`
73
- const clientBuildCommand = `vite build --config ./dist/vite/vite.config.client.js`
76
+ const serverBuildArgs = [viteBinPath, "build", "--config", "./dist/vite/vite.config.server.js", "--ssr"]
77
+ const clientBuildArgs = [viteBinPath, "build", "--config", "./dist/vite/vite.config.client.js"]
74
78
  const spawnBase = {
75
79
  cwd: dirname,
76
80
  stdio: "inherit",
@@ -80,11 +84,11 @@ async function build() {
80
84
 
81
85
  try {
82
86
  await Promise.all([
83
- runBuildStep(serverBuildCommand, {
87
+ runBuildStep(serverBuildArgs, {
84
88
  ...spawnBase,
85
89
  env: { ...baseEnv, CATALYST_VITE_CACHE_ID: "ssr" },
86
90
  }),
87
- runBuildStep(clientBuildCommand, {
91
+ runBuildStep(clientBuildArgs, {
88
92
  ...spawnBase,
89
93
  env: { ...baseEnv, CATALYST_VITE_CACHE_ID: "client" },
90
94
  }),
@@ -96,7 +100,7 @@ async function build() {
96
100
 
97
101
  console.log("✅ Server and client builds completed!")
98
102
 
99
- await runBuildStep("node ./dist/scripts/generateOfflineManifest.js", {
103
+ await runBuildStep(["./dist/scripts/generateOfflineManifest.js"], {
100
104
  ...spawnBase,
101
105
  env: baseEnv,
102
106
  })
@@ -68,6 +68,7 @@ async function loadAppRoutes(appRoot) {
68
68
  })
69
69
 
70
70
  try {
71
+ // nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - appRoot is the app's own build root and the remainder is a hardcoded literal path, not request input.
71
72
  const routesModule = await vite.ssrLoadModule(path.join(appRoot, "src/js/routes/utils.js"))
72
73
  return typeof routesModule.getRoutes === "function" ? routesModule.getRoutes() : []
73
74
  } finally {
@@ -23,6 +23,7 @@ export const printBundleInformation = () => {
23
23
  const files = fs.readdirSync(directoryPath)
24
24
  files.forEach((file) => {
25
25
  if (!file.includes("txt") && !file.includes("json")) {
26
+ // nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - file comes from fs.readdirSync(directoryPath), i.e. actual filenames already on disk in the build output, not request input.
26
27
  const filePath = path.join(directoryPath, file)
27
28
  const fileSize = getFileSizeSync(filePath)
28
29
  if (fileSize !== null) {
@@ -24,28 +24,30 @@ function startProd() {
24
24
 
25
25
  console.log("🚀 Starting production server...")
26
26
 
27
- const inspectFlag = commandLineArguments.includes("--inspect") ? "--inspect" : ""
28
- const command = `node ${inspectFlag} --import ${preInitPath} --loader ${loaderPath} ./dist/server/expressServer.js`
29
- spawnSync(command, [], {
30
- cwd: dirname,
31
- stdio: "inherit",
32
- shell: true,
33
- env: {
34
- ...process.env,
35
- src_path: process.env.PWD,
36
- NODE_ENV: "production",
37
- IS_DEV_COMMAND: false,
38
- APPLICATION: name || "catalyst_app",
39
- ...argumentsObject,
40
- filterKeys: JSON.stringify([
41
- "src_path",
42
- "NODE_ENV",
43
- "IS_DEV_COMMAND",
44
- "APPLICATION",
45
- ...Object.keys(argumentsObject),
46
- ]),
47
- },
48
- })
27
+ const inspectArgs = commandLineArguments.includes("--inspect") ? ["--inspect"] : []
28
+ spawnSync(
29
+ "node",
30
+ [...inspectArgs, "--import", preInitPath, "--loader", loaderPath, "./dist/server/expressServer.js"],
31
+ {
32
+ cwd: dirname,
33
+ stdio: "inherit",
34
+ env: {
35
+ ...process.env,
36
+ src_path: process.env.PWD,
37
+ NODE_ENV: "production",
38
+ IS_DEV_COMMAND: false,
39
+ APPLICATION: name || "catalyst_app",
40
+ ...argumentsObject,
41
+ filterKeys: JSON.stringify([
42
+ "src_path",
43
+ "NODE_ENV",
44
+ "IS_DEV_COMMAND",
45
+ "APPLICATION",
46
+ ...Object.keys(argumentsObject),
47
+ ]),
48
+ },
49
+ }
50
+ )
49
51
  }
50
52
 
51
53
  startProd()
@@ -22,11 +22,9 @@ function start() {
22
22
  const packageJson = JSON.parse(readFileSync(path.join(process.env.PWD, "package.json"), "utf-8"))
23
23
  const { name } = packageJson
24
24
 
25
- const command = `node --import ${preInitPath} --loader ${loaderPath} ./dist/server/expressServer.js`
26
- spawnSync(command, [], {
25
+ spawnSync("node", ["--import", preInitPath, "--loader", loaderPath, "./dist/server/expressServer.js"], {
27
26
  cwd: dirname,
28
27
  stdio: "inherit",
29
- shell: true,
30
28
  env: {
31
29
  ...process.env,
32
30
  src_path: process.env.PWD,
@@ -126,6 +126,7 @@ const isProduction = process.env.NODE_ENV === "production"
126
126
 
127
127
  function serveBuildFile(app, buildPath, urlPath, fileName, headers = {}) {
128
128
  app.get(urlPath, (_req, res, next) => {
129
+ // nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - fileName is always a hardcoded literal passed at each serveBuildFile call site, never derived from the request.
129
130
  const filePath = path.join(buildPath, fileName)
130
131
  if (!fs.existsSync(filePath)) return next()
131
132
  res.set(headers)
@@ -34,9 +34,11 @@ export function Head(props) {
34
34
  {metaTags && metaTags}
35
35
 
36
36
  {/* Inline critical CSS — prevents FOUC/CLS */}
37
+ {/* nosemgrep: typescript.react.security.audit.react-dangerouslysetinnerhtml.react-dangerouslysetinnerhtml - inlineCss is compiled CSS read from disk (readCssFromDisk), sourced from the app's own build output, never from request/user input. */}
37
38
  {inlineCss && <style dangerouslySetInnerHTML={{ __html: inlineCss }} />}
38
39
 
39
40
  {/* Deferred CSS from prior SSRs on this route — avoids late layout from post-body <style> */}
41
+ {/* nosemgrep: typescript.react.security.audit.react-dangerouslysetinnerhtml.react-dangerouslysetinnerhtml - deferredRouteInlineCss is compiled CSS read from disk (readCssFromDisk), sourced from the app's own build output, never from request/user input. */}
40
42
  {deferredRouteInlineCss && <style dangerouslySetInnerHTML={{ __html: deferredRouteInlineCss }} />}
41
43
 
42
44
  {/* JS modules */}
@@ -103,6 +103,7 @@ export const readCssFromDisk = (cssPaths = [], basePath) => {
103
103
  seen.add(asset)
104
104
  if (asset.startsWith("http")) continue
105
105
 
106
+ // nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - asset paths come from the Vite build manifest generated at build time, never from a request, so there is no user-controlled input here.
106
107
  const filePath = path.isAbsolute(asset) ? asset : path.join(basePath, asset.replace(/^\/+/, ""))
107
108
 
108
109
  try {
@@ -265,6 +265,7 @@ const _renderMarkUp = async (
265
265
  if (chunkExtractor) {
266
266
  const renderedKeys = chunkExtractor.getRenderedComponentKeys()
267
267
  this.push(
268
+ // nosemgrep: javascript.lang.security.audit.unknown-value-with-script-tag.unknown-value-with-script-tag - renderedKeys are internal bundler component-module keys tracked by ChunkExtractor, never request/user input, and are JSON.stringify-escaped before embedding.
268
269
  `<script>window.__SSR_RENDERED_COMPONENTS__=new Set(${JSON.stringify(renderedKeys)})</script>`
269
270
  )
270
271
  }
@@ -45,9 +45,11 @@ function loadAliases() {
45
45
  for (const [alias, aliasPath] of Object.entries(allAliases)) {
46
46
  if (aliasPath && typeof aliasPath === "string") {
47
47
  try {
48
+ // nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - appRoot is the app's own build root and aliasPath comes from the developer's package.json config, not request/user input.
48
49
  const resolvedPath = path.resolve(appRoot, ...aliasPath.split("/"))
49
50
  aliasCache[alias] = resolvedPath
50
51
  } catch (error) {
52
+ // nosemgrep: javascript.lang.security.audit.unsafe-formatstring.unsafe-formatstring - alias is a key from the developer's own package.json config, not attacker-controlled; console.warn here does no printf-style substitution.
51
53
  console.warn(`Failed to resolve alias ${alias}:`, error.message)
52
54
  }
53
55
  }
@@ -87,6 +89,7 @@ function resolveWithExtensions(basePath) {
87
89
  }
88
90
 
89
91
  for (const ext of extensions) {
92
+ // nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - basePath is derived from module import specifiers resolved by this plugin's own alias/relative-import logic, never from a request.
90
93
  const candidate = path.join(basePath, `index${ext}`)
91
94
  if (existsSync(candidate)) {
92
95
  return candidate
@@ -107,9 +110,11 @@ function resolveToAbsolutePath(importPath, importerId) {
107
110
  for (const [alias, aliasPath] of Object.entries(aliases)) {
108
111
  if (importPath === alias || importPath.startsWith(alias + "/")) {
109
112
  const remainingPath = importPath === alias ? "" : importPath.slice(alias.length + 1)
113
+ // nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - importPath comes from the app's own module import graph and aliasPath from the developer's package.json config, never from a request.
110
114
  const rawResolved = path.join(aliasPath, remainingPath)
111
115
  const finalResolved = resolveWithExtensions(rawResolved)
112
116
  if (finalResolved) {
117
+ // nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - finalResolved is derived from the same alias/import resolution above, not request input.
113
118
  return path.resolve(finalResolved)
114
119
  }
115
120
  }
@@ -118,6 +123,7 @@ function resolveToAbsolutePath(importPath, importerId) {
118
123
  // 2. Relative imports — resolve from the importer's directory
119
124
  if (importPath.startsWith("./") || importPath.startsWith("../")) {
120
125
  const importerDir = path.dirname(importerId)
126
+ // nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - importPath comes from the app's own module import graph and importerId from Vite's module resolution, never from a request.
121
127
  const absolutePath = path.resolve(importerDir, importPath)
122
128
  const resolved = resolveWithExtensions(absolutePath)
123
129
  if (resolved) {
@@ -149,6 +155,7 @@ function resolveImportPath(importPath, importerId) {
149
155
  // Fallback: strip leading ./ and return as-is
150
156
  return importPath.replace(/^\.\//, "")
151
157
  } catch (error) {
158
+ // nosemgrep: javascript.lang.security.audit.unsafe-formatstring.unsafe-formatstring - importPath comes from the app's own module import graph resolved by Vite at build time, not attacker-controlled; console.warn here does no printf-style substitution.
152
159
  console.warn(`Error resolving import path ${importPath}:`, error.message)
153
160
  return importPath.replace(/^\.\//, "")
154
161
  }
@@ -10,6 +10,7 @@ const emptyConfig = {
10
10
  export async function loadCustomViteConfig() {
11
11
  const configNames = ["buildConfig.js", "webpackConfig.js"]
12
12
  const configPath = configNames
13
+ // nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - configName is one of two hardcoded literals above, and process.env.src_path is the app root set by the framework's own build tooling, not request/user input.
13
14
  .map((configName) => path.join(process.env.src_path, configName))
14
15
  .find((candidatePath) => existsSync(candidatePath))
15
16
 
@@ -293,6 +293,7 @@ export function manifestCategorizationPlugin(options = {}) {
293
293
  // branch of the import graph, the affected CSS would vanish from server responses
294
294
  // with no runtime error — this check surfaces that as a build-time warning.
295
295
  function validateCssCoverage(categorizedManifest, buildDir) {
296
+ // nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - buildDir is the build tool's own output directory and the remaining segments are hardcoded literals, not request input.
296
297
  const cssDir = path.join(buildDir, "client", "assets", "css")
297
298
  if (!fs.existsSync(cssDir)) return
298
299
 
@@ -403,6 +404,7 @@ export function manifestCategorizationPlugin(options = {}) {
403
404
  })
404
405
  }
405
406
  } catch (err) {
407
+ // nosemgrep: javascript.lang.security.audit.unsafe-formatstring.unsafe-formatstring - importPath comes from the app's own module import graph resolved by Vite at build time, not attacker-controlled; console.log here does no printf-style substitution.
406
408
  console.log(`❌ Error resolving ${importPath}:`, err.message)
407
409
  }
408
410
  })
@@ -56,6 +56,7 @@ function loadAliases() {
56
56
  const resolvedPath = resolvePath(appRoot, ...aliasPath.split("/"))
57
57
  aliasCache[alias] = resolvedPath
58
58
  } catch (error) {
59
+ // nosemgrep: javascript.lang.security.audit.unsafe-formatstring.unsafe-formatstring - alias is a key from the developer's own package.json _moduleAliases config, not attacker-controlled; console.warn here does no printf-style substitution.
59
60
  console.warn(`Failed to resolve alias ${alias}:`, error.message)
60
61
  }
61
62
  }
@@ -100,6 +101,7 @@ function resolveWithExtensions(basePath) {
100
101
 
101
102
  // Try index files inside a directory
102
103
  for (const ext of extensions) {
104
+ // nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - basePath is derived from module import specifiers resolved by the Node loader itself, never from a request.
103
105
  const candidate = join(basePath, `index${ext}`)
104
106
  if (existsSync(candidate)) {
105
107
  return candidate
@@ -119,10 +121,12 @@ function resolveAlias(specifier, parentURL) {
119
121
  for (const [alias, aliasPath] of Object.entries(aliases)) {
120
122
  if (specifier.startsWith(alias + "/") || specifier === alias) {
121
123
  const remainingPath = specifier === alias ? "" : specifier.slice(alias.length + 1)
124
+ // nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - specifier comes from an ES module import statement in the app's own source code, and aliasPath from the developer's package.json config, never from a request.
122
125
  const rawResolvedPath = join(aliasPath, remainingPath)
123
126
  const finalResolvedPath = resolveWithExtensions(rawResolvedPath)
124
127
 
125
128
  if (finalResolvedPath) {
129
+ // nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - finalResolvedPath is derived from the same alias/import resolution above, not request input.
126
130
  return resolvePath(finalResolvedPath)
127
131
  }
128
132
  }
@@ -140,6 +144,7 @@ function resolveRelative(specifier, parentURL) {
140
144
  try {
141
145
  const parentPath = fileURLToPath(parentURL)
142
146
  const parentDir = dirname(parentPath)
147
+ // nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - specifier comes from an ES module import statement in the app's own source code, parentURL from the module loader itself, never from a request.
143
148
  const absolutePath = resolvePath(parentDir, specifier)
144
149
  return resolveWithExtensions(absolutePath)
145
150
  } catch {
@@ -225,6 +230,7 @@ export async function resolve(specifier, context, defaultResolve) {
225
230
  */
226
231
  function findNearestPackageType(dir) {
227
232
  while (dir !== dirname(dir)) {
233
+ // nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - dir is walked up from the module's own filesystem location via dirname(), never from a request; "package.json" is a hardcoded literal.
228
234
  const pkgPath = join(dir, "package.json")
229
235
  if (existsSync(pkgPath)) {
230
236
  try {
@@ -113,6 +113,7 @@ try {
113
113
  const packageJson = JSON.parse(packageJsonContent)
114
114
  _moduleAliases = packageJson._moduleAliases || {}
115
115
  } catch (error) {
116
+ // nosemgrep: javascript.lang.security.audit.unsafe-formatstring.unsafe-formatstring - packageJsonConfig is a build-time-resolved local path, not attacker-controlled; console.warn here does no printf-style substitution.
116
117
  console.warn(`Failed to read or parse package.json from ${packageJsonConfig}:`, error.message)
117
118
  }
118
119
 
@@ -122,6 +123,7 @@ try {
122
123
  catalyst_moduleAliases = catalystPackageJson._moduleAliases || {}
123
124
  } catch (error) {
124
125
  console.warn(
126
+ // nosemgrep: javascript.lang.security.audit.unsafe-formatstring.unsafe-formatstring - catalystPackageJsonConfig is a build-time-resolved local path, not attacker-controlled; console.warn here does no printf-style substitution.
125
127
  `Failed to read or parse catalyst package.json from ${catalystPackageJsonConfig}:`,
126
128
  error.message
127
129
  )
@@ -146,6 +148,7 @@ import { imageUrl, fontUrl } from "./scssParams.js"
146
148
  // options — so anything else is left alone and Vite falls back to its normal
147
149
  // auto-loaded postcss.config.js resolution.
148
150
  const resolvePostcssPlugins = () => {
151
+ // nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - process.env.src_path is the app root set by the framework's own build tooling, not request/user input.
149
152
  const configPath = path.join(process.env.src_path, "postcss.config.js")
150
153
  if (!fs.existsSync(configPath)) return null
151
154
 
@@ -168,6 +171,7 @@ const resolvePostcssPlugins = () => {
168
171
  return plugin(finalOptions)
169
172
  })
170
173
  } catch (error) {
174
+ // nosemgrep: javascript.lang.security.audit.unsafe-formatstring.unsafe-formatstring - configPath is a build-time-resolved local path, not attacker-controlled; console.warn here does no printf-style substitution.
171
175
  console.warn(`Failed to patch postcss config at ${configPath}:`, error.message)
172
176
  return null
173
177
  }
@@ -183,9 +187,11 @@ const alias = () => {
183
187
  return Object.keys(allAliases).reduce((moduleEnvMap, alias) => {
184
188
  if (allAliases[alias] && typeof allAliases[alias] === "string") {
185
189
  try {
190
+ // nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - process.env.src_path is the app root and allAliases comes from the developer's own package.json _moduleAliases config, not request/user input.
186
191
  const aliasPath = path.join(process.env.src_path, ...allAliases[alias].split("/"))
187
192
  moduleEnvMap[alias] = aliasPath
188
193
  } catch (error) {
194
+ // nosemgrep: javascript.lang.security.audit.unsafe-formatstring.unsafe-formatstring - alias is a key from the developer's own package.json _moduleAliases config, not attacker-controlled; console.warn here does no printf-style substitution.
189
195
  console.warn(`Failed to configure alias ${alias}:`, error.message)
190
196
  }
191
197
  }
@@ -161,6 +161,7 @@ function safeReadText(filePath, maxBytes = 8000) {
161
161
 
162
162
  function runGit(root, command) {
163
163
  try {
164
+ // nosemgrep: javascript.lang.security.detect-child-process.detect-child-process - command is always one of the fixed git/node/npm strings hardcoded at call sites, never external input.
164
165
  return execSync(command, {
165
166
  cwd: root,
166
167
  encoding: "utf8",
@@ -183,6 +184,7 @@ function slugify(input) {
183
184
  }
184
185
 
185
186
  function ensureFallbackDir(root) {
187
+ // nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - root is the project_path MCP tool argument, an intentional/documented parameter letting a local developer's coding agent point this tool at any project directory it already has filesystem access to (see mcp.js inputSchema). Not a network-facing input; the caller already has direct fs access via other tools. FALLBACK_DIR is a hardcoded literal.
186
188
  const dir = path.join(root, FALLBACK_DIR)
187
189
  fs.mkdirSync(dir, { recursive: true })
188
190
  return dir
@@ -406,7 +408,9 @@ function appendSection(sections, heading, content) {
406
408
  }
407
409
 
408
410
  function isPathInside(root, candidate) {
411
+ // nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - this is the containment-check helper itself (used by callers below to reject paths outside root); root/candidate here are the values being validated, not yet trusted for file access.
409
412
  const resolvedRoot = path.resolve(root)
413
+ // nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - this is the containment-check helper itself; candidate is the value being validated, not yet trusted for file access.
410
414
  const resolvedCandidate = path.resolve(candidate)
411
415
  const relative = path.relative(resolvedRoot, resolvedCandidate)
412
416
  return relative === "" || (relative && !relative.startsWith("..") && !path.isAbsolute(relative))
@@ -543,6 +547,7 @@ function normalizeImages(images, root) {
543
547
  continue
544
548
  }
545
549
 
550
+ // nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - raw is an MCP tool argument (attachment path) and root the project_path argument; the isPathInside() check on the next line rejects anything that resolves outside root before it is ever read.
546
551
  const absolutePath = path.isAbsolute(raw) ? path.resolve(raw) : path.resolve(root, raw)
547
552
  if (!isPathInside(root, absolutePath)) {
548
553
  localFiles.push({ path: raw, exists: false, rejected: true, reason: "outside_project_root" })
@@ -789,6 +794,7 @@ function collectRelatedFiles(root, query, explicitFiles) {
789
794
  const candidates = Array.isArray(explicitFiles) ? explicitFiles : []
790
795
  for (const file of candidates) {
791
796
  if (typeof file !== "string") continue
797
+ // nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - file is an MCP tool argument (explicit related-files list) and root the project_path argument; the isPathInside() check on the next line rejects anything that resolves outside root before it is ever read.
792
798
  const absolute = path.isAbsolute(file) ? path.resolve(file) : path.resolve(root, file)
793
799
  if (!isPathInside(root, absolute)) continue
794
800
  const content = safeReadText(absolute, 12000)
@@ -802,6 +808,7 @@ function collectRelatedFiles(root, query, explicitFiles) {
802
808
  const words = normalizeSearchText(query).split(/\s+/).filter(Boolean).slice(0, 5)
803
809
  if (words.length === 0) return files
804
810
 
811
+ // nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - root is the project_path MCP tool argument, an intentional/documented parameter (see mcp.js inputSchema); "src" is a hardcoded literal.
805
812
  const srcDir = path.join(root, "src")
806
813
  const matches = []
807
814
  function walk(dir) {
@@ -813,6 +820,7 @@ function collectRelatedFiles(root, query, explicitFiles) {
813
820
  }
814
821
  for (const entry of entries) {
815
822
  if (entry.name === "node_modules" || entry.name === ".git") continue
823
+ // nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - entry.name comes from fs.readdirSync(dir) above, i.e. actual filenames already on disk under srcDir, not request input.
816
824
  const full = path.join(dir, entry.name)
817
825
  if (entry.isDirectory()) {
818
826
  walk(full)
@@ -831,7 +839,9 @@ function collectRelatedFiles(root, query, explicitFiles) {
831
839
 
832
840
  function gatherGithubIssueContext(args = {}) {
833
841
  const root = getProjectRoot(args)
842
+ // nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - root is the project_path MCP tool argument, an intentional/documented parameter (see mcp.js inputSchema); "package.json" is a hardcoded literal.
834
843
  const pkg = safeReadJson(path.join(root, "package.json"))
844
+ // nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - root is the project_path MCP tool argument, an intentional/documented parameter (see mcp.js inputSchema); "config/config.json" is a hardcoded literal.
835
845
  const configJson = safeReadJson(path.join(root, "config", "config.json"))
836
846
  const branch = runGit(root, "git rev-parse --abbrev-ref HEAD")
837
847
  const commit = runGit(root, "git rev-parse --short HEAD")
@@ -874,6 +884,7 @@ function generateIssueMarkdown(args = {}) {
874
884
  const built = buildIssueBody(args, { enrich: true })
875
885
  const labels = resolveIssueLabels(args, built.body)
876
886
  const dir = ensureFallbackDir(root)
887
+ // nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - dir is the FALLBACK_DIR under the project_path MCP argument; the filename is a generated ISO timestamp plus slugify(title), which strips to [a-z0-9-] only, so it cannot contain path separators.
877
888
  const filePath = path.join(dir, `${new Date().toISOString().replace(/[:.]/g, "-")}-${slugify(title)}.md`)
878
889
 
879
890
  const markdown = [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "catalyst-core",
3
- "version": "0.3.0-beta.2",
3
+ "version": "0.3.0-beta.3",
4
4
  "type": "module",
5
5
  "main": "./dist/index.jsx",
6
6
  "description": "Web framework that provides great performance out of the box",
@@ -51,7 +51,7 @@
51
51
  "react-refresh": "^0.14.0",
52
52
  "react-router-dom": "^6.16.0",
53
53
  "redux": "^4.2.1",
54
- "sanitize-html": "^2.13.1",
54
+ "sanitize-html": "^2.17.6",
55
55
  "sass": "^1.89.2",
56
56
  "ua-parser-js": "^1.0.37",
57
57
  "vite": "^6.4.3",