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 . --code statically 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 --deep mode 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 users table. Passwords are bcrypt hashed (Laravel's hashed cast).
  • Per-user history: every scan is saved to the MySQL scans table 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 Email 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 is sentinel.

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 missing Secure/HttpOnly, CSRF turned off.
  • The deployment layer — Dockerfiles (root user, :latest, remote ADD, secrets baked into layers), Kubernetes and Compose (privileged, host namespaces, inline secrets), Terraform (0.0.0.0/0 ingress, 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 .env is 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.json exact pins, composer.lock) and flags pinned versions that match a bundled snapshot of well-known advisories (CVEs for popular packages like requests, 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 with git 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

Share this project:

Updates