Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions sysutils/os-splunk-hec/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
PLUGIN_NAME= splunk-hec
PLUGIN_VERSION= 1.0.0
PLUGIN_COMMENT= Splunk HEC log exporter for OPNsense
PLUGIN_MAINTAINER= you@example.com
PLUGIN_WWW= https://github.com/pvols79/os-splunk-hec
PLUGIN_LICENSES= BSD2CLAUSE

PLUGIN_ARCH_DEFAULT?= any

PLUGINSDIR= /usr/local/src/opnsense-plugins

.include "../opnsense-plugins/Mk/plugins.mk"
8 changes: 8 additions & 0 deletions sysutils/os-splunk-hec/pkg-descr
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
Splunk HTTP Event Collector (HEC) log exporter for OPNsense.

Forwards OPNsense system and firewall logs to a Splunk instance
via the HTTP Event Collector API. Supports configurable log sources,
on-disk payload caching with retry, and inode-based log rotation
detection.

WWW: https://github.com/pvols79/os-splunk-hec
37 changes: 37 additions & 0 deletions sysutils/os-splunk-hec/src/etc/rc.d/splunk_hec
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
#!/bin/sh
#
# OPNsense Splunk HEC Exporter — rc.d service script
#
# PROVIDE: splunk_hec
# REQUIRE: NETWORKING configd
# KEYWORD: shutdown

. /etc/rc.subr

name="splunk_hec"
rcvar="splunk_hec_enable"
pidfile="/var/run/${name}.pid"

# Use FreeBSD's daemon utility to properly detach the process.
# We set procname so rc.subr correctly maps the PID file to the PHP process.
command="/usr/sbin/daemon"
procname="/usr/local/bin/php"
command_args="-f -p ${pidfile} /usr/local/bin/php /usr/local/opnsense/scripts/OPNsense/SplunkHEC/Exporter.php"

load_rc_config $name

# Force YES because Exporter.php has its own internal check for $cfg['enabled']
# and exits gracefully if disabled. This bypasses the need for OPNsense rc.conf.d templates.
splunk_hec_enable="YES"

splunk_hec_stop()
{
if [ -f "${pidfile}" ]; then
kill $(cat "${pidfile}") >/dev/null 2>&1 || true
rm -f "${pidfile}"
fi
pkill -f "Exporter.php" >/dev/null 2>&1 || true
}
stop_cmd="splunk_hec_stop"

run_rc_command "$1"
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
<?php

/*
* Copyright (C) 2026 pvols79
* All rights reserved.
*
* SPDX-License-Identifier: BSD-2-Clause
*/

namespace OPNsense\SplunkHEC\Api;

use OPNsense\Base\ApiMutableModelControllerBase;
use OPNsense\Core\Backend;
use OPNsense\Core\Config;

