Security issues, secrets and dependency risk — anchored to a file and line, and on a second run, what changed since the last one.
Most static analysers hand you a list of problems, then hand you the same list next week — because they have no memory. A finding that moved down three lines reads as a new finding; one you already decided to live with comes back every run. People stop reading the list, which is the moment the tool stops working.
Vantage gives findings an identity that survives an edit. Analyse a repository twice and the second report says what actually changed: what you fixed, what appeared, what you had already accepted — and, uniquely, what came back. A finding you fixed once and have since reintroduced is reported as reopened rather than new, because "you added this" and "your fix did not hold" call for different reactions. Every finding links to its line in a real file view, and with an API key configured you can ask a model to explain it or propose a patch — scoped to that one finding, returned as a diff you review.
Findings can leave, too: export a report as SARIF 2.1.0 for GitHub code scanning or any SARIF viewer, or post one consolidated comment on a pull request saying what that branch changed.
Scope, stated plainly. The rule engine understands JavaScript, TypeScript and Python. Secret scanning and dependency advisories work on any repository; anything else gets structural metrics and little more. A scanner that overstates its reach is one you stop trusting after the first quiet run.
This repository is the web client. The rule engine, the API and the database live in
vantage-backend. You need both to run Vantage — see Getting started.
Captured from the live instance, unedited.
The report. expressjs/express on
master, compared against a run of the 4.19.0 tag. The summary names what to
do rather than which category scored worst, and underneath is the line no
first-time scan can produce: 10 findings resolved, 10 unchanged — and
1 reopened, a problem fixed in an earlier analysis that has come back.
Findings. psf/requests. Ordered by what
to fix first rather than by severity alone, the offending line in context, how
to fix it, and the three AI actions — each scoped to this one finding.
Worth reading closely. 5 of 13 findings · 8 metrics: measurements like "this
file is long" are real but not work, so they sit behind a count rather than
burying the rest. The private keys are recognised as TLS test fixtures —
reported for confirmation, not as an incident, and kept out of the score. And
every row is marked Likely, because the rules could not confirm them;
findings they are sure about carry no label at all.
The file viewer. The whole file, the tree beside it, and findings marked in the gutter. Folders show how many findings they contain.
Starting an analysis. A repository URL, a ZIP, or — signed in — a picker of your own repositories.
- Injection and friends — SQL injection, command injection,
eval, path traversal, SSRF, weak hashing, predictable randomness, permissive CORS and unverified JWTs, for JavaScript/TypeScript and Python. A dangerous sink alone is reported as evidence; a dangerous sink a request-derived value reaches is reported as a vulnerability, and the two are graded differently. - Known vulnerabilities from OSV.dev with real CVE/GHSA identifiers, for npm and PyPI, direct and transitive.
- Committed credentials — provider-shaped tokens and entropy-checked assignments, with the value redacted everywhere it appears.
- Correctness bugs in React and Python, and structural measurements (long files, deep nesting) taken with comments and string literals stripped out.
- Confidence on every finding, and it is on screen. A finding the rules could not confirm is labelled Likely or Needs review; only proven ones can cap the grade or lead the summary. Confirmed findings are left unlabelled — marking the norm turns a flag into decoration.
- Measured, not asserted.
docs/DETECTION_BASELINE.mdin the API repository records what the rules find across ten real repositories — four deliberately vulnerable, six ordinary — including the false positives that measurement found and the classes the rules do not attempt.
- Findings are prioritised, not just sorted. Severity × confidence × how actionable the category is, computed once on the server. A leaked credential outranks a high-severity guess, and no volume of measurements can bury a security finding.
- Measurements are separated from defects. "This file is 1,050 lines long" has no fix, only a judgement, so metrics sit behind a visible count and are weighted zero in the score. A large project is not gradeable down for being large.
- Every finding says what, why, where and how, with somewhere to read more. That is enforced across the whole rule set by a test, not by convention.
- Re-run and compare. One click on a report re-analyses the same repository at the same ref. The second report says what was resolved, what is new, and what is unchanged, using a fingerprint that survives a dependency version bump, a changed line count, or code inserted above.
- Reopened findings. A problem fixed in an earlier analysis and present again is reported as reopened, not as new — detected across a window of ten analyses, so a regression five pushes later is still recognised.
- Accept what you are living with. Mark a finding Not an issue with a reason and it stops appearing on future runs of that repository — reversibly, and never silently: the count stays on screen with a toggle to reveal it.
- Trend and churn. Score over time, and which files both change often and carry findings.
- Open the file. Every located finding links to its line in a full file view, with a tree beside it and findings marked in the gutter.
- Ask a model about one finding. Explain, propose a fix, or generate a test. Fixes come back as diffs you review; Vantage never writes to your working tree.
- Take the findings elsewhere. Export SARIF 2.1.0 — validated against the OASIS schema, carrying Vantage's own cross-run fingerprints so an importer inherits stable identity instead of re-deriving a worse one from line numbers.
- Comment on a pull request. One consolidated comment saying what that branch changed, edited in place on every push rather than added to.
- Shareable reports. A report has its own URL, survives a refresh, and renders as a preview card carrying the score and the severity split wherever it is posted.
- Keyboard-first. Command palette, filter focus, and next/previous finding.
- Works with no configuration. No API key and no database required; each absence is reported rather than hidden.
- Sign in with GitHub, optionally. Reports become yours, analyses spend your own GitHub rate limit rather than a shared one, and private repositories become analysable if you separately grant it.
flowchart LR
U[Repository URL<br/>or ZIP upload] --> F[Vantage web client]
F --> A[Vantage API]
A --> R[Rule engine]
R --> O[(OSV.dev<br/>advisories)]
R --> S[Scored report]
S --> D[(Postgres)]
S --> F
F -.->|one finding at a time| G[Gemini]
G -.->|explanation or diff| F
Analysis is a job, not a request: the client starts it and then watches genuine per-stage progress over Server-Sent Events while the API fetches, extracts, indexes and runs each rule.
Two deployable units in two repositories, released independently.
flowchart TB
subgraph browser [Browser]
UI[React 19 UI]
end
subgraph vercel [vantage-frontend · Vercel]
RSC[Server Components]
RH[Route handlers<br/>session-aware proxy]
OA[OAuth callback]
end
subgraph render [vantage-backend · Render]
API[FastAPI]
RULES[Rule engine]
end
DB[(Neon Postgres)]
GH[GitHub API]
GEM[Gemini]
UI --> RSC
UI --> RH
UI -->|SSE progress · ZIP upload| API
RSC --> API
RH --> API
OA --> GH
API --> RULES
API --> DB
API --> GH
API --> GEM
Most traffic is proxied through this server's route handlers, which keeps the API origin private and lets each call carry the session. Two paths deliberately go direct to the API: the SSE progress stream, because serverless platforms buffer streaming responses, and the ZIP upload, because they cap request bodies at a few megabytes.
The OAuth exchange happens here, not on the API. That keeps the client secret off the API entirely, and makes the session cookie first-party — a cookie set by the API would be third-party in production and blocked outright by Safari and by Firefox in strict mode. The API never sets a cookie; this server reads its own and forwards the session as a bearer token.
The AI call is scoped to a single finding. The client sends a report id, a finding id, and one value from a closed set. There is no free-text parameter, so the endpoint cannot be repurposed as a general model proxy, and analysed source reaches the model fenced and marked untrusted.
Fuller reasoning: docs/ARCHITECTURE.md for the browser
side, backend architecture
for the rule engine, finding identity and persistence.
| Layer | Technology | Purpose |
|---|---|---|
| Framework | Next.js 15 (App Router), React 19 | Server Components for the report pages; route handlers as a session-aware proxy |
| Language | TypeScript 5.7 | strict plus noUncheckedIndexedAccess; lib/types.ts mirrors the API's schemas |
| Styling | Tailwind CSS v4 | Semantic tokens in app/globals.css, exposed via @theme inline |
| Components | Radix UI, cmdk, lucide-react |
Accessible primitives, command palette, icons |
| Theming | next-themes |
Light and dark, following the system by default |
| Markdown | react-markdown, remark-gfm, rehype-sanitize |
Renders model output; raw HTML is never parsed |
| Highlighting | Shiki | Lazy-loaded, dual-theme code blocks |
| Charts | — | Hand-rolled SVG in lib/scale.ts and components/charts/ |
| Testing | Vitest, Testing Library | 157 tests, run in CI on every push |
| API | FastAPI · Python 3.12 · SQLAlchemy 2 | Separate repository |
Requires Node 20+ and Python 3.12+.
The two halves are separate repositories, so clone both:
git clone https://github.com/Asmodeus14/vantage-backend
git clone https://github.com/Asmodeus14/Vantage1 — API, in one terminal:
cd vantage-backend
python -m venv menv && menv/Scripts/activate # Linux/macOS: source menv/bin/activate
pip install -r requirements.txt
cp .env.example .env
python -m uvicorn app.main:app --reload --port 50002 — Web client, in another:
cd vantage-frontend
npm install
cp .env.example .env.local
npm run devOpen http://localhost:3000. The API's interactive docs are at http://127.0.0.1:5000/docs.
Neither an AI key nor a database is required, and every absence is reported rather than hidden:
| Missing | What happens |
|---|---|
GEMINI_API_KEY |
Analysis is unaffected. The AI actions render disabled with the reason shown — no canned response is ever substituted for a model. |
DATABASE_URL |
Reports are held in memory and cleared on restart. /api/health and the UI say so. |
| Sign-in variables | Public repositories still analyse. The sign-in control renders disabled, naming the variables that are missing. |
Copy .env.example to .env.local. This repository reads six variables; the
API has its own set, documented in
its README.
Required
| Variable | Purpose |
|---|---|
BACKEND_URL |
Where the API is, for Server Components and route handlers. Server-side only — never sent to the browser, so the API origin can stay private. Defaults to http://127.0.0.1:5000. |
NEXT_PUBLIC_BACKEND_URL |
The same API, but reachable from the browser. Needed for the two paths that cannot be proxied: the SSE progress stream and the ZIP upload. Defaults to http://127.0.0.1:5000. |
Optional — sign-in. All four are required together; with any missing, sign-in reports itself unconfigured and everything else still works.
| Variable | Purpose |
|---|---|
GITHUB_CLIENT_ID |
From your GitHub OAuth App. The API needs the same value. |
GITHUB_CLIENT_SECRET |
Used only in this server's OAuth callback. The API deliberately never reads it. |
INTERNAL_API_SECRET |
Authenticates this server to the API when exchanging a GitHub token for a session. Not a user credential. Must match the API exactly. |
SESSION_SECRET |
Signs the OAuth state parameter. Stateless, so sign-in works across multiple server instances. |
Create the OAuth App at https://github.com/settings/developers with the
callback URL <your-origin>/api/auth/github/callback. .env.example carries
the commands for generating the two shared secrets.
Never commit .env.local. It is gitignored; keep it that way.
app/
├── page.tsx Analyse — repository URL, ZIP upload, repo picker
├── analysing/[jobId]/ Live SSE progress
├── r/[id]/ Report — Overview · Findings · Dependencies · Activity
│ └── f/[...path]/ File viewer with finding gutter
├── history/ Past analyses, grouped by repository
├── settings/ Account, appearance, server capabilities, shortcuts
└── api/ Route handlers — proxy to the API, plus OAuth
components/
├── report/ Report surfaces, the file viewer, AI actions
├── charts/ Hand-rolled SVG chart primitives
├── markdown/ Model-output renderer and code blocks
└── ui/ Primitives on Radix, restyled to our tokens
lib/
├── types.ts Mirrors the API's schemas — the contract
├── api.ts Typed client with uniform structured errors
├── session.ts Signed OAuth state and session cookie (server-only)
├── scale.ts Chart maths — scales, ticks, path builders
└── use-analysis-stream.ts SSE state machine
docs/ Architecture, roadmap, brand, product audit
tests/ Vitest + Testing Library
| Command | What it does |
|---|---|
npm run dev |
Development server on port 3000 |
npm run build |
Production build |
npm start |
Serve the production build |
npm run lint |
ESLint |
npm run typecheck |
tsc --noEmit |
npm run test |
Vitest, once |
npm run test:watch |
Vitest in watch mode |
All four checks run in CI on every push and pull request. Run them before
opening one — build in particular, because several App Router mistakes surface
nowhere else.
app/dev/markdown renders every markdown fixture for visual inspection of the
model-output renderer.
| Shortcut | Action |
|---|---|
⌘/Ctrl K |
Command palette |
/ |
Focus the findings filter |
j / k |
Next / previous finding |
| Unit | Platform | Notes |
|---|---|---|
| Web client | Vercel | Set the six variables above. Leave the Output Directory unset — Next.js emits .next. |
| API | Render | render.yaml is committed in that repository; its start command runs migrations before the server. |
| Database | Neon Postgres | Optional. Without it, reports live in memory. |
Omitting the sign-in variables is supported — but omitting them by accident is
the likely mistake, so check /api/health after deploying. Free tiers sleep when
idle; the UI reports a waking backend rather than appearing hung.
Shipped:
- Rule engine over npm and PyPI dependencies, secrets, React and Python correctness, and structure
- Finding identity that survives an edit, and report-to-report diffing
- Accepting findings, per repository, reversibly
- File viewer with a finding gutter
- AI actions scoped to one finding, with whole-file context
- GitHub sign-in, report ownership, repository picker
- CI on both repositories
- Security rules for injection, traversal, SSRF, weak crypto and JWTs, graded by whether a request-derived value actually reaches the sink
- Prioritisation, and measurements separated from defects
- SARIF 2.1.0 export, validated against the OASIS schema
- One consolidated pull request comment, edited in place on re-runs
- Reopened findings — fixed once, back again, told apart from new
- Re-analyse a repository from its own report, at the same ref
- Preview cards for shared reports
- A measured detection baseline over a corpus of real applications
Planned:
- Reachability for transitive advisories — "your lockfile mentions a vulnerable package" versus "your code can reach it"
- A GitHub App, so pull request results become a check run that can block a merge rather than a comment. Needs job state to survive a restart of a host that sleeps — the comment above is the useful half that does not
- More Python lockfile formats (
Pipfile.lock,uv.lock) so range-declared projects get scanned, not just listed - Streaming AI responses
- Verify sign-in in Safari and Firefox strict mode
Known limitations are tracked honestly in docs/ROADMAP.md.
- Fork and branch from
master. - Make the change, with tests — and for a bug, a test that fails first.
- Run
npm run lint,npm run typecheck,npm run testandnpm run build. - Update the documentation in the same pull request.
- Open a pull request describing what changed and why.
CONTRIBUTING.md covers the conventions and, more usefully,
the things that are deliberate rather than accidental — so they are not
"simplified" away.
Never commit API keys or secrets; everything sensitive is an environment
variable, and .env.local is gitignored.
To report a vulnerability, use GitHub's private reporting rather than a public
issue. SECURITY.md has the details, along with what is already
defended: model output is never parsed as markup, the session cookie is
first-party and HttpOnly, and the OAuth state is signed so sign-in cannot
become an open redirect.
What is Vantage? A static analyser for repositories that remembers. It reports vulnerabilities, committed secrets, correctness bugs and structural problems anchored to a file and line — and, on a second run, what changed since the first.
Which model does it use?
Google Gemini, called by the API rather than the browser. The default is
gemini-3.5-flash-lite, with gemini-3.6-flash behind it: when the primary
fails transiently the request is retried on the next model in the chain, because
a 503 is a statement about one model's capacity rather than about the request.
Both the chain and the per-attempt timeouts are configurable. The measurements
the defaults were chosen from are in
docs/AI_LATENCY.md.
Do I need an API key? No. Analysis is entirely rule-based and works without one. A key only enables the three AI actions, which render disabled with the reason when it is absent.
Does it need a database?
No. Without DATABASE_URL reports are held in memory and cleared on restart,
and the UI says so.
Is it self-hostable? Yes — both halves. Neither requires an account with anything except GitHub, and only for optional sign-in.
Does it change my code? No. Proposed fixes are diffs you review. Vantage never writes to a working tree, which is also the backstop that stops a prompt injection becoming code execution.
Why two repositories?
They deploy independently to different platforms and have separate release
cadences. lib/types.ts mirrors the API's schemas to keep the contract explicit.
MIT.



