codegate-ai 0.14.1 → 0.14.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.
package/dist/cli.js CHANGED
@@ -296,7 +296,18 @@ function addScanCommand(program, version, deps) {
296
296
  ? true
297
297
  : (baseConfig.workflow_audits?.enabled ?? false),
298
298
  },
299
- scan_user_scope: options.includeUserScope === true ? true : (baseConfig.scan_user_scope ?? false),
299
+ // When the target was a single local file that got staged into a
300
+ // temp dir (explicitCandidates set), walking the full user-scope
301
+ // tree is off-target: the user asked to scan one file, not their
302
+ // whole home. Leaving user-scope on here let sibling findings
303
+ // (e.g. `~/.agents/skills/*/SKILL.md`) leak into single-file
304
+ // scans of configs like `.claude/settings.json`. Explicit opt-in
305
+ // via `--include-user-scope` still forces it on.
306
+ scan_user_scope: options.includeUserScope === true
307
+ ? true
308
+ : resolvedTarget.explicitCandidates && resolvedTarget.explicitCandidates.length > 0
309
+ ? false
310
+ : (baseConfig.scan_user_scope ?? false),
300
311
  };
301
312
  if (options.resetState) {
302
313
  const reset = deps.resetScanState ?? ((path) => resetScanState(path));
package/dist/pipeline.js CHANGED
@@ -202,9 +202,6 @@ function layer3ErrorFinding(resourceId, status, description) {
202
202
  suppressed: false,
203
203
  });
204
204
  }
