Sentinel — one-click website security audit
Sentinel is a security auditor for small businesses that don't have a security team. Paste your website address, click Scan, and get a plain-language report of what's wrong and exactly how to fix it.
Because it scans your live website over HTTP/TLS (black-box), it works on any site no matter what framework or language it's built in — WordPress, Shopify, Django, Rails, a custom Node app, plain HTML, anything with a URL.
Website checks run in the web app; deep project scans run from your terminal. The website (and dashboard) perform safe, passive website scans for any URL. The terminal (after you log in on the site) deep-scans projects you have access to —
sentinel . --codestatically analyses a whole codebase, its dependencies and its git history. You can only deep-scan code on your own machine, never attack an arbitrary website.⚠️ Only scan websites you own or have permission to test.
Just want to run it? → Run it locally (make)
What it checks
| Area | Examples |
|---|---|
| Encryption | HTTPS enforced, valid & current TLS certificate, no weak protocols, weak cipher suites, TLS 1.0/1.1 still enabled, HSTS preload eligibility |
| Security headers | CSP, HSTS, clickjacking, MIME-sniffing, referrer/permissions policy |
| DNS & email | SPF, DMARC, DKIM, MTA-STS, CAA, DNSSEC, open DNS zone transfer (AXFR) |
| Attack surface | Subdomain discovery from public Certificate Transparency logs; risky dev/staging/admin hosts |
| Exposed files | .env, .git, database dumps, backups, config files left public |
| Cookies | Missing Secure / HttpOnly / SameSite flags |
| Information leaks | Server/version disclosure, debug error pages, secret HTML comments |
| Injection | Reflected-input / possible XSS, open redirects (safe, non-destructive) |
| Malware | Crypto-miner & obfuscated-script heuristics, optional Google Safe Browsing |
| Other | Directory listing, mixed content, dangerous CORS |
Every finding comes with a severity, a plain-language explanation, a concrete fix, and compliance cross-references (OWASP Top 10, PCI-DSS, CIS Controls) so reports speak the language auditors and customers expect.
The DNS/email and attack-surface checks are fully passive — they query public DNS and Certificate Transparency infrastructure, never attacking your site — so they run in the standard (web app + API) profile alongside the other safe checks. The zone-transfer probe is SSRF-guarded: nameservers that resolve to internal addresses are skipped.
Deep project scan — from your terminal (--code)
The terminal's deep scan targets a project you have access to, not a live
website: sentinel . --code statically analyses an entire codebase plus its
dependencies and git history, offline and read-only. You can only run it against
code on your own machine, so it can never be pointed at someone else's site.
Active website attacking is intentionally not exposed. Earlier versions offered a
--deepmode that actively attacked a live URL (SQLi/XSS/etc.). That was removed from the CLI so the tool can only ever deep-scan projects you control — not arbitrary websites. The web app and REST API perform safe, passive website checks only.
See Scan your local codebase (--code) below
for everything the project scan covers (secrets, injection, vulnerable
dependencies, secrets-in-git-history, and more).
Accounts & dashboard
Accounts, authentication and scan history all live in the Laravel + MySQL app. The Python service is a stateless scanning engine — it just runs a scan and returns the report; it stores nothing.
- Auth: Laravel session auth against the
userstable. Passwords are bcrypt hashed (Laravel'shashedcast). - Per-user history: every scan is saved to the MySQL
scanstable and is only visible to the user who ran it (admins can see all). - The Laravel app shows a login screen, then a dashboard with a standard scan bar and a history sidebar, plus a "Use in terminal" page that walks users through installing the CLI, logging in, and deep-scanning their projects (whose results then sync back into this same history).
Seeded accounts
php artisan migrate:fresh --seed creates two accounts:
| Role | Password | |
|---|---|---|
| Admin (super-admin) | [email protected] |
admin12345 |
| Demo (user) | [email protected] |
demo12345 |
(Seeders: database/seeders/AdminUserSeeder.php and DemoUserSeeder.php.)
Roles: user vs admin
Every account has a role (user or admin); admins also have an is_super
flag.
- User → the standard dashboard: run scans, view their own history.
- Admin → an extra dashboard with full controls:
- Overview — platform stats (users, scans, project scans, average score, grade distribution).
- Users — list every account, promote/demote roles, delete users.
- All scans — view and delete any user's scan reports.
- Scanner — the normal scan tool (admins can scan too).
The seeded admin is the super-admin: only a super-admin can create/promote
other admins, and the super-admin can't be demoted or deleted (prevents lockout).
The sentinel.admin middleware guards /admin/* routes.
Architecture
backend/sentinel/ # Python: STATELESS scanning engine
core/ # framework-agnostic scanning library
checks/ # one module per check (+ checks/active/ for deep mode)
crawler.py # same-origin crawler (deep mode)
scanner.py # orchestrator
api/main.py # FastAPI: POST /api/scan -> ScanResult (no auth/DB)
cli.py # command-line tool
frontend-laravel/ # Laravel + Livewire: UI + accounts + history (MySQL)
app/Models/ # User, Scan (Eloquent)
app/Livewire/ # Login, Scanner, Admin\{Overview,Users,Scans}
app/Services/SentinelApi.php # calls the Python scanner
database/{migrations,seeders}/
Makefile # make dev backend / make dev frontend / make dev
The Python core has no web/auth/DB dependency — the CLI, the scan API and the
Laravel UI all sit on top of the same scan() function. Laravel owns all state
in MySQL and calls the scanner over HTTP (optionally with a shared
SENTINEL_INTERNAL_KEY header for server-to-server auth).
Quick start
Run it locally (make)
Everything is wired up in the root Makefile — there's no virtualenv to
activate and nothing to remember.
Prerequisites: Python 3.10+, PHP 8.2+, Composer,
make, and a MySQL server.
1. Get the code and create the database
git clone <your-repo-url> sentinel
cd sentinel
mysql -u root -e "CREATE DATABASE sentinel;" # any MySQL you can reach
2. Install both halves
make setup # backend virtualenv + composer install + a fresh .env
This creates backend/.venv, installs the Python dependencies, runs
composer install, and copies frontend-laravel/.env.example to .env with a
generated APP_KEY.
3. Point the app at your database
Edit frontend-laravel/.env so the DB_* values match your MySQL server (the
defaults are sentinel / dev / password on 127.0.0.1:3306). Leave
SENTINEL_API_URL=http://localhost:8000 — that's where the scanning engine runs.
4. Create the tables and the seeded accounts
make migrate # php artisan migrate --seed
You now have the two accounts from Seeded accounts:
[email protected] / admin12345 and [email protected] / demo12345.
5. Start it
The app is two processes — the Python scanning engine and the Laravel web app — so open two terminals, or start both at once:
# terminal 1
make dev backend # scanning engine -> http://127.0.0.1:8000/docs
# terminal 2
make dev frontend # web app -> http://127.0.0.1:8090
# ...or both in one terminal (Ctrl-C stops both)
make dev
Open http://127.0.0.1:8090, log in as [email protected] / demo12345,
paste a URL you own, and click Scan. The frontend needs the backend running
— scans fail with a connection error otherwise.
make dev-backend and make dev-frontend are accepted as aliases. Both targets
are self-installing: if backend/.venv or frontend-laravel/vendor is missing
they're built before the server starts, so on a fresh clone you can skip step 2
and go straight to make dev backend. The venv is also rebuilt automatically if
you move the project directory, since a virtualenv hardcodes its own path.
All targets
| Command | What it does |
|---|---|
make dev backend |
Scanning engine on :8000, auto-reloading on file changes |
make dev frontend |
Laravel dev server on :8090 |
make dev |
Both at once |
make setup |
Install backend + frontend dependencies |
make migrate |
php artisan migrate --seed — tables + admin/demo accounts |
make test |
Backend pytest + Laravel php artisan test |
make clean |
Delete the backend virtualenv |
make help |
List every target |
Ports are overridable on the command line:
make dev backend BACKEND_PORT=9000
make dev frontend FRONTEND_PORT=3000
If you change the backend port, update SENTINEL_API_URL in
frontend-laravel/.env to match.
Troubleshooting
| Symptom | Fix |
|---|---|
command not found: uvicorn |
You're running it by hand without the venv active. Use make dev backend, or source backend/.venv/bin/activate first — activation only applies to the shell you run it in. |
bad interpreter from a .venv command |
The project directory was moved; a venv can't be relocated. Run make clean, then make dev backend. |
error: externally-managed-environment |
pip is talking to system Python, not the venv. Use make setup. |
| Scans fail with a connection error | The backend isn't running, or SENTINEL_API_URL doesn't match its port. |
SQLSTATE[HY000] [1049] Unknown database |
Create the database (step 1) and check the DB_* values in frontend-laravel/.env. |
The rest of this section covers the same steps by hand.
Use it from your terminal (log in once — like Claude Code)
The whole point: a user logs in on the Sentinel website, and from then on can run scans from their own terminal, in any project, with results saved to their dashboard. There's nothing to configure and no API keys to paste.
# 1. install the CLI once (one command — no virtualenv to manage)
curl -fsSL https://your-sentinel-site/install.sh | sh
# 2. log in through your browser (opens a consent page on the website)
export SENTINEL_WEB_URL=https://your-sentinel-site # where your Sentinel lives
sentinel login # ✓ Logged in as [email protected]
# 3. use it anywhere — scans auto-sync to your dashboard history
cd ~/my-project
sentinel . # deep-scan this whole project
sentinel whoami # who am I logged in as?
sentinel logout # disconnect this terminal
Fetch install.sh from your own Sentinel — the URL in your browser's address
bar, e.g. http://127.0.0.1:8090/install.sh while running make dev. The site
stamps its own address and CLI source into the script as it serves it, so the
installer prints the exact SENTINEL_WEB_URL for step 2, and a Sentinel running
on your machine installs the CLI from your checkout rather than from GitHub.
Which repository a deployed site installs from is SENTINEL_CLI_REPO (see
frontend-laravel/config/sentinel.php) — fork owners should point it at theirs.
How the login works (browser "loopback" flow, like gcloud/gh):
sentinel login starts a tiny local server on 127.0.0.1, opens your browser to
the website's /cli/authorize consent page, and — once you click Approve —
the site redirects back to http://127.0.0.1:<port>/callback with a fresh
personal access token. The token only ever travels to your own machine and is
stored in ~/.sentinel/config.json (owner-only, chmod 600). Override per
command with SENTINEL_TOKEN / SENTINEL_WEB_URL, or skip saving a scan with
--no-sync.
The installer is a thin wrapper (it uses pipx/uv/pip under the hood); to install with pipx yourself instead:
pipx install ./backend # from a local checkout
pipx install 'git+https://github.com/<you>/sentinel.git#subdirectory=backend' # straight from git
Either way you get a sentinel command on your PATH, isolated in its own
virtualenv — the same way tools like ruff or claude are installed.
The package is
sentinel-scanner; the command it installs issentinel.
Backend (editable, for development)
make setup-backend does all of this for you. By hand:
cd backend
python3 -m venv .venv && source .venv/bin/activate
pip install -e ".[dev]" # ".[dev]" adds pytest; plain "-e ." for runtime only
pytest # run the test suite (or: make test-backend)
Remember that source .venv/bin/activate only affects the shell you run it in —
open a new terminal and you need to activate again, otherwise uvicorn and
sentinel won't be on your PATH. A virtualenv also can't be moved: if you
relocate the project directory, run make clean (or delete .venv) and let it
rebuild. The make targets use backend/.venv directly, so they work whether
or not your shell has it activated.
CLI:
sentinel . # deep-scan the current project (code + deps + git history)
sentinel ./path/to/project # deep-scan a specific directory
sentinel . --fail-on high # exit non-zero in CI if High+ issues
sentinel . --json # machine-readable
sentinel . --fast # stop early once a rule repeats a lot
The terminal has one scan: it deep-scans a project you have access to. It
can't be pointed at a live website — passive website scanning lives in the web
app and REST API. (sentinel . --code still works; --code is now the implied
default.)
Scan your local codebase (--code):
Point Sentinel at a local repo to statically analyse the source code for vulnerabilities and get fix suggestions — no website or deployment needed. This runs entirely offline and read-only (it never executes your code or makes network calls), so no ownership verification is required: it's your machine.
sentinel . --code # scan the current repo
sentinel ./path/to/project --code # scan a specific directory
sentinel . --code --json # machine-readable output
sentinel . --code --fail-on high # exit non-zero in CI on High+ findings
From the terminal this is a deep scan by default: it walks the whole project
and reports every occurrence, rather than stopping once a rule has matched a few
times. --fast restores the smaller caps (that is also what the web API uses,
since it has a request to answer). Nothing is ever dropped silently — if a cap is
reached, the remaining occurrences are counted and reported as a total.
It covers, across Python, PHP, JS/TS, Go, Ruby, Java and config files:
- Credentials — provider keys (AWS, GitHub, Stripe, AI providers, npm, Slack, Google, Twilio/SendGrid), private keys, JWTs, and database URLs with an embedded password.
- Injection reachable from a request — SQL, NoSQL, command, template (SSTI),
XXE, path traversal, SSRF, open redirect, and
eval/dynamic code. - Authentication and crypto — JWTs accepted without a valid signature,
session tokens from a predictable random source, passwords behind a fast hash,
secrets compared with
==(timing), ECB mode, hardcoded IVs, cookies missingSecure/HttpOnly, CSRF turned off. - The deployment layer — Dockerfiles (root user,
:latest, remoteADD, secrets baked into layers), Kubernetes and Compose (privileged, host namespaces, inline secrets), Terraform (0.0.0.0/0ingress, public buckets, unencrypted volumes), and GitHub Actions (pull_request_target, untrusted PR text interpolated into a shell command, secrets echoed to the log). - Files that shouldn't be in the repo at all — a committed
.env, private keys, database and SQL dumps, editor/merge backups, shell history. In a git repo only tracked files count, so a.gitignored.envis correctly quiet.
Each finding includes the file:line, a plain-language explanation, and a fix.
Secrets are redacted in the output, and node_modules/vendor/.venv/build
artifacts and lock/binary files are skipped automatically.
Rules are written to distinguish a method call from a language builtin —
model.eval() and request.exec() are not Python's eval/exec, and a
frontend calling its own API with a template literal is not SSRF. A scanner that
reports those trains people to stop reading it.
The code scan also does two extra offline passes:
- Dependency / SBOM scan. It parses your lockfiles and manifests
(
requirements.txt,Pipfile.lock,package-lock.json,package.jsonexact pins,composer.lock) and flags pinned versions that match a bundled snapshot of well-known advisories (CVEs for popular packages likerequests,lodash,guzzlehttp/guzzle, …). Only exact-pinned versions are evaluated, so there are no false positives from open ranges. This is a curated subset, not a complete database — for exhaustive, always-current coverage run a dedicated SCA tool (pip-audit,npm audit,osv-scanner) too. - Git-history secret scan. When the target is a git repository, it walks
every blob in history (read-only
git rev-list/cat-file) and reports secrets that were committed at any point — even if they were later deleted from the current files. A deleted-but-still-in-history key must be treated as leaked; the finding tells you to rotate it and purge history withgit filter-repo/BFG.
Both passes are offline and read-only (no network, no code execution).
Scanner API (stateless engine — Laravel calls this):
make dev backend # or, by hand:
uvicorn sentinel.api.main:app --reload --port 8000
# Standard: POST /api/scan {"url": "https://example.com"}
# Deep: POST /api/scan {"url": "https://example.com", "deep": true, "authorized": true, "verification_token": "<token>"}
# (the target must serve <token> at /.well-known/sentinel-verify-<token>.txt)
# Verify: POST /api/verify {"url": "https://example.com", "token": "<token>"}
# -> {"verified": bool, "method": "file"|"dns"|null, "path": "...", "dns_record": "sentinel-verify=<token>"}
# Optional: set SENTINEL_INTERNAL_KEY and send it as the X-Internal-Key header
# docs at http://localhost:8000/docs
Frontend — Laravel + Livewire (MySQL)
The UI is a Laravel + Livewire app (server-rendered PHP, reactive without hand-written JS). It owns all data in MySQL and calls the Python scanner.
make dev frontend does all of this for you. By hand:
cd frontend-laravel
composer install
# configure .env: DB_* (MySQL) and SENTINEL_API_URL=http://localhost:8000
php artisan key:generate # if APP_KEY is missing
php artisan migrate --seed # create tables + seed admin/demo accounts (make migrate)
php artisan serve --port 8090 # open http://localhost:8090
It uses Livewire components (no controllers): Auth\Login, Scanner, and
Admin\{Overview,Users,Scans}, with Eloquent User/Scan models. Auth is
Laravel's session guard; App\Services\SentinelApi calls the scanner over HTTP;
auth + sentinel.admin middleware guard the routes. Needs PHP 8.2+, Composer,
and a MySQL database.
Set GOOGLE_SAFE_BROWSING_KEY in the backend environment to enable the
reputable-blocklist check.
REST API (use it from the terminal)
The Laravel app exposes a token-authenticated JSON API under /api/v1, so you
can drive Sentinel from a script or the terminal. It uses the same accounts and
scan history as the web app. (The Python engine stays internal — the API calls
it for you.)
The API performs standard (passive) website scans only. Active website
attacking isn't offered anywhere; sending deep=1 returns 422. Deep project
scans are run from the terminal (sentinel . --code) and synced to history via
POST /api/v1/scans/import.
Auth is a personal access token sent as Authorization: Bearer <token>.
BASE=http://localhost:8090
# 1. Get a token (or POST /api/v1/register to create an account + token)
TOKEN=$(curl -s -X POST $BASE/api/v1/tokens \
-d '[email protected]&password=demo12345' | jq -r .token)
# 2. Run a standard scan
curl -s -X POST $BASE/api/v1/scans \
-H "Authorization: Bearer $TOKEN" -d 'url=example.com' | jq '.result.grade, .result.score'
# 3. List / fetch / delete your scans
curl -s $BASE/api/v1/scans -H "Authorization: Bearer $TOKEN"
curl -s $BASE/api/v1/scans/123 -H "Authorization: Bearer $TOKEN"
curl -s -X DELETE $BASE/api/v1/scans/123 -H "Authorization: Bearer $TOKEN"
# Deep project scans run in the terminal and sync to this same history:
# sentinel . --code
| Method & path | Auth | What it does |
|---|---|---|
POST /api/v1/register |
— | Create an account, returns a token |
POST /api/v1/tokens |
— | Email+password → new token |
GET /api/v1/me |
token | Current account |
DELETE /api/v1/tokens/current |
token | Revoke the current token |
GET /api/v1/scans |
token | List your scans (paginated) |
POST /api/v1/scans |
token | Run a standard scan (deep=1 is rejected — use the CLI) |
POST /api/v1/scans/import |
token | Save a locally-run (deep/--code) scan to history — used by the CLI after sentinel login |
GET /api/v1/scans/{id} |
token | One scan report (yours only) |
DELETE /api/v1/scans/{id} |
token | Delete a scan |
Tokens are stored hashed; the plaintext (prefixed stnl_) is shown once, and the
public endpoints are rate-limited. Deep/active vulnerability scanning lives only
in the CLI, which verifies you own the domain before testing.
Deploy (free)
Run the whole app for free on an Oracle Cloud Always Free VM (Nginx + PHP-FPM
for Laravel, the FastAPI engine as a systemd service, SQLite for storage — no
cold starts, no external DB). A ready-to-run kit lives in
deploy/oracle/:
sudo git clone <your-repo-url> /opt/sentinel
sudo bash /opt/sentinel/deploy/oracle/setup.sh yourdomain.com
It installs everything, configures the services, sets production env (debug off,
fresh APP_KEY, a shared internal key), runs migrations, and prints the next
steps (HTTPS via certbot, creating your admin). Full walkthrough — including the
Oracle firewall rules you must add — is in
deploy/oracle/DEPLOY.md.
Safety & scope
- All active checks are non-destructive: GET-only probes and a single benign, inert marker for the XSS/redirect tests. Nothing is submitted, written, or deleted.
- The API refuses to scan private/loopback/internal addresses (SSRF guard).
- Heuristic checks (malware, XSS) are flagged as possible and recommend manual confirmation.
License
Released under the MIT License.
Built With
- authentication
- cli
- javascript
- laravel
- php
- python
- restapi
Log in or sign up for Devpost to join the conversation.