Metadata-Version: 2.5
Name: webvigil
Version: 1.0.0
Summary: A web application vulnerability scanner for developers — safe by default, CI-friendly.
Project-URL: Homepage, https://github.com/ryanvmorais/webvigil
Project-URL: Documentation, https://github.com/ryanvmorais/webvigil/tree/main/docs
Project-URL: Repository, https://github.com/ryanvmorais/webvigil
Project-URL: Issues, https://github.com/ryanvmorais/webvigil/issues
Project-URL: Changelog, https://github.com/ryanvmorais/webvigil/blob/main/CHANGELOG.md
Project-URL: Security policy, https://github.com/ryanvmorais/webvigil/blob/main/SECURITY.md
Author: Ryan Morais
License: Apache-2.0
License-File: LICENSE
License-File: NOTICE
Keywords: appsec,dast,owasp,scanner,security,vulnerability,web
Classifier: Development Status :: 5 - Production/Stable
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: Information Technology
Classifier: License :: OSI Approved :: Apache Software License
Classifier: Natural Language :: English
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Security
Classifier: Topic :: Software Development :: Quality Assurance
Classifier: Typing :: Typed
Requires-Python: >=3.12
Requires-Dist: cryptography>=50.0.2
Requires-Dist: httpx[http2]>=0.28.1
Requires-Dist: jinja2>=3.1.6
Requires-Dist: packaging>=26.3
Requires-Dist: pydantic>=2.13.5
Requires-Dist: rich>=15.0.0
Requires-Dist: selectolax<1,>=0.4.13
Requires-Dist: typer>=0.27.2
Provides-Extra: web
Requires-Dist: alembic>=1.20.0; extra == 'web'
Requires-Dist: argon2-cffi>=25.1.0; extra == 'web'
Requires-Dist: fastapi>=0.142.2; extra == 'web'
Requires-Dist: pyjwt>=2.15.1; extra == 'web'
Requires-Dist: python-multipart>=0.0.32; extra == 'web'
Requires-Dist: sqlmodel>=0.0.47; extra == 'web'
Requires-Dist: uvicorn[standard]>=0.52.4; extra == 'web'
Description-Content-Type: text/markdown

# WebVigil

