diff --git a/security/lightscope/LICENSE b/security/lightscope/LICENSE new file mode 100644 index 0000000000..74f8f91833 --- /dev/null +++ b/security/lightscope/LICENSE @@ -0,0 +1,25 @@ +BSD 2-Clause License + +Copyright (c) 2025, Eric Kapitanski +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/security/lightscope/Makefile b/security/lightscope/Makefile new file mode 100644 index 0000000000..721b24598f --- /dev/null +++ b/security/lightscope/Makefile @@ -0,0 +1,8 @@ +PLUGIN_NAME= lightscope +PLUGIN_VERSION= 1.0 +PLUGIN_COMMENT= LightScope Cybersecurity Research Honeypot and Telescope +PLUGIN_MAINTAINER= e@alumni.usc.edu +PLUGIN_WWW= https://thelightscope.com +PLUGIN_DEPENDS= lightscope + +.include "../../Mk/plugins.mk" diff --git a/security/lightscope/README.md b/security/lightscope/README.md new file mode 100644 index 0000000000..53c9687110 --- /dev/null +++ b/security/lightscope/README.md @@ -0,0 +1,204 @@ +# LightScope for OPNsense + +OPNsense plugin for LightScope network security monitoring. + +## Features + +- Captures blocked TCP SYN packets from pf firewall via pflog0 +- Configurable honeypot ports with PROXY protocol forwarding +- Uploads security telemetry to thelightscope.com for analysis +- Privacy-preserving IP randomization + +## Requirements + +- OPNsense 24.x or later (FreeBSD 14.x) +- pf firewall with logging enabled +- Python 3.11 + +## Installation + +### From Package + +```bash +pkg install /path/to/os-lightscope-1.0.pkg +``` + +### Building from Source + +```bash +# Clone into OPNsense plugins directory +cd /usr/plugins/security +git clone lightscope + +# Or copy files +cp -r /path/to/OPNsense/* /usr/plugins/security/lightscope/ + +# Build +cd /usr/plugins/security/lightscope +make package + +# Install +pkg install work/pkg/os-lightscope-1.0.pkg +``` + +## Configuration + +Configuration file: `/usr/local/etc/lightscope.conf` + +```ini +[Settings] +# Database identifier - auto-generated if empty +database = + +# IP randomization key - auto-generated if empty +randomization_key = + +# Comma-separated honeypot ports (leave empty to disable) +honeypot_ports = 8080,2323,8443,3389,5900 + +# Remote honeypot server +honeypot_server = 128.9.28.79 +honeypot_ssh_port = 12345 +honeypot_telnet_port = 12346 +``` + +## Usage + +### Start the Service + +```bash +# One-time start +service os-lightscope onestart + +# Enable at boot +sysrc lightscope_enable="YES" +service os-lightscope start +``` + +### Check Status + +```bash +# View running processes +ps aux | grep lightscope + +# Check honeypot ports +sockstat -l | grep -E "8080|2323|8443|3389|5900" +``` + +### View Logs + +Logs are output to stdout/stderr. When running via rc.d, check: +```bash +tail -f /var/log/messages | grep lightscope +``` + +## How It Works + +1. **pf firewall** blocks unwanted traffic and logs to pflog0 interface +2. **pflog_reader** captures packets from pflog0, parses TCP SYN packets +3. **packet_handler** processes packets and prepares for upload +4. **uploader** batches and sends data to thelightscope.com +5. **honeypot** listens on configured ports and forwards connections + +### Data Flow + +``` +pflog0 → pflog_reader → packet_handler → uploader → thelightscope.com + ↑ +honeypot ports → honeypot → honeypot_uploader ┘ +``` + +### Data Storage + +All data is kept in memory only. No disk writes for packet data. +- Queue max size: 100,000 items +- Batch size: 600 items (or 5 second idle flush) +- On upload failure: retry after 5 seconds + +## Testing + +```bash +# Load pf modules +kldload pf pflog + +# Create pflog interface +ifconfig pflog0 create + +# Enable pf with a test block rule +pfctl -e +echo 'block out log quick proto tcp from any to any port 9998' | pfctl -f - + +# Start service +service os-lightscope onestart + +# Generate blocked traffic +nc -w 1 8.8.8.8 9998 + +# Check logs for: +# "pflog_reader: sent X packets" +# "uploader: Sent X items" +``` + +## Troubleshooting + +### pflog_reader not capturing packets + +1. Verify pf is enabled: `pfctl -s info` +2. Verify pflog0 exists: `ifconfig pflog0` +3. Verify rules have `log` keyword: `pfctl -sr` +4. Test capture directly: `tcpdump -i pflog0 -c 5` + +### Service won't start + +1. Check python3 symlink exists: `ls -la /usr/local/bin/python3` +2. If missing: `ln -s /usr/local/bin/python3.11 /usr/local/bin/python3` + +### Packets captured but not uploaded + +1. Check network connectivity to thelightscope.com +2. Look for upload errors in logs + +## Dependencies + +Installed automatically via pkg: +- python311 +- py311-dpkt +- py311-requests +- py311-psutil +- py311-pypcap + +## Files + +``` +/usr/local/etc/lightscope.conf.sample - Sample configuration +/usr/local/etc/lightscope.conf - Active configuration (created on first run) +/usr/local/etc/rc.d/os-lightscope - Service script +/usr/local/opnsense/scripts/lightscope/ - Python scripts + ├── lightscope_daemon.py - Main daemon + ├── pflog_reader.py - Packet capture + ├── honeypot.py - Honeypot listener + └── uploader.py - Data upload +``` + +## Technical Notes + +### pflog Header Size + +FreeBSD 14.x uses a 72-byte pflog header (see `/usr/include/net/if_pflog.h`). +The IP packet starts at offset 72 in captured pflog packets. + +### Honeypot PROXY Protocol + +Honeypot connections are forwarded with PROXY protocol v1 header: +``` +PROXY TCP4 +``` + +## License + +BSD 2-Clause License - see LICENSE file + +## Support + +- Dashboard: https://thelightscope.com +- Issues: https://github.com/Thelightscope/thelightscope/issues diff --git a/security/lightscope/pkg-descr b/security/lightscope/pkg-descr new file mode 100644 index 0000000000..472078884c --- /dev/null +++ b/security/lightscope/pkg-descr @@ -0,0 +1,10 @@ +LightScope is a network security monitoring tool that detects unwanted +network traffic and provides honeypot functionality. + +Features: +- Captures and analyzes blocked TCP SYN packets via pflog +- Configurable honeypot ports with PROXY protocol forwarding +- Uploads security telemetry to thelightscope.com for analysis +- Privacy-preserving IP randomization + +WWW: https://thelightscope.com diff --git a/security/lightscope/src/+POST_DEINSTALL b/security/lightscope/src/+POST_DEINSTALL new file mode 100755 index 0000000000..07241b5dfe --- /dev/null +++ b/security/lightscope/src/+POST_DEINSTALL @@ -0,0 +1,10 @@ +#!/bin/sh + +# Stop LightScope service if running +/usr/local/etc/rc.d/os-lightscope onestop 2>/dev/null || true + +# Reload firewall rules to remove LightScope rules +# This ensures the block+log rule is removed immediately +/usr/local/sbin/configctl filter reload 2>/dev/null || true + +echo "LightScope has been uninstalled." diff --git a/security/lightscope/src/+POST_INSTALL b/security/lightscope/src/+POST_INSTALL new file mode 100755 index 0000000000..bf52d8e5bb --- /dev/null +++ b/security/lightscope/src/+POST_INSTALL @@ -0,0 +1,16 @@ +#!/bin/sh + +# Set executable permissions on scripts +chmod +x /usr/local/opnsense/scripts/lightscope/*.py 2>/dev/null +chmod +x /usr/local/opnsense/scripts/lightscope/*.sh 2>/dev/null +chmod +x /usr/local/etc/rc.d/os-lightscope 2>/dev/null + +# Install Python dependencies via pip if not already present +if ! /usr/local/bin/python3.11 -c "import dpkt" 2>/dev/null; then + echo "Installing LightScope Python dependencies..." + /usr/local/bin/python3.11 -m ensurepip --default-pip 2>/dev/null || true + /usr/local/bin/python3.11 -m pip install --quiet dpkt requests psutil pypcap +fi + +echo "LightScope installed successfully." +echo "Configure via Services -> LightScope in the web UI." diff --git a/security/lightscope/src/etc/inc/plugins.inc.d/lightscope.inc b/security/lightscope/src/etc/inc/plugins.inc.d/lightscope.inc new file mode 100644 index 0000000000..c88a4912e5 --- /dev/null +++ b/security/lightscope/src/etc/inc/plugins.inc.d/lightscope.inc @@ -0,0 +1,158 @@ + + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + * AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, + * OR CONSEQUENTIAL DAMAGES ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE. + */ + +function lightscope_enabled() +{ + $model = new \OPNsense\Lightscope\Lightscope(); + return (string)$model->general->enabled == '1'; +} + +function lightscope_services() +{ + $services = array(); + + if (lightscope_enabled()) { + $services[] = array( + 'description' => gettext('LightScope Cybersecurity Research Honeypot and Telescope'), + 'configd' => array( + 'restart' => array('lightscope restart'), + 'start' => array('lightscope start'), + 'stop' => array('lightscope stop'), + ), + 'name' => 'lightscope', + 'pidfile' => '/var/run/lightscope.pid' + ); + } + + return $services; +} + +function lightscope_syslog() +{ + $logfacilities = array(); + + $logfacilities['lightscope'] = array( + 'facility' => array('lightscope'), + ); + + return $logfacilities; +} + +function lightscope_firewall(\OPNsense\Firewall\Plugin $fw) +{ + // Always add block+log rule when plugin is installed - no config dependency + // This ensures pflog captures blocked TCP traffic for LightScope visibility + // Uses very high priority to run last, catching traffic that falls through + $fw->registerFilterRule( + 999999, // highest priority - run as late as possible + array( + 'type' => 'block', + 'from' => 'any', + 'to' => '(self)', + 'direction' => 'in', + 'protocol' => 'tcp', + 'descr' => 'LightScope: log blocked TCP', + 'log' => true, + '#ref' => 'lightscope_blocklog' + ), + null + ); + + // Honeypot rules depend on config - wrap in try/catch for resilience + try { + $model = new \OPNsense\Lightscope\Lightscope(); + + if ((string)$model->general->enabled != '1') { + return; + } + + $honeypot_ports = (string)$model->general->honeypot_ports; + + if (empty($honeypot_ports)) { + return; + } + + // Parse comma-separated ports + $ports = array_map('trim', explode(',', $honeypot_ports)); + $valid_ports = array(); + + foreach ($ports as $port) { + if (is_numeric($port) && $port >= 1 && $port <= 65535) { + $valid_ports[] = $port; + } + } + + if (empty($valid_ports)) { + return; + } + + // Create firewall rule to allow honeypot traffic + $fw->registerFilterRule( + 100000, // priority - run late so other rules take precedence + array( + 'from' => 'any', + 'to' => '(self)', + 'direction' => 'in', + 'protocol' => 'tcp', + 'to_port' => implode(' ', $valid_ports), + 'descr' => 'LightScope honeypot ports', + 'log' => true, + '#ref' => 'lightscope_honeypot' + ), + null + ); + } catch (Exception $e) { + // Model loading failed - log rule is still added, honeypot rules skipped + // This ensures basic functionality even if config is broken + } +} + +function lightscope_xmlrpc_sync() +{ + $result = array(); + $result['id'] = 'lightscope'; + $result['section'] = 'OPNsense.Lightscope'; + $result['description'] = gettext('LightScope Security Monitor'); + $result['services'] = array('lightscope'); + return array($result); +} + +function lightscope_configure() +{ + return array( + 'bootup' => array('lightscope_configure_do') + ); +} + +function lightscope_configure_do($verbose = false) +{ + if (lightscope_enabled()) { + if ($verbose) { + echo 'Starting LightScope...'; + } + configd_run('lightscope start'); + if ($verbose) { + echo "done.\n"; + } + } +} diff --git a/security/lightscope/src/etc/rc.d/os-lightscope b/security/lightscope/src/etc/rc.d/os-lightscope new file mode 100755 index 0000000000..4761f56d66 --- /dev/null +++ b/security/lightscope/src/etc/rc.d/os-lightscope @@ -0,0 +1,86 @@ +#!/bin/sh +# +# PROVIDE: lightscope +# REQUIRE: DAEMON +# KEYWORD: shutdown +# +# Add the following lines to /etc/rc.conf to enable lightscope: +# +# lightscope_enable="YES" +# + +. /etc/rc.subr + +name="lightscope" +rcvar="${name}_enable" + +load_rc_config $name + +# Check OPNsense model for enabled state if not explicitly set +if [ -z "${lightscope_enable}" ]; then + if /usr/local/sbin/pluginctl -g OPNsense.Lightscope.general.enabled 2>/dev/null | grep -q "1"; then + lightscope_enable="YES" + else + lightscope_enable="NO" + fi +fi + +: ${lightscope_config:="/usr/local/etc/lightscope.conf"} + +pidfile="/var/run/${name}.pid" +procname="/usr/local/bin/lightscope" + +start_cmd="${name}_start" +stop_cmd="${name}_stop" +status_cmd="${name}_status" + +start_precmd="${name}_prestart" + +lightscope_start() +{ + echo "Starting ${name}." + /usr/sbin/daemon -c -f -p ${pidfile} -o /var/log/lightscope.log /usr/local/bin/lightscope +} + +lightscope_stop() +{ + if [ -f ${pidfile} ]; then + echo "Stopping ${name}." + kill -TERM $(cat ${pidfile}) 2>/dev/null + rm -f ${pidfile} + else + echo "${name} is not running." + fi +} + +lightscope_status() +{ + if [ -f ${pidfile} ] && kill -0 $(cat ${pidfile}) 2>/dev/null; then + echo "${name} is running as pid $(cat ${pidfile})." + return 0 + else + echo "${name} is not running." + return 1 + fi +} + +lightscope_prestart() +{ + # Copy sample config if no config exists + if [ ! -f "${lightscope_config}" ]; then + if [ -f "${lightscope_config}.sample" ]; then + cp "${lightscope_config}.sample" "${lightscope_config}" + echo "Created ${lightscope_config} from sample" + fi + fi + + # Ensure pflog0 interface exists + if ! ifconfig pflog0 > /dev/null 2>&1; then + echo "Warning: pflog0 interface not found. Creating..." + ifconfig pflog0 create + fi + + return 0 +} + +run_rc_command "$1" diff --git a/security/lightscope/src/opnsense/mvc/app/controllers/OPNsense/Lightscope/Api/ServiceController.php b/security/lightscope/src/opnsense/mvc/app/controllers/OPNsense/Lightscope/Api/ServiceController.php new file mode 100644 index 0000000000..9fee18647b --- /dev/null +++ b/security/lightscope/src/opnsense/mvc/app/controllers/OPNsense/Lightscope/Api/ServiceController.php @@ -0,0 +1,49 @@ + + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + * AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, + * OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +namespace OPNsense\Lightscope\Api; + +use OPNsense\Base\ApiMutableServiceControllerBase; +use OPNsense\Core\Backend; + +class ServiceController extends ApiMutableServiceControllerBase +{ + protected static $internalServiceClass = '\OPNsense\Lightscope\Lightscope'; + protected static $internalServiceEnabled = 'general.enabled'; + protected static $internalServiceName = 'lightscope'; + + /** + * Override reconfigure to skip template generation (we don't use templates) + */ + public function reconfigureAction() + { + $backend = new Backend(); + $response = $backend->configdRun('lightscope reconfigure'); + return array("status" => "ok", "response" => $response); + } +} diff --git a/security/lightscope/src/opnsense/mvc/app/controllers/OPNsense/Lightscope/Api/SettingsController.php b/security/lightscope/src/opnsense/mvc/app/controllers/OPNsense/Lightscope/Api/SettingsController.php new file mode 100644 index 0000000000..051f9f9dac --- /dev/null +++ b/security/lightscope/src/opnsense/mvc/app/controllers/OPNsense/Lightscope/Api/SettingsController.php @@ -0,0 +1,41 @@ + + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + * AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, + * OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +namespace OPNsense\Lightscope\Api; + +use OPNsense\Base\ApiMutableModelControllerBase; + +/** + * Class SettingsController + * @package OPNsense\Lightscope\Api + */ +class SettingsController extends ApiMutableModelControllerBase +{ + protected static $internalModelClass = '\OPNsense\Lightscope\Lightscope'; + protected static $internalModelName = 'lightscope'; +} diff --git a/security/lightscope/src/opnsense/mvc/app/controllers/OPNsense/Lightscope/Api/StatusController.php b/security/lightscope/src/opnsense/mvc/app/controllers/OPNsense/Lightscope/Api/StatusController.php new file mode 100644 index 0000000000..e3bb11d3fe --- /dev/null +++ b/security/lightscope/src/opnsense/mvc/app/controllers/OPNsense/Lightscope/Api/StatusController.php @@ -0,0 +1,66 @@ + + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + * AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, + * OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +namespace OPNsense\Lightscope\Api; + +use OPNsense\Base\ApiControllerBase; +use OPNsense\Core\Backend; + +class StatusController extends ApiControllerBase +{ + /** + * Get LightScope status including database name and dashboard URL + * @return array status information + */ + public function statusAction() + { + $response = json_decode(trim((new Backend())->configdRun('lightscope status')), true); + if ($response !== null) { + return $response; + } + return [ + 'status' => 'unknown', + 'database' => '', + 'dashboard_url' => '', + 'error' => 'Unable to determine LightScope status' + ]; + } + + /** + * Get LightScope service logs + * @return array log content + */ + public function logsAction() + { + $response = json_decode(trim((new Backend())->configdRun('lightscope logs')), true); + if ($response !== null) { + return $response; + } + return ['logs' => 'Unable to retrieve logs']; + } +} diff --git a/security/lightscope/src/opnsense/mvc/app/controllers/OPNsense/Lightscope/IndexController.php b/security/lightscope/src/opnsense/mvc/app/controllers/OPNsense/Lightscope/IndexController.php new file mode 100644 index 0000000000..d03b18c61c --- /dev/null +++ b/security/lightscope/src/opnsense/mvc/app/controllers/OPNsense/Lightscope/IndexController.php @@ -0,0 +1,45 @@ + + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + * AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, + * OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +namespace OPNsense\Lightscope; + +/** + * Class IndexController + * @package OPNsense\Lightscope + */ +class IndexController extends \OPNsense\Base\IndexController +{ + /** + * Default index action + */ + public function indexAction() + { + $this->view->settings = $this->getForm("settings"); + $this->view->pick('OPNsense/Lightscope/index'); + } +} diff --git a/security/lightscope/src/opnsense/mvc/app/controllers/OPNsense/Lightscope/forms/settings.xml b/security/lightscope/src/opnsense/mvc/app/controllers/OPNsense/Lightscope/forms/settings.xml new file mode 100644 index 0000000000..4667955b0c --- /dev/null +++ b/security/lightscope/src/opnsense/mvc/app/controllers/OPNsense/Lightscope/forms/settings.xml @@ -0,0 +1,14 @@ +
+ + lightscope.general.enabled + + checkbox + Enable or disable the LightScope security monitoring service. + + + lightscope.general.honeypot_ports + + text + Comma-separated list of TCP ports to forward to the USC honeypot for analysis. You can view your honeypot data on the Full Dashboard. Example input: 8080,2323,8443,3389,5900 + +
diff --git a/security/lightscope/src/opnsense/mvc/app/models/OPNsense/Lightscope/ACL/ACL.xml b/security/lightscope/src/opnsense/mvc/app/models/OPNsense/Lightscope/ACL/ACL.xml new file mode 100644 index 0000000000..643696ac38 --- /dev/null +++ b/security/lightscope/src/opnsense/mvc/app/models/OPNsense/Lightscope/ACL/ACL.xml @@ -0,0 +1,9 @@ + + + Services: LightScope + + ui/lightscope/* + api/lightscope/* + + + diff --git a/security/lightscope/src/opnsense/mvc/app/models/OPNsense/Lightscope/Lightscope.php b/security/lightscope/src/opnsense/mvc/app/models/OPNsense/Lightscope/Lightscope.php new file mode 100644 index 0000000000..01e137b295 --- /dev/null +++ b/security/lightscope/src/opnsense/mvc/app/models/OPNsense/Lightscope/Lightscope.php @@ -0,0 +1,14 @@ + + * All rights reserved. + */ + +namespace OPNsense\Lightscope; + +use OPNsense\Base\BaseModel; + +class Lightscope extends BaseModel +{ +} diff --git a/security/lightscope/src/opnsense/mvc/app/models/OPNsense/Lightscope/Lightscope.xml b/security/lightscope/src/opnsense/mvc/app/models/OPNsense/Lightscope/Lightscope.xml new file mode 100644 index 0000000000..d98eff1f2c --- /dev/null +++ b/security/lightscope/src/opnsense/mvc/app/models/OPNsense/Lightscope/Lightscope.xml @@ -0,0 +1,42 @@ + + //OPNsense/Lightscope + 1.0.0 + LightScope Cybersecurity Research Honeypot and Telescope + + + + 0 + Y + + + N + /^[a-z0-9_]{0,63}$/ + Database name must be lowercase alphanumeric with underscores, max 63 chars + + + N + /^[a-z0-9_]{0,63}$/ + + + 8080,2323,8443,3389,5900 + N + /^[0-9,\s]*$/ + Enter comma-separated port numbers (e.g., 8080,2323,8443) + + + 128.9.28.79 + N + + + 12345 + 1 + 65535 + + + 12346 + 1 + 65535 + + + + diff --git a/security/lightscope/src/opnsense/mvc/app/models/OPNsense/Lightscope/Menu/Menu.xml b/security/lightscope/src/opnsense/mvc/app/models/OPNsense/Lightscope/Menu/Menu.xml new file mode 100644 index 0000000000..70a6d39755 --- /dev/null +++ b/security/lightscope/src/opnsense/mvc/app/models/OPNsense/Lightscope/Menu/Menu.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/security/lightscope/src/opnsense/mvc/app/views/OPNsense/Lightscope/index.volt b/security/lightscope/src/opnsense/mvc/app/views/OPNsense/Lightscope/index.volt new file mode 100644 index 0000000000..48bde8f5e7 --- /dev/null +++ b/security/lightscope/src/opnsense/mvc/app/views/OPNsense/Lightscope/index.volt @@ -0,0 +1,244 @@ +{# + # Copyright (c) 2025 Eric Kapitanski + # University of Southern California Information Sciences Institute + # All rights reserved. + #} + + + +
+
+

