refactor: simplify MCP tools — merge attach into create, clean up agents

- Merge attach_enrichment_to_target into create_enrichment (target_id required)
- Merge attach_ticket_to_case into create_ticket (case_id required)
- Merge get_case_discussions into list_cases (include_discussions param)
- Remove comment_ai/summary_ai from update_case, add comment/summary
- Remove unused agent files (agent_cmdb, agent_report, simpler/agents)
- Update all skill files to match new MCP interfaces
This commit is contained in:
rookit
2026-05-18 16:24:33 +08:00
parent f94b92e7d7
commit 201f92e2b2
14 changed files with 56 additions and 692 deletions
-169
View File
@@ -1,169 +0,0 @@
import os
from typing import Annotated, List, Literal, Any
from langchain.agents import create_agent
from langchain_core.messages import HumanMessage, AIMessage
from langchain_core.runnables import RunnableConfig
from langgraph.graph import END, StateGraph, add_messages
from langgraph.graph.state import CompiledStateGraph
from langgraph.prebuilt import ToolNode
from pydantic import BaseModel, Field
from Lib.baseplaybook import LanggraphPlaybook
from Lib.configs import DATA_DIR
from Lib.llmapi import load_system_prompt_template
from PLUGINS.LLM.llmapi import LLMAPI
# Modify the following functions to the actual CMDB API
from PLUGINS.Mock.CMDB.CMDB import get_ci_context_tool, fuzzy_search_ci_tool, get_cis_by_software_tool, get_cis_by_port_tool, get_cis_by_service_tool, \
get_cis_by_user_tool
AGENT_NODE = "AGENT_NODE"
TOOL_NODE = "TOOL_NODE"
MAX_ITERATIONS = 2
class AgentState(BaseModel):
messages: Annotated[List[Any], add_messages] = Field(default_factory=list)
loop_count: int = Field(default=0, description="Count of agent iterations")
class AgentCMDB(object):
@staticmethod
def cmdb_query_asset(
query: Annotated[str, "The CMDB query in natural language (e.g., 'Find asset with IP 10.10.10.10')"]
) -> Annotated[str, "A JSON containing asset details"]:
"""
Query internal asset information from CMDB.
"""
agent = AgentGraphCMDB()
result = agent.cmdb_query(query)
return result
# Use langgraph to create a CMDB query agent for finer-grained control
class AgentGraphCMDB(LanggraphPlaybook):
def __init__(self):
super().__init__() # do not delete this code
self.init()
def init(self):
tools = [
get_ci_context_tool,
fuzzy_search_ci_tool,
get_cis_by_software_tool,
get_cis_by_port_tool,
get_cis_by_service_tool,
get_cis_by_user_tool,
]
tool_node = ToolNode(tools, name=TOOL_NODE)
def route_after_agent(state: AgentState) -> Literal[TOOL_NODE, END]:
if state.loop_count >= MAX_ITERATIONS:
self.logger.debug(f"Max iterations ({MAX_ITERATIONS}) reached, ending agent.")
return END
last_message = state.messages[-1]
if last_message.tool_calls:
return TOOL_NODE
return END
def agent_node(state: AgentState):
self.logger.debug(f"Agent Node Invoked (Loop: {state.loop_count})")
system_prompt_template = self.load_system_prompt_template(f"system")
system_message = system_prompt_template.format()
messages = [
system_message,
*state.messages
]
if state.loop_count >= MAX_ITERATIONS - 1:
self.logger.warning("Approaching max iterations, forcing agent to provide final answer.")
stop_instruction = (
"\n\n[SYSTEM NOTICE]: You have reached the search limit. "
"Do not call any more tools. Please provide your final conclusion "
"based ONLY on the information gathered above."
)
messages.append(HumanMessage(content=stop_instruction))
llm_api = LLMAPI()
base_llm = llm_api.get_model(tag=["fast"])
response: AIMessage = base_llm.invoke(messages)
else:
llm_api = LLMAPI()
llm = llm_api.get_model(tag=["fast", "function_calling"])
llm_with_tools = llm.bind_tools(tools)
response: AIMessage = llm_with_tools.invoke(messages)
if state.loop_count >= MAX_ITERATIONS - 1:
if response.tool_calls:
self.logger.info("Stripping hallucinated tool calls in final round.")
response.tool_calls = []
return {"messages": [response], "loop_count": state.loop_count + 1}
workflow = StateGraph(AgentState)
workflow.add_node(AGENT_NODE, agent_node)
workflow.add_node(TOOL_NODE, tool_node)
workflow.set_entry_point(AGENT_NODE)
workflow.add_conditional_edges(AGENT_NODE, route_after_agent)
workflow.add_edge(TOOL_NODE, AGENT_NODE)
self.graph: CompiledStateGraph = workflow.compile(checkpointer=self.get_checkpointer())
def cmdb_query(self, query):
self.graph.checkpointer.delete_thread(self.module_name)
config = RunnableConfig()
config["configurable"] = {"thread_id": self.module_name}
self.agent_state = AgentState(messages=[HumanMessage(content=query)], loop_count=0)
response = self.graph.invoke(self.agent_state, config)
return response['messages'][-1].content
# Use the create_agent method to create a CMDB query agent for a simpler implementation
def cmdb_query(
query: Annotated[str, "The CMDB query in natural language (e.g., 'Find asset with IP 10.10.10.10')"]
) -> Annotated[str, "The query result in JSON format"]:
"""
Query internal asset information from CMDB using natural language.
"""
llm_api = LLMAPI()
llm = llm_api.get_model(tag=["fast", "function_calling"])
CMDB_AGENT_TOOLS = [
get_ci_context_tool,
fuzzy_search_ci_tool,
get_cis_by_software_tool,
get_cis_by_port_tool,
get_cis_by_service_tool,
get_cis_by_user_tool,
]
prompt_path = os.path.join(DATA_DIR, "Agent_CMDB", "system.md")
system_prompt_template = load_system_prompt_template(prompt_path)
agent = create_agent(
model=llm,
tools=CMDB_AGENT_TOOLS,
system_prompt=system_prompt_template.format(),
)
response = agent.invoke({"messages": [HumanMessage(content=query)]})
result = response['messages'][-1].content
return result
if __name__ == "__main__":
query = "Find asset information for IP address 192.168.10.5"
# result = cmdb_query(query)
agent = AgentGraphCMDB()
result = agent.cmdb_query(query)
print(result)
-149
View File
@@ -1,149 +0,0 @@
from typing import Annotated, List, Any
from langchain_core.messages import AIMessage, HumanMessage
from langgraph.graph import END, StateGraph, add_messages
from langgraph.graph.state import CompiledStateGraph
from pydantic import BaseModel, Field, ConfigDict
from Lib.api import get_current_time_str
from Lib.baseplaybook import LanggraphPlaybook
from PLUGINS.LLM.llmapi import LLMAPI
AGENT_NODE_SUMMARIZE = "SUMMARIZE"
AGENT_NODE_REPORT = "REPORT"
MAX_EVIDENCE_CHARS = 120000
_graph_agent_instance = None
def get_report_graph() -> CompiledStateGraph:
global _graph_agent_instance
if _graph_agent_instance is None:
_graph_agent_instance = GraphAgentReport()
return _graph_agent_instance.graph
def _normalize_content(content: Any) -> str:
if content is None:
return ""
if isinstance(content, str):
return content
return str(content)
def _build_evidence_entries(messages: List[Any]) -> List[dict]:
entries = []
for idx, message in enumerate(messages, 1):
role = getattr(message, "type", message.__class__.__name__)
content = _normalize_content(getattr(message, "content", ""))
tool_calls = getattr(message, "tool_calls", None)
if tool_calls:
content = f"{content}\n\nTool calls: {tool_calls}"
entries.append({"id": f"E{idx}", "role": role, "content": content})
return entries
def _build_evidence_text(entries: List[dict]) -> str:
lines = []
for entry in entries:
lines.append(f"{entry['id']} | role={entry['role']} | {entry['content']}")
return "\n\n".join(lines)
def _build_citations(entries: List[dict]) -> List[dict]:
citations = []
for entry in entries:
excerpt = entry["content"].replace("\n", " ").strip()
if len(excerpt) > 500:
excerpt = excerpt[:500] + "..."
citations.append({"id": entry["id"], "role": entry["role"], "excerpt": excerpt})
return citations
def _split_entries(entries: List[dict], max_chars: int) -> List[List[dict]]:
chunks = []
current = []
current_len = 0
for entry in entries:
entry_text = f"{entry['id']} | role={entry['role']} | {entry['content']}"
entry_len = len(entry_text) + 2
if current and current_len + entry_len > max_chars:
chunks.append(current)
current = []
current_len = 0
current.append(entry)
current_len += entry_len
if current:
chunks.append(current)
return chunks
class AgentState(BaseModel):
model_config = ConfigDict(arbitrary_types_allowed=True)
messages: Annotated[List[Any], add_messages] = Field(default_factory=list)
summary_digest: str = Field(default="")
report_markdown: str = Field(default="")
citations: List[dict] = Field(default_factory=list)
class GraphAgentReport(LanggraphPlaybook):
def __init__(self):
super().__init__()
self._summary_prompt_template = self.load_system_prompt_template("summary")
self._summary_merge_prompt_template = self.load_system_prompt_template("summary_merge")
self._report_prompt_template = self.load_system_prompt_template("system")
self._llm_api = LLMAPI()
self._llm_base = self._llm_api.get_model(tag=["powerful"])
self.graph = self._build_graph()
def _build_graph(self) -> CompiledStateGraph:
def summarize_node(state: AgentState):
entries = _build_evidence_entries(state.messages)
citations = _build_citations(entries)
evidence_text = _build_evidence_text(entries)
if len(evidence_text) <= MAX_EVIDENCE_CHARS:
system_message = self._summary_prompt_template.format(
CURRENT_UTC_TIME=get_current_time_str(),
REPORT_LANGUAGE="English"
)
human_message = HumanMessage(content=evidence_text)
response: AIMessage = self._llm_base.invoke([system_message, human_message])
return {"summary_digest": response.content, "citations": citations}
chunks = _split_entries(entries, MAX_EVIDENCE_CHARS)
partials = []
for chunk in chunks:
chunk_text = _build_evidence_text(chunk)
system_message = self._summary_prompt_template.format(
CURRENT_UTC_TIME=get_current_time_str(),
REPORT_LANGUAGE="English"
)
human_message = HumanMessage(content=f"PARTIAL_DIGEST: true\n\n{chunk_text}")
response: AIMessage = self._llm_base.invoke([system_message, human_message])
partials.append(response.content)
merge_system_message = self._summary_merge_prompt_template.format(
CURRENT_UTC_TIME=get_current_time_str(),
REPORT_LANGUAGE="English"
)
merge_human_message = HumanMessage(content="\n\n".join(partials))
merged: AIMessage = self._llm_base.invoke([merge_system_message, merge_human_message])
return {"summary_digest": merged.content, "citations": citations}
def report_node(state: AgentState):
system_message = self._report_prompt_template.format(
CURRENT_UTC_TIME=get_current_time_str(),
REPORT_LANGUAGE="English"
)
human_message = HumanMessage(content=state.summary_digest)
response: AIMessage = self._llm_base.invoke([system_message, human_message])
return {
"messages": [response],
"report_markdown": response.content
}
workflow = StateGraph(AgentState)
workflow.add_node(AGENT_NODE_SUMMARIZE, summarize_node)
workflow.add_node(AGENT_NODE_REPORT, report_node)
workflow.set_entry_point(AGENT_NODE_SUMMARIZE)
workflow.add_edge(AGENT_NODE_SUMMARIZE, AGENT_NODE_REPORT)
workflow.add_edge(AGENT_NODE_REPORT, END)
return workflow.compile()
-55
View File
@@ -1,55 +0,0 @@
from typing import Annotated
from langchain.agents import create_agent
from langchain_core.messages import HumanMessage
from AGENTS.agent_siem import tools
from Lib.api import get_current_time_str
from Lib.configs import DATA_DIR
from Lib.llmapi import load_system_prompt_template
from Lib.log import logger
from PLUGINS.LLM.llmapi import LLMAPI
# Alternative, simpler agent implementation using create_agent
def create_siem_agent(
query: Annotated[str, "A natural language query for SIEM."]
) -> Annotated[str, "A summary of the findings from the SIEM search."]:
"""
a simpler, stateless agent created using the create_agent factory function from langchain.agents.
"""
logger.info(f"Creating SIEM agent with query: {query[:100]}...")
prompt_path = os.path.join(DATA_DIR, "Agent_SIEM", "system_prompt.md")
logger.debug(f"Loading system prompt from: {prompt_path}")
system_prompt = load_system_prompt_template(prompt_path).format(CURRENT_UTC_TIME=get_current_time_str())
logger.debug(f"System prompt loaded successfully")
llm_api = LLMAPI()
llm = llm_api.get_model(tag=["fast", "function_calling"])
logger.debug(f"LLM model obtained with tags: ['fast', 'function_calling']")
logger.debug(f"Creating agent with {len(tools)} tools")
agent = create_agent(llm, tools, system_prompt=system_prompt)
logger.debug(f"Agent created successfully")
logger.info(f"Invoking agent...")
response = agent.invoke({"messages": [HumanMessage(content=query)]})
logger.info(f"Agent invocation completed")
result = response['messages'][-1].content
logger.info(f"Agent result extracted, result length: {len(result)} characters")
return result
if __name__ == "__main__":
import os
import django
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "ASP.settings")
django.setup()
print("\n--- Using create_agent for Query ---")
test_query = "Have there been any suspicious logins for the user 'admin' on Windows machines?"
result_simple = create_siem_agent(test_query)
print(result_simple)
-2
View File
@@ -1,2 +0,0 @@
You are a CMDB query assistant. You can call appropriate CMDB tools to query based on the user's natural language query request and return the results in JSON
format.
-35
View File
@@ -1,35 +0,0 @@
You are a SOC summarization subgraph. Your task is to read the provided evidence list and produce a summary digest for report generation.
Hard rules:
- Output language: {REPORT_LANGUAGE}.
- Use ONLY information present in the evidence list. Do not invent facts.
- Every material claim MUST be backed by a cited Evidence ID.
- Do not drop information. If you must compress, keep all material facts and preserve all IOCs, assets, identities, alerts, and timestamps.
- The evidence list is ordered and uses Evidence IDs (E1, E2, ...). Use these IDs in your output.
- Output must be valid JSON only, no surrounding text.
If the input begins with "PARTIAL_DIGEST: true", produce a partial digest in the same JSON schema, preserving all items from the chunk.
JSON schema to output:
{
"evidence_index": [{"id": "E1", "role": "human|ai|tool|system|other", "excerpt": "..."}],
"facts": [{"statement": "...", "evidence": ["E1", "E2"]}],
"timeline": [{"time": "...", "event": "...", "evidence": ["E1"]}],
"observations": [{"category": "Network|Endpoint|Identity|Email|Cloud|Other", "detail": "...", "evidence": ["E1"]}],
"entities": {
"assets": ["..."],
"identities": ["..."],
"iocs": ["..."],
"alerts": ["..."],
"tools_queries": ["..."],
"files": ["..."],
"processes": ["..."],
"urls_domains": ["..."],
"emails": ["..."],
"ips": ["..."],
"hashes": ["..."]
},
"decisions_actions": [{"action": "...", "evidence": ["E1"]}],
"uncertainties": ["..."]
}
-16
View File
@@ -1,16 +0,0 @@
You are a SOC digest merger. Your task is to merge multiple JSON summary digests into one comprehensive digest.
Hard rules:
- Output language: {REPORT_LANGUAGE}.
- Use ONLY information present in the partial digests. Do not invent facts.
- Do not drop information. If duplicates exist, consolidate but keep all unique facts and evidence IDs.
- Output must be valid JSON only, no surrounding text.
Merge logic:
- evidence_index: union by id; keep the most informative excerpt.
- facts, timeline, observations, decisions_actions: union items; merge if statements are equivalent; combine evidence lists.
- entities: union unique items per list.
- uncertainties: union unique items.
Output JSON schema must match the summary digest schema exactly.
-115
View File
@@ -1,115 +0,0 @@
You are a SOC reporting subgraph. Your task is to read the provided summary digest and generate a cybersecurity analysis
report in Markdown.
Hard rules:
- Output language: {REPORT_LANGUAGE}. If the user explicitly requests another language in the messages, follow the user
request.
- Use ONLY information present in the summary digest. Do not invent facts.
- Every material claim MUST be backed by a cited Evidence ID.
- If evidence is missing, write "Unknown" or "Not provided in messages".
- The output must strictly follow the fixed template and headings below.
- Evidence IDs must reference the order of the input messages, starting from E1 for the first message.
- Provide a concise evidence excerpt in the Evidence Index.
- Do not add extra sections or reorder headings.
Report time: {CURRENT_UTC_TIME}
# SOC Cybersecurity Analysis Report
## Executive Summary
- Overall assessment:
- Severity:
- Rationale (Evidence: )
## Detection and Alert Context
- Detection sources:
- Rule/signature/alert IDs:
- Initial trigger:
- Confidence level:
## Scope and Assumptions
- Data sources: Summary digest only
- Time window inferred:
- Environment inferred:
- Known limitations:
## Key Findings (TL;DR)
-
## Timeline (Inferred)
| Time (UTC if available) | Event | Supporting Evidence IDs |
|-------------------------|-------|-------------------------|
| | | |
## Observations and Evidence
### Network
-
### Endpoint
-
### Identity and Access
-
### Email and Collaboration
-
### Cloud and SaaS
-
### Other
-
## Asset and Identity Context
- Affected assets (hostnames/IPs/roles):
- Affected identities (users/roles/privilege):
- Business criticality:
## Threat Assessment
- Likely attack path / kill chain:
- MITRE ATT&CK mapping (Technique + rationale + Evidence IDs):
- Adversary intent or goal (if inferable):
## Impact and Exposure Assessment
- Affected assets/users:
- Potential impact types:
- Exposure window:
- Potential blast radius:
## Containment, Eradication, and Recovery
### Immediate (0-24h)
-
### Short-term (1-7d)
-
### Long-term Hardening
-
## Validation Steps
-
## Open Questions and Data Requests
-
## Lessons Learned (Evidence-based only)
-
## Appendix
### Indicators (IOCs)
-
### Evidence Index
- E1:
@@ -1,7 +1,7 @@
---
name: asp-alert-en
description: 'Review ASP alerts, update AI analysis fields, inspect alert discussions, or attach enrichment after analysis.'
argument-hint: 'review alert <alert_id> | list alerts [filters] | update alert <alert_id> <fields>'
description: 'Review ASP alerts for triage analysis.'
argument-hint: 'review alert <alert_id> | list alerts [filters]'
compatibility: connect to asp mcp server
metadata:
author: Funnywolf
@@ -21,17 +21,13 @@ An alert is secondary data in ASP. Each alert belongs to a case, and each alert
- The user gives an alert ID and wants a quick review, inspection, or summary.
- The user wants to find alerts by status, severity, confidence, or correlation UID.
- The user wants to inspect discussion context for an alert.
- The user wants to update AI analysis fields on an alert.
- The user wants to attach enrichment to an alert after analysis.
## Operating Rules
- Keep the response focused on triage value rather than repeating schema fields.
- If the user is working on a specific alert, prefer `list_alerts(alert_id=<id>, limit=1)` because the current MCP surface does not expose a separate `get_alert` tool.
- If the user wants to save structured analysis back onto the alert, use the `asp-enrichment-en` skill.
Note: alerts only support `severity_ai`, `confidence_ai`, and `comment_ai` updates; verdict and summary fields belong to case updates.
- Alerts are currently read-only. If the user wants to save structured analysis back onto the alert, use the `asp-enrichment-en` skill.
## Additional Information
@@ -41,10 +37,8 @@ Note: alerts only support `severity_ai`, `confidence_ai`, and `comment_ai` updat
## Decision Flow
1. If the user provides a specific alert ID or says "open", "show", "review", or "summarize" an alert, call `list_alerts(alert_id=<id>, limit=1)`.
2. If the user wants discussion context, call `get_alert_discussions(alert_id)` after retrieving the alert.
3. If the user wants to browse or compare alerts, use `list_alerts` with supported filters.
4. If the user wants to update AI severity, AI confidence, or AI comment, call `update_alert`.
5. If the user wants to attach analysis results, intelligence, or structured context to the alert, use the `asp-enrichment-en` skill.
2. If the user wants to browse or compare alerts, use `list_alerts` with supported filters.
3. If the user wants to attach analysis results, intelligence, or structured context to the alert, use the `asp-enrichment-en` skill.
## SOP
@@ -54,15 +48,13 @@ Note: alerts only support `severity_ai`, `confidence_ai`, and `comment_ai` updat
2. If the user only needs the basic alert information, call `list_alerts(alert_id=<id>, limit=1)`.
3. If the result is empty, state that the alert was not found.
4. Parse the first JSON record.
5. If the user wants analyst discussion context, call `get_alert_discussions(alert_id)`.
6. Present only the most useful triage fields.
5. Present only the most useful triage fields.
Preferred response structure:
- `Alert`: alert ID, title or name, severity, status, confidence, correlation UID.
- `Timeline`: created or updated time when present.
- `Key Context`: source, rule, category, owner, or other high-signal fields.
- `Discussions`: only the most relevant analyst or system notes when needed.
- `Assessment`: short triage judgment.
### List Alerts
@@ -80,31 +72,19 @@ Preferred response structure:
Then add one short explanation line when needed.
### Update Alert AI Fields
1. Require `alert_id`.
2. Extract only supported AI fields: `severity_ai`, `confidence_ai`, and `comment_ai`.
3. Call `update_alert` with only the changed fields.
4. If the result is `None`, state that the alert was not found.
5. Confirm only the fields that changed.
## Clarification Rules
- Ask for `alert_id` only when it is missing for alert-related actions.
- Ask for enum clarification only when the requested value does not map cleanly to ASP values.
- If the user says "lower confidence", "raise severity", or "leave a note", map it directly to the matching AI field when the intent is clear.
## Output Rules
- Be concise.
- Do not output raw JSON unless the user explicitly asks for it.
- Prefer triage wording over schema wording.
- If alert data and discussion context are both used, merge them into one coherent view.
- State blockers clearly: alert not found, unsupported filter, invalid enum value, or incomplete follow-up payload.
- State blockers clearly: alert not found, unsupported filter, invalid enum value.
## Failure Handling
- If the alert does not exist, say so directly.
- If filters return no results, say so directly and suggest the most useful refinement.
- If the requested update field is unsupported, say which alert fields are writable.
- If the enrichment input is incomplete, ask one focused follow-up instead of guessing.
@@ -1,7 +1,7 @@
---
name: asp-alert-zh
description: '查 ASP 告警、更新 AI 分析字段、查看告警讨论。'
argument-hint: 'review alert <alert_id> | list alerts [filters] | update alert <alert_id> <fields>'
description: '查 ASP 告警并进行分诊分析。'
argument-hint: 'review alert <alert_id> | list alerts [filters]'
compatibility: connect to asp mcp server
metadata:
author: Funnywolf
@@ -21,13 +21,13 @@ Alert 是 ASP 中的二级数据,每个 Alert 都会挂载到一个 Case,一个
- 用户给出一个告警 ID,希望快速查看、审查或总结。
- 用户希望按状态、严重级别、置信度或 correlation UID 查找告警。
- 用户想查看某条告警的分析讨论内容。
- 用户想更新告警上的 AI 分析字段。
- 用户想在分析后把 enrichment 附加到告警。
## 运行规则
- 回复要聚焦于分诊价值,而不是原样回显 schema 字段。
- Alert 当前为只读接口,如需更新分析结果请使用 enrichment。
- 如果用户需要保存分析结果或结构化上下文到告警上,使用 `asp-enrichment-zh` skill。
## 补充信息
@@ -35,11 +35,9 @@ Alert 是 ASP 中的二级数据,每个 Alert 都会挂载到一个 Case,一个
## 决策流程
1. 如果用户提供了具体告警 ID,或要求open”“show”“review”“summarize某条告警,调用 `list_alerts(alert_id=<id>, limit=1)`
2. 如果用户要求讨论上下文,在取回告警后调用 `get_alert_discussions(alert_id)`
3. 如果用户要浏览或对比多条告警,使用带支持过滤条件的 `list_alerts`
4. 如果用户要更新 AI severity、AI confidence 或 AI comment,调用 `update_alert`
5. 如果用户要附加分析结果、情报或结构化上下文,使用 `asp-enrichment-zh` skill。
1. 如果用户提供了具体告警 ID,或要求"open""show""review""summarize"某条告警,调用 `list_alerts(alert_id=<id>, limit=1)`
2. 如果用户要浏览或对比多条告警,使用带支持过滤条件的 `list_alerts`
3. 如果用户要附加分析结果、情报或结构化上下文,使用 `asp-enrichment-zh` skill
## SOP
@@ -49,15 +47,13 @@ Alert 是 ASP 中的二级数据,每个 Alert 都会挂载到一个 Case,一个
2. 如果只需要快速查看告警基本信息,调用 `list_alerts(alert_id=<id>, limit=1)` 即可。
3. 如果结果为空,直接说明找不到该告警。
4. 解析第一条 JSON 记录。
5. 如果用户要求分析讨论上下文,调用 `get_alert_discussions(alert_id)`
6. 只呈现最有价值的分诊字段。
5. 只呈现最有价值的分诊字段
首选回复结构:
- `Alert`:alert ID、标题或名称、严重级别、状态、置信度、correlation UID。
- `Timeline`:存在时给出创建或更新时间。
- `Key Context`:来源、规则、类别、负责人或其他高信号字段。
- `Discussions`:只在需要时给出最相关的分析或系统备注。
- `Assessment`:简短分诊判断。
### 列出告警
@@ -75,30 +71,19 @@ Alert 是 ASP 中的二级数据,每个 Alert 都会挂载到一个 Case,一个
然后在需要时补一句简短解释。
### 更新告警 AI 字段
1. 要求提供 `alert_id`
2. 只提取支持的 AI 字段:`severity_ai``confidence_ai``comment_ai`
3. 仅带变更字段调用 `update_alert`
4. 如果结果为 `None`,说明找不到该告警。
5. 只确认实际修改的字段。
## 澄清规则
- 只有在缺少告警相关操作所需参数时才询问 `alert_id`
- 只有当请求值不能清晰映射到 ASP 枚举时,才要求用户澄清枚举值。
- 如果用户说“降低 confidence”“提高 severity”或“留个备注”,在意图明确时直接映射到对应 AI 字段。
## 输出规则
- 保持简洁。
- 除非用户明确要求,否则不要输出原始 JSON。
- 优先使用分诊语义,而不是 schema 语义。
- 如果同时用了告警数据和讨论内容,要合并成一个连贯视图
- 清楚指出阻塞项:告警不存在、不支持的过滤条件、无效枚举值,或不完整的追加载荷。
- 清楚指出阻塞项:告警不存在、不支持的过滤条件、无效枚举值
## 失败处理
- 如果告警不存在,直接说明。
- 如果过滤无结果,直接说明并建议最有用的收敛方式。
- 如果请求更新的字段不受支持,明确指出哪些告警字段是可写的。
@@ -1,6 +1,6 @@
---
name: asp-artifact-en
description: 'Find artifacts by IOC and attach enrichment to artifacts.'
description: 'Find artifacts by IOC.'
argument-hint: 'review artifact <artifact_id> | list artifacts [filters]'
compatibility: connect to asp mcp server
metadata:
@@ -19,15 +19,13 @@ Artifacts are created automatically by system processes. You can list and analyz
## When to Use
- The user wants to find artifacts by value, type, role, owner, or reputation.
- The user wants to attach enrichment or structured analysis to an artifact.
- The user wants to find artifacts by value, type, role, or owner.
## Operating Rules
- Treat artifacts as the smallest investigation object on the platform.
- Use `list_artifacts` for lookup and review.
- If the user wants to save analysis on the artifact itself, use `create_enrichment` plus `attach_enrichment_to_target`.
- For the full enrichment persistence workflow, use the `asp-enrichment-en` skill.
- If the user wants to save analysis on the artifact, use the `asp-enrichment-en` skill.
## Additional Information
@@ -51,8 +49,8 @@ Artifacts are created automatically by system processes. You can list and analyz
Preferred response structure:
| Artifact ID | Value | Type | Role | Owner | Reputation | Summary |
|-------------|-------|------|------|-------|------------|---------|
| Artifact ID | Value | Type | Role | Owner | Summary |
|-------------|-------|------|------|-------|---------|
Then add one short explanation line when needed.
@@ -19,15 +19,13 @@ artifact 由系统自动创建,用户只能查询和分析已有 artifact,
## 适用场景
- 用户想按 value、type、role、owner 或 reputation 查找 artifact。
- 用户想给 artifact 附加 enrichment 或结构化分析。
- 用户想按 value、type、role、owner 查找 artifact。
## 运行规则
- 把 artifact 视为平台里的最小调查对象。
- 查询和审查时使用 `list_artifacts`
- 如果用户想把分析结果保存到 artifact 本身,使用 `create_enrichment``attach_enrichment_to_target`
- 如需完整的 enrichment 持久化流程,使用 `asp-enrichment-zh` skill。
- 如需保存分析结果到 artifact,使用 `asp-enrichment-zh` skill
## 补充信息
@@ -50,8 +48,8 @@ artifact 由系统自动创建,用户只能查询和分析已有 artifact,
首选回复结构:
| Artifact ID | Value | Type | Role | Owner | Reputation | Summary |
|-------------|-------|------|------|-------|------------|---------|
| Artifact ID | Value | Type | Role | Owner | Summary |
|-------------|-------|------|------|-------|---------|
然后在需要时补一句简短解释。
@@ -70,4 +68,3 @@ artifact 由系统自动创建,用户只能查询和分析已有 artifact,
- 如果没有匹配的 artifact,直接说明,并建议最有用的收敛方式。
- 如果目标 artifact 不存在,直接说明。
@@ -1,11 +1,11 @@
---
name: asp-enrichment-en
description: 'Save structured data as enrichment and attach it to a case, alert, or artifact.'
argument-hint: 'create enrichment for <case|alert|artifact> <target_id> | attach enrichment to <case|alert|artifact> <target_id>'
argument-hint: 'create enrichment <target_id> [fields]'
compatibility: connect to asp mcp server
metadata:
author: Funnywolf
version: 0.1.0
version: 0.2.0
mcp-server: asp
category: cyber security
tags: [ enrichment, analysis, context, investigation ]
@@ -21,55 +21,37 @@ Use this skill when analysis results need to be saved back into ASP as structure
- The user wants to save structured analysis, intelligence, or investigation conclusions.
- The user wants to attach context to a case, alert, or artifact.
- The user wants to persist SIEM findings, threat intel, asset context, or analyst conclusions.
- The user already has an enrichment and wants to reuse it on a target object.
## Operating Rules
- Treat enrichment as the platform's structured result layer, not as a generic comment field.
- When the goal is to persist analysis on a `case`, `alert`, or `artifact`, use this skill.
- Separate creation from attachment.
- Use `create_enrichment` for a new result record.
- Use `attach_enrichment_to_target` only after you have the enrichment row_id.
- Use `create_enrichment` to create the enrichment record and attach it to the target in one step.
- Keep the payload compact and actionable.
- Use the object-specific skill first when the user is still inspecting the object, and use this skill when saving the result.
## Additional Information
- `row_id` is the UUID for each enrichment record and is used for data association.
- `enrichment_id` is the human-readable unique ID for each enrichment record.
## Decision Flow
1. If the user wants to save a new structured result, call `create_enrichment` first.
2. If the user wants to attach the result to a case, alert, or artifact, call `attach_enrichment_to_target`.
3. If the user already has an enrichment row_id, skip creation and attach it directly.
4. If the user is still exploring the object rather than saving a result, use the corresponding object skill first.
1. If the user wants to save a new structured result on a target object, call `create_enrichment(target_id=..., ...)`.
2. If the user is still exploring the object rather than saving a result, use the corresponding object skill first.
## SOP
### Create And Attach New Enrichment
### Create Enrichment
1. Require `target_id` such as `case_000001`, `alert_000001`, or `artifact_000001`.
2. Convert the user's analysis into a compact structured enrichment payload.
3. Call `create_enrichment` and keep the returned enrichment row_id.
4. Call `attach_enrichment_to_target(target_id=<target_id>, enrichment_row_id=<created_row_id>)`.
5. Confirm that the enrichment was created and attached successfully.
3. Call `create_enrichment(target_id=<target_id>, name=..., type=..., ...)`.
4. Confirm the created enrichment row_id and that it is attached to the target.
Preferred response structure:
- `Target ID`: target ID
- `Enrichment`: created enrichment row_id
### Attach Existing Enrichment
1. Require `target_id` and `enrichment_row_id`.
2. Call `attach_enrichment_to_target(target_id=<target_id>, enrichment_row_id=<enrichment_row_id>)`.
3. Confirm that the enrichment was attached successfully.
## Clarification Rules
- Ask for `target_id` only when it is missing.
- Ask for the enrichment row_id only when the user wants to reuse an existing enrichment and did not provide it.
- If the user only says "save this result", infer the most obvious target object from the current request when it is clear, and prefer Case.
## Output Rules
@@ -83,4 +65,3 @@ Preferred response structure:
- If the target object does not exist, say so directly.
- If the enrichment payload is incomplete, ask one focused follow-up instead of guessing.
- If attachment fails because the enrichment row_id is missing, ask for it or create a new enrichment first.
@@ -1,11 +1,11 @@
---
name: asp-enrichment-zh
description: '把结构化数据保存为 enrichment,并附加到 case、alert 或 artifact。'
argument-hint: 'create enrichment for <case|alert|artifact> <target_id> | attach enrichment to <case|alert|artifact> <target_id>'
argument-hint: 'create enrichment <target_id> [fields]'
compatibility: connect to asp mcp server
metadata:
author: Funnywolf
version: 0.1.0
version: 0.2.0
mcp-server: asp
category: cyber security
tags: [ enrichment, analysis, context, investigation ]
@@ -14,64 +14,47 @@ metadata:
# ASP Enrichment
当数据需要以结构化上下文形式保存回 ASP 且挂载到对应 case , alert 或 artifact 时,使用这个 skill。
当数据需要以结构化上下文形式保存回 ASP 且挂载到对应 casealert 或 artifact 时,使用这个 skill。
## 适用场景
- 用户想保存结构化分析、情报或调查结论。
- 用户想把上下文附加到 case、alert 或 artifact。
- 用户想持久化 SIEM 发现、威胁情报、资产上下文或分析师结论。
- 用户已经有 enrichment,希望把它复用并附加到目标对象。
## 运行规则
- 把 enrichment 视为平台的结构化结果层,而不是普通评论字段。
- 当目标是把分析结果持久化到 `case``alert``artifact` 上时,使用这个 skill。
- 区分“创建 enrichment”和“附加 enrichment”两个动作
- 新结果记录使用 `create_enrichment`
- 只有在已经拿到 enrichment row_id 后,才使用 `attach_enrichment_to_target`
- 使用 `create_enrichment` 创建 enrichment 记录并自动附加到目标对象
- enrichment payload 保持紧凑且可操作。
- 查看对象本身时优先使用对象对应的 skill;保存结果时再使用本 skill。
## 补充信息
- row_id 为每条 enrichment 记录的UUID,用于数据关联. enrichment_id 是每条 enrichment 记录人类可读的唯一ID
## 决策流程
1. 如果用户想保存新的结构化结果,调用 `create_enrichment`
2. 如果用户想把结果附加到 case、alert 或 artifact,调用 `attach_enrichment_to_target`
3. 如果用户已经有现成的 enrichment row_id,跳过创建,直接附加。
4. 如果用户还处于对象探索阶段而不是保存结果,先使用对应对象 skill。
1. 如果用户想在目标对象上保存新的结构化结果,调用 `create_enrichment(target_id=..., ...)`
2. 如果用户还处于对象探索阶段而不是保存结果,先使用对应对象 skill
当你已经有明确的分析结论,例如 verdict、TTP 集合、风险评级或缓解建议,并且这些内容需要保存在目标对象上时,就切换到这个 skill。
## SOP
### 创建并附加新的 Enrichment
### 创建 Enrichment
1. 要求提供`target_id` (比如 case_000001 / alert_000001 / artifact_000001)
1. 要求提供 `target_id`如 case_000001 / alert_000001 / artifact_000001
2. 把用户的分析整理成紧凑的结构化 enrichment payload。
3. 调用 `create_enrichment` 并保留返回的 enrichment row_id
4. 调用`attach_enrichment_to_target(target_id=<target_id>, enrichment_row_id=<created_row_id>)`
5. 确认 enrichment 已创建并附加成功。
3. 调用 `create_enrichment(target_id=<target_id>, name=..., type=..., ...)`
4. 确认创建后的 enrichment row_id 及其已附加到目标对象
首选回复结构:
- `Target ID`:目标 ID
- `Enrichment`:创建出的 enrichment row_id
### 附加已有 Enrichment
1. 要求提供 `target_id``enrichment_row_id`
2. 调用`attach_enrichment_to_target(target_id=<target_id>, enrichment_row_id=<enrichment_row_id>)`
3. 确认 enrichment 已附加成功。
## 澄清规则
- 只有在缺失时才询问 `target_id`
- 只有当用户要复用现有 enrichment 且未提供时,才询问 enrichment row_id
- 如果用户只说“把这个结果保存一下”,在上下文明确时推断最明显的目标对象,优先选择 Case。
- 如果用户只说”把这个结果保存一下”,在上下文明确时推断最明显的目标对象,优先选择 Case
## 输出规则
@@ -84,4 +67,3 @@ metadata:
- 如果目标对象不存在,直接说明。
- 如果 enrichment payload 不完整,只问一个聚焦问题,不要猜测。
- 如果附加失败是因为缺少 enrichment row_id,就要求用户提供,或先创建新的 enrichment。
+12 -30
View File
@@ -193,6 +193,8 @@ def list_artifacts(
# Enrichment
def create_enrichment(
target_id: Annotated[str, Field(
description="Target object ID to attach the enrichment to; must start with case_, alert_, or artifact_ (挂载富化的目标对象 ID,须以 case_、alert_ 或 artifact_ 开头)")],
name: Annotated[str, Field(description="Enrichment name (富化名称)")] = "",
type: Annotated[EnrichmentType, Field(description="Enrichment type (富化类型)")] = EnrichmentType.OTHER,
provider: Annotated[EnrichmentProvider, Field(description="Enrichment provider (富化提供商)")] = EnrichmentProvider.OTHER,
@@ -201,7 +203,7 @@ def create_enrichment(
desc: Annotated[str, Field(description="Enrichment summary (富化摘要)")] = "",
data: Annotated[str, Field(description="Detailed enrichment JSON string (详细富化 JSON 字符串)")] = ""
) -> Annotated[str, Field(description="Created enrichment record row ID (创建的 Enrichment 行 ID)")]:
"""Create one enrichment record. (创建一条富化记录)"""
"""Create one enrichment record and attach it to a target case, alert, or artifact. (创建一条富化记录并挂载到目标 Case、Alert 或 Artifact)"""
model = EnrichmentModel()
model.name = name
model.type = type
@@ -210,38 +212,19 @@ def create_enrichment(
model.src_url = src_url
model.desc = desc
model.data = data
return Enrichment.create(model)
enrichment_row_id = Enrichment.create(model)
def attach_enrichment_to_target(
target_id: Annotated[str, Field(
description="Target object ID to receive the enrichment; must start with case_, alert_, or artifact_ (接收富化的目标对象 ID,须以 case_、alert_ 或 artifact_ 开头)")],
enrichment_row_id: Annotated[
str, Field(description="Enrichment record row ID returned by create_enrichment (由 create_enrichment 返回的 Enrichment 行 ID)")]
) -> Annotated[
Optional[str], Field(description="Attached enrichment record row ID, or None if target not found (挂载后的 Enrichment 行 ID,目标不存在时返回 None)")]:
"""Attach one existing enrichment record to an existing case, alert, or artifact. (将已有富化记录挂载到 Case、Alert 或 Artifact)"""
normalized_target_id = target_id.strip().lower()
if normalized_target_id.startswith("case_"):
return Case.attach_enrichment(
case_id=target_id,
enrichment_row_id=enrichment_row_id
)
Case.attach_enrichment(case_id=target_id, enrichment_row_id=enrichment_row_id)
elif normalized_target_id.startswith("alert_"):
Alert.attach_enrichment(alert_id=target_id, enrichment_row_id=enrichment_row_id)
elif normalized_target_id.startswith("artifact_"):
Artifact.attach_enrichment(artifact_id=target_id, enrichment_row_id=enrichment_row_id)
else:
raise ValueError("target_id must start with one of: case_, alert_, artifact_")
if normalized_target_id.startswith("alert_"):
return Alert.attach_enrichment(
alert_id=target_id,
enrichment_row_id=enrichment_row_id
)
if normalized_target_id.startswith("artifact_"):
return Artifact.attach_enrichment(
artifact_id=target_id,
enrichment_row_id=enrichment_row_id
)
raise ValueError("target_id must start with one of: case_, alert_, artifact_")
return enrichment_row_id
# Ticket
@@ -475,7 +458,6 @@ REGISTERED_MCP_TOOLS = [
# enrichment
create_enrichment,
attach_enrichment_to_target,
# playbook
list_available_playbook_definitions,