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.
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.
/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
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
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
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');
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);
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.
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');
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/
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
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
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
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.
https://sentinel.yourdomain.com — dashboard loads with a green Ollama: nemotron-3-nano:30b-cloud status dot in the sidebar.http://testphp.vulnweb.com, scan type Quick Scan, click Launch Scan.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.