SignalScope
SignalScope is a local multi-source security analytics agent. Its primary mode connects to Graylog through MCP, discovers the streams and fields already available in your environment, and uses stream profiles to normalize events, build baselines, correlate entities, and explain anomalies with a local LLM.
Its running only locally and if using LLM it's running also locally so no data is sent or exposed outside.
FortiGate is one supported example. The same workflow applies to DNS/AdGuard, Windows Event Logs, Sysmon, Nginx, Squid, VPN, Proxmox, Filebeat-collected logs, and future Graylog streams.
The Python module and legacy fgai command remain available for compatibility. New installations can use signalscope.
The prioritized implementation plan is tracked in ROADMAP.md.
Autoblocking is dry-run by default. The tool will not block RFC1918, loopback, multicast, link-local, reserved, or allowlisted addresses unless you change the code.
Screenshots
Settings
Findings
Quick Start
python -m venv .venv
source .venv/bin/activate
pip install -e .
Or use the helper script, which creates/uses .venv automatically and runs pip install -e .:
./start.sh
./start.sh status
./start.sh analyze
./start.sh stop
./start.sh starts three local background processes:
- Optional UDP syslog listener writing
logs/fg_syslog.jsonl - Continuous monitor writing
state/fgai-status.json - Local dashboard at
http://127.0.0.1:8088
Primary Workflow: Graylog MCP
Graylog 7.1 MCP is the primary log-source integration. In the dashboard, open
Settings, select Graylog MCP, provide the MCP URL and a read-only API token,
then load and enable the streams to analyze. SignalScope uses MCP list_streams,
list_fields, search_messages, and aggregate_messages to work with existing
log sources rather than requiring every source to be forwarded locally.
The token field accepts a raw Graylog API token, the Base64 value after Basic ,
or a complete Basic <value> header. Tokens are stored only in the local runtime
configuration and are never returned by the dashboard API.
Use Load selected stream fields after choosing a stream. The field table shows
Graylog datatype/capability metadata and lets you select an entity field, a time
field, and categorical/numeric fields for the stream profile. Profiles are stored
under graylog_stream_profiles in state/fgai-config.json.
Enabled streams are normalized through the same event model. Stream profiles define the entity, timestamp, categorical, and numeric fields used for baselines. The dashboard and Ollama then correlate behavior across sources, for example a client IP appearing in FortiGate, AdGuard/DNS, Windows Security, Nginx, Squid, VPN, or Proxmox.
Correlation is entity-aware rather than FortiGate-specific. SignalScope recognizes
common IP fields such as srcip, source_ip, remote_addr, and Windows event
IP fields; account fields such as username, user, and TargetUserName; and
host fields such as hostname, computer, and winlog_computer_name. Configure
the exact entity field per stream in the profile when your Graylog schema differs.
Each profile baseline is stored per stream, entity, selected field, and five-minute bucket. Once enough history exists, SignalScope compares the current rate or numeric value to the same UTC weekday/hour where possible, then falls back to the stream's overall history. Repeated MCP pages are fingerprinted so the same Graylog event is not learned repeatedly. Related anomalies, profile deviations, and multi-stream correlations are grouped into investigation incidents with a compact evidence timeline.
With a stream profile in place, SignalScope also builds independent burst baselines for authentication failures, DNS queries, and deny/block actions when those events are present. These are evaluated per configured entity, so a Windows account, DNS client, or firewall source is compared to its own history.
Replay a historic JSONL or Graylog export without changing the live baseline:
signalscope replay --logs exports/windows-history.jsonl --stream-id <configured-stream-id>
Replay uses a temporary SQLite baseline and evaluates events in timestamp order. It reports detector counts and the findings that would have been generated. Use the configured stream ID so the export is evaluated with that stream's profile.
Replay directly from Graylog MCP without touching the live baseline:
signalscope replay-graylog --range-seconds 86400
By default this uses the enabled streams from the dashboard configuration. Limit
the run to one or more streams with repeated --stream-id flags. To test a
candidate detector/profile configuration before applying it, compare it against
the current runtime config:
signalscope replay-graylog --range-seconds 86400 --compare-config-file exports/candidate-config.json
The comparison reports detector-count, field-finding, and source-anomaly deltas using the same fetched event window.
The current MCP endpoint is http://<graylog-host>:9000/api/mcp. Enable it in
Graylog under System -> Configurations -> MCP and use stream IDs internally;
the fgAI stream picker resolves titles in the UI.
For live monitoring, graylog_range_seconds controls how far back each MCP poll
searches. The default is one hour, which keeps findings and correlations more
stable than a very short window while still limiting MCP query cost.
Monitoring Export
The dashboard also exposes Prometheus text metrics at:
http://127.0.0.1:8088/metrics
This endpoint is passive and has no Prometheus or Grafana dependency. It reports low-cardinality event counts, anomaly severities, incident counts, baseline readiness, and Graylog MCP health. Use it later as a Prometheus scrape target or as input for a Checkmk local check. Do not use source IPs, domains, or raw event IDs as metric labels.
Enable cached Ollama analyst notes in the dashboard:
FGAI_LLM=1 OLLAMA_MODEL=llama3.1 ./start.sh restart
The monitor refreshes deterministic detections every FGAI_MONITOR_INTERVAL seconds and refreshes the LLM note every FGAI_LLM_INTERVAL seconds, default 300.
The script activates .venv inside the script process. If you also want your current shell prompt to show the venv, run:
source .venv/bin/activate
For UDP 514, the script starts only the listener command with sudo:
FGAI_SYSLOG_PORT=514 ./start.sh
The syslog receiver rotates the active JSONL input at 25 MB by default. Rotated files are gzip-compressed and 14 archives are retained. Override this when needed:
FGAI_LOG_ROTATE_BYTES=$((100 * 1024 * 1024)) FGAI_LOG_ROTATE_COUNT=30 ./start.sh restart
The continuous monitor also stores a local SQLite behavior baseline at
state/fgai-baseline.sqlite3. A source becomes baseline-ready after 12 completed
five-minute windows. Historical rate and hitcount-rate deviations then contribute
to its anomaly score. Set FGAI_BASELINE_DB to use another location.
Analyze local logs:
fgai analyze-logs --logs logs/fg_syslog.jsonl
Open the live UI after ./start.sh:
xdg-open http://127.0.0.1:8088
Score likely traffic anomalies:
fgai detect-anomalies --logs logs/fg_syslog.jsonl --min-score 35
fgai detect-anomalies --logs logs/fg_syslog.jsonl --min-score 35 --llm --llm-timeout 300
Generate response and policy recommendations:
fgai recommend --logs logs/fg_syslog.jsonl --min-score 35
Optional external reputation enrichment is disabled by default. To use VirusTotal for public source/destination IP reputation:
export FGAI_THREAT_INTEL=1
export ABUSEIPDB_API_KEY='...'
fgai recommend --logs logs/fg_syslog.jsonl --min-score 35 --threat-intel
VirusTotal is also supported:
export FGAI_THREAT_INTEL=1
export FGAI_THREAT_INTEL_PROVIDER=virustotal
export VIRUSTOTAL_API_KEY='...'
fgai recommend --logs logs/fg_syslog.jsonl --min-score 35 --threat-intel
Threat intelligence responses are cached locally in state/threat-intel-cache.json. Successful results are reused for seven days by default, failures for one hour, and SignalScope permits at most 100 new provider lookups per UTC day. Cached responses are returned even after that budget is reached. Tune these safeguards with FGAI_THREAT_INTEL_TTL_SECONDS, FGAI_THREAT_INTEL_ERROR_TTL_SECONDS, and FGAI_THREAT_INTEL_DAILY_LIMIT.
Listen for FortiGate syslog locally:
fgai listen-syslog --port 5514 --output logs/fg_syslog.jsonl
Run the listener quietly in the background:
./start.sh
Stop the background listener:
./start.sh stop
UDP port 514 normally needs root privileges on Linux:
sudo .venv/bin/fgai listen-syslog --port 514 --output logs/fg_syslog.jsonl
Test FortiGate API access:
export FORTIGATE_HOST=192.0.2.10
export FORTIGATE_API_TOKEN='...'
export FORTIGATE_VERIFY_TLS=false
fgai test-connection
fgai fetch-policies --output exports/policies.json
Audit a FortiGate policy export:
fgai audit-policies --config exports/fortigate.conf
Or fetch policies through the FortiGate API and audit that JSON:
fgai fetch-policies --output exports/policies.json
fgai audit-policies --config exports/policies.json --llm --llm-timeout 300
Find block candidates without changing the firewall:
fgai suggest-blocks --logs logs/fg_syslog.jsonl
Execute guarded quarantine actions:
export FORTIGATE_HOST=192.0.2.10
export FORTIGATE_API_TOKEN='...'
fgai suggest-blocks --logs logs/fg_syslog.jsonl --execute --expiry-minutes 60
Optional local LLM summary through Ollama:
ollama pull llama3.3
fgai analyze-logs --logs logs/fg_syslog.jsonl --llm --llm-timeout 300
For slower machines or large models:
OLLAMA_MODEL=llama3.1 OLLAMA_TIMEOUT=300 fgai analyze-logs --logs logs/fg_syslog.jsonl --llm
Optional FortiGate Input
Synthetic Windows Test Input
For testing a Graylog Beats input without a Windows host, generate Windows Security-style JSONL events locally, then use Filebeat to ship them over TCP:
python scripts/generate_windows_events.py --interval 0.5
filebeat -e -c examples/filebeat-windows-synthetic.yml
Update the absolute JSONL path and Graylog host in the Filebeat template first.
Route stream_hint: Windows to a dedicated Graylog stream, then enable that
stream in SignalScope and configure a profile such as entity user or
source_ip, categorical event_id, status, logon_type, and numeric fields
when present. Filebeat uses its Logstash output to communicate with Graylog's
Beats input on TCP 5044. Graylog Beats input documentation
For logs, configure FortiGate syslog to write into a local file such as logs/fg_syslog.jsonl. The parser supports common key/value syslog lines and JSONL.
For policies, export a FortiOS config backup and pass it to audit-policies.
Example FortiGate syslog target, run on the FortiGate CLI and replace the server IP with this machine:
config log syslogd setting
set status enable
set server "192.0.2.50"
set port 5514
set mode udp
set format default
end
Environment
FORTIGATE_HOST: firewall hostname or IP.FORTIGATE_API_TOKEN: REST API token.FORTIGATE_VERIFY_TLS:trueorfalse, defaults totrue.FGAI_ALLOWLIST: comma-separated IPs/CIDRs never to block.OLLAMA_HOST: defaults tohttp://127.0.0.1:11434.OLLAMA_MODEL: defaults tollama3.1.OLLAMA_TIMEOUT: Ollama request timeout in seconds, defaults to180.FGAI_LLM: set to1to enable dashboard Ollama analyst notes.FGAI_LLM_INTERVAL: seconds between dashboard LLM notes, defaults to300.FGAI_THREAT_INTEL: set to1to enable external threat intelligence lookups.ABUSEIPDB_API_KEY: AbuseIPDB API key for public IP reputation enrichment.ABUSEIPDB_MAX_AGE_DAYS: report age window for AbuseIPDB, defaults to90.FGAI_THREAT_INTEL_PROVIDER:auto,abuseipdb, orvirustotal.VIRUSTOTAL_API_KEY: VirusTotal API key for public IP reputation enrichment.
Safety Model
The agent separates detection from enforcement:
- UTM events are scored from FortiGate logs (
ips,virus,anomaly,ddos,webfilter,app-ctrl,waf,dns). - Source IPs must be globally routable and outside the allowlist.
- Blocking requires
--execute. - The FortiGate API call is limited to the quarantine/banned user monitor endpoint.

