Skip to main content

Vulkro

The review that reads every line before it ships.

This is the review of your codebase. Vulkro reads your application on your machine, follows untrusted input to the place it does damage, and prints the path it took to get there. Vulkro for Salesforce is the same review pointed at your Salesforce build: what changes is the subject, not the product. Same code in, same verdict out, which is the only reason it is safe to gate a deploy on.

  • Runs offline
  • Deterministic
  • One binary
  • Your code stays put
$ vulkro scan .
  1,284 files  ·  47 endpoints  ·  no network calls

[CRITICAL] SQL injection in order lookup
           checkout.py:214 request.args to cursor.execute, 4 hops

[HIGH] Missing authorization on invoice download
           routes/invoice.ts:47 lookup is not scoped to the caller

  16 more at medium and below  ·  exit 1
A scan on a mid-sized service. Nothing left the machine to produce it.

01 / What it reads

Five languages, read through the frameworks they are actually written in.

Deep code analysis covers 5 languages: Python, JavaScript, TypeScript, Go, Java. Each one is parsed through its frameworks rather than as generic text, because a route declared by a decorator and a route assembled by a builder call are the same door to whoever is knocking on it.

Python

Django, Flask and FastAPI routing, ORM query construction, template rendering and the serialization layer that turns a request body into an object.

JavaScript and TypeScript

Express, Koa, NestJS and Next.js, including nested routers and mount prefixes, plus the query builders and ORMs the handlers call into.

Go

net/http, Gin, Echo and chi, including route paths assembled from variables and constants rather than written out as string literals, and gRPC services.

Java

Spring controllers and JAX-RS resources, JDBC and JPA sinks, and the XML parsers behind entity-expansion bugs. Taint analysis stays inside a single file: chapter 02 states exactly where that line sits.

Everything sitting next to the code

Dockerfiles and compose files, Terraform, Kubernetes manifests, CI configuration, dependency manifests and lockfiles. These are read on the same pass, whatever language they sit beside.

What it does not read

PHP, C#, Ruby, Kotlin and Rust source get no code analysis. Those projects still get their manifests, containers and secrets read, but if deep analysis of that language is the reason you are here, this is the wrong tool today.

The full language and framework table

02 / Depth by language

Breadth is easy to claim. This is the table that states depth.

A wall of checkmarks tells you nothing, so the blank cell is the point of this one. Java gets route mapping, framework awareness, dedicated detectors and same-file taint. It does not get cross-file taint, and no wording on this page pretends otherwise.

analysis depth by language6 languages x 5 capabilities
Analysis depth for each supported language, by capability. Each cell is full, partial or none.
LanguageRoute mappingentry pointsTaint, same filesource to sinkTaint, across filesvia call graphFramework awarenessrouters, ORMsDedicated detectorslanguage rules
PythonDjango, Flask, FastAPIFullFullFullFullFull
JavaScriptExpress, Koa, Next.jsFullFullFullFullFull
TypeScriptExpress, NestJS, Next.jsFullFullFullFullFull
Gonet/http, Gin, Echo, chiFullFullFullFullFull
Javasame-file taint onlyFullFullNoneFullFull
Terraform, Dockerfileconfiguration, not dataflowNoneNoneNoneFullFull
  • Fullruns on every scan of that language
  • Partiallimited to the cases named in the row
  • Nonenot analysed at this depth today

Cross-file taint means a source in one file reaching a sink in another through the call graph. Java is same-file today: a tainted value that leaves the method is not followed into the callee, so a Java result on a given codebase is narrower than a Python, Go or TypeScript one.

Analysis depth per language. The empty cells are stated here rather than discovered during your evaluation.

03 / Taint and dataflow

It follows the data, then says whether anything stood in the way.

Three passes: inside a function, between functions in the same file, and across files down the call graph. A finding is raised only when untrusted input reaches a dangerous sink with nothing in the path that neutralises it, and the hops are recorded as evidence instead of asserted as a conclusion.

Cross-file taint runs up to four hops for Python, JavaScript, TypeScript and Go. Java stays inside one file: a tainted value that leaves the method is not followed into the callee.

vulkro explain VULK-10424 hops
Dataflow pathSQL injection in order lookupVULK-1042
  1. 01checkout.py:214request handlersource

    order_id read from request.args, no type or format check

  2. 02services/orders.py:88helper

    passed through lookup_order(order_id) unchanged

  3. 03db/query_builder.py:41query builder

    concatenated into the WHERE clause with an f-string

  4. 04db/session.py:57sinksink

    cursor.execute(sql) runs the assembled statement

4 hops resolved. No sanitiser between the source and the sink.

The dataflow path behind one finding: every hop cites a file and a line.

The path is the product. A row that cannot name its source, its hops and its sink is a guess, and a guess is what makes a security report unreadable by the third page. When the engine cannot resolve a call it says so on the finding rather than filling the gap in, which is why the confidence ladder in chapter 08 is meaningful.

