- FastAPI backend: auth, findings, dashboard, attack paths, footprint, AI translator, risk calculator, PDF report generator - Next.js frontend: Vault dashboard, login, findings table, finding detail with AI coach, digital footprint, reports - PostgreSQL data model: tenants, users, assets, findings, risk scores, audit reports, attack paths - Docker Compose + Dockerfiles for all services - Demo seed data: Acme Corp with 6 findings and 90-day risk score history - AI Risk Translator (OpenAI/Anthropic) with plain-English business impact - Role-based access: executive / it_admin / trustos_admin - Scope-lock engine: authorization required before any assessment Stage 1-8 complete: Phase 1 Vault Audit product ready
190 lines
7.2 KiB
Python
190 lines
7.2 KiB
Python
"""
|
|
AI Risk Translator — calls OpenAI/Anthropic to generate plain-English
|
|
business-impact explanations for security findings.
|
|
"""
|
|
from datetime import datetime
|
|
from typing import Optional
|
|
from app.db.session import AsyncSessionLocal
|
|
from app.models.models import Finding, AttackPath
|
|
from sqlalchemy import select
|
|
import json
|
|
import logging
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
TRANSLATION_SYSTEM_PROMPT = """You are TrustOS, an AI cyber resilience advisor.
|
|
Your role is to translate technical cybersecurity findings into clear, plain-English
|
|
business impact statements for executive and non-technical audiences.
|
|
|
|
Rules:
|
|
- Never use CVE IDs, CVSS scores, or technical jargon in the executive summary
|
|
- Always frame risk in terms of business impact: customers, revenue, operations, reputation
|
|
- Be direct and calm — not alarmist, not dismissive
|
|
- Always provide a clear recommended action
|
|
- Output must be valid JSON matching the schema provided
|
|
|
|
Output JSON schema:
|
|
{
|
|
"summary": "One sentence: what this is in plain English",
|
|
"business_impact": "1-2 sentences: what could happen to the business if exploited",
|
|
"impact_level": "Low|Medium|High|Critical",
|
|
"remediation_steps": "3-5 concrete steps to fix this, numbered",
|
|
"fix_priority": "urgent|soon|planned"
|
|
}"""
|
|
|
|
|
|
async def _call_llm(prompt: str) -> Optional[str]:
|
|
"""Call the configured LLM provider. Returns raw text response."""
|
|
from app.core.config import settings
|
|
try:
|
|
if settings.AI_PROVIDER == "openai" and settings.OPENAI_API_KEY:
|
|
from openai import AsyncOpenAI
|
|
client = AsyncOpenAI(api_key=settings.OPENAI_API_KEY)
|
|
resp = await client.chat.completions.create(
|
|
model="gpt-4o-mini",
|
|
messages=[
|
|
{"role": "system", "content": TRANSLATION_SYSTEM_PROMPT},
|
|
{"role": "user", "content": prompt}
|
|
],
|
|
temperature=0.3,
|
|
response_format={"type": "json_object"},
|
|
)
|
|
return resp.choices[0].message.content
|
|
elif settings.AI_PROVIDER == "anthropic" and settings.ANTHROPIC_API_KEY:
|
|
from anthropic import AsyncAnthropic
|
|
client = AsyncAnthropic(api_key=settings.ANTHROPIC_API_KEY)
|
|
resp = await client.messages.create(
|
|
model="claude-3-haiku-20240307",
|
|
max_tokens=1024,
|
|
system=TRANSLATION_SYSTEM_PROMPT,
|
|
messages=[{"role": "user", "content": prompt}],
|
|
)
|
|
return resp.content[0].text
|
|
else:
|
|
logger.warning("No AI provider configured — skipping translation")
|
|
return None
|
|
except Exception as e:
|
|
logger.error(f"LLM call failed: {e}")
|
|
return None
|
|
|
|
|
|
async def translate_finding_async(finding_id: str):
|
|
"""Background task: generate AI translation for a finding and persist it."""
|
|
async with AsyncSessionLocal() as db:
|
|
result = await db.execute(select(Finding).where(Finding.id == finding_id))
|
|
finding = result.scalar_one_or_none()
|
|
if not finding:
|
|
return
|
|
|
|
prompt = f"""Translate this cybersecurity finding:
|
|
|
|
Title: {finding.title}
|
|
Severity: {finding.severity.value}
|
|
Category: {finding.category.value}
|
|
CVE ID: {finding.cve_id or 'N/A'}
|
|
CVSS Score: {finding.cvss_score or 'N/A'}
|
|
Technical Description: {finding.technical_description or 'Not provided'}
|
|
Affected Component: {finding.affected_component or 'Unknown'}
|
|
|
|
Provide the JSON output as specified."""
|
|
|
|
raw = await _call_llm(prompt)
|
|
if not raw:
|
|
return
|
|
|
|
try:
|
|
data = json.loads(raw)
|
|
finding.ai_summary = data.get("summary")
|
|
finding.ai_business_impact = data.get("business_impact")
|
|
finding.ai_impact_level = data.get("impact_level")
|
|
finding.ai_remediation_steps = data.get("remediation_steps")
|
|
finding.ai_fix_priority = data.get("fix_priority")
|
|
finding.ai_generated_at = datetime.utcnow()
|
|
await db.commit()
|
|
logger.info(f"AI translation complete for finding {finding_id}")
|
|
except (json.JSONDecodeError, KeyError) as e:
|
|
logger.error(f"Failed to parse AI response for finding {finding_id}: {e}")
|
|
|
|
|
|
async def answer_finding_question(finding: Finding, question: str) -> str:
|
|
"""AI Security Coach: answer a specific question about a finding."""
|
|
prompt = f"""A security professional is asking about this finding:
|
|
|
|
Title: {finding.title}
|
|
Summary: {finding.ai_summary or finding.technical_description}
|
|
Business Impact: {finding.ai_business_impact or 'See technical description'}
|
|
Category: {finding.category.value}
|
|
|
|
Their question: {question}
|
|
|
|
Answer in 2-4 sentences. Be specific to this finding. Use plain English."""
|
|
|
|
system = "You are TrustOS AI Security Coach. Answer questions about specific security findings clearly and directly. Do not use CVE IDs or CVSS in your answers."
|
|
|
|
from app.core.config import settings
|
|
try:
|
|
if settings.AI_PROVIDER == "openai" and settings.OPENAI_API_KEY:
|
|
from openai import AsyncOpenAI
|
|
client = AsyncOpenAI(api_key=settings.OPENAI_API_KEY)
|
|
resp = await client.chat.completions.create(
|
|
model="gpt-4o-mini",
|
|
messages=[
|
|
{"role": "system", "content": system},
|
|
{"role": "user", "content": prompt}
|
|
],
|
|
temperature=0.5,
|
|
)
|
|
return resp.choices[0].message.content
|
|
except Exception as e:
|
|
logger.error(f"AI coach call failed: {e}")
|
|
|
|
return "AI explanation is not available. Please review the technical description and remediation steps."
|
|
|
|
|
|
async def generate_attack_path_narrative(finding_id: str):
|
|
"""Generate an AI-written attack path narrative for a finding."""
|
|
async with AsyncSessionLocal() as db:
|
|
result = await db.execute(select(Finding).where(Finding.id == finding_id))
|
|
finding = result.scalar_one_or_none()
|
|
if not finding:
|
|
return
|
|
|
|
prompt = f"""Create an attack path for this vulnerability:
|
|
|
|
Title: {finding.title}
|
|
Summary: {finding.ai_summary or finding.technical_description}
|
|
Category: {finding.category.value}
|
|
Severity: {finding.severity.value}
|
|
|
|
Provide:
|
|
1. A plain-English narrative (2-3 sentences): how an attacker could exploit this path from the internet to sensitive data
|
|
2. A JSON list of nodes: [{{"id": "1", "label": "Internet", "type": "attacker", "risk_level": "none"}}, ...]
|
|
- types: attacker, entry_point, pivot, target
|
|
- risk_level: none, low, medium, high, critical
|
|
3. A JSON list of edges: [{{"source": "1", "target": "2"}}, ...]
|
|
|
|
Output JSON:
|
|
{{
|
|
"narrative": "...",
|
|
"nodes": [...],
|
|
"edges": [...]
|
|
}}"""
|
|
|
|
raw = await _call_llm(prompt)
|
|
if not raw:
|
|
return
|
|
|
|
try:
|
|
data = json.loads(raw)
|
|
path = AttackPath(
|
|
finding_id=finding_id,
|
|
title=f"Attack path: {finding.title}",
|
|
ai_narrative=data.get("narrative"),
|
|
nodes_json=json.dumps(data.get("nodes", [])),
|
|
edges_json=json.dumps(data.get("edges", [])),
|
|
)
|
|
db.add(path)
|
|
await db.commit()
|
|
except Exception as e:
|
|
logger.error(f"Attack path generation failed for {finding_id}: {e}")
|