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: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ AI-powered threat modeling that turns architecture diagrams and business context
> This is a public demo environment. Please do not upload sensitive or confidential architecture diagrams.
> For sensitive use cases, use the local CLI or Web UI.

> [!IMPORTANT]
> AI can make mistakes. Do not trust Threat Thinker's output as-is; review the results and judge their correctness before using them.

<img width="360" alt="threat-thinker-logo" src="./docs/images/threat-thinker-logo.png" />


Expand Down
12 changes: 12 additions & 0 deletions src/threat_thinker/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,18 @@
Constants and prompts for Threat Thinker
"""

AI_OUTPUT_DISCLAIMER_EN = (
"AI can make mistakes. Do not trust Threat Thinker's output as-is; "
"review the results and judge their correctness before using them."
)
AI_OUTPUT_DISCLAIMER_JA = (
"AIは間違うことがあります。Threat Thinkerによる出力をそのまま信頼せず、"
"結果の正しさを確認してから利用してください。"
)
AI_OUTPUT_DISCLAIMER_MD = (
f"> [!IMPORTANT]\n> {AI_OUTPUT_DISCLAIMER_EN}\n> {AI_OUTPUT_DISCLAIMER_JA}"
)

# LLM-based attribute inference prompts
HINT_SYSTEM = (
"You are Threat Thinker. Infer practical attributes for threat modeling from a graph skeleton. "
Expand Down
31 changes: 27 additions & 4 deletions src/threat_thinker/exporters.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@
from html import escape
from typing import Any, Dict, List, Optional, Tuple

from threat_thinker.constants import (
AI_OUTPUT_DISCLAIMER_EN,
AI_OUTPUT_DISCLAIMER_JA,
AI_OUTPUT_DISCLAIMER_MD,
)
from threat_thinker.models import Edge, Graph, ImportMetrics, Node, Threat
from threat_thinker.zone_utils import zone_path_names

Expand Down Expand Up @@ -110,7 +115,7 @@ def export_md(threats: List[Threat], output_file: str = None) -> str:
"""
Export threats to Markdown format
"""
md_content = "# Threat Analysis Report\n\n"
md_content = f"# Threat Analysis Report\n\n{AI_OUTPUT_DISCLAIMER_MD}\n\n"

if not threats:
md_content += "No threats identified.\n"
Expand Down Expand Up @@ -202,19 +207,27 @@ def export_html(
"""