04 / The findings surface

Every finding arrives with its receipts attached.

A finding is a row: severity, CWE, rule id, the file and line it was reported at, the dataflow path behind it, and a stable fingerprint so the same finding is the same row on the next scan and in the next format.

console - findings4 findings
SeverityFindingCWERule
CRITSQL injection in order lookupcheckout.py:214CWE-89VULK-1042
HIGHMissing authorization on invoice downloadroutes/invoice.ts:47CWE-639VULK-2117
HIGHSSRF via user-supplied URLproxy-handler.ts:23CWE-918VULK-1180
MEDHardcoded API token committed to the repositoryconfig/stripe.js:9CWE-798VULK-3304
Findings ordered by severity. Every row cites the file and the line it was reported at.

Confidence travels beside severity on every row, which is the dial chapter 08 turns, and vulkro explain prints the reasoning behind a rule with no model anywhere in the loop. The fingerprint is what lets a pull-request gate tell a new finding from one that has been sitting in the file since 2019.

05 / Attack surface inventory

The endpoints you forgot you shipped.

Before looking for a single bug, the engine builds the inventory: every route the frameworks declare, the handler behind it, and whether reaching it requires a session. A missing authorization check is only visible once you know the door exists.

vulkro discover --format table3 subprojects

487 routes discovered in 3 subprojects (Express, FastAPI, chi). 12 reachable without authentication. Showing 9.

Discovered HTTP routes with handler location, authentication state and finding count.
MethodPathSubprojectHandlerAuthFindings
POST/api/v1/invoices/{id}/refundservices/billingroutes/invoice.ts:47anonymousHIGH2
GET/api/v1/invoices/{id}services/billingroutes/invoice.ts:112authenticatedMED1
POST/api/v1/webhooks/paymentsservices/billingroutes/webhooks.ts:29unknownMED1
POST/checkout/confirmapps/storefrontcheckout.py:214authenticatedCRIT1
GET/cartapps/storefrontapi/cart.py:88anonymousno findings
GET/healthzapps/storefrontapi/health.py:12anonymousno findings
POST/v1/sessionservices/identityinternal/http/session.go:64anonymousno findings
DELETE/v1/session/{sid}services/identityinternal/http/session.go:118authenticatedno findings
GET/internal/admin/usersservices/identityinternal/http/admin.go:31unknownCRIT1
Unknown means the handler runs behind a guard Vulkro could not resolve to a session check. It is listed, not scored.
The endpoint inventory Vulkro builds before it looks for a single bug.

Route recovery in a mixed-language repository is its own problem, and it is where most inventories quietly go empty. A sub-project is registered by its build marker (a pom.xml, a pyproject.toml, a go.mod) even when an enclosing project declares a different primary language, so a FastAPI backend living under a TypeScript monorepo, or a Spring backend beside a front end, is scanned as itself rather than dropped because the repository root voted for one language. Paths assembled from variables and constants are resolved rather than skipped, because a route is no less real for being built at startup.

06 / Supply chain

Dependency findings ranked by whether your code can reach them.

A version-range match tells you a package is vulnerable somewhere. Reachability asks whether any call in your project actually arrives at the affected symbol, and ranks the list accordingly. It is a ranking signal, never a proof of exploitability.

VULKRO_SCA_REACHABLE=1 vulkro scan .offline bundle
  • CRITorg.apache.commons:commons-text@1.9MavenCVE-2022-42889reachable

    pom.xmlStringSubstitutor.replace called from ReportController.render at src/main/java/com/acme/report/ReportController.java:88

  • HIGHlodash@4.17.20npmCVE-2021-23337GHSA-35jh-r3h4-6jhmreachable

    package-lock.jsontemplate() called from buildInvoice at routes/invoice.ts:47, two hops from the POST /invoices route

  • MEDurllib3@1.26.4PyPICVE-2021-33503unknown

    poetry.lockadvisory declares no vulnerable symbols, so no reachability claim is made and the severity is left as matched

  • INFOgithub.com/gin-gonic/gin@1.7.7GoCVE-2023-29401unreachable

    go.sumContext.FileAttachment is not called from the forward closure of any route, main, or plugin hookdowngraded from medium

  • INFOtime@0.1.44crates.ioCVE-2020-26235unreachable

    Cargo.lockpulled in transitively by chrono; no project function in the forward closure calls the affected symboldowngraded from high

Unreachable findings are downgraded, not dropped. Severity falls to Info, the row keeps its call-graph reason, and it stays in the report and in the JSON. A call site missed through dynamic dispatch shows up as down-ranked rather than as absent.

Manifests and lockfiles read: npm, PyPI, Go modules, crates.io, Maven. A dependency declared anywhere else is not in the SBOM and is not matched against the bundle, so it is reported as unread rather than as clean.