205
- function isRegistryMetadataResource(resourceId) {
206
- return (resourceId.startsWith("npm:") || resourceId.startsWith("pypi:") || resourceId.startsWith("git:"));
207
- }
208
205
  export function layer3OutcomesToFindings(outcomes, options = {}) {
209
206
  const findings = [];
210
207
  for (const outcome of outcomes) {
@@ -219,11 +216,17 @@ export function layer3OutcomesToFindings(outcomes, options = {}) {
219
216
  const parsed = parseLayer3Response(outcome.resourceId, outcome.result.metadata);
220
217
  const derived = deriveLayer3ToolFindings(outcome.resourceId, outcome.result.metadata, options);
221
218
  const combined = [...parsed, ...derived];
219
+ // If a Layer 3 resource was fetched successfully but carries no
220
+ // actionable metadata (no `findings[]`, no `tools[]`), that is not an
221
+ // issue with the scan target itself — it usually means the default
222
+ // no-outbound-call resource executor recorded only a URL stub, or that
223
+ // a host-configured MCP endpoint simply returned an unrecognised
224
+ // payload. Previously we emitted a LOW `layer3-network_error`
225
+ // "schema mismatch" finding whose `file_path` was the remote URL,
226
+ // which leaked host-level noise into every per-target scan report.
227
+ // Fetch-level anomalies that are unrelated to the scan target are now
228
+ // dropped silently for all resource kinds.
222
229
  if (combined.length === 0) {
223
- if (isRegistryMetadataResource(outcome.resourceId)) {
224
- continue;
225
- }
226
- findings.push(layer3ErrorFinding(outcome.resourceId, "network_error", "Deep scan response schema mismatch: expected metadata.findings[] or metadata.tools[]"));
227
230
  continue;
228
231
  }
229
232
  findings.push(...combined);
package/dist/scan.js CHANGED
@@ -121,6 +121,75 @@ function isRegularFile(path) {
121
121
  return false;
122
122
  }
123
123
  }
124
+ /** True when `candidatePath` resolves at or below `root`. */
125
+ function isPathInside(root, candidatePath) {
126
+ const resolvedCandidate = resolve(candidatePath);
127
+ const resolvedRoot = resolve(root);
128
+ if (resolvedCandidate === resolvedRoot) {
129
+ return true;
130
+ }
131
+ const rel = relative(resolvedRoot, resolvedCandidate);
132
+ if (rel === "" || rel === ".") {
133
+ return true;
134
+ }
135
+ if (rel.startsWith("..")) {
136
+ return false;
137
+ }
138
+ // On Windows, relative() may return an absolute path across drives.
139
+ if (rel.includes(":")) {
140
+ return false;
141
+ }
142
+ return true;
143
+ }
144
+ /**
145
+ * Decide whether a user-scope candidate at `candidatePath` should be attached
146
+ * to a scan of `scanTarget` rooted at `homeDir`.
147
+ *
148
+ * User-scope patterns (e.g. `~/.agents/skills/*/SKILL.md`) walk the whole
149
+ * home directory, so they can match files belonging to completely unrelated
150
+ * skills or agents. When the scan target is itself a specific location
151
+ * **inside** the user's home — e.g. scanning a single skill directory or a
152
+ * single config file like `~/.claude/settings.json` — any user-scope match
153
+ * that does not belong to that target is a cross-scan leak and must be
154
+ * dropped.
155
+ *
156
+ * Three cases:
157
+ * - `scanTarget` is a directory inside `homeDir`: only keep candidates inside
158
+ * that directory (existing PR #53 behavior).
159
+ * - `scanTarget` is a file inside `homeDir`: only keep candidates that resolve
160
+ * to that exact file. "Inside" semantics do not apply to files, so the
161
+ * previous check let every sibling through.
162
+ * - `scanTarget` lives outside the home directory (or cannot be stat'd,
163
+ * e.g. a URL or a staged path that has been cleaned up): user-scope
164
+ * matches are accepted as legitimate host-wide context.
165
+ */
166
+ function shouldKeepUserScopeCandidate(scanTarget, homeDir, candidatePath) {
167
+ if (!isPathInside(homeDir, scanTarget)) {
168
+ return true;
169
+ }
170
+ // Follow symlinks the same way the rest of the scan code does (walker,
171
+ // wildcard-base check, `isRegularFile`): `statSync` resolves them. If the
172
+ // target cannot be stat'd (missing / permission denied / URL that was never
173
+ // a local path), fall through to the pre-PR-#53 outside-home behavior so
174
+ // we do not over-filter project-scope scans on unusual inputs.
175
+ let targetStat;
176
+ try {
177
+ targetStat = statSync(scanTarget);
178
+ }
179
+ catch {
180
+ return true;
181
+ }
182
+ if (targetStat.isFile()) {
183
+ // Nothing is "inside" a file. The only user-scope candidate that can
184
+ // legitimately belong to a file-target scan is the file itself.
185
+ return resolve(candidatePath) === resolve(scanTarget);
186
+ }
187
+ if (targetStat.isDirectory()) {
188
+ return isPathInside(scanTarget, candidatePath);
189
+ }
190
+ // Sockets, devices, etc. — behave like the outside-home case.
191
+ return true;
192
+ }
124
193
  function toUserReportPath(pattern) {
125
194
  const normalized = normalizeUserScopePattern(pattern);
126
195
  return `~/${normalized}`;
@@ -261,6 +330,14 @@ function collectSelectedCandidates(absoluteTarget, walkedFiles, patterns, option
261
330
  const userPattern = normalizeUserScopePattern(candidate.pattern);
262
331
  if (userPattern.includes("*")) {
263
332
  for (const match of collectUserScopeWildcardMatches(options.homeDir, userPattern)) {
333
+ // A scan whose target itself lives under the user's home directory
334
+ // (e.g. a single skill at `~/.codex/skills/foo`) must only report
335
+ // findings about files inside that target. User-scope wildcards
336
+ // walk the whole home tree, so they can match sibling skills or
337
+ // other agents that belong to different scans; drop those here.
338
+ if (!shouldKeepUserScopeCandidate(absoluteTarget, options.homeDir, match.absolutePath)) {
339
+ continue;
340
+ }
264
341
  const reportPath = toUserReportPath(match.relativePath);
265
342
  if (!matchesCollectionKinds(reportPath, options.collectKinds)) {
266
343
  continue;
@@ -280,6 +357,9 @@ function collectSelectedCandidates(absoluteTarget, walkedFiles, patterns, option
280
357
  if (!existsSync(absolutePath) || !isRegularFile(absolutePath)) {
281
358
  continue;
282
359
  }
360
+ if (!shouldKeepUserScopeCandidate(absoluteTarget, options.homeDir, absolutePath)) {
361
+ continue;
362
+ }
283
363
  const reportPath = toUserReportPath(userPattern);
284
364
  if (!matchesCollectionKinds(reportPath, options.collectKinds)) {
285
365
  continue;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codegate-ai",
3
- "version": "0.14.1",
3
+ "version": "0.14.3",
4
4
  "description": "Pre-flight security scanner for AI coding tool configurations.",
5
5
  "license": "MIT",
6
6
  "type": "module",