Nxellent
A full-stack Web3 security platform that correlates Smart Contract risks with Frontend vulnerabilities.
Architecture Overview
An event-driven distributed system built around a trust boundary. A NestJS API accepts scan requests and does no analysis itself; it enqueues jobs onto BullMQ and returns immediately. A separate pool of worker processes, deployed on their own VM rather than alongside the API, consumes the queue and spawns a hardened, single-use Docker container per job to execute untrusted project code. Results flow back through Redis Pub/Sub into Server-Sent Events, so the browser sees progress without polling.
The Challenges
Problem
Auditing a dApp means running `npm install` and the project's build scripts on code we have no reason to trust. That is not an edge case we tolerate. It is the core operation, and npm lifecycle scripts are arbitrary code execution by design. Running it on the API host would hand an attacker a shell on the machine holding every other client's scan results.
Solution
Every job gets its own Docker container, built to be hostile to whatever runs inside it: Linux capabilities dropped to the minimum, `no-new-privileges` set so a setuid binary cannot escalate, a non-root user inside the container, CPU and memory ceilings so a fork bomb or a memory balloon starves only itself, and the project source mounted as a temporary volume. The container is destroyed on completion or timeout, so there is no reuse, so nothing an attacker writes to disk survives into the next client's scan. The worker pool runs on a VM separate from the API, so even a full container escape lands somewhere that holds no user data and no production credentials.
worker.ts / docker-compose.vm.yml
Problem
It would be easy to describe the above as isolation and stop there. It is not isolation in the sense that word implies to a security reviewer, and the platform's own threat model has to say so.
Solution
A container is a shared-kernel boundary. Dropped capabilities and `no-new-privileges` raise the cost of an escape considerably, but a kernel vulnerability defeats all of it at once, because the untrusted code and the host are running against the same kernel. The honest description of what we built is defence in depth with a blast radius deliberately kept small, not a guarantee that untrusted code cannot get out. Two things follow from that. First, the placement decision matters more than the container flags. Putting workers on a disposable VM with no access to the primary database is what actually bounds the damage. Second, if I rebuilt this the container boundary would be replaced with a microVM (Firecracker) or a gVisor sandbox, so the untrusted build gets its own kernel and the guarantee matches the claim.
Problem
The premise of the product is that most dApp compromises are not smart-contract bugs. They are hijacked frontends, dependency-confusion attacks and DNS takeovers. But scoring both halves and reporting two numbers puts the correlation back on the reader, and a reader who could do that correlation did not need the tool.
Solution
The scoring model treats specific pairs as multiplicative rather than additive, because the risks compound in a way the individual findings do not show. An unrevoked program upgrade authority is a known and often accepted design choice on its own. A frontend with an injectable dependency is a routine finding on its own. Together they are a live path to a drained protocol: an attacker who owns the bundle can point users at a malicious instruction while the contract's own authority remains capable of changing behaviour underneath them. The engine encodes those pairings explicitly, so the report explains why a combination is worse than its parts rather than leaving two independent scores side by side.
scoring.service.ts
Problem
We had to quantify risk across Anchor program logic and JavaScript/React frontend code, two ecosystems whose findings share no severity scale, no tooling and no common notion of what 'critical' means. Averaging them produces the exact failure the product exists to prevent: a clean contract masking an insecure frontend, presented as a healthy overall grade.
Solution
A weighted normalization layer ingests raw signals from Semgrep on the frontend and custom on-chain probes on the Solana side, mapping both into a shared severity vocabulary before any arithmetic happens. On top of that sits a criticality rule that overrides the weighting: a single critical finding on either side caps the total project grade at a D no matter how clean the other side is. The cap exists precisely because a weighted average is mathematically reasonable and, here, produces a dangerous answer.
scoring.service.ts
Problem
Security scans are CPU-intensive and can take minutes. Allowing synchronous API requests would crash the server under load.
Solution
Implemented an asynchronous producer-consumer pattern using BullMQ and Redis. The API pushes a job ID and returns 'pending' immediately. A pool of isolated worker processes consumes the queue with exponential backoff for retries, and jobs carry hard timeouts so a hung build is reaped rather than occupying a worker indefinitely. This decouples the user interface from the heavy compute load and, just as importantly, keeps the untrusted-code execution on the far side of a queue boundary from the request-handling process.
contract-analyzer.processor.ts
Problem
Since scans run in background workers, the frontend had no way of knowing when a job finished without polling the API every second.
Solution
Built a Redis Pub/Sub bridge. When an isolated worker finishes a stage it publishes to a Redis channel. The API server subscribes and pushes the update to the client over Server-Sent Events, which suits a one-way progress stream better than a WebSocket and survives reconnection without extra bookkeeping. The result is a live scan log with no client-side polling.
notifications-pubsub.service.ts