125 lines
5.9 KiB
Python
125 lines
5.9 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
import os
|
|
from urllib import request
|
|
|
|
from .models import BlockCandidate, Finding
|
|
|
|
|
|
def ollama_summary(
|
|
findings: list[Finding],
|
|
candidates: list[BlockCandidate],
|
|
model: str | None = None,
|
|
*,
|
|
analysis: dict[str, object] | None = None,
|
|
timeout: int | None = None,
|
|
) -> str:
|
|
host = os.getenv("OLLAMA_HOST", "http://127.0.0.1:11434").rstrip("/")
|
|
selected_model = model or os.getenv("OLLAMA_MODEL", "llama3.1")
|
|
selected_timeout = timeout or int(os.getenv("OLLAMA_TIMEOUT", "180"))
|
|
prompt = {
|
|
"analysis": analysis or {},
|
|
"findings": [finding.__dict__ for finding in findings],
|
|
"block_candidates": [
|
|
{"src_ip": candidate.src_ip, "score": candidate.score, "reasons": candidate.reasons}
|
|
for candidate in candidates[:20]
|
|
],
|
|
}
|
|
body = json.dumps(
|
|
{
|
|
"model": selected_model,
|
|
"stream": False,
|
|
"options": {
|
|
"num_predict": 350,
|
|
"temperature": 0.2,
|
|
},
|
|
"prompt": (
|
|
"You are SignalScope, a local security operations analyst. Analyze only the supplied telemetry. "
|
|
"Never describe the input as JSON, a SIEM object, a dataset, or an array. Never ask the user what to focus on. "
|
|
"Return exactly these short sections: Assessment, Priority entities, Evidence, Recommended next action. "
|
|
"Use actual entity names, stream names, counts, scores, and field deviations from the supplied data. "
|
|
"If evidence is insufficient, say that explicitly and name the missing field or stream. "
|
|
"Do not recommend blocking private/internal client IPs unless the data explicitly proves compromise. "
|
|
f"\n\nTelemetry:\n{json.dumps(prompt)}"
|
|
),
|
|
}
|
|
).encode("utf-8")
|
|
req = request.Request(f"{host}/api/generate", data=body, method="POST", headers={"Content-Type": "application/json"})
|
|
with request.urlopen(req, timeout=selected_timeout) as response:
|
|
data = json.loads(response.read().decode("utf-8"))
|
|
return str(data.get("response", "")).strip()
|
|
|
|
|
|
def ollama_dashboard_assessment(analysis: dict[str, object], model: str | None = None, timeout: int | None = None) -> str:
|
|
compact = {
|
|
"summary": analysis.get("summary", {}),
|
|
"anomaly_summary": analysis.get("anomaly_summary", {}),
|
|
"top_anomalies": analysis.get("anomalies", [])[:5],
|
|
"top_recommendations": analysis.get("recommendations", [])[:5],
|
|
"block_candidates": analysis.get("block_candidates", [])[:5],
|
|
"policy_findings": analysis.get("policy_findings", [])[:5],
|
|
"event_context": analysis.get("event_context", {}),
|
|
"diagnostics": analysis.get("diagnostics", {}),
|
|
"capabilities": analysis.get("capabilities", {}),
|
|
"cross_source_correlations": analysis.get("cross_source_correlations", [])[:20],
|
|
"incidents": analysis.get("incidents", [])[:10],
|
|
"profile_suggestions": analysis.get("profile_suggestions", [])[:10],
|
|
"field_deviations": analysis.get("field_deviations", {}),
|
|
"feedback": analysis.get("feedback", []),
|
|
}
|
|
return ollama_summary(
|
|
[],
|
|
[],
|
|
model,
|
|
analysis={
|
|
"task": (
|
|
"Write a concise dashboard analyst note. Compare activity across every listed entity, "
|
|
"identify the most unusual entity or behavior, and state the next investigation step. "
|
|
"Mention policyid=0 as implicit deny/drop, not an editable policy."
|
|
),
|
|
"data": compact,
|
|
},
|
|
timeout=timeout,
|
|
)
|
|
|
|
|
|
def ollama_profile_advice(suggestions: list[dict[str, object]], model: str | None = None, timeout: int | None = None) -> list[dict[str, object]]:
|
|
host = os.getenv("OLLAMA_HOST", "http://127.0.0.1:11434").rstrip("/")
|
|
selected_model = model or os.getenv("FGAI_PROFILE_ADVISOR_MODEL", "qwen3:8b")
|
|
selected_timeout = timeout or int(os.getenv("FGAI_PROFILE_ADVISOR_TIMEOUT", "120"))
|
|
compact = [
|
|
{
|
|
"stream_id": item.get("stream_id"),
|
|
"stream_name": item.get("stream_name"),
|
|
"events": item.get("events"),
|
|
"common_fields": item.get("common_fields", [])[:20],
|
|
"heuristic_profile": item.get("profile", {}),
|
|
}
|
|
for item in suggestions[:10]
|
|
]
|
|
body = json.dumps(
|
|
{
|
|
"model": selected_model,
|
|
"stream": False,
|
|
"format": "json",
|
|
"options": {"num_predict": 1200, "temperature": 0.1},
|
|
"prompt": (
|
|
"You are SignalScope's local profile advisor. Infer stream profile mappings from observed field statistics. "
|
|
"Return only valid JSON with this schema: "
|
|
"{\"profiles\":[{\"stream_id\":\"...\",\"entity_fields\":[\"...\"],\"timestamp_field\":\"...\","
|
|
"\"categorical_fields\":[\"...\"],\"numeric_fields\":[\"...\"],\"detectors\":{\"auth_failure\":{\"enabled\":true,\"minimum\":5,\"z_threshold\":3}},"
|
|
"\"reason\":\"short reason\"}]}. "
|
|
"Use only field names present in common_fields or heuristic_profile. Do not include raw message/full_message fields. "
|
|
"Allowed detectors are auth_failure, dns_query, deny_action. Prefer canonical fields such as username, hostname, eventid, srcip, dstip when present. "
|
|
f"\n\nObserved streams:\n{json.dumps(compact, sort_keys=True)}"
|
|
),
|
|
}
|
|
).encode("utf-8")
|
|
req = request.Request(f"{host}/api/generate", data=body, method="POST", headers={"Content-Type": "application/json"})
|
|
with request.urlopen(req, timeout=selected_timeout) as response:
|
|
data = json.loads(response.read().decode("utf-8"))
|
|
payload = json.loads(str(data.get("response", "{}")))
|
|
profiles = payload.get("profiles", [])
|
|
return profiles if isinstance(profiles, list) else []
|