![Python](https://img.shields.io/badge/Python-3.12+-3776AB?logo=python&logoColor=white)
![deps](https://img.shields.io/badge/deps-uv-DE5FE9)
[![CI](https://github.com/ryanvmorais/webvigil/actions/workflows/ci.yml/badge.svg)](https://github.com/ryanvmorais/webvigil/actions/workflows/ci.yml)
[![PyPI](https://img.shields.io/pypi/v/webvigil)](https://pypi.org/project/webvigil/)
[![Release](https://img.shields.io/github/v/release/ryanvmorais/webvigil)](https://github.com/ryanvmorais/webvigil/releases)
[![Image](https://img.shields.io/badge/ghcr.io-webvigil-2496ED?logo=docker&logoColor=white)](https://github.com/ryanvmorais/webvigil/pkgs/container/webvigil)
![Lint](https://img.shields.io/badge/lint-ruff-261230)
![Format](https://img.shields.io/badge/style-black-000000)
![Types](https://img.shields.io/badge/types-mypy%20strict-2A6DB2)
![License](https://img.shields.io/badge/license-Apache%202.0-blue)

**WebVigil is an open-source web application vulnerability scanner (DAST) for developers.**
A Python engine and CLI that runs passive, production-safe checks by default and, behind an
explicit opt-in, an Active Mode for reflected and stored XSS, SQL injection, SSRF, command
injection, XXE, file upload and more, with every active finding confirmed against a baseline.
It writes JSON, SARIF 2.1.0, HTML and Markdown reports and fails a build with `--fail-on`,
so it fits CI.

It helps teams find and fix security problems in web apps, both **in development**
(terminal, CI, pre-deploy) and **in production** (non-intrusive checks that are safe to run
against live systems). WebVigil ships as a reusable **scan engine**, a **CLI**, and an
optional **web dashboard** built on top of the same engine.

> **Status:** the planned coverage roadmap is complete and `1.0.0` is the first release. The
> CLI, the exit codes, the configuration keys, the check ids and the JSON report follow
> [Semantic Versioning](https://github.com/ryanvmorais/webvigil/blob/main/docs/stability.md); see the [changelog](https://github.com/ryanvmorais/webvigil/blob/main/CHANGELOG.md). [Scope and limitations](#scope-and-limitations) says what
> WebVigil deliberately does not do.

![WebVigil dashboard (optional web UI) — scan detail page showing findings grouped by severity](https://raw.githubusercontent.com/ryanvmorais/webvigil/main/assets/dashboard-scan-detail.png)

## WebVigil vs. other scanners

| | WebVigil | OWASP ZAP | Nuclei |
|---|---|---|---|
| Distribution | Library + CLI + optional dashboard | GUI + daemon (Docker/desktop) | CLI + YAML templates |
| Safe by default | Passive-only unless `--mode active` is explicit | Active scan is a common default workflow | Mostly read-only, template-dependent |
| Confirmed findings | Every active finding is proven against a per-request baseline | Signature + active-scan rules | Signature/template match |
| Best for | CI-native, in-band checks on your own app | Deep manual pentesting with a GUI/proxy | Fast sweeps against thousands of known CVE/misconfig templates |
| License | Apache-2.0 | Apache-2.0 | MIT |

WebVigil isn't a replacement for either — pair it with [Nuclei](https://github.com/projectdiscovery/nuclei)
for known-CVE template matching, or ZAP for manual, proxy-driven testing. See
[Scope and limitations](#scope-and-limitations) for the full boundary.

---

## Safe by default

WebVigil runs in **Safe Mode** by default: it only observes responses, headers, TLS
configuration, cookies, and page content. It never sends attack payloads and is safe to
point at production. The one request it adds beyond plain page fetches is a single GET
with an `Origin` header, used to detect reflective CORS — still a read-only GET, still
restricted to the target scope.

**Active Mode** (payload injection: XSS, SQLi, SSRF, etc.) is opt-in, requires an explicit
authorization flag, and is intended for development and staging environments only.

---

## Responsible use

A scanner sends requests to a system, so who may point it where matters as much as what it
finds.

- **Authorized targets only.** Scan systems you own or have explicit permission to test.
  Scanning someone else's system without permission may be illegal where you live, and
  downloading this tool is not that permission.
- **Active Mode has a gate.** It needs `--mode active` **and** `--authorized-by "<who /
  engagement>"`; without the name WebVigil refuses (exit code `5`), and the name is written
  into the report.
- **Anything that writes, logs in or phones out is off by default**, each behind its own
  switch: `--stored-xss`, `--file-upload`, `--submit-post-forms` and `--confirm-csrf` write to
  the target; `--login-url` creates a session; `--osv-online` sends library names to OSV.dev.
- **Always on:** a scope guard that keeps every request on the target host, a request budget,
  a concurrency cap and a delay, and `robots.txt` is honoured unless you turn it off.
- **What WebVigil will not do:** brute-force credentials, evade a WAF or an IDS, keep access,
  take data out, or exploit beyond one bounded proof. That is not on the roadmap
  ([Scope and limitations](#scope-and-limitations)).
- **Demos stay on safe targets.** The screenshots, recordings and posts for this project scan
  the bundled test app (`scripts/serve-fixture-app.py`) or the maintainer's own systems,
  never a third party's.

Found a vulnerability **in WebVigil**? Report it privately, see [SECURITY.md](https://github.com/ryanvmorais/webvigil/blob/main/SECURITY.md).
Found one in someone else's system with it? Tell its owner, and follow their disclosure policy.

---

## Coverage

WebVigil was built milestone by milestone against a planned roadmap. Every entry below is
in the code and links to its docs; each was designed as a spec first, under
[`specs/`](https://github.com/ryanvmorais/webvigil/blob/main/specs/). The milestones are the history of the build, not published versions:
`1.0.0` is the first release and contains all of them.

| Milestone | Focus | Status |
|---|---|---|
| `v0.1` | Security headers, technology-disclosure headers, cookie flags, TLS/HTTPS configuration, CORS misconfiguration (all passive) | shipped |
| `v0.2` | Web API (FastAPI + SQLite, persistent scans, queued execution) | shipped |
| `v0.3` | Web dashboard (Next.js) | shipped |
| `v0.4` | Passive dependency fingerprinting: client-side JS libraries + known-vulnerability matching against a vendored Retire.js database ([docs](https://github.com/ryanvmorais/webvigil/blob/main/docs/dependency-fingerprinting.md)) | shipped |
| `v0.5` | Information disclosure: stack traces and directory listings (passive), plus opt-in probing for exposed `.git`/`.env`/backups/debug endpoints ([docs](https://github.com/ryanvmorais/webvigil/blob/main/docs/information-disclosure.md)) | shipped |
| `v0.6` | Active Mode: injection testing — reflected XSS, SQL injection (error/boolean/time), path traversal, open redirect ([docs](https://github.com/ryanvmorais/webvigil/blob/main/docs/active-injection.md)) | shipped |
| `v0.7` | Authenticated scanning (static cookies), CSRF detection, and form-driven crawling ([docs](https://github.com/ryanvmorais/webvigil/blob/main/docs/authenticated-scanning.md)) | shipped |
| `v0.8` | Stored / persistent XSS: opt-in two-phase inject-then-recrawl detection ([docs](https://github.com/ryanvmorais/webvigil/blob/main/docs/active-injection.md#stored-xss----stored-xss-opt-in)) | shipped |
| `v0.9` | In-band SSRF: cloud metadata service, loopback / internal resources, `file://` — detected from the target's own responses ([docs](https://github.com/ryanvmorais/webvigil/blob/main/docs/active-injection.md#ssrf--cloud-metadata-loopback-file)) | shipped |
| `v0.10` | Opt-in OSV.dev online advisory lookup for dependency fingerprinting, augmenting the vendored Retire.js database ([docs](https://github.com/ryanvmorais/webvigil/blob/main/docs/dependency-fingerprinting.md#osvdev-online-provider---osv-online)) | shipped |
| `v0.11` | Active Mode: in-band OS command injection (arithmetic echo + time-based) and server-side template injection ([docs](https://github.com/ryanvmorais/webvigil/blob/main/docs/active-injection.md#command-injection--ssti)) | shipped |
| `v0.12` | Active Mode: request-envelope injection — CRLF / response splitting, host-header injection, opt-in in-band XXE, and an HTTP-methods (TRACE / verb) check ([docs](https://github.com/ryanvmorais/webvigil/blob/main/docs/active-injection.md#request-envelope-injection)) | shipped |
| `v0.13` | Header / bearer authentication (`--header`), OpenAPI / Swagger import to seed the crawl and the injection pass (`--openapi`), and four passive checks: missing Subresource Integrity, mixed content, session identifier in a URL, private IP in a body ([docs](https://github.com/ryanvmorais/webvigil/blob/main/docs/api-scanning.md)) | shipped |
| `v0.14` | Active Mode: LDAP / XPath / SSI injection (in-band, error signature + differential), and opt-in unrestricted file-upload testing (`--file-upload`) — a benign marker uploaded with a dangerous name / type, then fetched back to prove execution, inline rendering, or a path-traversal write ([docs](https://github.com/ryanvmorais/webvigil/blob/main/docs/active-injection.md#file-upload----file-upload-opt-in)) | shipped |
| `v0.16` | Active Mode: in-band expression-language injection — Spring SpEL, Struts OGNL, JEXL, MVEL, Unified EL. The dialect is named from the evidence, and a pure static-call probe separates a sandboxed evaluator (HIGH) from a reachable type system (CRITICAL) ([docs](https://github.com/ryanvmorais/webvigil/blob/main/docs/active-injection.md#expression-language-injection)) | shipped |
| `v0.17` | Active Mode: opt-in CSRF confirmation (`--confirm-csrf`) — each state-changing form is submitted as a control and as cross-site-shaped replays (foreign `Origin`, token removed or altered), and reported only when the server accepts the replay ([docs](https://github.com/ryanvmorais/webvigil/blob/main/docs/authenticated-scanning.md#active-csrf-confirmation----confirm-csrf-opt-in)) | shipped |
| `v0.18` | Active Mode: opt-in `POST` crawling (`--submit-post-forms`) — the crawler submits candidate forms (urlencoded, multipart without a file) and `--openapi` `POST` operations once with benign values, reads each answer as a page, and follows what it links to ([docs](https://github.com/ryanvmorais/webvigil/blob/main/docs/authenticated-scanning.md#post-forms----submit-post-forms-opt-in)) | shipped |
| `v0.19` | Active Mode: automated login (`--login-url`, `--username`, `--password-env`) — WebVigil finds the login form, submits the account once, keeps the session and logs in again when it drops; the password only ever comes from the environment or a prompt ([docs](https://github.com/ryanvmorais/webvigil/blob/main/docs/authenticated-scanning.md#automated-login----login-url-opt-in)) | shipped |
| `v0.20` | Session-security checks: `session.id.weak` (short, low-entropy, numeric, counter, timestamp or repeated ids, plus opt-in anonymous sampling with `--sample-sessions`), `session.fixation` (the session id survives the login, confirmed with one request) and `session.logout.not-invalidated` (`--test-logout`, Active Mode). Findings name the cookie and never carry a value ([docs](https://github.com/ryanvmorais/webvigil/blob/main/docs/authenticated-scanning.md#session-security-checks)) | shipped |

> The ten opt-in switches, all off by default: `--probe` (sensitive-path probing),
> `--stored-xss`, `--file-upload`, `--confirm-csrf` and `--submit-post-forms` (all write to
> the target), `--xxe` (re-types POST bodies as XML), `--osv-online` (sends library names to
> `api.osv.dev`), `--login-url` (a real login request with the account you give it; Active Mode only),
> `--sample-sessions` (a few cookie-less `GET`s of the entry URL) and `--test-logout` (ends the
> scan's own session; Active Mode only). Everything else only reads.

---

## Scope and limitations

WebVigil is deliberately bounded. It is an **in-band** scanner: the engine talks only to the
target, sends a small static set of payloads, and confirms every active finding against a
per-request baseline. That line keeps it fast, low-noise, and safe to distribute as a
open-source tool with no hosted service — at the cost of the classes below. For each, the thing
to reach for instead.

- **JavaScript-rendered apps.** The crawler parses HTML; it runs no headless browser, so a
  SPA that builds its DOM in JS exposes almost no surface to the crawl (the scan warns when
  the entry page looks like one). *Instead:* point
  `--openapi` at the app's schema to seed the crawl and the injection pass directly, or
  feed URLs collected by your own browser-based crawler.

- **Blind / out-of-band vulnerabilities.** Blind SSRF, blind command injection, blind /
  OOB XXE, blind stored XSS with no reflected marker, and HTTP request smuggling all need
  either a collaborator server the scanner hosts (public domain, DNS/HTTP listeners) or
  raw-socket control of request framing. Both cross the "engine talks only to the target"
  rule. *Instead:* pair WebVigil with your own collaborator — Burp Collaborator,
  [interactsh](https://github.com/projectdiscovery/interactsh). Background:
  [why blind SSRF is the one thing WebVigil won't do](https://github.com/ryanvmorais/webvigil/blob/main/docs/notes/why-not-oast.md).

- **Some active-scan classes.** `eval()` code
  injection, NoSQL injection, HTTP parameter pollution, remote file inclusion, DOM XSS,
  and verb-based auth bypass are out — each is either deferred, has no reliable in-band
  oracle, or needs a browser. The full table of what is covered and what is not, with the
  reason for each, is in
  [`docs/active-injection.md`](https://github.com/ryanvmorais/webvigil/blob/main/docs/active-injection.md#coverage-boundaries).

- **Authentication.** WebVigil authenticates with a **static** cookie (`--cookie`) or
  header (`--header`) you supply, or — in Active Mode — logs in to a **form** itself
  (`--login-url`) and re-logs in when the session drops. It does not drive CAPTCHA, MFA,
  an SSO / OAuth flow or a JSON token login. It checks session security (weak ids,
  fixation, a logout that does not invalidate) but never guesses an id. *Instead:* log in
  with your browser and paste the session cookie. See
  [authenticated scanning](https://github.com/ryanvmorais/webvigil/blob/main/docs/authenticated-scanning.md).

- **Not a template scanner.** There is no Nuclei-style CVE-template database, no CMS or
  plugin enumeration, no exploit matching beyond the vendored Retire.js data and the
  opt-in OSV.dev lookup. *Instead:* run [Nuclei](https://github.com/projectdiscovery/nuclei)
  alongside it.

- **Not a fuzzer.** Payloads are a small, documented, in-repository set with no mutation
  engine and no WAF-evasion tuning; WebVigil tests the parameters a target actually
  exposes, not guessed ones. *Instead:* a dedicated fuzzer (ffuf, wfuzz) for parameter
  mining and brute force.

- **Not an exploitation framework.** A confirmed SQLi is proved with one bounded marker,
  not by dumping the database; a confirmed traversal reads one known file, not the whole
  disk. WebVigil stops at proof.

- **API-scanning edges.** `--openapi` reads **JSON** only (convert a YAML document first)
  and fuzzes query / path / form-urlencoded-body parameters — not individual fields of a
  JSON request body. These are current limits, not permanent ones.
  See [API scanning](https://github.com/ryanvmorais/webvigil/blob/main/docs/api-scanning.md).

---

## Install

Python 3.12 or newer. The package has the engine and the CLI; the dashboard and the bundled
test app live in this repository only.

```bash
pipx install webvigil                  # or: uv tool install webvigil, or: pip install webvigil
webvigil version

# or, without installing Python packages:
docker run --rm ghcr.io/ryanvmorais/webvigil scan https://your-app.example
```

The image is built for `linux/amd64` and `linux/arm64` and carries signed build provenance and
an SBOM (see [releasing](https://github.com/ryanvmorais/webvigil/blob/main/docs/releasing.md#every-release)). `webvigil[web]` adds the Web API.

## Quick start

> The steps below run from a clone of this repository, because the test app ships with it, and
> need [uv](https://docs.astral.sh/uv/). `your-app.example` stands for **a system you own or
> may test**: replace it. With the package installed, drop the `uv run`.

Try it first on a target built to be attacked, on your own machine: a test app that ships with
this repository, or [OWASP Juice Shop](https://github.com/juice-shop/juice-shop) or
[DVWA](https://github.com/digininja/DVWA) on `localhost`.

```bash
git clone https://github.com/ryanvmorais/webvigil && cd webvigil
uv sync --all-extras                                 # the test app needs the web extra
uv run python scripts/serve-fixture-app.py &        # an intentionally vulnerable app on 127.0.0.1:9100
uv run webvigil scan http://127.0.0.1:9100 --mode active --authorized-by "trying WebVigil"   # about 2 minutes: it has slow, time-based cases
```

Then on your own app:

```bash
uv run webvigil scan https://your-app.example
uv run webvigil scan https://your-app.example --format html --output report.html
uv run webvigil scan https://your-app.example --probe   # also probe for exposed .git/.env/backups
uv run webvigil scan https://your-app.example --mode active --authorized-by "you / engagement"  # injection + in-band SSRF testing
uv run webvigil scan https://your-app.example --mode active --authorized-by me --stored-xss     # + stored XSS (writes markers)
uv run webvigil scan https://your-app.example --mode active --authorized-by me --file-upload    # + file-upload testing (writes files)
uv run webvigil scan https://your-app.example --mode active --authorized-by me --submit-post-forms  # + the crawler submits POST forms (writes)
uv run webvigil scan https://your-app.example --mode active --authorized-by me --cookie "session=<paste>" --confirm-csrf  # + CSRF confirmation (submits forms)
uv run webvigil scan https://your-app.example --cookie "session=<paste from your browser>"      # authenticated scan
WEBVIGIL_LOGIN_PASSWORD=... uv run webvigil scan https://your-app.example --mode active --authorized-by me \
  --login-url https://your-app.example/signin --username scanner@example.com                    # log in by itself
uv run webvigil scan https://your-app.example --sample-sessions                                 # also judge fresh anonymous session ids (GET-only)
WEBVIGIL_LOGIN_PASSWORD=... uv run webvigil scan https://your-app.example --mode active --authorized-by me \
  --login-url https://your-app.example/signin --username scanner@example.com --test-logout      # + logout test (ends the scan's own session)
uv run webvigil scan https://your-app.example --openapi ./openapi.json                          # seed the scan from an API schema
uv run webvigil scan https://your-app.example --osv-online                                      # also check libraries against OSV.dev
uv run webvigil list-checks
uv run webvigil report report.json --format md      # re-render a saved scan, offline
```

By default the scan prints a summary to your terminal (stderr): a severity-count table
followed by one line per finding.

![A webvigil scan of the bundled test app in Active Mode: the authorization banner, the severity-count table and one coloured line per finding](https://raw.githubusercontent.com/ryanvmorais/webvigil/main/assets/cli-scan-demo.svg)

*A real run against the bundled test app, kept to its first screen by `| head -40`. The command
is typed for the recording and the wait in the middle is cut (the test app has slow, time-based
cases, so the real scan takes about a minute and a half). The source is
[`assets/cli-scan-demo.cast`](https://github.com/ryanvmorais/webvigil/blob/main/assets/cli-scan-demo.cast); play it with `asciinema play`.*

With `--format` it writes the report to stdout (or `--output PATH`), so you can pipe it.

Use in CI — fail the build on high-severity findings:

```bash
uv run webvigil scan "$TARGET_URL" --format sarif --output results.sarif --fail-on high
```

Exit codes: `0` clean · `3` findings at or above `--fail-on` · `4` operational error
(bad target, unreachable host, config) · `5` Active Mode without `--authorized-by`.

To scan a target on your own machine from the image, add `--network host` (Linux) or use
`host.docker.internal` as the host name (Docker Desktop).

---

## Web API

An optional FastAPI service keeps a history of scans in SQLite and runs them through the
same engine. It is single-user and local-first.

```bash
pip install "webvigil[web]"        # or: uv sync --all-extras
webvigil-web serve                 # http://127.0.0.1:8000  (OpenAPI docs at /docs)
```

The database is created and migrated on first start; open `/docs` to create the account.
See [docs/web-api.md](https://github.com/ryanvmorais/webvigil/blob/main/docs/web-api.md) for configuration, auth, and backup.

---

## Web UI

A Next.js dashboard for the Web API: first-run setup, login, scan history, a new-scan form,
scan detail with filterable findings, report preview and download, the check catalogue, and
a settings screen. It is a thin client of the API — it never talks to the engine.

```bash
# API on :8000 in one shell (see above), then:
cd web
pnpm install
pnpm dev                           # http://localhost:3000

# or the whole stack in containers:
docker compose up --build          # dashboard on http://localhost:3000 (published on 127.0.0.1 only)
```

The dashboard calls same-origin `/api/*`; Next proxies that to the API, so no CORS is
involved. See [docs/web-ui.md](https://github.com/ryanvmorais/webvigil/blob/main/docs/web-ui.md).

The dashboard lives in this repository only; it is **not** in the PyPI package. `pip install
webvigil` gives the engine and the CLI, and `webvigil[web]` adds the Web API.

---

## Development

```bash
uv sync
uv run ruff check .
uv run black --check .
uv run mypy src
uv run lint-imports    # the engine must not import Typer/Rich/FastAPI/SQLModel/Uvicorn
uv run pytest

# Web UI (in web/):
pnpm install && pnpm lint && pnpm typecheck && pnpm test && pnpm build
pnpm test:e2e         # Playwright: setup → scan → report → logout, fully offline
```

See [docs/](https://github.com/ryanvmorais/webvigil/blob/main/docs/) for architecture, [the stack and why each piece was chosen](https://github.com/ryanvmorais/webvigil/blob/main/docs/stack.md), and a guide to writing your own checks.

---

## Design notes

Short essays on the reasoning behind specific decisions — the *why* behind a spec's ADRs,
written up on their own. Full index in [docs/notes/](https://github.com/ryanvmorais/webvigil/blob/main/docs/notes/).

- [The OAST problem: why blind SSRF is the one thing WebVigil won't do](https://github.com/ryanvmorais/webvigil/blob/main/docs/notes/why-not-oast.md)
- [False-positive discipline in an active scanner](https://github.com/ryanvmorais/webvigil/blob/main/docs/notes/false-positive-discipline.md)
- [Detecting stored XSS means writing data you can't take back](https://github.com/ryanvmorais/webvigil/blob/main/docs/notes/stored-xss-markers.md)
- [Keeping a security engine honest with import-linter](https://github.com/ryanvmorais/webvigil/blob/main/docs/notes/engine-boundaries.md)
- [OSV.dev without an API key](https://github.com/ryanvmorais/webvigil/blob/main/docs/notes/osv-without-a-key.md)

---

## License

[Apache-2.0](https://github.com/ryanvmorais/webvigil/blob/main/LICENSE)
