Scan an unreleased managed package offline
An unreleased managed package is not just a repository. For most Salesforce ISVs it is the company. The Apex, the Lightning components, the flows and the metadata are the whole asset, and they are worth something precisely because nobody outside the team has read them yet.
That makes the standard security-scanning arrangement awkward. The category is built on the assumption that you will hand your source to someone else's pipeline: push the branch, let a hosted analyser clone it, wait for a dashboard. For a finished, released, public codebase, that trade is unremarkable. For a package that has not shipped, it means the first outside party to hold a complete copy of your unreleased product is a vendor you evaluated in an afternoon.
Vulkro for Salesforce is built the other way around. The analysis happens on the machine holding the code. There is no upload step to opt out of, because there is no upload step.
What offline means here
"Runs locally" is easy to say and easy to fudge. A tool can run a local agent that streams your syntax tree to a server for the expensive part. A tool can keep the code local and post every finding, with its file path and code excerpt, to a hosted issue tracker. Both of those are described as local scanning somewhere on the internet today.
So the claim worth making is narrower and checkable: no command uploads your source, and a scan transmits nothing about your code at all. The one outbound call a plain scan makes is a usage heartbeat that carries no code, no paths and no findings, and the next section prints its whole payload. Anything that would send a finding somewhere else is a separate flag you type on purpose. The scanner parses Apex, Lightning Web Components, Aura, Visualforce, flows and metadata XML from disk, runs its dataflow analysis in process, and writes the report to disk or to your terminal. Nothing in that path opens a socket.
The command
Point it at the package directory. That is the whole invocation.
VULKRO_OFFLINE=1 vulkro-sf scan ./force-app
scan takes a path to an SFDX project or to a retrieved metadata folder,
and defaults to the current directory when you omit it. If your package
lives as retrieved metadata with no sfdx-project.json beside it, add
--metadata-only and it will scan the folder as it stands. --format json or --format sarif gives you a machine-readable result for a
pipeline. The exit code is the contract: 0 for a clean scan, 1 when
findings were reported, 2 for an error.
VULKRO_OFFLINE=1 is the part that matters here, and the next section
explains exactly what it changes.
What leaves the machine, stated precisely
Being exact about this is the entire point of the page, so here is the whole list rather than a slogan.
By default, signed in and online, the CLI makes two background calls. The
first is a usage heartbeat to api.vulkro.com, at most every six hours
(and at most once an hour after a scan). It always carries the product
name, an install identifier, the binary version, the operating system,
the install's own licence state, a timestamp and a scan counter. Only when
they apply, it adds a device token while you are signed in, the language
slugs the last scan touched, the command names run since the last call,
and the editor and its version when the VS Code extension started the scan.
The second, on a signed-in machine, is a licence refresh that sends only
the device token. Neither carries file paths, project names, namespace
prefixes, findings, rule IDs or source. That is the entire payload, which
is why it is short enough to print.
The same connection is what refreshes your entitlement. Vulkro is online-first: the CLI holds a short-lived signed lease and renews it on those calls, and keeps scanning for up to fourteen days after your last licence refresh, so a disconnected week never interrupts you.
Everything else is opt-in and named. A live dependency lookup only
happens if you ask for it. Posting a finding summary to a chat channel
only happens if you pass the flag that does it. The report generators
write files. None of them are on by default, and none of them run during
a plain scan.
Running with no network access, and the trade-offs
Setting VULKRO_OFFLINE=1 refuses every outbound call, including the
heartbeat and the entitlement refresh. Nothing is sent, nothing is
attempted, and the scan runs exactly as it does online because the
analysis never needed the network in the first place.
You can verify that rather than trust it. On Linux, run the scan inside a network namespace with no route out:
unshare -rn /bin/bash -c 'VULKRO_OFFLINE=1 vulkro-sf scan ./force-app'
The scan completes. If it were quietly phoning home for anything load bearing, it could not.
The honest cost: with all egress refused, the CLI cannot renew its lease, so a machine that is permanently disconnected needs an air-gap licence file installed instead. That is the escape hatch for genuinely isolated build environments: a Pro subscriber creates the signed file in the customer portal, and license@vulkro.com helps with anything unusual. It is not the default path, and it is not something you need for the common case, which is a laptop or a build agent that has a network but is not allowed to put your package on someone else's server.
Full detail on both, including the per-call inventory, is in offline mode.
What the scan actually reads in a package
A managed package is not just Apex, and a review does not treat it that way. The scan walks the whole source directory:
- Apex classes and triggers, with cross-method dataflow, so a tainted value that enters a controller and reaches a dynamic SOQL call three methods later is followed rather than guessed at.
- Authorization enforcement, which is the single most common reason a
package comes back with findings: object and field permission checks,
sharing declarations, and the API version fork. From v67 (Summer '26),
SOQL, SOSL, DML and
Databasemethods default to user mode, an undeclared class defaults towith sharing, andWITH SECURITY_ENFORCEDis removed and will not compile. A package that still carries it needs migrating, and the scan tells you where. - Lightning Web Components and Aura, for the DOM sinks and the unescaped-render paths that turn a component property into script.
- Visualforce, for the same class of problem in the older surface.
- Flows and metadata XML, including the permission sets and profiles your package ships, which is where over-broad access usually gets granted by accident rather than by decision.
- Secrets and named credentials, because a hardcoded key in a package ships to every subscriber org that installs it.
Before you submit
Two commands turn a scan into a submission decision.
vulkro-sf presubmit . runs a focused checklist of ten checks and prints
each one as PASS or FAIL, which is the right shape for a developer loop
or a CI gate: the question is not "audit everything" but "is this change
safe to submit?" The path argument defaults to the current directory, and
--format json gives a machine-readable result for a gate that blocks on
any FAIL. The details are in the
presubmit reference.
vulkro-sf appexchange-report . renders the readiness report structured
the way a reviewer reads it, with PASS, FAIL and NOT EVALUATED sections
kept distinct so you can see what the scanner covered and what it did
not. That command requires Pro. The 14-day Pro trial covers a
submission cycle, so you can run it against the real package before
deciding anything.
Both run on the same local pass over the same source, with the same egress behaviour described above.
Why scanning before you submit saves a review round
The scanners that gate a listing run inside the review pipeline, after you have submitted. That is a reasonable design for the platform and a bad one for your calendar, because the first time you see the output is the first time you have already spent a submission attempt. A solution typically takes four to five weeks to get through review, and for a paid solution each attempt carries a $999 submission fee (Salesforce).
There is no official first-pass failure rate. The figure partners cite most often is about half, and that is an industry estimate rather than a published number. Either way, the arithmetic only points one direction: the cheapest place to fail the review is on your own machine, before the package has ever left it.
Next steps
- Vulkro for Salesforce - the full picture: what it scans, what it reports, and how the AgentExchange (formerly AppExchange) readiness workflow fits together.
vulkro-sf presubmit- the ten-check submission gate, its flags and its exit codes.- Offline mode - the per-call egress
inventory, the
VULKRO_OFFLINEbehaviour table, and how to verify the contract yourself.
Start a 14-day Pro trial at checkout in the customer portal; the plans are on the pricing page. It becomes paid Pro on day 15 unless you cancel, from the Billing page in the portal.