🚀 Day 17: 突破 AOB 框架霸权 ------ 插件界面重构与大屏呈现
今日目标:
- 破解 UCC 前端框架 :AOB前端采用了UCC框架。想要修改默认的Input界面,必须突破UCC限制。因此,我们需要修改隐藏的
globalConfig.json,将默认的强制输入框伪装成Source Index,并新增Target Index。 - 底层规范对齐 :修改
inputs.conf.spec并清理本地脏数据,彻底消灭 Splunk 守护进程的Invalid key合规性报错。 - 读写链路隔离 (代码重构) :部署包含
validate_input契约和get_arg方法的终极版 Python 核心引擎,确保 AI 准确从源拉取数据、向目标池写战报。 - 掌握
vi极客粘贴术 :将关联了新 Target Index 的高管大屏与导航栏强行注入系统底层 (default目录)。
🕵️♂️ 架构 Context:为什么我们要进行全栈解耦?
在 AOB 默认界面中,系统强行绑定了一个 Index 字段。如果在代码中既用它做"被分析日志的源头",又用它做"AI 战报的写入目标",极易导致大模型把自己的上一轮战报又当作异常抓取进去,形成无限死循环。 因此,我们必须在界面和代码上,将 源数据池 (Source Index) 和 战报存储池 (Target Index) 彻底解耦。
💻 实战步骤 1:破解 UCC 框架,重构输入界面
AOB 的前端界面是由底层的 UCC (Universal Configuration Console) 框架通过一个 JSON 文件动态渲染的。
1. 修改核心渲染配置 (globalConfig.json) 进入前端静态编译目录:
bash
cd /opt/splunk/etc/apps/TA-peak-llm-analyzer/appserver/static/js/build/
vi globalConfig.json
搜索 "field": "index" 的 JSON 块,将其 label 修改为源数据含义;并在其下方新增 target_index 字段:
json
{
"field": "index",
"label": "Source Index",
"help": "Logs will be read from this index.",
"type": "singleSelect",
"defaultValue": "main",
"options": {
"endpointUrl": "data/indexes",
"denyList": "^_.*$",
"createSearchChoice": true
},
"required": true,
"validators": [
{
"type": "string",
"minLength": 1,
"maxLength": 80,
"errorMsg": "Length of index name should be between 1 and 80."
}
]
},
{
"field": "target_index",
"label": "Target Index",
"help": "Store the AI PEAK report logs generated by the code, as well as the data required for the Dashboard display.",
"type": "singleSelect",
"defaultValue": "main",
"options": {
"endpointUrl": "data/indexes",
"denyList": "^_.*$",
"createSearchChoice": true
},
"required": true,
"validators": [
{
"type": "string",
"minLength": 1,
"maxLength": 80,
"errorMsg": "Length of index name should be between 1 and 80."
}
]
}
2. 补齐底层合规规范 (inputs.conf.spec) 为了防止 Splunk 报错 Invalid key,我们必须给新增的 target_index 在"规范字典"里上户口,并清除以前测试遗留的脏数据。
bash
cd /opt/splunk/etc/apps/TA-peak-llm-analyzer/README/
vi inputs.conf.spec
将你的配置块修改为极其干净的形态(删除旧的 custom_message 等垃圾):
ini
[peak_hunter_task://<name>]
target_index = Store the AI PEAK report logs generated by the code, as well as the data required for the Dashboard display.
3. 清除本地脏数据 (inputs.conf) 如果你的 local/inputs.conf 里还有以前随便建的 test 任务,删掉它们!
bash
vi /opt/splunk/etc/apps/TA-peak-llm-analyzer/local/inputs.conf
(确保证只保留你正在使用的正式数据输入任务)。
💻 实战步骤 2:注入终极版 Python 核心代码
请进入 bin/ 目录修改你的核心脚本:
bash
cd /opt/splunk/etc/apps/TA-peak-llm-analyzer/bin/
vi input_module_peak_hunter_task.py
(💡 记得在 vi 中按 Esc,输入 :set paste 并回车,再按 i 键进行无损粘贴!)
这是包含了AOB 强制校验契约 (validate_input) 、正确的参数获取方法 (get_arg) 以及 Source/Target 完美读写隔离 的最终生产级代码:
python
import os
import sys
import time
import datetime
import json
import uuid
import requests
import splunklib.client as client
import splunklib.results as results
# ==========================================
# HELPER 1: Execute AI Generated SPL
# ==========================================
def execute_ai_spl(helper, service, spl_query):
spl_query = spl_query.strip()
if not spl_query.startswith("search") and not spl_query.startswith("|"):
spl_query = "search " + spl_query
kwargs_oneshot = {"output_mode": "json"}
helper.log_debug(f"[Agentic Engine] Executing SPL: {spl_query}")
try:
search_results = service.jobs.oneshot(spl_query, **kwargs_oneshot)
reader = results.JSONResultsReader(search_results)
result_data = [res for res in reader if isinstance(res, dict)]
helper.log_info(f"[Agentic Engine] SUCCESS: Found {len(result_data)} events.")
return result_data
except Exception as e:
helper.log_error(f"[Agentic Engine] FAILED execution: {str(e)}")
return []
# ==========================================
# HELPER 2: Fetch Real Logs (Context Distillation)
# ==========================================
def fetch_rare_logs(helper, service, source_index):
helper.log_info(f"Fetching real rare logs from source index: {source_index}...")
spl = f"search index={source_index} | head 5 | table _raw"
try:
results_data = execute_ai_spl(helper, service, spl)
if not results_data:
helper.log_debug("No logs returned from source index.")
return None
raw_logs = [item.get("_raw", "") for item in results_data if "_raw" in item]
payload = "\n".join(raw_logs)
MAX_CHARS = 6000
if len(payload) > MAX_CHARS:
payload = payload[:MAX_CHARS] + "\n\n...[TRUNCATED DUE TO CONTEXT LIMITS]..."
return payload
except Exception as e:
helper.log_error(f"Failed to fetch rare logs: {str(e)}")
return None
# ==========================================
# HELPER 3: Universal Token Extractor (FinOps)
# ==========================================
def extract_token_usage(helper, response_json, response_headers):
try:
if "usage" in response_json:
usage = response_json["usage"]
if "total_tokens" in usage:
return int(usage["total_tokens"])
elif "prompt_tokens" in usage and "completion_tokens" in usage:
return int(usage["prompt_tokens"]) + int(usage["completion_tokens"])
header_keys = [k.lower() for k in response_headers.keys()]
for key in header_keys:
if "token-usage" in key or "x-ratelimit-usage" in key:
return int(response_headers.get(key, 0))
except Exception as e:
helper.log_error(f"[FinOps Warning] Failed to parse tokens: {str(e)}")
return 0
# ==========================================
# HELPER 4: The LLM API Connector
# ==========================================
def call_llm_api(helper, api_key, base_url, model, system_prompt, user_prompt, max_tokens):
headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}
payload = {
"model": model,
"messages": [{"role": "system", "content": system_prompt}, {"role": "user", "content": user_prompt}],
"response_format": {"type": "json_object"},
"max_tokens": max_tokens
}
endpoint = base_url if base_url.endswith("/chat/completions") else f"{base_url.rstrip('/')}/chat/completions"
try:
response = requests.post(endpoint, headers=headers, json=payload, timeout=120)
response.raise_for_status()
response_json = response.json()
llm_content = response_json["choices"][0]["message"]["content"]
total_tokens = extract_token_usage(helper, response_json, response.headers)
return llm_content, total_tokens
except requests.exceptions.RequestException as e:
helper.log_error(f"Network error: {str(e)}")
raise
# ==========================================
# VALIDATION: AOB Required Contract
# ==========================================
def validate_input(helper, definition):
"""
Mandatory placeholder to satisfy Splunk AOB's SDK checking mechanism.
Prevents 'AttributeError: get_param/validate_input' crashes.
"""
pass
# ==========================================
# MAIN WORKFLOW: The Autonomous Agent
# ==========================================
def collect_events(helper, ew):
helper.log_info("PEAK AI Hunter: LIVE MODE INITIALIZED.")
cycle_start_time = time.time()
hunt_session_id = str(uuid.uuid4())
try:
session_key = getattr(helper, 'session_key', None) or getattr(helper._input_definition, 'metadata', {}).get('session_key')
service = client.Service(token=session_key)
api_key = helper.get_global_setting("api_key")
base_url = helper.get_global_setting("base_url")
model_name = helper.get_global_setting("model_name")
# [CRITICAL UPDATE]: Decoupled Read/Write Indexes
source_index = helper.get_output_index() or "main"
target_index = helper.get_arg("target_index") or "peaklog"
if not api_key or not base_url:
raise ValueError("API Key or Base URL is missing.")
# --- PHASE 1: PREPARE ---
rare_logs_payload = fetch_rare_logs(helper, service, source_index)
if not rare_logs_payload:
helper.log_info("No anomalous logs found. Terminating early.")
return
sys_prompt_prepare = "You are a Senior Threat Hunter. Reply ONLY in JSON. Schema: 'analysis' (string), 'hypotheses' (array). Each hypothesis MUST have 'ABLE' (Actor, Behavior, Location, Evidence), 'spl_round_1_validation', and 'spl_round_2_drilldown'."
usr_prompt_prepare = f"Analyze these logs:\n{rare_logs_payload}\n\nGenerate exactly 2 hypotheses. CRITICAL: For SPL, strictly start with 'search index={source_index}'. Output ONLY JSON."
blueprint_text, prep_tokens = call_llm_api(helper, api_key, base_url, model_name, sys_prompt_prepare, usr_prompt_prepare, max_tokens=1500)
ai_hunting_plan = json.loads(blueprint_text.strip())
hypotheses = ai_hunting_plan.get("hypotheses", [])
ew.write_event(helper.new_event(
source=helper.get_input_type(), index=target_index, sourcetype="_json", time=time.time(),
data=json.dumps({"session_id": hunt_session_id, "event_type": "PEAK_Plan", "timestamp": round(time.time(), 3), "content": ai_hunting_plan}, ensure_ascii=False)
))
# --- PHASE 2: EXECUTE ---
all_hunt_evidence = []
for i, hyp in enumerate(hypotheses):
hyp_start = time.time()
spl_r1 = hyp.get("spl_round_1_validation", "").replace("{source_index}", source_index)
spl_r2 = hyp.get("spl_round_2_drilldown", "").replace("{source_index}", source_index)
r1_hits = len(execute_ai_spl(helper, service, spl_r1))
r2_hits = len(execute_ai_spl(helper, service, spl_r2))
able_data = hyp.get('ABLE', {})
behavior_text = able_data.get('Behavior', 'Unknown') if isinstance(able_data, dict) else str(able_data)
all_hunt_evidence.append({
"hypothesis_id": hyp.get("hypothesis_id", i+1), "threat_behavior": behavior_text,
"round_1_hit_count": r1_hits, "round_2_hit_count": r2_hits,
"execution_duration_sec": round(time.time() - hyp_start, 2)
})
ew.write_event(helper.new_event(
source=helper.get_input_type(), index=target_index, sourcetype="_json", time=time.time(),
data=json.dumps({"session_id": hunt_session_id, "event_type": "PEAK_Evidence", "timestamp": round(time.time(), 3), "content": all_hunt_evidence}, ensure_ascii=False)
))
# --- PHASE 3: ACT ---
sys_prompt_act = "You are a Security Director. Output ONLY valid JSON. Keep summaries under 30 words. Keys: 'executive_summary', 'threat_qualification', 'risk_score', 'recommended_alert_spl'."
usr_prompt_act = f"Here is the execution evidence:\n{json.dumps(all_hunt_evidence)}\n\nBased on these hits, qualify the threat, assign a score (0-100), and write alert SPL."
report_text, act_tokens = call_llm_api(helper, api_key, base_url, model_name, sys_prompt_act, usr_prompt_act, max_tokens=800)
try:
final_report = json.loads(report_text.strip())
except json.JSONDecodeError:
final_report = {"executive_summary": "LLM output truncated.", "risk_score": -1, "raw": report_text}
ew.write_event(helper.new_event(
source=helper.get_input_type(), index=target_index, sourcetype="_json", time=time.time(),
data=json.dumps({"session_id": hunt_session_id, "event_type": "PEAK_Final_Report", "timestamp": round(time.time(), 3), "total_tokens_used": prep_tokens + act_tokens, "content": final_report}, ensure_ascii=False)
))
except Exception as e:
error_msg = str(e)
helper.log_error(f"FATAL Pipeline Crash: {error_msg}")
try:
fallback_index = target_index if 'target_index' in locals() else "main"
ew.write_event(helper.new_event(
source=helper.get_input_type(), index=fallback_index, sourcetype="_json", time=time.time(),
data=json.dumps({"session_id": hunt_session_id, "event_type": "PEAK_Error", "timestamp": round(time.time(), 3), "error_message": error_msg}, ensure_ascii=False)
))
except Exception:
pass
💻 实战步骤 3:底层注入高管大屏 (XML)
现在,我们将把基于新目标索引 (peaklog) 编写的大屏注入系统。
1. 注入大屏 XML
bash
cd /opt/splunk/etc/apps/TA-peak-llm-analyzer/default/data/ui/views/
vi peak_executive_dashboard.xml
- 按
Esc-> 输入:set paste-> 按i。将下方代码粘贴进去。注意:大屏显示的各个Panel中的数据每2分钟自动刷新一次,如果需要修改刷新时间,修改语句"2m"中的数值即可。
xml
<dashboard version="1.1">
<label>PEAK AI Hunter - Executive & FinOps Dashboard</label>
<description>Automated Threat Hunting Analytics & Token Consumption Tracking</description>
<row>
<panel>
<single>
<title>Latest Risk Score</title>
<search>
<query>index=peaklog event_type="PEAK_Final_Report" | stats latest(content.risk_score)</query>
<earliest>-24h@h</earliest>
<latest>now</latest>
<refresh>2m</refresh>
</search>
<option name="rangeValues">[0,30,70,100]</option>
<option name="rangeColors">["0x53a051","0xf8be34","0xf1813f","0xdc4e41"]</option>
<option name="useColors">1</option>
</single>
</panel>
<panel>
<single>
<title>Average Risk Score (Today)</title>
<search>
<query>index=peaklog event_type="PEAK_Final_Report" | where _time >= relative_time(now(), "@d") | stats avg(content.risk_score) as avg_score | eval avg_score=round(avg_score, 2)</query>
<earliest>@d</earliest>
<latest>now</latest>
<refresh>2m</refresh>
</search>
</single>
</panel>
</row>
<row>
<panel>
<single>
<title>Total Token Usage (Month)</title>
<search>
<query>index=peaklog event_type="PEAK_Final_Report" | stats sum(total_tokens_used)</query>
<earliest>@mon</earliest>
<latest>now</latest>
<refresh>2m</refresh>
</search>
<option name="underLabel">Tokens Consumed This Month</option>
</single>
</panel>
<panel>
<single>
<title>Token Usage (Today)</title>
<search>
<query>index=peaklog event_type="PEAK_Final_Report" | where _time >= relative_time(now(), "@d") | stats sum(total_tokens_used)</query>
<earliest>@d</earliest>
<latest>now</latest>
<refresh>2m</refresh>
</search>
<option name="underLabel">Tokens Consumed Today</option>
</single>
</panel>
</row>
<row>
<panel>
<single>
<title>Successful Hunts (Today)</title>
<search>
<query>index=peaklog (event_type="PEAK_Plan" OR event_type="PEAK_Evidence" OR event_type="PEAK_Final_Report") | stats count by session_id | search count=3 | stats count</query>
<earliest>@d</earliest>
<latest>now</latest>
<refresh>2m</refresh>
</search>
<option name="rangeValues">[0]</option>
<option name="rangeColors">["0xdc4e41","0x53a051"]</option>
<option name="useColors">1</option>
<option name="underLabel">Completed Closed-Loop Sessions</option>
</single>
</panel>
<panel>
<single>
<title>Anomalous/Failed Hunts (Today)</title>
<search>
<query>index=peaklog (event_type="PEAK_Plan" OR event_type="PEAK_Evidence" OR event_type="PEAK_Final_Report" OR event_type="PEAK_Error") | stats count, latest(event_type) as last_evt by session_id | where count < 3 OR last_evt="PEAK_Error" | stats count</query>
<earliest>@d</earliest>
<latest>now</latest>
<refresh>2m</refresh>
</search>
<option name="rangeValues">[0]</option>
<option name="rangeColors">["0x53a051","0xdc4e41"]</option>
<option name="useColors">1</option>
<option name="underLabel">Crashed or Incomplete Sessions</option>
</single>
</panel>
</row>
<row>
<panel>
<table>
<title>Recent AI Hunting Reports Summary (Transaction View)</title>
<search>
<query>
index=peaklog sourcetype="_json" (event_type="PEAK_Plan" OR event_type="PEAK_Evidence" OR event_type="PEAK_Final_Report" OR event_type="PEAK_Error")
| stats
min(timestamp) as Start_Time_Epoch,
max(timestamp) as End_Time_Epoch,
latest(content.threat_qualification) as Verdict,
latest(content.risk_score) as Risk_Score,
latest(content.executive_summary) as AI_Summary,
sum(total_tokens_used) as Tokens,
latest(error_message) as Error
by session_id
| eval Duration = round(End_Time_Epoch - Start_Time_Epoch, 2)
| eval Start_Time = strftime(Start_Time_Epoch, "%Y-%m-%d %H:%M:%S")
| sort - Start_Time_Epoch
| table Start_Time, Verdict, Risk_Score, Tokens, Duration, AI_Summary, Error
</query>
<earliest>-7d@h</earliest>
<latest>now</latest>
<refresh>2m</refresh>
</search>
<option name="drilldown">none</option>
</table>
</panel>
</row>
</dashboard>
2. 强行注入导航栏 XML
bash
cd ../nav/
vi default.xml
- 使用
:set paste模式粘贴:
xml
<nav search_view="search" color="#2C3E50">
<view name="peak_executive_dashboard" default="true" label="Executive Dashboard" />
<view name="inputs" label="Data Inputs" />
<view name="configuration" label="Configuration" />
<view name="search" label="Search" />
</nav>
🔄 实战步骤 4:暴力重启与验收验证
为了让所有 JSON 界面修改、配置字典修复以及 Python 代码生效:
- 重启守护进程 (清空配置警告):在终端运行
/opt/splunk/bin/splunk restart - 刷新前端缓存 :浏览器访问
https://localhost:8000/en-US/debug/refresh并点击 Refresh。 - 建立目标池 :去 Splunk Web Settings -> Indexes 创建一个名为
peaklog的索引。
🎯 最终验收: 一进入插件,你将看到深色的导航栏与零报错的大屏。在 Data Inputs 配置中,清晰的 Source Index 与 Target Index 分隔开了读写流。等待下一轮任务触发,大模型将稳定地在后台完成分析,并将最终的 PEAK 战报安全降落在你的大屏上!
(🚨 终极警告:永远不要再回到 AOB 界面点击 Save,否则它将无情地抹除你今天所有的心血!)