Dependency findings ranked by whether your code reaches the vulnerable symbol, not by the version range alone.

5 manifest formats are parsed: npm, PyPI, Go modules, crates.io, Maven. Five manifest formats are parsed. The default published bundle currently ships npm and PyPI; the wider signed bundle covers Go modules, crates.io and Maven. Matching happens entirely on your machine against a local bundle, and every bundle is checksummed on the way in. Unreachable findings are downgraded rather than dropped: a call site the analysis missed through dynamic dispatch then shows up down-ranked instead of absent, which is the failure mode you can live with. When an advisory lands at 3am, vulkro respond answers whether that package or version is anywhere in your project from a cached reverse index, offline, in about a second.

How the CVE bundle is builtSupply-chain coverage

07 / Secrets, IaC and containers

The credentials, the configuration, and what actually shipped in the image.

Application code is one layer of the same repository. The engine reads the rest of it on the same pass, and treats the built container image as a separate question with its own command.

Secrets: 104 provider rules

Credentials are matched by the shape the provider actually issues rather than by guessing at high-entropy strings, so a base64 blob is not a finding and a live key is. The git history is walked too, because deleting the line never rotated the key. Checking whether a found key still works is opt-in and is the one part of a scan that touches the network.

Infrastructure as code

Terraform, Kubernetes manifests and compose files read against an embedded misconfiguration catalog covering AWS, Azure and GCP: public storage, over-broad roles, missing encryption, trust policies that anyone can assume. Offline, from the catalog compiled into the binary.

Dockerfile linting

The build recipe itself: base images pinned to a known-vulnerable tag, a floating tag or no tag at all, and a final image that still runs as root. This runs inside the ordinary scan, because the Dockerfile is a file in your repository like any other.

Scanning a built image is a different question

Linting a Dockerfile tells you how an image is meant to be built. Scanning the image tells you what ended up inside it, including packages no line of your Dockerfile mentions: transitive OS libraries, a jar a base image carries, a Python wheel installed three layers down. Teams buy these separately, so Vulkro answers them separately.

vulkro container reads a saved tar or a local image reference and inventories the OS package databases (apk and dpkg, with rpm through the system tool) plus the application layer: Java jar manifests, node_modules packages, and Python dist-info metadata. OCI whiteout markers are honoured, so a package deleted by a later layer is not reported as present. Nested and fat jars are not recursed into today. Nothing is pulled from a registry: the scan reads the image and the local bundle, and the inventory never leaves the machine.

$ vulkro container ./api-1.9.2.tar
  apk + dpkg  ·  jars, node_modules, dist-info

[HIGH] openssl 3.0.11-r0
           CVE-2023-5678, fixed in 3.0.12-r0

[HIGH] log4j-core 2.17.0
           app/lib/log4j-core-2.17.0.jar from the base image

  layers whited out later are skipped  ·  offline match
A built image read from disk. No registry, no daemon reaching out.

08 / Adopting it on an existing codebase

Day one on a codebase with ten years of history.

The first honest objection to any scanner is the number it prints on the first run. Three controls exist for exactly that, and none of them is a quiet instruction to ignore the report.

Gate on new findings only

vulkro gate --base origin/main scans the working tree and the base ref, then reports only findings that exist in one and not the other. Exit code 1 fires on new findings alone, so ten years of inherited debt never blocks a pull request. For pipelines that prefer a committed snapshot to a git ref, vulkro baseline writes one and scan --ratchet fails only on what appeared since.

Suppress in the source, with an expiry

A vulkro-disable-next-line comment carrying the rule id and an until= date sits directly above the line it excuses. The suppression shows up in the diff, gets reviewed like code, and stops working on the date you set. No separate ignore file quietly drifting away from the thing it silenced.

Turn the volume, not the alarm off

Confidence is a ladder, not a switch: --min-confidence runs at low, medium (the default) or high. Point the release gate at high so it fires only on the strongest evidence, then read medium as a backlog and low as a forensic sweep. Nothing is deleted at any rung; the ranking is what moves.

The practical order is: run once locally to see the real number, take a baseline, put the gate on new findings at high confidence, then work the medium tier down as a backlog rather than as an emergency. Exit codes stay boring throughout: 0 for a clean scan, 1 when findings are reported, 2 when the run itself failed. A gate that cannot tell a crash from a clean scan is not a gate.

vulkro

HIGHBroken authorization on invoice download

routes/invoice.ts:47VULK-1042CWE-639

The handler looks the invoice up by the id in the path and returns the file. Nothing scopes that lookup to req.org, and requireAuth only proves the caller is signed in, not that the invoice belongs to them. Any authenticated user can download invoices from another organization by changing the number in the URL. The check belongs in the query, not in the response.

Suggested change

