<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: VIctor Martins</title>
    <description>The latest articles on DEV Community by VIctor Martins (@0xvmartins).</description>
    <link>https://dev.to/0xvmartins</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4137219%2Fce94b069-4ea6-4616-8b25-b10902756fa5.jpg</url>
      <title>DEV Community: VIctor Martins</title>
      <link>https://dev.to/0xvmartins</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/0xvmartins"/>
    <language>en</language>
    <item>
      <title>capsurface: reviewing capability changes in npm dependencies</title>
      <dc:creator>VIctor Martins</dc:creator>
      <pubDate>Tue, 22 Sep 2026 15:06:19 +0000</pubDate>
      <link>https://dev.to/0xvmartins/capsurface-reviewing-capability-changes-in-npm-dependencies-1h1</link>
      <guid>https://dev.to/0xvmartins/capsurface-reviewing-capability-changes-in-npm-dependencies-1h1</guid>
      <description>&lt;p&gt;A dependency bot opens a PR. One version changes, the tests pass, and the&lt;br&gt;
changelog says “maintenance.”&lt;/p&gt;

&lt;p&gt;You could read the package diff. You could also finish the thing you were doing before the bot opened twelve PRs.&lt;/p&gt;

&lt;p&gt;I built &lt;a href="https://github.com/VictorMartins3/capsurface" rel="noopener noreferrer"&gt;capsurface&lt;/a&gt; to make that review easier. It scans package source for filesystem, network, process and credential access, then compares the results against a baseline committed to your repository. When an update adds something that needs review, the report includes the file, line and reason for blocking it.&lt;/p&gt;

&lt;p&gt;Version 0.1.0 is available on npm. The scanner runs offline, has zero required&lt;br&gt;
dependencies and is MIT licensed. It cannot supply the afternoon you would&lt;br&gt;
need to audit every dependency, but it can give you somewhere to start.&lt;/p&gt;
&lt;h2&gt;
  
  
  A patch release with something extra
&lt;/h2&gt;

&lt;p&gt;The repository includes two versions of a fictional package, &lt;code&gt;handy-color-utils&lt;/code&gt;.&lt;br&gt;
Version 2.3.0 converts hex colors to RGB. Version 2.3.1 still converts hex colors&lt;br&gt;
to RGB, but has developed an interest in your npm credentials.&lt;/p&gt;

&lt;p&gt;The public API is unchanged. The extra work happens in a new postinstall script&lt;br&gt;
that reads files and uses network and process APIs. The package continuing to&lt;br&gt;
do its original job is part of what makes a change like this easy to overlook.&lt;/p&gt;

&lt;p&gt;Here is a selection of what the comparison reports:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Added indicator&lt;/th&gt;
&lt;th&gt;Source in version 2.3.1&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Filesystem read&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;scripts/setup.js:12&lt;/code&gt;, &lt;code&gt;fs.readFileSync(p, 'utf8')&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Network module&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;scripts/setup.js:7&lt;/code&gt;, &lt;code&gt;require('https')&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Process module&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;scripts/setup.js:8&lt;/code&gt;, &lt;code&gt;require('child_process')&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Credential-shaped path&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;scripts/setup.js:18&lt;/code&gt;, &lt;code&gt;process.env.HOME + '/.npmrc'&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Installation hook&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;package.json&lt;/code&gt;, &lt;code&gt;postinstall&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;You can run this example with Node.js and Bash:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone &lt;span class="nt"&gt;--branch&lt;/span&gt; v0.1.0 &lt;span class="nt"&gt;--depth&lt;/span&gt; 1 https://github.com/VictorMartins3/capsurface.git
&lt;span class="nb"&gt;cd &lt;/span&gt;capsurface
bash examples/run-demo.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The demo scans both versions and compares them. The check returns exit code 1;&lt;br&gt;
the demo script treats that expected failure as success. It reads the fixture&lt;br&gt;
files without executing their code.&lt;/p&gt;

&lt;p&gt;This is a synthetic example. It gives you a way to inspect the output and&lt;br&gt;
change the inputs yourself, rather than taking a screenshot on trust.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why keep a baseline?
&lt;/h2&gt;

&lt;p&gt;A package using &lt;code&gt;https&lt;/code&gt; is usually unremarkable. A package gaining access to&lt;br&gt;
&lt;code&gt;https&lt;/code&gt; after an update gives you a specific change to examine. Perhaps it now&lt;br&gt;
fetches a binary. Perhaps a previously local operation sends data to a server.&lt;br&gt;
Either way, there is source to look at and a decision to make.&lt;/p&gt;

