Project / Security Lab

Sentinel — AI Vulnerability Scanner

A fully self-hosted AI-powered penetration testing platform. Local LLM orchestrates seven security tools, analyzes their output for real exploitable vulnerabilities, and writes professional pentest reports. Runs on your own infrastructure with zero per-scan cost and no external API dependencies — a local alternative to commercial tools that charge $50 per scan.

This page walks through everything needed to replicate Sentinel on a Linux box. Stack: Ubuntu + Apache + PHP + MariaDB + Ollama (Nemotron) + Nmap + Nikto + SQLMap + WhatWeb + Dirb + Subfinder + Nuclei. Cloudflare Tunnel handles SSL — no Let's Encrypt required.

Ubuntu 22.04
Apache 2.4
PHP 8.2
MariaDB
Ollama + Nemotron
Nmap
Nikto
SQLMap
WhatWeb
Dirb
Subfinder
Nuclei

How It Works

A scan kicks off from the web dashboard with a target URL and optional source code path. Sentinel runs through five phases: Recon (Nmap, WhatWeb, Dirb, Subfinder, HTTP headers), Vuln Scanning (Nikto, Nuclei, SQLMap), AI Analysis (Nemotron reads every tool's raw output and extracts real exploitable findings), Code Review (when a source path is provided, Nemotron performs white-box OWASP analysis on the codebase), and Report Generation (Nemotron compiles findings into a professional Markdown pentest report).

The entire pipeline runs on local infrastructure. Ollama hosts the Nemotron-3-Nano 30B model with a 1M-token context window, which is large enough to feed it whole tool dumps plus source files in a single analysis call. Progress and logs stream live to the dashboard. Typical quick scan takes ~8 minutes end to end — versus 1–1.5 hours and $50 per scan for comparable commercial tools.

File Structure

/var/www/html/sentinel/
├── index.php                 # Dashboard — scan stats and activity feed
├── config.php                # DB creds, Ollama URL, default model
├── scans.php                 # Scan list / history
├── new_scan.php              # Launch new scan form
├── scan_detail.php           # Live progress, logs, findings
├── report.php                # Rendered Markdown pentest report
├── settings.php              # Tool paths + Ollama config
├── schedules.php             # Scheduled scan management
├── cron_runner.php           # Cron-driven scheduled scan launcher
├── includes/
│   ├── db.php                # PDO connection
│   ├── ollama.php            # Nemotron client (analyze / report)
│   ├── scanner.php           # Tool orchestrator (7 tools, 5 phases)
│   └── tools.php             # Individual tool wrappers
├── api/
│   ├── launch.php            # Background scan launcher (nohup)
│   ├── progress.php          # Live progress polling
│   └── logs.php              # Live log stream
├── assets/css/               # Dark dashboard theme
├── assets/js/                # Live updates + log streaming
├── templates/                # Header, sidebar, footer partials
├── reports/                  # Generated Markdown reports
└── uploads/                  # Optional source code uploads

Step 1 — Install System Packages

sudo apt update
sudo apt install -y apache2 php php-cli php-mysql php-curl php-xml php-mbstring \
                    mariadb-server nmap nikto sqlmap whatweb dirb \
                    curl jq unzip git ffmpeg
sudo systemctl enable --now apache2 mariadb

Step 2 — Install Subfinder + Nuclei

These two ship from ProjectDiscovery as versioned zip releases. Pull the latest directly from GitHub:

cd /tmp

# Subfinder — latest release
SUBFINDER_URL=$(curl -s https://api.github.com/repos/projectdiscovery/subfinder/releases/latest | grep "browser_download_url.*linux_amd64.zip" | cut -d '"' -f 4)
wget "$SUBFINDER_URL"
unzip -o subfinder_*_linux_amd64.zip
sudo mv subfinder /usr/local/bin/
sudo chmod +x /usr/local/bin/subfinder
rm subfinder_*_linux_amd64.zip

# Nuclei — latest release
NUCLEI_URL=$(curl -s https://api.github.com/repos/projectdiscovery/nuclei/releases/latest | grep "browser_download_url.*linux_amd64.zip" | cut -d '"' -f 4)
wget "$NUCLEI_URL"
unzip -o nuclei_*_linux_amd64.zip
sudo mv nuclei /usr/local/bin/
sudo chmod +x /usr/local/bin/nuclei
rm nuclei_*_linux_amd64.zip

# Pull Nuclei's template library (~5000 templates)
nuclei -update-templates

Step 3 — MariaDB Database

Create the database and a dedicated user:

sudo mysql -u root -p
CREATE DATABASE sentinel_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'sentinel_user'@'localhost' IDENTIFIED BY 'your_secure_password_here';
GRANT ALL PRIVILEGES ON sentinel_db.* TO 'sentinel_user'@'localhost';
FLUSH PRIVILEGES;

Now build the schema. At the MariaDB prompt:

USE sentinel_db;

CREATE TABLE scans (
    id INT AUTO_INCREMENT PRIMARY KEY,
    scan_name VARCHAR(255) NOT NULL,
    target_url VARCHAR(500) NOT NULL,
    source_path VARCHAR(500) DEFAULT NULL,
    scan_type ENUM('full', 'recon', 'vuln_analysis', 'quick') DEFAULT 'full',
    status ENUM('pending', 'running', 'analyzing', 'complete', 'failed', 'cancelled') DEFAULT 'pending',
    progress INT DEFAULT 0,
    current_phase VARCHAR(100) DEFAULT NULL,
    started_at DATETIME DEFAULT NULL,
    completed_at DATETIME DEFAULT NULL,
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);

CREATE TABLE scan_results (
    id INT AUTO_INCREMENT PRIMARY KEY,
    scan_id INT NOT NULL,
    tool_name VARCHAR(50) NOT NULL,
    phase VARCHAR(50) NOT NULL,
    raw_output LONGTEXT,
    parsed_data JSON DEFAULT NULL,
    status ENUM('pending', 'running', 'complete', 'failed') DEFAULT 'pending',
    started_at DATETIME DEFAULT NULL,
    completed_at DATETIME DEFAULT NULL,
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (scan_id) REFERENCES scans(id) ON DELETE CASCADE
);

CREATE TABLE findings (
    id INT AUTO_INCREMENT PRIMARY KEY,
    scan_id INT NOT NULL,
    severity ENUM('critical', 'high', 'medium', 'low', 'info') DEFAULT 'info',
    category VARCHAR(100) NOT NULL,
    title VARCHAR(500) NOT NULL,
    description LONGTEXT,
    affected_url VARCHAR(500) DEFAULT NULL,
    affected_code TEXT DEFAULT NULL,
    proof_of_concept TEXT DEFAULT NULL,
    remediation TEXT DEFAULT NULL,
    ai_confidence DECIMAL(5,2) DEFAULT NULL,
    source_tool VARCHAR(50) DEFAULT NULL,
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (scan_id) REFERENCES scans(id) ON DELETE CASCADE
);

CREATE TABLE reports (
    id INT AUTO_INCREMENT PRIMARY KEY,
    scan_id INT NOT NULL,
    report_type ENUM('full', 'executive', 'technical') DEFAULT 'full',
    content LONGTEXT,
    file_path VARCHAR(500) DEFAULT NULL,
    generated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (scan_id) REFERENCES scans(id) ON DELETE CASCADE
);

CREATE TABLE scan_logs (
    id INT AUTO_INCREMENT PRIMARY KEY,
    scan_id INT NOT NULL,
    log_level ENUM('info', 'warning', 'error', 'debug') DEFAULT 'info',
    message TEXT NOT NULL,
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (scan_id) REFERENCES scans(id) ON DELETE CASCADE
);

CREATE TABLE settings (
    id INT AUTO_INCREMENT PRIMARY KEY,
    setting_key VARCHAR(100) UNIQUE NOT NULL,
    setting_value TEXT,
    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);

INSERT INTO settings (setting_key, setting_value) VALUES
('ollama_url', 'http://localhost:11434'),
('ollama_model', 'nemotron-3-nano:30b-cloud'),
('max_concurrent_tools', '3'),
('scan_timeout_minutes', '120'),
('nmap_path', '/usr/bin/nmap'),
('nikto_path', '/usr/bin/nikto'),
('sqlmap_path', '/usr/bin/sqlmap'),
('whatweb_path', '/usr/bin/whatweb'),
('dirb_path', '/usr/bin/dirb'),
('subfinder_path', '/usr/local/bin/subfinder'),
('nuclei_path', '/usr/local/bin/nuclei');

Step 3b — Scheduled Scans Schema (Optional)

If you want scheduled / recurring scans, add this schema on top of the base one. It drives schedules.php and cron_runner.php:

USE sentinel_db;

CREATE TABLE scan_schedules (
    id INT AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(255) NOT NULL,
    target_url VARCHAR(500) NOT NULL,
    source_path VARCHAR(500) DEFAULT NULL,
    scan_type ENUM('full', 'recon', 'vuln_analysis', 'quick') DEFAULT 'recon',
    frequency ENUM('hourly', 'daily', 'weekly', 'monthly') DEFAULT 'weekly',
    day_of_week TINYINT DEFAULT NULL,
    hour_of_day TINYINT DEFAULT 3,
    enabled TINYINT(1) DEFAULT 1,
    last_run_at DATETIME DEFAULT NULL,
    last_scan_id INT DEFAULT NULL,
    next_run_at DATETIME DEFAULT NULL,
    total_runs INT DEFAULT 0,
    notes TEXT DEFAULT NULL,
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    INDEX idx_enabled_next (enabled, next_run_at)
);

ALTER TABLE scans ADD COLUMN schedule_id INT DEFAULT NULL AFTER scan_type;
ALTER TABLE scans ADD INDEX idx_schedule (schedule_id);

Step 4 — Ollama + Nemotron

Install Ollama and pull the Nemotron-3-Nano 30B model with its 1M context window:

curl -fsSL https://ollama.com/install.sh | sh
sudo systemctl enable --now ollama

# Sign in to Ollama cloud to access cloud-hosted models
ollama signin

# Pull the model (cloud variant — 1M context, no local GPU hit)
ollama pull nemotron-3-nano:30b-cloud

Confirm the model shows up:

curl http://localhost:11434/api/tags | jq '.models[].name'

You should see nemotron-3-nano:30b-cloud in the list. Match OLLAMA_MODEL_DEFAULT in config.php to this exact string.

Step 5 — Deploy the App

Drop the project files into /var/www/html/sentinel/ and update config.php with your DB credentials:

sudo nano /var/www/html/sentinel/config.php
define('DB_HOST', 'localhost');
define('DB_NAME', 'sentinel_db');
define('DB_USER', 'sentinel_user');
define('DB_PASS', 'your_secure_password_here');
define('OLLAMA_URL', 'http://localhost:11434');
define('OLLAMA_MODEL_DEFAULT', 'nemotron-3-nano:30b-cloud');

Step 6 — Permissions

Ensure the web user can write reports and uploads, and is still restricted elsewhere:

sudo mkdir -p /var/www/html/sentinel/reports
sudo mkdir -p /var/www/html/sentinel/uploads
sudo chown -R www-data:www-data /var/www/html/sentinel/
sudo chmod -R 755 /var/www/html/sentinel/
sudo chmod -R 775 /var/www/html/sentinel/reports/
sudo chmod -R 775 /var/www/html/sentinel/uploads/

Step 7 — Grant www-data Tool Access

Apache's www-data needs passwordless sudo for each security tool so scans can launch from the web UI. Scope the sudoers entry tightly to just these binaries:

echo 'www-data ALL=(ALL) NOPASSWD: /usr/bin/nmap, /usr/bin/nikto, /usr/bin/sqlmap, /usr/bin/whatweb, /usr/bin/dirb, /usr/local/bin/subfinder, /usr/local/bin/nuclei' | sudo tee /etc/sudoers.d/sentinel
sudo chmod 440 /etc/sudoers.d/sentinel

Step 8 — Apache Virtual Host

Sentinel uses absolute asset paths (/assets/css/...), so give it its own vhost. Replace sentinel.yourdomain.com with your Cloudflare Tunnel hostname:

sudo nano /etc/apache2/sites-available/sentinel.conf
<VirtualHost *:80>
    ServerName sentinel.yourdomain.com
    DocumentRoot /var/www/html/sentinel

    <Directory /var/www/html/sentinel>
        AllowOverride All
        Require all granted
    </Directory>

    ErrorLog ${APACHE_LOG_DIR}/sentinel_error.log
    CustomLog ${APACHE_LOG_DIR}/sentinel_access.log combined
</VirtualHost>
sudo a2ensite sentinel.conf
sudo a2enmod rewrite headers
sudo systemctl restart apache2

Step 9 — Cloudflare Tunnel

Skip Let's Encrypt. Install cloudflared, create a tunnel, and route your hostname to http://localhost:80. Cloudflare handles SSL at the edge.

cloudflared tunnel login
cloudflared tunnel create sentinel
cloudflared tunnel route dns sentinel sentinel.yourdomain.com
sudo cloudflared service install

Step 10 — Scheduled Scans Cron (Optional)

If you applied the scan_schedules schema from Step 3b, wire up the cron runner as www-data so schedules fire automatically:

sudo touch /var/log/sentinel_cron.log
sudo chown www-data:www-data /var/log/sentinel_cron.log
sudo crontab -u www-data -e

Add:

* * * * * /usr/bin/php /var/www/html/sentinel/cron_runner.php >> /var/log/sentinel_cron.log 2>&1

The runner checks scan_schedules each minute, picks any due entries, and launches scans in the background via nohup. Overlap prevention means a new run is skipped if the previous one is still in progress.

Verify

Note — config.php is intentionally excluded from the zip. Copy config.example.php to config.php and fill in your DB credentials, Ollama URL, and model name before hitting the dashboard. Keep the /etc/sudoers.d/sentinel entry scoped to just the seven tool binaries — never grant the web user broader sudo access.