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 +12 -1
- package/dist/pipeline.js +10 -7
- package/dist/scan.js +80 -0
- package/package.json +1 -1
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
|
-
|
|
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;
|