Repository navigation
feat(nsolid): ns-analyze-behaviors explains a capability the package's code never imports, and calls from code it compiled from a string - #79
Conversation
…s code never imports list_behaviors now marks a behavior notInScan when the package's published code imports none of the modules it was seen calling. The skill says what that means, and how app_packages' failing checks (uses-dynamic-require, uses-eval) can explain it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
WalkthroughDocs-only change to
Commit Assessment
Verdict: APPROVE — all three findings from the latest human recheck are verifiably fixed at |
| - `compare_release_calls` for the app's release that brought it shows the call against the release before, and the package upgrades that came with it. | ||
| - `find_package` with the package name shows which apps and processes run which versions of it. | ||
| - `list_vulnerabilities` with `where: '"<package>" in packages'` shows whether the version is a known malicious or vulnerable release (`MAL-…` reports are malicious packages). | ||
| - For a `notInScan` behavior, `app_packages` with the `app` lists the version's failing checks. `risk/uses-dynamic-require` (a module imported by a computed name) or `risk/uses-eval` (code compiled from a string) could explain how it reached the module. With neither, it got there by an unusual loader (such as `module.constructor._load`) or through code that isn't in the published package, fetched at run time: either is a strong sign. |
There was a problem hiding this comment.
[P2] Align the notInScan investigation with the backend's current gates
The current backend does not mark a scanned version notInScan when it contains a computed import or eval: seenNotInScan explicitly returns false for either scan.dynamicImport.count > 0 or scan.eval.count > 0, and its unit tests cover both exclusions.
This new guidance therefore sends the analyst looking for checks that prevent the mark from being emitted for that version. Please update this paragraph and the examples above to explain that the mark requires a complete scan with neither condition. If those checks appear alongside an existing mark, first verify the exact package version and scan freshness rather than presenting them as its normal explanation.
There was a problem hiding this comment.
Right, thanks. Fixed in 1c68a46:
- Step 1 now says the mark needs a scan that read all of the version's code and found no computed import and no
eval. So neither can explain it. - This step lists what can: a function the app or another package hands it from the module, a loader the scan doesn't follow, or code on disk that isn't the published package's.
- If
risk/uses-dynamic-requireorrisk/uses-evaldo show up next to the mark, the skill treats it as a stale scan or another version. It confirms the exact version and when that version was scanned before using them.
… a computed import or eval N|Solid marks a behavior `notInScan` only when the scan read all of the version's code and found no computed import and no `eval`, so neither can explain it. The skill sent the analyst looking for those checks. It now lists what can: a function handed to the package from the module, a loader the scan doesn't follow, or code on disk that isn't the published package's. If those checks do show up next to the mark, the scan changed since the behavior was marked or they belong to another version, so the skill checks the exact version and when it was scanned first. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…iled from a string list_behaviors now marks a behavior `compiled` when its calls came from code the package compiled with `new Function` or `eval`, and weighs it a step more. The skill says what the mark means and how to judge it: a package that compiles code by design, such as a template engine, rarely runs programs or reaches the network from that code. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
@dobleuber, ready for another look when you have a moment. Your P2 is fixed in 1c68a46: the skill now says the |
dobleuber
left a comment
There was a problem hiding this comment.
Rechecked at 596d50c. The earlier notInScan gating feedback is addressed. Three remaining findings are attached inline.
| - Each behavior names the app, the package and its versions, what it `does`, its severity, the modules and functions involved, when it was first and last seen, and how many profiles showed it against how many before did not. | ||
| - `versionChange` means it arrived with a new version of the package while earlier versions never did it. That is how a compromised release of a dependency shows at run time, and it weighs a step more. | ||
| - `notInScan` means the package was seen running programs or compiling code that its published code never imports. N|Solid marks it only when the scan read all of that version's code and found no computed import and no `eval`, so neither of those explains it. It weighs a step more too, and the package version fails `observed-not-in-scan` in `app_packages`. | ||
| - `compiled` means the calls came from code the package compiled from a string (`new Function`, `eval`): the shape of an injected payload, which is how a remote code execution runs what it was handed. The compiling itself shows nowhere; the calls count for the package whose compiled code made them. It weighs a step more too. |
There was a problem hiding this comment.
[P2] Qualify which package compiled the code
compiled establishes that the call came through string-compiled code, but it does not establish which package created that code. For example, package A can create a function with new Function and hand it to package B. If B invokes it and it calls a Node.js built-in, the profiler attributes the call to the nearest identifiable package on that stack, B. I reproduced that attribution with a synthetic stack and an ordinary-code control.
Saying B compiled the function would send the investigation to the wrong package. Please describe the flag as a call from string-compiled code, and qualify package attribution rather than treating it as proof of compiler provenance.
There was a problem hiding this comment.
Right: a call from string-compiled code counts for the nearest package below it on the stack, the one that called it. So it shows who ran that code, not who compiled it. 2e113e2 says so, and that another package may have compiled the function and handed it over (a callback, an option).
| - `list_vulnerabilities` with `where: '"<package>" in packages'` shows whether the version is a known malicious or vulnerable release (`MAL-…` reports are malicious packages). | ||
| - For a `notInScan` behavior, `app_packages` with the `app` shows the version's `observed-not-in-scan` check and the modules it named. What can explain it: the app or another package hands it a function from that module (a callback or an option); a loader the scan doesn't follow (such as `module.constructor._load`); or code on disk that isn't the published package's, patched into the image or written at run time. The last is a strong sign, and so is a `drift` behavior on the same package. | ||
| - If that version's checks also include `risk/uses-dynamic-require` or `risk/uses-eval`, they don't explain the mark: the scan changed since the behavior was marked, or the checks are for another of its versions. Confirm the exact version and when it was scanned before drawing anything from them. | ||
| - For a `compiled` behavior, ask whether the package compiles code by design (a template engine, a schema validator or an expression evaluator does) and whether the calls fit what it compiles. Such packages rarely run programs or reach the network from that code. With `notInScan` or `versionChange` as well, treat it as a likely compromise. |
There was a problem hiding this comment.
[P2] Preserve the expected-use check before concluding likely compromise
A legitimate template-engine upgrade can intentionally start reading partials through node:fs from generated render functions. That can produce both compiled and versionChange, even when the calls fit the package's documented purpose.
The final sentence nevertheless instructs the analyst to treat that combination as a likely compromise, overriding the expected-use assessment earlier in the paragraph and the later distinction between unexplained and expected behavior. Please make this conclusion conditional on the calls remaining unexpected or unexplained, or on corroborating evidence.
There was a problem hiding this comment.
Agreed. In 2e113e2 it's a likely compromise only while the calls stay unexplained or other evidence backs it, with your template-engine case as the example. The check also asks about a package that hands it functions, since that one may be the compiler.
| - `compare_release_calls` for the app's release that brought it shows the call against the release before, and the package upgrades that came with it. | ||
| - `find_package` with the package name shows which apps and processes run which versions of it. | ||
| - `list_vulnerabilities` with `where: '"<package>" in packages'` shows whether the version is a known malicious or vulnerable release (`MAL-…` reports are malicious packages). | ||
| - For a `notInScan` behavior, `app_packages` with the `app` shows the version's `observed-not-in-scan` check and the modules it named. What can explain it: the app or another package hands it a function from that module (a callback or an option); a loader the scan doesn't follow (such as `module.constructor._load`); or code on disk that isn't the published package's, patched into the image or written at run time. The last is a strong sign, and so is a `drift` behavior on the same package. |
There was a problem hiding this comment.
[P3] Read the observed modules from list_behaviors
app_packages exposes the per-version failing check as group/name:severity (with its source), but its MCP response does not include the check title, details, or modules. Following this step therefore cannot retrieve the promised module evidence from that tool.
Please use app_packages to confirm the version's observed-not-in-scan check and point to list_behaviors.modules for the observed modules.
There was a problem hiding this comment.
Right, app_packages gives the check only as group/name:severity. In 2e113e2 the failing check comes from app_packages and the modules from the behavior's modules in list_behaviors.
…iler A call from code compiled from a string counts for the nearest package below it on the stack, the one that called it. Another package may have compiled that function and handed it over, so the skill no longer says the named package compiled it. A compiled behavior that also has notInScan or versionChange is a likely compromise only while its calls stay unexplained or other evidence backs it: a template engine's new version may read partials through node:fs by design. app_packages names the failing observed-not-in-scan check but not the modules; those are the behavior's modules in list_behaviors. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
dobleuber
left a comment
There was a problem hiding this comment.
One remaining finding from the full-range re-review at 2e113e2 is attached inline.
| - Each behavior names the app, the package and its versions, what it `does`, its severity, the modules and functions involved, when it was first and last seen, and how many profiles showed it against how many before did not. | ||
| - `versionChange` means it arrived with a new version of the package while earlier versions never did it. That is how a compromised release of a dependency shows at run time, and it weighs a step more. | ||
| - `notInScan` means the package was seen running programs or compiling code that its published code never imports. N|Solid marks it only when the scan read all of that version's code and found no computed import and no `eval`, so neither of those explains it. It weighs a step more too, and the package version fails `observed-not-in-scan` in `app_packages`. | ||
| - `compiled` means the calls came from code compiled from a string (`new Function`, `eval`): the shape of an injected payload, which is how a remote code execution runs what it was handed. It doesn't say which package compiled that code: the calls count for the nearest package below it on the stack, the one that called it, and another package may have compiled it and handed it over (a callback, an option). It weighs a step more too. |
There was a problem hiding this comment.
[P2] Limit compiled to at least one observed call
The behavior-level compiled flag is true if any observed call came from string-compiled code; it does not apply to every entry in modules or functions. The aggregation retains all function names while combining the flag with a maximum/OR.
I reproduced a single package version with a generated node:fs.readFileSync call and an ordinary node:fs.writeFileSync call. Both functions belong to the same capability record, which has compiled: true. Saying "the calls came from code compiled from a string" can therefore make the analyst attribute both calls to generated code and explain away an ordinary call as expected template activity.
Please say that at least one observed call came from string-compiled code, and clarify that this flag alone does not identify which listed calls came through generated code.
The N|Solid MCP server's
list_behaviorsnow marks two kinds of behavior, andns-analyze-behaviorsexplains both.notInScan: the package was seen running programs or compiling code, but its published code imports none of the Node.js modules that do it. N|Solid marks it only when the scan read all of that version's code and found no computed import and noeval. So neither explains the mark, and the skill says what can:If the version's checks show
risk/uses-dynamic-requireorrisk/uses-evalanyway, the skill treats the scan as stale or the checks as another version's, and confirms which before using them. The package version also failsobserved-not-in-scaninapp_packages.compiled: the calls came from code the package compiled from a string (new Function,eval). That's the shape of an injected payload. The skill asks whether the package compiles code by design: a template engine, a schema validator or an expression evaluator does, and such packages rarely run programs or reach the network from that code. WithnotInScanorversionChangeas well, it treats the behavior as a likely compromise.Each mark weighs a step more. No tool or argument changes.
🤖 Generated with Claude Code