Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -77,4 +77,5 @@ backend/detections/mitre_attack_stub.json
frontend/tsconfig.tsbuildinfo

# Local AI tool config — not shared to the repo
CLAUDE.md
CLAUDE.md
frontend/next-env.d.ts
321 changes: 310 additions & 11 deletions backend/ai_assistant/assistant.py

Large diffs are not rendered by default.

83 changes: 80 additions & 3 deletions backend/ai_assistant/chat_agent.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import json
import json
import logging
import re
from dataclasses import dataclass
Expand Down Expand Up @@ -317,6 +317,73 @@
return {}


def _chat_content(response: Dict[str, Any]) -> str:
choices = response.get("choices") if isinstance(response, dict) else []
if not isinstance(choices, list) or not choices:
return ""
message = choices[0].get("message") or {}
return str(message.get("content") or "").strip()


def _preload_chat_skills(user_input: str, recommended_skills: List[str], overrides: Optional[Dict[str, Any]] = None) -> Tuple[List[str], List[str], List[Dict[str, Any]]]:
if not recommended_skills:
return [], [], []
names = [str(name).strip() for name in recommended_skills if str(name).strip()]
list_exec_id = start_mcp_execution("list_skills", {}, source="internal")
try:
catalog = list_skills()
list_success = True
list_content = "\n".join(catalog)
except Exception as exc:
catalog = []
list_success = False
list_content = f"Skill listing failed: {exc}"
finish_mcp_execution(list_exec_id, list_success, error="" if list_success else list_content)
update_mcp_stats("list_skills", list_success)
available = [name for name in names if name in catalog] if catalog else names
trace = [
{"type": "model_call", "iteration": 0, "purpose": "skill_selection"},
{"type": "tool_calls_detected", "iteration": 0, "count": 1},
{"type": "tool_call", "iteration": 0, "tool": "list_skills", "source": "internal", "arguments": {}, "automatic": True, "execution_id": list_exec_id},
{"type": "tool_result", "iteration": 0, "tool": "list_skills", "source": "internal", "success": list_success, "content": list_content, "automatic": True, "execution_id": list_exec_id},
]
router_prompt = ('You are a SOC skill router. Select only relevant skills from this list based on the user request. '
'Return JSON only: {"skills":["exact skill name"]}. Return {"skills":[]} if none apply. Never invent names.\n'
Comment on lines +350 to +351
+ 'Available skills: ' + json.dumps(available, ensure_ascii=False) + '\nUser request: ' + user_input)
selected_names = []
try:
response = _call_openai_chat([{"role":"system","content":router_prompt},{"role":"user","content":user_input}], [], overrides=overrides)
text = _chat_content(response)
match = re.search(r'\{.*\}', text, flags=re.DOTALL)
payload = json.loads(match.group(0) if match else text)
values = payload.get("skills") if isinstance(payload, dict) else []
if isinstance(values, list):
selected_names = [str(value).strip() for value in values if str(value).strip() in available]
except Exception:
logger.exception("Chat skill preselection failed")
instructions = []
for name in selected_names:
read_exec_id = start_mcp_execution("read_skill", {"skill_name": name}, source="internal")
doc = read_skill(name)
if doc and doc.content:
finish_mcp_execution(read_exec_id, True)
update_mcp_stats("read_skill", True)
record_skill_call(name, True)
instructions.append(f"SKILL: {doc.name}\n{doc.content[:12000]}")
trace.extend([
{"type": "tool_call", "iteration": 0, "tool": "read_skill", "source": "internal", "arguments": {"skill_name": name}, "automatic": True, "execution_id": read_exec_id},
{"type": "tool_result", "iteration": 0, "tool": "read_skill", "source": "internal", "success": True, "content": doc.content[:2000], "automatic": True, "execution_id": read_exec_id},
])
else:
finish_mcp_execution(read_exec_id, False, error=f"Skill not found: {name}")
update_mcp_stats("read_skill", False)
record_skill_call(name, False)
trace.extend([
{"type": "tool_call", "iteration": 0, "tool": "read_skill", "source": "internal", "arguments": {"skill_name": name}, "automatic": True, "execution_id": read_exec_id},
{"type": "tool_result", "iteration": 0, "tool": "read_skill", "source": "internal", "success": False, "content": f"Skill not found: {name}", "automatic": True, "execution_id": read_exec_id},
])
return selected_names, instructions, trace