has_graph = bool(graph and (graph.nodes or graph.edges))
disclaimer_en = _safe(AI_OUTPUT_DISCLAIMER_EN)
disclaimer_ja = _safe(AI_OUTPUT_DISCLAIMER_JA)
if not threats and not has_graph:
content = """<!DOCTYPE html>
content = f"""<!DOCTYPE html>
<html lang=\"en\">
<head>
<meta charset=\"UTF-8\" />
<title>Threat Analysis Report</title>
<style>
body { font-family: Arial, sans-serif; margin: 32px; color: #0f172a; }
.empty { font-style: italic; color: #475569; }
body {{ font-family: Arial, sans-serif; margin: 32px; color: #0f172a; }}
.empty {{ font-style: italic; color: #475569; }}
.disclaimer {{ border-left: 4px solid #f59e0b; background: #fffbeb; padding: 12px 16px; margin: 16px 0 24px; color: #78350f; }}
.disclaimer p {{ margin: 4px 0; }}
</style>
</head>
<body>
<h1>Threat Analysis Report</h1>
<div class=\"disclaimer\">
<p>{disclaimer_en}</p>
<p>{disclaimer_ja}</p>
</div>
<p class=\"empty\">No threats identified.</p>
</body>
</html>"""
Expand Down Expand Up @@ -337,6 +350,10 @@ def resolve_edge(edge_id: str) -> str:
html_parts.append(" .sev-Medium { background: #fef9c3; color: #b45309; }")
html_parts.append(" .sev-Low { background: #dcfce7; color: #166534; }")
html_parts.append(" .meta { color: #475569; font-size: 14px; }")
html_parts.append(
" .disclaimer { border-left: 4px solid #f59e0b; background: #fffbeb; padding: 12px 16px; margin: 16px 0 24px; color: #78350f; }"
)
html_parts.append(" .disclaimer p { margin: 4px 0; }")
html_parts.append(" .section { margin-top: 24px; }")
html_parts.append(" .mapping-list { list-style: disc; margin-left: 20px; }")
html_parts.append(
Expand All @@ -356,6 +373,10 @@ def resolve_edge(edge_id: str) -> str:
html_parts.append("</head>")
html_parts.append("<body>")
html_parts.append(" <h1>Threat Analysis Report</h1>")
html_parts.append(' <div class="disclaimer">')
html_parts.append(f" <p>{disclaimer_en}</p>")
html_parts.append(f" <p>{disclaimer_ja}</p>")
html_parts.append(" </div>")
if not threats:
html_parts.append(' <p class="meta">No threats identified.</p>')

Expand Down Expand Up @@ -1208,6 +1229,8 @@ def export_diff_md(diff_data: Dict, out_path: Optional[str] = None) -> str:
lines = []
lines.append("# System Architecture and Threat Model Diff Report")
lines.append("")
lines.append(AI_OUTPUT_DISCLAIMER_MD)
lines.append("")
lines.append(f"**Generated:** {diff_data.get('generated_at', '')}")
lines.append("")
lines.append(f"**Before:** {diff_data.get('before_file', '')}")
Expand Down
5 changes: 4 additions & 1 deletion src/threat_thinker/webui.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import gradio as gr

import threat_thinker.main as cli
from threat_thinker.constants import AI_OUTPUT_DISCLAIMER_MD
from threat_thinker.input_loader import (
INPUT_FORMAT_DRAWIO,
INPUT_FORMAT_IR,
Expand Down Expand Up @@ -710,7 +711,9 @@ def _build_webui() -> gr.Blocks:
"""Construct the Gradio Web UI without launching a server."""
with gr.Blocks(title="Threat Thinker WebUI") as demo:
gr.Markdown(
"## Threat Thinker WebUI\nAnalyze system diagrams for security threats or compare threat reports."
"## Threat Thinker WebUI\n"
"Analyze system diagrams for security threats or compare threat reports.\n\n"
f"{AI_OUTPUT_DISCLAIMER_MD}"
)

with gr.Tabs():
Expand Down
36 changes: 35 additions & 1 deletion tests/test_exporters.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,18 @@
# Add src to Python path
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))

from threat_thinker.exporters import export_json, export_md, diff_reports, export_html
from threat_thinker.constants import (
AI_OUTPUT_DISCLAIMER_EN,
AI_OUTPUT_DISCLAIMER_JA,
AI_OUTPUT_DISCLAIMER_MD,
)
from threat_thinker.exporters import (
diff_reports,
export_diff_md,
export_html,
export_json,
export_md,
)
from threat_thinker.models import Threat, ImportMetrics, Graph, Node, Edge


Expand Down Expand Up @@ -123,6 +134,8 @@ def test_export_empty_threats_markdown(self):
result = export_md(threats, None)

assert "# Threat Analysis Report" in result
assert AI_OUTPUT_DISCLAIMER_EN in result
assert AI_OUTPUT_DISCLAIMER_JA in result
assert "No threats identified" in result

def test_export_single_threat_markdown(self):
Expand Down Expand Up @@ -155,6 +168,8 @@ def test_export_single_threat_markdown(self):
result = export_md(threats, None)

assert "XSS Attack" in result
assert AI_OUTPUT_DISCLAIMER_EN in result
assert AI_OUTPUT_DISCLAIMER_JA in result
assert "Medium" in result
assert "No input sanitization" in result
assert "Frontend" in result
Expand Down Expand Up @@ -206,6 +221,8 @@ def test_export_empty_threats_html(self):
result = export_html(threats, None, None)

assert "Threat Analysis Report" in result
assert "AI can make mistakes" in result
assert AI_OUTPUT_DISCLAIMER_JA in result
assert "No threats identified" in result

def test_export_empty_threats_keeps_graph_when_available(self):
Expand Down Expand Up @@ -262,6 +279,8 @@ def test_export_single_threat_with_graph_mapping(self):
result = export_html([threat], None, graph)

assert "SQL Injection" in result
assert "AI can make mistakes" in result
assert AI_OUTPUT_DISCLAIMER_JA in result
assert "API Service" in result # node mapping
assert "Database" in result
assert "queries" in result # edge label mapping
Expand Down Expand Up @@ -304,6 +323,21 @@ def test_export_html_escapes_content(self):
class TestDiffReports:
"""Test cases for diff_reports function"""

def test_export_diff_markdown_includes_disclaimer(self):
diff_data = {
"generated_at": "2026-04-15T00:00:00Z",
"before_file": "before.json",
"after_file": "after.json",
"graph_changes": {},
"threat_changes": {},
}

result = export_diff_md(diff_data)

assert AI_OUTPUT_DISCLAIMER_MD in result
assert AI_OUTPUT_DISCLAIMER_EN in result
assert AI_OUTPUT_DISCLAIMER_JA in result

def test_diff_identical_reports(self):
"""Test diffing identical reports"""
threat_data = {
Expand Down
Loading