class ServiceController extends ApiMutableModelControllerBase
{
/** @var string model class name */
protected static $internalModelName = 'SplunkHEC';

/** @var string fully-qualified model class */
protected static $internalModelClass = 'OPNsense\\SplunkHEC\\SplunkHEC';

/**
* GET /api/splunkhec/service/get
*
* Return all current settings (general + logs sections).
* mapDataToFormUI() uses the top-level keys to populate form fields
* by dot-notation ID (e.g. general.enabled, logs.system).
*/
public function getAction()
{
$result = [];
if ($this->request->isGet()) {
$mdl = $this->getModel();
$nodes = $mdl->getNodes();
$result['general'] = $nodes['general'];
$result['logs'] = $nodes['logs'];
}
return $result;
}

/**
* POST /api/splunkhec/service/set
*
* Persist settings submitted from the UI. Save only — does NOT restart
* the daemon. The separate reconfigureAction() does that, called after
* this by SimpleActionButton's data-endpoint lifecycle.
*
* Note: setBase() in OPNsense 26.7+ is UUID-based (grid rows only).
* For flat settings pages we use setNodes() + validate() directly.
*/
public function setAction()
{
$result = ['result' => 'failed'];

if ($this->request->isPost()) {
$mdl = $this->getModel();
$post = $this->request->getPost();

$mdl->setNodes($post);

$valMsgs = $mdl->validate();
if (count($valMsgs) > 0) {
$result['validations'] = $valMsgs;
} else {
$mdl->serializeToConfig();
Config::getInstance()->save();
$this->writeIniConfig($mdl);

// Restart daemon as part of save — avoids a separate
// reconfigure API call from the frontend (which has auth issues).
try {
$backend = new Backend();
$backend->configdRun('splunk_hec restart');
} catch (\Exception $e) {
syslog(LOG_WARNING, 'SplunkHEC: restart failed: ' . $e->getMessage());
}

$result['result'] = 'saved';
}
}

return $result;
}

/**
* POST /api/splunkhec/service/reconfigure
*
* Apply the saved configuration by restarting the daemon.
* Called by SimpleActionButton after setAction() succeeds.
* This is the endpoint that controls the spinner lifecycle.
*/
public function reconfigureAction()
{
$result = ['result' => 'failed'];

if ($this->request->isPost()) {
try {
$backend = new Backend();
$backend->configdRun('splunk_hec restart');
$result['result'] = 'ok';
} catch (\Exception $e) {
syslog(LOG_WARNING, 'SplunkHEC: reconfigure failed: ' . $e->getMessage());
}
}

return $result;
}

/**
* GET /api/splunkhec/service/status
*/
public function statusAction()
{
$backend = new Backend();
$response = trim($backend->configdRun('splunk_hec status'));
return ['status' => ($response === 'running') ? 'running' : 'stopped'];
}

/**
* Serialize current model values into the INI file consumed by Exporter.php.
* Written to /var/etc/splunk_hec.conf on every successful save.
*/
private function writeIniConfig($mdl)
{
$g = $mdl->general;
$l = $mdl->logs;

$ini = "; Auto-generated by OPNsense SplunkHEC plugin — do not edit.\n";
$ini .= "[splunk_hec]\n";
$ini .= 'enabled = ' . (string)$g->enabled . "\n";
$ini .= 'use_gzip = ' . (string)$g->use_gzip . "\n";
$ini .= 'enable_telemetry = ' . (string)$g->enable_telemetry . "\n";
$ini .= 'verify_ssl = ' . (string)$g->verify_ssl . "\n";
$ini .= 'token = ' . (string)$g->token . "\n";
$ini .= 'endpoint = ' . (string)$g->endpoint . "\n";
$ini .= 'cache_size = ' . (string)$g->cache_size . "\n";
$ini .= 'cache_time = ' . (string)$g->cache_time . "\n";
$ini .= "\n[logs]\n";
$ini .= 'system = ' . (string)$l->system . "\n";
$ini .= 'filter = ' . (string)$l->filter . "\n";
$ini .= 'audit = ' . (string)$l->audit . "\n";
$ini .= 'dhcpd = ' . (string)$l->dhcpd . "\n";
$ini .= 'lighttpd = ' . (string)$l->lighttpd . "\n";
$ini .= 'ntpd = ' . (string)$l->ntpd . "\n";
$ini .= 'openvpn = ' . (string)$l->openvpn . "\n";
$ini .= 'routing = ' . (string)$l->routing . "\n";
$ini .= 'suricata = ' . (string)$l->suricata . "\n";
$ini .= 'unbound = ' . (string)$l->unbound . "\n";
$ini .= 'kea = ' . (string)$l->kea . "\n";
$ini .= 'dnsmasq = ' . (string)$l->dnsmasq . "\n";
$ini .= 'wireguard = ' . (string)$l->wireguard . "\n";
$ini .= 'suricata_eve = ' . (string)$l->suricata_eve . "\n";
$ini .= 'portalauth = ' . (string)$l->portalauth . "\n";
$ini .= 'crowdsec = ' . (string)$l->crowdsec . "\n";
$ini .= 'elasticsearch = ' . (string)$l->elasticsearch . "\n";
$ini .= 'zenarmor = ' . (string)$l->zenarmor . "\n";

@mkdir('/var/etc', 0755, true);
file_put_contents('/var/etc/splunk_hec.conf', $ini);
}

/**
* Check which log files exist on the firewall
* @return array
*/
public function checklogsAction(): array
{
$paths = [
'system' => '/var/log/system/latest.log',
'filter' => '/var/log/filter/latest.log',
'audit' => '/var/log/audit/latest.log',
'dhcpd' => '/var/log/dhcpd/latest.log',
'kea' => '/var/log/kea/latest.log',
'dnsmasq' => '/var/log/dnsmasq/latest.log',
'lighttpd' => '/var/log/lighttpd/latest.log',
'ntpd' => '/var/log/ntpd/latest.log',
'openvpn' => '/var/log/openvpn/latest.log',
'wireguard' => '/var/log/wireguard/latest.log',
'routing' => '/var/log/routing/latest.log',
'suricata' => '/var/log/suricata/latest.log',
'suricata_eve' => '/var/log/suricata/eve.json',
'unbound' => '/var/log/unbound/latest.log',
'portalauth' => '/var/log/portalauth/latest.log',
'crowdsec' => '/var/log/crowdsec/latest.log',
'elasticsearch' => '/var/log/elasticsearch/latest.log',
'zenarmor' => '/usr/local/zenarmor/output/active/temp'
];

$result = [];
foreach ($paths as $key => $path) {
$result[$key] = file_exists($path);
}

return $result;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php

/*
* Copyright (C) 2026 pvols79
* All rights reserved.
*
* SPDX-License-Identifier: BSD-2-Clause
*/

namespace OPNsense\SplunkHEC;

class GeneralController extends \OPNsense\Base\IndexController
{
public function indexAction()
{
$this->view->pick('OPNsense/SplunkHEC/index');
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<acl>
<page-splunkhec>
<name>Splunk HEC</name>
<patterns>
<pattern>/api/splunkhec/*</pattern>
<pattern>/ui/splunkhec/*</pattern>
</patterns>
</page-splunkhec>
</acl>
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<menu>
<Services>
<SplunkHEC VisibleName="Splunk HEC Export" url="/ui/splunkhec/general" order="850"/>
</Services>
</menu>
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

/*
* Copyright (C) 2026 pvols79
* All rights reserved.
*
* SPDX-License-Identifier: BSD-2-Clause
*/

namespace OPNsense\SplunkHEC;

use OPNsense\Base\BaseModel;

class SplunkHEC extends BaseModel
{
}
Loading