def run_chat_agent(
user_input: str,
history_messages: Optional[List[Dict[str, Any]]] = None,
Expand All @@ -335,9 +402,12 @@
"When responding to the user, include a short analysis summary in a second paragraph starting with "
"\"AI thinking:\" (max 80 words)."
)
preloaded_names, preloaded_instructions, preload_trace = _preload_chat_skills(user_input, recommended_skills, overrides=overrides)
if recommended_skills:
skills_hint = ", ".join([f"`{s}`" for s in recommended_skills])
system_prompt += f"\nRecommended skills: {skills_hint}. Use read_skill to load details when needed."
system_prompt += f"\nAvailable skills: {skills_hint}. You may still call read_skill for additional details."
if preloaded_instructions:
system_prompt += "\nSelected skill instructions (apply only when supported by evidence):\n" + "\n\n".join(preloaded_instructions)

messages: List[Dict[str, Any]] = [{"role": "system", "content": system_prompt}]
for msg in history_messages:
Expand All @@ -350,6 +420,11 @@

internal_tools, internal_handlers = _internal_tools()
external_tools, external_mapping = _external_tools(overrides=overrides)
if preloaded_names:
internal_tools = [
tool for tool in internal_tools
if ((tool.get("function") or {}).get("name") != "read_skill")
]
tools = internal_tools + external_tools

max_iter_value = None
Expand All @@ -362,7 +437,7 @@
except Exception:
max_iterations = 6