Line beforeLine afterChangeSource
@@ -45,7 +45,11 @@ router.get('/invoices/:id/download')
4545router.get('/invoices/:id/download', requireAuth, async (req, res) => {
46Removed line. const invoice = await db.invoice.findUnique({
47Removed line. where: {id: req.params.id},
46Added line. const invoice = await db.invoice.findFirst({
47Added line. where: {id: req.params.id, orgId: req.org.id},
4848 });
4949 
50Added line. if (!invoice) {
51Added line. return res.status(404).send('not found');
52Added line. }
53Added line.  
5054 return res.download(invoice.path);
5155});

vulkro scan · exit 1 · 1 high, 0 criticalRan on your runner. The code never left it.

What the gate posts on a pull request: one new finding, why it is wrong, and a patch the author can read.

09 / Output and evidence

One scan, then the artifacts other people ask you for.

24 machine-readable output formats come out of a single run: SARIF for the code host, NDJSON for the log pipeline, JUnit for the test lane, CSV and PDF for the humans, and pull-request comment formats for four hosts.

Inventory, then exploitability

CycloneDX and SPDX SBOMs describe what you ship. A CBOM isolates the cryptography, which is the artifact a post-quantum readiness review asks for and a library SBOM cannot answer. OpenVEX and CycloneDX-VEX then carry the exploitability verdict per CVE, and a not-affected statement cites the reachability call chain that justifies it, so an auditor can trace the reasoning rather than trust the label.

Control-level evidence

Compliance packs map findings into SOC 2 (including the full Trust Service Criteria), ISO 27001, HIPAA, PCI DSS 4.0 and NIST 800-53, writing one file per control that cites the findings and endpoints it was built from. A GDPR Article 30 records-of-processing template comes off the same pass. An evidence graph exports the whole thing as one versioned document an agent can read as ground truth.

vulkro cra-bundle . --framework soc2-fullexit 1, findings present
  • cra-readiness.zip/built on this machine
    • index.htmlreadiness one-pager
    • compliance/soc2-full, 61 controls
      • manifest.jsonframework, scan id, control summary
      • summary.mdevery control with its status
      • findings.csvfinding to control mapping, flat
      • soc2-full.htmlper-control evidence table
      • controls/one file per control
        • CC6.1.jsonPass
        • CC6.6.jsonPartial, 2 findings
        • CC7.2.jsonFail, 1 finding
        • CC8.1.jsonPass
        • P4.1.jsonPass
        • ...56 more control files
      • README.mdwhat each file is, and what it is not
    • sbom/
      • cyclonedx.jsonCycloneDX 1.6
      • spdx.jsonSPDX 2.3
    • vex/
      • openvex.jsonOpenVEX 0.2.0, per CVE

$ vulkro compliance-pack . --framework soc2-full --output ./compliance/

vulkro cra-bundle runs the same pack and writes it into one zip with the SBOM and VEX documents beside it. Every file is generated locally: nothing is uploaded, and each control file cites the findings and endpoints it was built from.

A compliance pack as it lands on disk: one file per control, each citing what it was built from.

10 / Where the review happens

A review nobody runs is not a review.

One binary is four surfaces: the scanner, a language server for the editor, an MCP server for the assistant writing the code, and the backend for the local console. Nothing extra to install, nothing to keep in version sync.

vulkro serve - 127.0.0.1:8723local desktop console

Overview

last scan 11.4s ago

1,284

Files read

47

Endpoints mapped

6

Dependency findings

11.4s

Scan time

CRITICAL1HIGH4MEDIUM9LOW4

Every view here is a projection of one offline scan: findings, dependencies, endpoints, compliance, and the diff against the previous run.

checkout-serviceScan complete: 1,284 filesoffline: no network calls
The local console reads the scan database on disk. It runs on your machine and has nowhere to upload to.

11 / One engine, more than one job

The same review, pointed at a different subject.

Vulkro reads your codebase. Vulkro for Salesforce reads your Salesforce build, including the org configuration around it. Same parser, same call graph, same taint engine, same finding format: what changes is the subject under review and the artifact it hands back.

The reviewVulkro

Reviews your code

Goes through every line of your codebase before a release, the way a senior engineer would if they had the time, and tells you what a customer could exploit.

You get: what to fix, and a pass or fail on the release
 Vulkro for Salesforce

Reviews your Salesforce build

The same review for the part of your business that runs on Salesforce, including the settings in the org itself, and what the AppExchange security review will ask you for.

You get: a straight answer on whether you are ready to submitVulkro for Salesforce
The red teamVulkro RedComing soon

Attacks what they found

Takes the review and works out what an attacker would actually do with it: which small problems chain together into a real break-in, and which ones are noise.

You get: the attack, step by step, before someone else runs itHow it works

One engine behind all three, so the red team works from what the review already foundIt all runs on your machine. Your code never leaves it.