add mcp earch
This commit is contained in:
@@ -9,6 +9,8 @@ DEFAULT_CONFIG: dict[str, object] = {
|
||||
"log_source": "local_syslog",
|
||||
"graylog_mcp_url": "",
|
||||
"graylog_stream": "",
|
||||
"graylog_query": "*",
|
||||
"graylog_field_mapping": "",
|
||||
"llm_enabled": False,
|
||||
"llm_model": "",
|
||||
"threat_intel_enabled": False,
|
||||
|
||||
@@ -62,7 +62,7 @@ HTML = """<!doctype html>
|
||||
<div data-view="overview" class="active"><section class="panel"><h2>AI Assessment</h2><div id="llmAssessment" class="muted">LLM assessment disabled.</div></section><section class="split"><div class="panel"><h2>Anomalies</h2><div id="anomalies"></div></div><div class="panel"><h2>Recommendations</h2><div id="recommendations"></div></div></section></div>
|
||||
<div data-view="findings"><section class="split"><div class="panel"><h2>Block Candidates</h2><div id="blocks"></div></div><div class="panel"><h2>Threat Intelligence</h2><div id="reputation"></div></div></section><section class="panel"><h2>Policy Findings</h2><div id="policies"></div></section></div>
|
||||
<div data-view="diagnostics"><section class="panel"><h2>Diagnostics</h2><div id="diagnostics"></div></section></div>
|
||||
<div data-view="settings"><section class="panel"><h2>Runtime Configuration</h2><form id="settingsForm"><div class="grid"><label>Log source<br><select name="log_source"><option value="local_syslog">Local syslog</option><option value="graylog_mcp">Graylog MCP</option></select></label><label>Graylog MCP URL<br><input name="graylog_mcp_url" type="url" placeholder="https://graylog.example/api/mcp"></label><label>Graylog stream<br><input name="graylog_stream" placeholder="FortiGate stream ID or name"></label><label>Graylog MCP token<br><input name="graylog_mcp_token" type="password" placeholder="Leave blank to keep current token"></label><label>Ollama model<br><input name="llm_model" placeholder="llama3.1"></label><label><input name="llm_enabled" type="checkbox"> Enable Ollama analysis</label><label><input name="threat_intel_enabled" type="checkbox"> Enable threat intelligence</label></div><p><button type="submit">Save configuration</button> <span id="settingsResult" class="muted"></span></p></form></section></div>
|
||||
<div data-view="settings"><section class="panel"><h2>Runtime Configuration</h2><form id="settingsForm"><div class="grid"><label>Log source<br><select name="log_source"><option value="local_syslog">Local syslog</option><option value="graylog_mcp">Graylog MCP</option></select></label><label>Graylog MCP URL<br><input name="graylog_mcp_url" type="url" placeholder="https://graylog.example/api/mcp"></label><label>Graylog stream<br><input name="graylog_stream" placeholder="FortiGate stream ID or name"></label><label>Graylog query<br><input name="graylog_query" placeholder="*"></label><label>Graylog field mapping (JSON)<br><textarea name="graylog_field_mapping" placeholder='{"srcip":"client_ip","dstip":"server_ip","action":"event_action"}'></textarea></label><label>Graylog MCP token<br><input name="graylog_mcp_token" type="password" placeholder="Leave blank to keep current token"></label><label>Ollama model<br><input name="llm_model" placeholder="llama3.1"></label><label><input name="llm_enabled" type="checkbox"> Enable Ollama analysis</label><label><input name="threat_intel_enabled" type="checkbox"> Enable threat intelligence</label></div><p><button type="submit">Save configuration</button> <span id="settingsResult" class="muted"></span></p></form></section></div>
|
||||
</main>
|
||||
<script>
|
||||
function esc(value) {
|
||||
|
||||
@@ -74,3 +74,6 @@ class GraylogMcpClient:
|
||||
names = [str(item.get("name")) for item in tool_list if isinstance(item, dict) and item.get("name")]
|
||||
version = initialized.get("result", {}).get("serverInfo", {}).get("version", "") if isinstance(initialized.get("result"), dict) else ""
|
||||
return {"status": "connected", "tools": names, "server_version": version}
|
||||
|
||||
def call_tool(self, name: str, arguments: dict[str, object]) -> dict[str, object]:
|
||||
return self._call("tools/call", {"name": name, "arguments": arguments})
|
||||
|
||||
69
src/fgai/graylog_source.py
Normal file
69
src/fgai/graylog_source.py
Normal file
@@ -0,0 +1,69 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from collections.abc import Iterable
|
||||
|
||||
from .graylog_mcp import GraylogMcpClient
|
||||
from .models import LogEvent
|
||||
|
||||
|
||||
DEFAULT_FIELD_MAP = {
|
||||
"srcip": ("srcip", "src_ip", "source_ip", "client_ip"),
|
||||
"dstip": ("dstip", "dst_ip", "destination_ip", "server_ip"),
|
||||
"dstport": ("dstport", "dst_port", "destination_port"),
|
||||
"srcport": ("srcport", "src_port", "source_port"),
|
||||
"eventtime": ("eventtime", "timestamp", "time"),
|
||||
"severity": ("severity", "level"),
|
||||
"action": ("action", "event_action", "disposition"),
|
||||
}
|
||||
|
||||
|
||||
def _records(value: object) -> Iterable[dict[str, object]]:
|
||||
if isinstance(value, dict):
|
||||
for key in ("messages", "results", "events", "data"):
|
||||
if isinstance(value.get(key), list):
|
||||
yield from (item for item in value[key] if isinstance(item, dict))
|
||||
return
|
||||
if value:
|
||||
yield value
|
||||
elif isinstance(value, list):
|
||||
yield from (item for item in value if isinstance(item, dict))
|
||||
|
||||
|
||||
class GraylogStreamSource:
|
||||
def __init__(self, client: GraylogMcpClient, stream: str, query: str = "*", field_mapping: str = "") -> None:
|
||||
self.client = client
|
||||
self.stream = stream
|
||||
self.query = query or "*"
|
||||
try:
|
||||
self.mapping = json.loads(field_mapping) if field_mapping else {}
|
||||
except json.JSONDecodeError as exc:
|
||||
raise RuntimeError("invalid_graylog_field_mapping") from exc
|
||||
if not isinstance(self.mapping, dict):
|
||||
raise RuntimeError("invalid_graylog_field_mapping")
|
||||
|
||||
def fetch(self) -> tuple[list[LogEvent], dict[str, object]]:
|
||||
status = self.client.probe()
|
||||
result = self.client.call_tool("search_messages", {"query": self.query, "stream": self.stream, "limit": 1000})
|
||||
content = result.get("result", {}).get("content", []) if isinstance(result.get("result"), dict) else []
|
||||
records: list[dict[str, object]] = []
|
||||
for item in content if isinstance(content, list) else []:
|
||||
if isinstance(item, dict) and item.get("type") == "text":
|
||||
try:
|
||||
records.extend(_records(json.loads(str(item.get("text", "")))))
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
events = [self._event(record) for record in records]
|
||||
status.update({"source": "graylog_mcp", "events_fetched": len(events)})
|
||||
return events, status
|
||||
|
||||
def _event(self, record: dict[str, object]) -> LogEvent:
|
||||
fields = {str(key).lower(): str(value) for key, value in record.items() if value is not None}
|
||||
for canonical, candidates in DEFAULT_FIELD_MAP.items():
|
||||
mapped = self.mapping.get(canonical)
|
||||
candidates = (str(mapped),) if mapped else candidates
|
||||
for candidate in candidates:
|
||||
if candidate.lower() in fields:
|
||||
fields[canonical] = fields[candidate.lower()]
|
||||
break
|
||||
return LogEvent(raw=json.dumps(record, sort_keys=True), fields=fields)
|
||||
@@ -8,6 +8,7 @@ from .anomaly import anomaly_summary, detect_source_anomalies
|
||||
from .baseline import BaselineStore
|
||||
from .config import ConfigStore
|
||||
from .graylog_mcp import GraylogMcpClient
|
||||
from .graylog_source import GraylogStreamSource
|
||||
from .llm import ollama_dashboard_assessment
|
||||
from .logs import local_in_failures, read_events, summarize_events, top_field_values
|
||||
from .mitigation import parse_allowlist, suggest_block_candidates
|
||||
@@ -26,26 +27,27 @@ def build_status(
|
||||
baseline_path: str | None = None,
|
||||
config_path: str | None = None,
|
||||
) -> dict[str, object]:
|
||||
events = read_events(log_path) if Path(log_path).exists() else []
|
||||
baseline = BaselineStore(baseline_path) if baseline_path else None
|
||||
profiles = baseline.profiles({event.src_ip for event in events if event.src_ip}) if baseline else {}
|
||||
anomalies = detect_source_anomalies(events, limit=anomaly_limit, baselines=profiles)
|
||||
baseline_events = baseline.ingest(events) if baseline else 0
|
||||
config_store = ConfigStore(config_path) if config_path else None
|
||||
config_exists = bool(config_store and config_store.path.exists())
|
||||
runtime_values = config_store.read() if config_exists and config_store else {}
|
||||
runtime_config = config_store.public() if config_store else {}
|
||||
events = read_events(log_path) if Path(log_path).exists() else []
|
||||
mcp_status: dict[str, object] = {"status": "not_configured"}
|
||||
if runtime_values.get("log_source") == "graylog_mcp":
|
||||
url = str(runtime_values.get("graylog_mcp_url", ""))
|
||||
token = str(runtime_values.get("graylog_mcp_token", ""))
|
||||
url, token = str(runtime_values.get("graylog_mcp_url", "")), str(runtime_values.get("graylog_mcp_token", ""))
|
||||
if not url or not token:
|
||||
mcp_status = {"status": "missing_configuration"}
|
||||
events = []
|
||||
else:
|
||||
try:
|
||||
mcp_status = GraylogMcpClient(url, token).probe()
|
||||
events, mcp_status = GraylogStreamSource(GraylogMcpClient(url, token), str(runtime_values.get("graylog_stream", "")), str(runtime_values.get("graylog_query", "*")), str(runtime_values.get("graylog_field_mapping", ""))).fetch()
|
||||
except RuntimeError as exc:
|
||||
mcp_status = {"status": "error", "error": str(exc)}
|
||||
events = []
|
||||
baseline = BaselineStore(baseline_path) if baseline_path else None
|
||||
profiles = baseline.profiles({event.src_ip for event in events if event.src_ip}) if baseline else {}
|
||||
anomalies = detect_source_anomalies(events, limit=anomaly_limit, baselines=profiles)
|
||||
baseline_events = baseline.ingest(events) if baseline else 0
|
||||
intel_ips = sorted(
|
||||
{
|
||||
ip
|
||||
|
||||
Reference in New Issue
Block a user