&lt;p&gt;capsurface records the observed capabilities in &lt;code&gt;capsurface.lock.json&lt;/code&gt;. The&lt;br&gt;
baseline belongs in version control so a reviewer can see what was accepted.&lt;br&gt;
Generating the file takes a command; reviewing what goes into it takes longer.&lt;br&gt;
Committing the JSON does not retroactively perform that review.&lt;/p&gt;

&lt;p&gt;The scanner looks at supported source files throughout a package, including&lt;br&gt;
code under &lt;code&gt;test/&lt;/code&gt; and &lt;code&gt;docs/&lt;/code&gt;, as well as installation scripts. Directory names&lt;br&gt;
do not determine when code can run. The &lt;a href="https://blog.npmjs.org/post/180565383195/details-about-the-event-stream-incident" rel="noopener noreferrer"&gt;event-stream incident&lt;/a&gt;&lt;br&gt;
is a useful example: malicious code loaded encrypted data disguised as a test&lt;br&gt;
fixture during the targeted build. Blocking installation scripts alone does&lt;br&gt;
not inspect code that an application later imports.&lt;/p&gt;

&lt;p&gt;That is a reason to inspect dependency content. It is not evidence that&lt;br&gt;
capsurface would have caught event-stream: I have not reproduced that attack&lt;br&gt;
against its original artifacts, and encrypted payloads can hide the indicators&lt;br&gt;
this scanner looks for.&lt;/p&gt;
&lt;h2&gt;
  
  
  Using it on a project
&lt;/h2&gt;

&lt;p&gt;Install the CLI separately from the project you want to inspect:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--global&lt;/span&gt; &lt;span class="nt"&gt;--ignore-scripts&lt;/span&gt; capsurface@0.1.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In that project, install dependencies with scripts disabled and scan the tree:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm ci &lt;span class="nt"&gt;--ignore-scripts&lt;/span&gt;
capsurface scan-tree node_modules &lt;span class="nt"&gt;--out&lt;/span&gt; .capsurface/manifests
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Inspect the manifests before accepting them as your starting point:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;capsurface baseline .capsurface/manifests &lt;span class="nt"&gt;--out&lt;/span&gt; capsurface.lock.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Commit &lt;code&gt;capsurface.lock.json&lt;/code&gt;. Keep the generated manifests and reports out of&lt;br&gt;
Git. For a later dependency update, repeat the installation and scan, then run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;capsurface review .capsurface/manifests &lt;span class="nt"&gt;--baseline&lt;/span&gt; capsurface.lock.json &lt;span class="nt"&gt;--out&lt;/span&gt; review.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Markdown report includes blocking reasons and source locations. A blocked&lt;br&gt;
review returns 1 while still writing the report. Add &lt;code&gt;--lockfile package-lock.json&lt;/code&gt;&lt;br&gt;
to include npm dependency origins, or &lt;code&gt;--fail-on-new&lt;/code&gt; to require approval for&lt;br&gt;
new packages too. JSON and SARIF output are also available.&lt;/p&gt;

&lt;p&gt;There is a &lt;a href="https://github.com/VictorMartins3/capsurface/blob/v0.1.0/examples/workflows/capsurface.yml" rel="noopener noreferrer"&gt;GitHub Action and example workflow&lt;/a&gt;&lt;br&gt;
for running this on PRs, including Dependabot and Renovate updates. The report&lt;br&gt;
appears in the workflow job summary. Start with &lt;code&gt;report-only: 'true'&lt;/code&gt; to see how&lt;br&gt;
much review your dependency updates need. Invalid or incomplete scans still&lt;br&gt;
fail; report-only applies to the policy findings.&lt;/p&gt;

&lt;p&gt;One detail matters here: a PR can change both a dependency and its baseline.&lt;br&gt;
The Action retains the comparison against the target branch's baseline, then&lt;br&gt;
checks the proposed baseline separately. Accepting a change should not remove&lt;br&gt;
it from the reviewer's view.&lt;/p&gt;
&lt;h2&gt;
  
  
  Accepting one package's changes
&lt;/h2&gt;