trace: List[Dict[str, Any]] = []
trace: List[Dict[str, Any]] = list(preload_trace)
iteration = 0
for _ in range(max_iterations):
iteration += 1
Expand Down Expand Up @@ -436,6 +511,8 @@
parts = content.split("AI thinking:", 1)
content = parts[0].strip()
summary = parts[1].strip()
if not content.strip():
content = summary or "AI analysis completed, but no user-facing response was returned."
trace.append({"type": "assistant_response", "iteration": iteration, "content": content})
if summary:
trace.append({"type": "analysis_summary", "iteration": iteration, "content": summary})
Expand Down
26 changes: 1 addition & 25 deletions backend/ai_assistant/skills.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,13 +47,6 @@ def _apply_soc_ticket_triage(
if not isinstance(assistant.get("next_tasks"), list):
assistant["next_tasks"] = []

if not assistant["next_tasks"]:
assistant["next_tasks"] = [
{"title": "Validate affected scope", "detail": "Confirm impacted hosts/users from ticket context."},
{"title": "Correlate recent logs", "detail": "Review recent timeline and related alerts for recurrence."},
{"title": "Prepare containment plan", "detail": "Draft immediate containment actions for approval."},
]

if not isinstance(assistant.get("alert_explanation"), str):
assistant["alert_explanation"] = "Initial triage completed using ticket context, timeline, and similar cases."

Expand Down Expand Up @@ -86,26 +79,9 @@ def apply_local_skills(
if not routes:
return assistant

applied_routes: set[str] = set()
if "ticket_triage" in routes or "soc-ticket-triage" in routes:
_apply_soc_ticket_triage(assistant, ticket=ticket, timeline=timeline)
applied_routes.add("ticket_triage")
applied_routes.add("soc-ticket-triage")
if "incident_summary" in routes or "incident-summary" in routes:
_apply_incident_summary(assistant, ticket=ticket, timeline=timeline)
applied_routes.add("incident_summary")
applied_routes.add("incident-summary")

if isinstance(skills, list):
for s in skills:
if not isinstance(s, dict):
continue
if s.get("enabled") is False:
continue
route = str(s.get("route") or s.get("name") or "").strip()
name = str(s.get("name") or route).strip()
if not name:
continue
if route in applied_routes:
record_skill_call(name, True)

return assistant
5 changes: 4 additions & 1 deletion backend/ai_assistant/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -253,9 +253,12 @@ def ai_chat(request):
except Exception:
pass

chat_input = str(data.get("message") or "")
if ticket:
chat_input += "\n\nCurrent case raw alert JSON:\n" + str(ticket.alert_message or "")
try:
result = run_chat_agent(
user_input=data.get("message"),
user_input=chat_input,
history_messages=data.get("messages") or [],
overrides=overrides,
recommended_skills=recommended_skills,
Expand Down
13 changes: 13 additions & 0 deletions backend/skills/cloud-iam-anomaly/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
name: "Cloud IAM anomaly triage"
description: "Analyze suspicious cloud identity and access events, including role changes, new keys, unusual regions, and privilege escalation. Recommend evidence-preserving response actions."
---
# Cloud IAM anomaly triage

Analyze suspicious cloud identity and access events, including role changes, new keys, unusual regions, and privilege escalation. Recommend evidence-preserving response actions.

Output requirements:
- Cite observed evidence from the ticket.
- Mark unknown values as unavailable.
- Return structured JSON fields when requested.
- Do not change ticket status or execute commands.
13 changes: 13 additions & 0 deletions backend/skills/data-exfiltration-detection/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
name: "Data exfiltration detection"
description: "Assess possible data movement using destinations, protocols, volume, files, and user context. Identify evidence gaps and recommend low-risk validation and containment actions."
---
# Data exfiltration detection

Assess possible data movement using destinations, protocols, volume, files, and user context. Identify evidence gaps and recommend low-risk validation and containment actions.

Output requirements:
- Cite observed evidence from the ticket.
- Mark unknown values as unavailable.
- Return structured JSON fields when requested.
- Do not change ticket status or execute commands.
13 changes: 13 additions & 0 deletions backend/skills/dns-c2-investigation/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
name: "DNS command and control investigation"
description: "Analyze suspicious DNS behavior such as high entropy, beaconing, rare domains, unusual record types, and query volume. Distinguish indicators from confirmed C2 and recommend safe validation."
---
# DNS command and control investigation

Analyze suspicious DNS behavior such as high entropy, beaconing, rare domains, unusual record types, and query volume. Distinguish indicators from confirmed C2 and recommend safe validation.

Output requirements:
- Cite observed evidence from the ticket.
- Mark unknown values as unavailable.
- Return structured JSON fields when requested.
- Do not change ticket status or execute commands.
13 changes: 13 additions & 0 deletions backend/skills/incident-containment-planning/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
name: "Incident containment planning"
description: "Create a prioritized, reversible containment plan based only on observed evidence. Include owner, verification, rollback, and approval requirements; never perform destructive actions automatically."
---
# Incident containment planning

Create a prioritized, reversible containment plan based only on observed evidence. Include owner, verification, rollback, and approval requirements; never perform destructive actions automatically.

Output requirements:
- Cite observed evidence from the ticket.
- Mark unknown values as unavailable.
- Return structured JSON fields when requested.
- Do not change ticket status or execute commands.
13 changes: 13 additions & 0 deletions backend/skills/insider-threat-assessment/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
name: "Insider threat assessment"
description: "Assess possible insider risk from unusual access, downloads, privilege use, and policy violations. Maintain neutral language, minimize personal data, and recommend auditable investigative steps."
---
# Insider threat assessment

Assess possible insider risk from unusual access, downloads, privilege use, and policy violations. Maintain neutral language, minimize personal data, and recommend auditable investigative steps.

Output requirements:
- Cite observed evidence from the ticket.
- Mark unknown values as unavailable.
- Return structured JSON fields when requested.
- Do not change ticket status or execute commands.
13 changes: 13 additions & 0 deletions backend/skills/malware-execution-analysis/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
name: "Malware execution analysis"
description: "Analyze process execution, downloaded files, hashes, parent-child chains, persistence, and host impact. Separate observed facts from hypotheses and propose safe collection and containment steps."
---
# Malware execution analysis

Analyze process execution, downloaded files, hashes, parent-child chains, persistence, and host impact. Separate observed facts from hypotheses and propose safe collection and containment steps.

Output requirements:
- Cite observed evidence from the ticket.
- Mark unknown values as unavailable.
- Return structured JSON fields when requested.
- Do not change ticket status or execute commands.
13 changes: 13 additions & 0 deletions backend/skills/phishing-triage/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
name: "Phishing email triage"
description: "Analyze suspicious email indicators, sender authentication, URLs, attachments, user impact, and recommended containment. Never claim a link or attachment is malicious without evidence. Return concise findings and follow-up tasks."
---
# Phishing email triage

Analyze suspicious email indicators, sender authentication, URLs, attachments, user impact, and recommended containment. Never claim a link or attachment is malicious without evidence. Return concise findings and follow-up tasks.

Output requirements:
- Cite observed evidence from the ticket.
- Mark unknown values as unavailable.
- Return structured JSON fields when requested.
- Do not change ticket status or execute commands.
13 changes: 13 additions & 0 deletions backend/skills/ransomware-response/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
name: "Ransomware response"
description: "Triage ransomware indicators such as mass file changes, encryption processes, ransom notes, and lateral movement. Prioritize isolation, evidence preservation, recovery coordination, and safe next tasks."
---
# Ransomware response

Triage ransomware indicators such as mass file changes, encryption processes, ransom notes, and lateral movement. Prioritize isolation, evidence preservation, recovery coordination, and safe next tasks.

Output requirements:
- Cite observed evidence from the ticket.
- Mark unknown values as unavailable.
- Return structured JSON fields when requested.
- Do not change ticket status or execute commands.
13 changes: 13 additions & 0 deletions backend/skills/suspicious-login-investigation/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
name: "Suspicious login investigation"
description: "Investigate unusual authentication activity using source IP, account, time, geolocation, MFA, and privilege context. Recommend validation steps and avoid changing ticket status."
---
# Suspicious login investigation

Investigate unusual authentication activity using source IP, account, time, geolocation, MFA, and privilege context. Recommend validation steps and avoid changing ticket status.

Output requirements:
- Cite observed evidence from the ticket.
- Mark unknown values as unavailable.
- Return structured JSON fields when requested.
- Do not change ticket status or execute commands.
13 changes: 13 additions & 0 deletions backend/skills/vulnerability-prioritization/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
name: "Vulnerability prioritization"
description: "Prioritize vulnerabilities using asset criticality, exploitability, exposure, known exploitation, compensating controls, and business impact. Do not invent CVEs or affected versions."
---
# Vulnerability prioritization

Prioritize vulnerabilities using asset criticality, exploitability, exposure, known exploitation, compensating controls, and business impact. Do not invent CVEs or affected versions.

Output requirements:
- Cite observed evidence from the ticket.
- Mark unknown values as unavailable.
- Return structured JSON fields when requested.
- Do not change ticket status or execute commands.
7 changes: 7 additions & 0 deletions backend/tickets/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -668,6 +668,7 @@ def ai_assistant(self, request, ticket_number=None):
)
try:
assistant = result.get("assistant") if isinstance(result, dict) else None
assistant_raw = result.get("assistant_raw") if isinstance(result, dict) else None
if assistant:
completed = assistant.get("completed_tasks") if isinstance(assistant, dict) else None
next_tasks = assistant.get("next_tasks") if isinstance(assistant, dict) else None
Expand Down Expand Up @@ -715,6 +716,12 @@ def ai_assistant(self, request, ticket_number=None):
log_entry="\n".join([l for l in log_lines if l]),
created_by=request.user,
)
elif isinstance(assistant_raw, str) and assistant_raw.strip():
TicketWorkLog.objects.create(
ticket=ticket,
log_entry=f"AI Raw Response:\n{assistant_raw.strip()}",
created_by=request.user,
)
except Exception:
# Do not block API response on logging failure
pass
Expand Down
6 changes: 0 additions & 6 deletions frontend/next-env.d.ts

This file was deleted.

1 change: 1 addition & 0 deletions frontend/next.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ try {
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
allowedDevOrigins: ['192.168.31.66'],
skipTrailingSlashRedirect: true,
// Keep `next start` compatible in local/dev environments.
// Set NEXT_OUTPUT_MODE=standalone only when you explicitly run standalone server.
Expand Down
14 changes: 0 additions & 14 deletions frontend/src/modules/aiAssistant/AiAssistantSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -959,20 +959,6 @@ export default function AiAssistantSettings() {
return <Tag>Not Added</Tag>;
},
},
{
title: 'Action',
key: 'action',
width: 140,
render: (_: any, row: any) => {
const cfg = skillConfigMap.get(row?.name);
const label = cfg ? (cfg.enabled ? 'Enabled' : 'Enable') : 'Add';
return (
<Button size="small" disabled={isReadonly || cfg?.enabled} onClick={() => onEnableCatalogSkill(row)}>
{label}
</Button>
);
},
},
]}
/>
</Card>
Expand Down
Loading
Loading