Day 17: 突破 AOB 框架霸权 —— 插件界面重构与大屏呈现


🚀 Day 17: 突破 AOB 框架霸权 ------ 插件界面重构与大屏呈现

今日目标

  1. 破解 UCC 前端框架 :AOB前端采用了UCC框架。想要修改默认的Input界面,必须突破UCC限制。因此,我们需要修改隐藏的 globalConfig.json,将默认的强制输入框伪装成 Source Index,并新增 Target Index
  2. 底层规范对齐 :修改 inputs.conf.spec 并清理本地脏数据,彻底消灭 Splunk 守护进程的 Invalid key 合规性报错。
  3. 读写链路隔离 (代码重构) :部署包含 validate_input 契约和 get_arg 方法的终极版 Python 核心引擎,确保 AI 准确从源拉取数据、向目标池写战报。
  4. 掌握 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 &amp; FinOps Dashboard</label>
  <description>Automated Threat Hunting Analytics &amp; 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 &gt;= 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 &gt;= 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 &lt; 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 代码生效:

  1. 重启守护进程 (清空配置警告):在终端运行 /opt/splunk/bin/splunk restart
  2. 刷新前端缓存 :浏览器访问 https://localhost:8000/en-US/debug/refresh 并点击 Refresh。
  3. 建立目标池 :去 Splunk Web Settings -> Indexes 创建一个名为 peaklog 的索引。

🎯 最终验收: 一进入插件,你将看到深色的导航栏与零报错的大屏。在 Data Inputs 配置中,清晰的 Source IndexTarget Index 分隔开了读写流。等待下一轮任务触发,大模型将稳定地在后台完成分析,并将最终的 PEAK 战报安全降落在你的大屏上!

(🚨 终极警告:永远不要再回到 AOB 界面点击 Save,否则它将无情地抹除你今天所有的心血!)

相关推荐
用户938515635071 小时前
知识库预处理实战:从URL加载到语义分块的全链路解析
javascript·人工智能·全栈
何时梦醒1 小时前
🧠 大模型知识蒸馏:小模型如何"偷师"大模型?
人工智能
萧青山1 小时前
AI+HI人机协同范式:从ElementsClaw到政务智能体的分层进化论
人工智能·机器学习·政务·ai+hi人机协同
武子康1 小时前
HunyuanVideo 全家族选型:原版 13B / I2V / Avatar / Foley / 1.5 8.3B 怎么分工
人工智能·llm·aigc
wenzhangli71 小时前
OODER Studio设计工具导入体系深度解析
人工智能·自动化
qiaozhangmenai1 小时前
AI经营增长系统:企业数字化转型进入智能体时代
人工智能
水如烟1 小时前
孤能子视角:闭合与回环——神经系统的开放拓扑与血液循环的封闭回路
人工智能
Micheal_NET1 小时前
Microsoft Agent Framework-C#企业级 AI Agent 平台架构
人工智能·架构
小马过河R1 小时前
微前端实践:从概念、原理到 qiankun 落地
前端·人工智能·前端框架