&lt;p&gt;If an update is expected, you can approve the installation identified by the&lt;br&gt;
report instead of regenerating the whole baseline:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;capsurface approve .capsurface/manifests &lt;span class="nt"&gt;--baseline&lt;/span&gt; capsurface.lock.json &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--id&lt;/span&gt; &amp;lt;review-id&amp;gt; &lt;span class="nt"&gt;--reason&lt;/span&gt; &lt;span class="s2"&gt;"Reviewed the new HTTP client"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Replace &lt;code&gt;&amp;lt;review-id&amp;gt;&lt;/code&gt; with the ID from the report. The approval covers that&lt;br&gt;
installation's observed changes and binds them to its version and file content.&lt;br&gt;
Other packages stay pending. Approvals can have an expiration, and a stale&lt;br&gt;
review or incomplete scan cannot be approved.&lt;/p&gt;

&lt;p&gt;Approving one installation leaves the remaining findings visible. It helps&lt;br&gt;
avoid the familiar debugging technique of updating the expected result until&lt;br&gt;
the test agrees with you. Someone still needs to read the change and explain&lt;br&gt;
why it is acceptable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Analysis and its limits
&lt;/h2&gt;

&lt;p&gt;The default scanner uses source-text heuristics. Experimental &lt;code&gt;--deep&lt;/code&gt; adds&lt;br&gt;
optional Acorn and acorn-typescript parsers to resolve supported aliases,&lt;br&gt;
loader forms and static expressions. It also adds detail about operations&lt;br&gt;
such as process execution and environment enumeration. Neither mode executes&lt;br&gt;
the package.&lt;/p&gt;

&lt;p&gt;You can also scan before installation with &lt;code&gt;scan-lock&lt;/code&gt;. It takes local tarballs&lt;br&gt;
mapped to an npm v2/v3 lockfile, verifies their integrity and scans their&lt;br&gt;
contents. Downloading them is a separate step. This first version rejects&lt;br&gt;
workspace links, Git/local dependencies and bundled dependency trees; its&lt;br&gt;
baselines are separate from installed-tree baselines.&lt;/p&gt;

&lt;p&gt;There are straightforward ways for a capability diff to miss malicious&lt;br&gt;
behavior. A package that already reads credentials and makes network requests&lt;br&gt;
could misuse those capabilities without adding a new category. An encrypted&lt;br&gt;
payload may conceal its behavior. Finding a credential read and a network call&lt;br&gt;
in the same file does not prove that one sends data to the other.&lt;/p&gt;

&lt;p&gt;Benign updates also add capabilities, so some findings will require review&lt;br&gt;
without being security problems. capsurface does not enforce runtime&lt;br&gt;
permissions or establish that a package is safe.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://github.com/VictorMartins3/capsurface/blob/v0.1.0/docs/VERIFICATION.md" rel="noopener noreferrer"&gt;verification notes&lt;/a&gt;&lt;br&gt;
record the test inputs, scanner revisions and coverage limits. For example,&lt;br&gt;
archive scanning was compared with independent system-tar extraction across&lt;br&gt;
28 published tarballs containing 2,488 source files. The manifests and content&lt;br&gt;
hashes matched on those inputs. That checks the archive path; it is not a&lt;br&gt;
malware detection rate.&lt;/p&gt;

&lt;h2&gt;
  
  
  Trying 0.1.0
&lt;/h2&gt;

&lt;p&gt;I'd like feedback on the review itself: can you understand why an update was&lt;br&gt;
blocked, find the relevant source and decide what to approve? An unclear&lt;br&gt;
report is a useful bug. So is a small fixture that demonstrates a missed or&lt;br&gt;
incorrectly attributed capability.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://github.com/VictorMartins3/capsurface" rel="noopener noreferrer"&gt;repository&lt;/a&gt; has the demo,&lt;br&gt;
CLI documentation and issue tracker. Use the &lt;a href="https://github.com/VictorMartins3/capsurface/blob/v0.1.0/SECURITY.md" rel="noopener noreferrer"&gt;security policy&lt;/a&gt;&lt;br&gt;
for security-sensitive reports.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.npmjs.com/package/capsurface" rel="noopener noreferrer"&gt;Install from npm&lt;/a&gt; ·&lt;br&gt;
&lt;a href="https://github.com/VictorMartins3/capsurface/releases/tag/v0.1.0" rel="noopener noreferrer"&gt;0.1.0 release notes&lt;/a&gt;&lt;/p&gt;

</description>
      <category>security</category>
      <category>npm</category>
      <category>javascript</category>
      <category>node</category>
    </item>
  </channel>
</rss>