LightScope Cybersecurity Research Honeypot and Telescope

+
+
+ +
+
+ + + + + + + + + + + + + + + + + + + +
{{ lang._('Service Status') }} + {{ lang._('Loading...') }} +
+
+
+ + + + +
+ + + {{ partial("layout_partials/base_form", ['fields': settings, 'id': 'frm_GeneralSettings']) }} + +
+ + Important: Make sure logging is enabled on your firewall block/reject rules or LightScope can't see that traffic! LightScope works by examining your firewall logs for dropped/rejected traffic as they are created. Logging accepted traffic is not required, as LightScope will ignore anything accepted anyway. You don't need to retain logs or can use small log sizes, but logging must be enabled on your explicit drop/reject rules. +
+ +
+ + +
+
+ + + + +
+
+ +
+ + +
+
+

+ {{ lang._('Service Logs') }} + +

+
Loading logs...
+
+
+
+
diff --git a/security/lightscope/src/opnsense/scripts/lightscope/logs.py b/security/lightscope/src/opnsense/scripts/lightscope/logs.py new file mode 100755 index 0000000000..d2c5e56d18 --- /dev/null +++ b/security/lightscope/src/opnsense/scripts/lightscope/logs.py @@ -0,0 +1,46 @@ +#!/usr/local/bin/python3 +""" +logs.py - Returns LightScope logs as JSON +""" + +import json +import os +import subprocess + +LOG_FILE = "/var/log/lightscope.log" +MAX_LINES = 100 + +def get_logs(): + """Get recent LightScope logs.""" + logs = "" + + # Try reading from log file first + if os.path.exists(LOG_FILE): + try: + with open(LOG_FILE, 'r') as f: + lines = f.readlines() + logs = ''.join(lines[-MAX_LINES:]) + except Exception as e: + logs = f"Error reading log file: {e}" + + # If no log file, try syslog + if not logs: + try: + proc = subprocess.run( + ['grep', '-i', 'lightscope', '/var/log/system.log'], + capture_output=True, + text=True, + timeout=5 + ) + if proc.stdout: + lines = proc.stdout.strip().split('\n') + logs = '\n'.join(lines[-MAX_LINES:]) + else: + logs = "No logs found. Service may not have been started yet." + except Exception as e: + logs = f"Error reading syslog: {e}" + + return {"logs": logs} + +if __name__ == "__main__": + print(json.dumps(get_logs())) diff --git a/security/lightscope/src/opnsense/scripts/lightscope/reconfigure.sh b/security/lightscope/src/opnsense/scripts/lightscope/reconfigure.sh new file mode 100755 index 0000000000..4505b703b2 --- /dev/null +++ b/security/lightscope/src/opnsense/scripts/lightscope/reconfigure.sh @@ -0,0 +1,39 @@ +#!/bin/sh +# +# Reconfigure LightScope - regenerate config and restart service +# +# Copyright (c) 2025 Eric Kapitanski +# University of Southern California Information Sciences Institute +# + +CONFIG_FILE="/usr/local/etc/lightscope.conf" + +# Update honeypot_ports from OPNsense model (preserves database ID) +# Note: pluginctl returns empty string if not set, which is valid (no honeypot ports) +HONEYPOT_PORTS=$(/usr/local/sbin/pluginctl -g OPNsense.Lightscope.general.honeypot_ports 2>/dev/null) +if [ -f "$CONFIG_FILE" ]; then + # Check if line exists + if grep -q "^honeypot_ports" "$CONFIG_FILE"; then + sed -i '' "s/^honeypot_ports.*/honeypot_ports = $HONEYPOT_PORTS/" "$CONFIG_FILE" + else + # Add the line if it doesn't exist + echo "honeypot_ports = $HONEYPOT_PORTS" >> "$CONFIG_FILE" + fi + echo "Updated honeypot_ports to: $HONEYPOT_PORTS" +fi + +# Reload firewall rules (for honeypot ports) +/usr/local/etc/rc.filter_configure + +# Check if service should be enabled +ENABLED=$(/usr/local/sbin/pluginctl -g OPNsense.Lightscope.general.enabled 2>/dev/null) + +if [ "$ENABLED" = "1" ]; then + # Restart service if enabled + /usr/local/etc/rc.d/os-lightscope onerestart +else + # Stop service if disabled + /usr/local/etc/rc.d/os-lightscope onestop 2>/dev/null +fi + +exit 0 diff --git a/security/lightscope/src/opnsense/scripts/lightscope/status.py b/security/lightscope/src/opnsense/scripts/lightscope/status.py new file mode 100755 index 0000000000..c60acbfb55 --- /dev/null +++ b/security/lightscope/src/opnsense/scripts/lightscope/status.py @@ -0,0 +1,243 @@ +#!/usr/local/bin/python3 +""" +status.py - Returns LightScope status as JSON for API/widget +""" + +import json +import os +import re +import socket +import configparser +import subprocess + +CONFIG_FILE = "/usr/local/etc/lightscope.conf" +DASHBOARD_URL = "https://thelightscope.com/light_table" + + +def get_firewall_allowed_ports(): + """ + Get list of ports that have PASS/ALLOW rules in the firewall. + These are ports where legitimate services may be running. + Returns a set of individual ports AND a list of (start, end) tuples for ranges. + """ + allowed_ports = set() + allowed_ranges = [] + + try: + # Get active pf rules + result = subprocess.run( + ["pfctl", "-sr"], + capture_output=True, + text=True, + timeout=5 + ) + + if result.returncode == 0: + for line in result.stdout.split('\n'): + # Look for pass rules with port specifications + # Example: pass in on em0 proto tcp from any to any port = 22 + # Example: pass in on em0 proto tcp from any to any port 1:4343 + if line.startswith('pass') and 'proto tcp' in line: + # Match port ranges like "port 1:4343" or "port 80:443" + range_match = re.search(r'port\s*[=]?\s*(\d+):(\d+)', line) + if range_match: + start_port = int(range_match.group(1)) + end_port = int(range_match.group(2)) + allowed_ranges.append((start_port, end_port)) + continue # Don't also match as single port + + # Match single port patterns like "port = 22" or "port 22" + port_match = re.search(r'port\s*[=]?\s*(\d+)(?![\d:])', line) + if port_match: + allowed_ports.add(int(port_match.group(1))) + + # Match port lists in braces like "port { 22 80 443 }" + brace_match = re.search(r'port\s*[=]?\s*\{([^}]+)\}', line) + if brace_match: + ports_str = brace_match.group(1) + for p in re.findall(r'\d+', ports_str): + allowed_ports.add(int(p)) + except Exception as e: + pass + + return allowed_ports, allowed_ranges + + +def is_port_allowed_by_firewall(port, allowed_ports, allowed_ranges): + """Check if a port is allowed by firewall rules (including ranges).""" + if port in allowed_ports: + return True + for start, end in allowed_ranges: + if start <= port <= end: + return True + return False + + +def get_lightscope_pids(): + """Get all lightscope-related PIDs (main daemon and child processes).""" + pids = set() + try: + # Get all processes matching lightscope_daemon + result = subprocess.run( + ["pgrep", "-f", "lightscope_daemon"], + capture_output=True, + text=True, + timeout=5 + ) + if result.returncode == 0: + for line in result.stdout.strip().split('\n'): + if line.strip(): + try: + pids.add(int(line.strip())) + except ValueError: + pass + except Exception: + pass + return pids + + +def is_port_in_use_by_other(port, lightscope_pids=None): + """ + Check if a port is in use by a service OTHER than lightscope. + Returns True if another service is using it, False if available or used by lightscope. + """ + if lightscope_pids is None: + lightscope_pids = set() + + try: + # Use sockstat to see what's listening on the port + result = subprocess.run( + ["sockstat", "-4", "-l", "-p", str(port)], + capture_output=True, + text=True, + timeout=5 + ) + + if result.returncode == 0: + lines = result.stdout.strip().split('\n') + # Skip header line, check remaining lines + for line in lines[1:]: + if line.strip(): + # sockstat output: USER COMMAND PID FD PROTO LOCAL FOREIGN + parts = line.split() + if len(parts) >= 3: + try: + sock_pid = int(parts[2]) + # Check if this is one of our lightscope processes + if sock_pid in lightscope_pids: + return False # It's our honeypot, not a conflict + except ValueError: + pass + # Something else is using the port + return True + + # No one listening - port is available + return False + + except Exception: + # Fall back to socket bind test + try: + test_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + test_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + test_sock.settimeout(0.5) + test_sock.bind(("", port)) + test_sock.close() + return False + except OSError: + return True + + +def get_port_status(ports_string): + """ + Check status of honeypot ports against firewall rules and port usage. + Returns dict with port status info. + """ + port_status = {} + + if not ports_string: + return port_status + + allowed_ports, allowed_ranges = get_firewall_allowed_ports() + lightscope_pids = get_lightscope_pids() + + for p in ports_string.split(','): + p = p.strip() + if p.isdigit(): + port = int(p) + if 1 <= port <= 65535: + if is_port_allowed_by_firewall(port, allowed_ports, allowed_ranges): + # Port has a firewall ALLOW rule - potential conflict + port_status[port] = "firewall_conflict" + elif is_port_in_use_by_other(port, lightscope_pids): + # Port is already in use by another service (not lightscope) + port_status[port] = "in_use" + else: + # Port is safe for honeypot or already opened by lightscope + port_status[port] = "ok" + + return port_status + + +def get_status(): + """Get LightScope status information.""" + result = { + "status": "stopped", + "database": "", + "dashboard_url": "", + "honeypot_ports": "", + "port_status": {}, + "config_exists": False + } + + # Check if config exists + if os.path.exists(CONFIG_FILE): + result["config_exists"] = True + try: + config = configparser.ConfigParser() + config.read(CONFIG_FILE) + + database = config.get('Settings', 'database', fallback='').strip() + if database: + result["database"] = database + result["dashboard_url"] = f"{DASHBOARD_URL}/{database}" + + result["honeypot_ports"] = config.get('Settings', 'honeypot_ports', fallback='') + except Exception as e: + result["config_error"] = str(e) + + # Check if service is running via pid file + pidfile = "/var/run/lightscope.pid" + try: + if os.path.exists(pidfile): + with open(pidfile, 'r') as f: + pid = int(f.read().strip()) + # Check if process is actually running + os.kill(pid, 0) # Signal 0 just checks if process exists + result["status"] = "running" + else: + result["status"] = "stopped" + except (ProcessLookupError, ValueError, PermissionError): + result["status"] = "stopped" + except Exception: + result["status"] = "unknown" + + # Check process count + try: + proc = subprocess.run( + ["pgrep", "-f", "lightscope_daemon"], + capture_output=True, + text=True, + timeout=5 + ) + pids = proc.stdout.strip().split('\n') + result["process_count"] = len([p for p in pids if p]) + except Exception: + result["process_count"] = 0 + + # Check port status against firewall rules + result["port_status"] = get_port_status(result["honeypot_ports"]) + + return result + +if __name__ == "__main__": + print(json.dumps(get_status())) diff --git a/security/lightscope/src/opnsense/service/conf/actions.d/actions_lightscope.conf b/security/lightscope/src/opnsense/service/conf/actions.d/actions_lightscope.conf new file mode 100644 index 0000000000..6815958ba1 --- /dev/null +++ b/security/lightscope/src/opnsense/service/conf/actions.d/actions_lightscope.conf @@ -0,0 +1,30 @@ +[start] +command:/usr/local/etc/rc.d/os-lightscope onestart +type:script +message:starting lightscope service + +[stop] +command:/usr/local/etc/rc.d/os-lightscope onestop +type:script +message:stopping lightscope service + +[restart] +command:/usr/local/etc/rc.d/os-lightscope onerestart +type:script +message:restarting lightscope service + +[status] +command:/usr/local/opnsense/scripts/lightscope/status.py +type:script_output +message:requesting lightscope status + +[reconfigure] +command:/usr/local/opnsense/scripts/lightscope/reconfigure.sh +type:script +message:reconfiguring lightscope +description:Regenerate lightscope configuration + +[logs] +command:/usr/local/opnsense/scripts/lightscope/logs.py +type:script_output +message:retrieving lightscope logs diff --git a/security/lightscope/src/opnsense/www/js/widgets/Lightscope.js b/security/lightscope/src/opnsense/www/js/widgets/Lightscope.js new file mode 100644 index 0000000000..d1638735bf --- /dev/null +++ b/security/lightscope/src/opnsense/www/js/widgets/Lightscope.js @@ -0,0 +1,109 @@ +/* + * Copyright (c) 2025 USC Information Sciences Institute + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + * AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, + * OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +export default class Lightscope extends BaseTableWidget { + constructor() { + super(); + } + + getGridOptions() { + return { + sizeToContent: 300 + }; + } + + getMarkup() { + let $container = $('
'); + let $lightscopeTable = this.createTable('lightscopeStatusTable', { + headerPosition: 'left' + }); + $container.append($lightscopeTable); + return $container; + } + + async onWidgetTick() { + const data = await this.ajaxCall('/api/lightscope/status/status'); + + if (!data) { + this.displayError(this.translations.noData); + return; + } + + if (!this.dataChanged('lightscope-data', data)) { + return; + } + + let rows = []; + + // Status row + let statusText = data.status === 'running' + ? ` ${this.translations.running}` + : ` ${this.translations.stopped}`; + rows.push({ + column1: this.translations.status, + column2: statusText + }); + + // Dashboard link row + if (data.dashboard_url) { + rows.push({ + column1: this.translations.dashboard, + column2: ` + ${this.translations.viewReports} + ` + }); + } + + // Database ID row + if (data.database) { + let shortDb = data.database.length > 30 + ? data.database.substring(0, 30) + '...' + : data.database; + rows.push({ + column1: this.translations.databaseId, + column2: `${shortDb}` + }); + } + + // Honeypot ports row + if (data.honeypot_ports) { + rows.push({ + column1: this.translations.honeypotPorts, + column2: data.honeypot_ports + }); + } + + // Process count + if (data.process_count !== undefined) { + rows.push({ + column1: this.translations.processes, + column2: data.process_count + }); + } + + super.updateTable('lightscopeStatusTable', rows); + } +} diff --git a/security/lightscope/src/opnsense/www/js/widgets/Metadata/Lightscope.xml b/security/lightscope/src/opnsense/www/js/widgets/Metadata/Lightscope.xml new file mode 100644 index 0000000000..033d341906 --- /dev/null +++ b/security/lightscope/src/opnsense/www/js/widgets/Metadata/Lightscope.xml @@ -0,0 +1,20 @@ + + + Lightscope.js + + /api/lightscope/status/status + + + LightScope + Status + Running + Stopped + Dashboard + View Reports + Database ID + Honeypot Ports + Processes + Could not fetch LightScope status + + +