diff --git a/README.md b/README.md
index 5c46bab..8ef903a 100644
--- a/README.md
+++ b/README.md
@@ -37,6 +37,7 @@ If you find STRIDE GPT useful, please consider supporting the project:
- **MITRE ATT&CK & ATLAS mapping**: Threats are annotated with standardized adversary technique IDs (MITRE ATT&CK Enterprise for traditional infrastructure attacks, ATLAS for ML/LLM-specific attacks) — surfaced as columns in markdown, linked pills in HTML, and `mitre_attack` properties in SARIF
- **Architectural pattern detection**: Automatically detects RAG pipelines, multi-agent systems, code execution environments, tool ecosystems, and more from application descriptions (inspired by [CSA MAESTRO](https://cloudsecurityalliance.org/blog/2025/02/06/agentic-ai-threat-modeling-framework-maestro))
- Multi-modal: Use architecture diagrams, flowcharts, etc. as inputs for threat modelling across all supported vision-capable models
+- **Data Flow Diagrams**: Generate DFDs from your application description (or parse an uploaded DFD image), edit the Mermaid source live, and feed the confirmed diagram back into the Threat Model and Attack Tree prompts as the authoritative system model. CLI `/analyze` also emits a system-level DFD alongside its findings
- Generates attack trees to enumerate possible attack paths
- Suggests possible mitigations for identified threats
- Supports DREAD risk scoring for identified threats
@@ -70,6 +71,10 @@ This video is an excellent resource for anyone interested in understanding how S
## Changelog
+### Unreleased
+
+- **Data Flow Diagram support** (closes #56): New "Data Flow Diagram" tab in the Streamlit UI generates DFDs from the application description, parses uploaded DFD images via vision-capable models, and renders an editable Mermaid source pane with a live preview. Ticking *"Use this DFD for the threat model"* stores the confirmed diagram in session state and splices it into subsequent Threat Model and Attack Tree prompts as the authoritative system model — closing the description → DFD → review → refined threat model loop the issue asks for. The CLI's `/analyze` agent also produces a system-level DFD after synthesis, rendered as a Mermaid block in the markdown report, carried in the JSON `data_flow_diagram` field, and shown in the HTML view via a CDN-loaded Mermaid runtime. DFD generation is wrapped so a bad diagram never fails a good report.
+
### Version 0.17 (latest)
- **MITRE ATT&CK and ATLAS integration** (closes #36): Threats emitted by `/analyze` and `/quick` can now carry standardized adversary technique IDs from MITRE ATT&CK Enterprise (v17.1) and MITRE ATLAS (2026.05). Two new progressive-disclosure reference cards (`mitre_enterprise`, `mitre_atlas`) instruct the agent to use only the catalogued IDs and names, eliminating ID hallucination. Cards are regenerated from upstream STIX/YAML via `scripts/refresh_mitre_cards.py` rather than written from model recall. Techniques render as a `MITRE ATT&CK` column in markdown tables, clickable sky-tinted pills in HTML, and `properties.mitre_attack` arrays in SARIF.
diff --git a/apps/web/attack_tree.py b/apps/web/attack_tree.py
index 53604d8..37ec1ff 100644
--- a/apps/web/attack_tree.py
+++ b/apps/web/attack_tree.py
@@ -8,7 +8,12 @@
# Function to create a prompt to generate an attack tree
def create_attack_tree_prompt(
- app_type, authentication, internet_facing, sensitive_data, app_input
+ app_type,
+ authentication,
+ internet_facing,
+ sensitive_data,
+ app_input,
+ confirmed_dfd: str | None = None,
):
prompt = f"""APPLICATION TYPE: {app_type}
AUTHENTICATION METHODS: {authentication}
@@ -17,6 +22,10 @@ def create_attack_tree_prompt(
APPLICATION DESCRIPTION: {app_input}
"""
+ if confirmed_dfd:
+ from stride_gpt.core.prompts import dfd_to_prompt_section
+ prompt += dfd_to_prompt_section(confirmed_dfd)
+
# Add GenAI attack vectors for both Generative AI and Agentic AI applications
if app_type in ["Generative AI application", "Agentic AI application"]:
prompt += """
diff --git a/apps/web/dfd.py b/apps/web/dfd.py
new file mode 100644
index 0000000..a07239c
--- /dev/null
+++ b/apps/web/dfd.py
@@ -0,0 +1,111 @@
+"""Streamlit-facing provider wrappers for Data Flow Diagram generation.
+
+Mirrors `apps/web/attack_tree.py`: thin per-provider wrappers around the
+core `generate_dfd` / `parse_dfd_from_image` functions, with session-state
+plumbing for thinking/reasoning displays.
+"""
+
+from __future__ import annotations
+
+import streamlit as st
+
+from stride_gpt.core.dfd import generate_dfd, parse_dfd_from_image
+from stride_gpt.core.schemas import LLMConfig
+
+
+__all__ = [
+ "get_dfd_anthropic",
+ "get_dfd_from_image_anthropic",
+ "get_dfd_from_image_google",
+ "get_dfd_from_image_openai",
+ "get_dfd_google",
+ "get_dfd_groq",
+ "get_dfd_lm_studio",
+ "get_dfd_mistral",
+ "get_dfd_openai",
+]
+
+
+# ---------------------------------------------------------------------------
+# Generation from a textual description
+# ---------------------------------------------------------------------------
+
+
+def get_dfd_openai(api_key: str, model_name: str, prompt: str) -> str:
+ config = LLMConfig(provider="OpenAI API", model_name=model_name, api_key=api_key)
+ mermaid, _ = generate_dfd(config, prompt)
+ return mermaid
+
+
+def get_dfd_anthropic(api_key: str, model_name: str, prompt: str) -> str:
+ config = LLMConfig(
+ provider="Anthropic API",
+ model_name=model_name,
+ api_key=api_key,
+ use_thinking=st.session_state.get("use_thinking", False),
+ )
+ mermaid, response = generate_dfd(config, prompt)
+ if response.thinking:
+ st.session_state["last_thinking_content"] = response.thinking
+ return mermaid
+
+
+def get_dfd_google(api_key: str, model_name: str, prompt: str) -> str:
+ config = LLMConfig(provider="Google AI API", model_name=model_name, api_key=api_key)
+ mermaid, response = generate_dfd(config, prompt)
+ if response.thinking:
+ st.session_state["last_thinking_content"] = response.thinking
+ return mermaid
+
+
+def get_dfd_groq(api_key: str, model_name: str, prompt: str) -> str:
+ config = LLMConfig(provider="Groq API", model_name=model_name, api_key=api_key)
+ mermaid, response = generate_dfd(config, prompt)
+ if response.reasoning:
+ with st.expander("View model's reasoning process", expanded=False):
+ st.write(response.reasoning)
+ return mermaid
+
+
+def get_dfd_lm_studio(
+ lm_studio_endpoint: str, model_name: str, prompt: str, api_key: str = "not-needed"
+) -> str:
+ config = LLMConfig(
+ provider="LM Studio Server",
+ model_name=model_name,
+ api_key=api_key,
+ api_base=lm_studio_endpoint,
+ )
+ mermaid, _ = generate_dfd(config, prompt)
+ return mermaid
+
+
+def get_dfd_mistral(api_key: str, model_name: str, prompt: str) -> str:
+ config = LLMConfig(provider="Mistral API", model_name=model_name, api_key=api_key)
+ mermaid, _ = generate_dfd(config, prompt)
+ return mermaid
+
+
+# ---------------------------------------------------------------------------
+# Parsing a user-uploaded DFD image
+# ---------------------------------------------------------------------------
+
+
+def get_dfd_from_image_openai(api_key: str, model_name: str, base64_image: str) -> str:
+ config = LLMConfig(provider="OpenAI API", model_name=model_name, api_key=api_key)
+ mermaid, _ = parse_dfd_from_image(config, base64_image)
+ return mermaid
+
+
+def get_dfd_from_image_anthropic(
+ api_key: str, model_name: str, base64_image: str, media_type: str = "image/png"
+) -> str:
+ config = LLMConfig(provider="Anthropic API", model_name=model_name, api_key=api_key)
+ mermaid, _ = parse_dfd_from_image(config, base64_image, media_type=media_type)
+ return mermaid
+
+
+def get_dfd_from_image_google(api_key: str, model_name: str, base64_image: str) -> str:
+ config = LLMConfig(provider="Google AI API", model_name=model_name, api_key=api_key)
+ mermaid, _ = parse_dfd_from_image(config, base64_image)
+ return mermaid
diff --git a/apps/web/main.py b/apps/web/main.py
index 6a700d0..fbd5f49 100644
--- a/apps/web/main.py
+++ b/apps/web/main.py
@@ -33,6 +33,18 @@
get_attack_tree_lm_studio,
get_attack_tree_mistral,
)
+from dfd import (
+ get_dfd_anthropic,
+ get_dfd_from_image_anthropic,
+ get_dfd_from_image_google,
+ get_dfd_from_image_openai,
+ get_dfd_google,
+ get_dfd_groq,
+ get_dfd_lm_studio,
+ get_dfd_mistral,
+ get_dfd_openai,
+)
+from stride_gpt.core.prompts import create_dfd_prompt
from dread import (
create_dread_assessment_prompt,
dread_json_to_markdown,
@@ -1003,10 +1015,26 @@ def on_model_selection_change():
)
+# ------------------ Provider API keys (module scope) ------------------ #
+#
+# `load_env_variables()` reads .env into FUNCTION-LOCAL variables, and the
+# sidebar's password fields only write to st.session_state — neither path
+# defines `openai_api_key` / `anthropic_api_key` / etc. at module scope.
+# Hoist them here, after the sidebar has run, so the bare names referenced
+# throughout the tab bodies (image analysis, threat model, attack tree,
+# mitigations, DREAD, test cases) resolve to the live values. Streamlit's
+# hot-reload was masking this by leaking stale locals across reruns.
+openai_api_key = st.session_state.get("openai_api_key", "")
+anthropic_api_key = st.session_state.get("anthropic_api_key", "")
+google_api_key = st.session_state.get("google_api_key", "")
+mistral_api_key = st.session_state.get("mistral_api_key", "")
+groq_api_key = st.session_state.get("groq_api_key", "")
+
+
# ------------------ Main App UI ------------------ #
-tab1, tab2, tab3, tab4, tab5 = st.tabs(
- ["Threat Model", "Attack Tree", "Mitigations", "DREAD", "Test Cases"]
+tab1, tab2, dfd_tab, tab3, tab4, tab5 = st.tabs(
+ ["Threat Model", "Attack Tree", "Data Flow Diagram", "Mitigations", "DREAD", "Test Cases"]
)
with tab1:
@@ -1196,13 +1224,17 @@ def encode_image(uploaded_file):
if threat_model_submit_button and st.session_state.get("app_input"):
app_input = st.session_state["app_input"] # Retrieve from session state
- # Generate the prompt using the create_prompt function
+ # Generate the prompt using the create_prompt function. If the user
+ # has confirmed a DFD on the Data Flow Diagram tab, splice it in as
+ # an authoritative system model so the threats align with the
+ # diagram's components and trust boundaries.
threat_model_prompt = create_threat_model_prompt(
app_type,
authentication,
internet_facing,
sensitive_data,
app_input,
+ confirmed_dfd=st.session_state.get("confirmed_dfd"),
)
# Clear thinking content when switching models or starting a new operation
@@ -1341,13 +1373,16 @@ def encode_image(uploaded_file):
if attack_tree_submit_button and st.session_state.get("app_input"):
app_input = st.session_state.get("app_input")
- # Generate the prompt using the create_attack_tree_prompt function
+ # Generate the prompt using the create_attack_tree_prompt function.
+ # When a DFD has been confirmed on the Data Flow Diagram tab it
+ # is appended so the attack paths reference the same components.
attack_tree_prompt = create_attack_tree_prompt(
app_type,
authentication,
internet_facing,
sensitive_data,
app_input,
+ confirmed_dfd=st.session_state.get("confirmed_dfd"),
)
# Clear thinking content when switching models or starting a new operation
@@ -1447,6 +1482,222 @@ def encode_image(uploaded_file):
st.error(f"Error generating attack tree: {e}")
+# ------------------ Data Flow Diagram Generation ------------------ #
+
+with dfd_tab:
+ st.markdown(
+ """
+A Data Flow Diagram (DFD) makes the system under analysis legible: it names every external entity,
+process, and data store, and shows the data flows between them. Crucially, it lets you mark
+**trust boundaries** — the lines a threat must cross to attack the system. DFDs are foundational to
+threat modelling; reviewing one is the cleanest way to confirm STRIDE-GPT has understood your system
+the same way you have.
+
+Three ways to use this tab:
+
+1. **Generate a DFD** from your application description, then edit it to match your understanding.
+2. **Upload an existing DFD image** to convert it into editable Mermaid form.
+3. Once the diagram looks right, tick **Use this DFD for the threat model** to feed it into the next
+ Threat Model and Attack Tree runs.
+"""
+ )
+ st.markdown("""---""")
+
+ # --- Section 1: Inputs (generate from description OR parse an image) --- #
+
+ dfd_input_col1, dfd_input_col2 = st.columns([1, 1])
+
+ with dfd_input_col1:
+ st.markdown("**Generate from description**")
+ dfd_generate_button = st.button(
+ label="Generate DFD",
+ help="Uses your application description and sidebar settings to draft a DFD.",
+ )
+
+ with dfd_input_col2:
+ st.markdown("**Or parse an existing DFD image**")
+ # Vision support set is the same as for architecture diagrams.
+ dfd_vision_supported = (
+ (model_provider == "OpenAI API" and (
+ selected_model.startswith("gpt-4") or selected_model.startswith("gpt-5")
+ ))
+ or model_provider == "Google AI API"
+ or (model_provider == "Anthropic API" and selected_model.startswith("claude"))
+ )
+ if dfd_vision_supported:
+ uploaded_dfd_image = st.file_uploader(
+ "Upload DFD image",
+ type=["jpg", "jpeg", "png"],
+ key="dfd_image_uploader",
+ help="PNG or JPEG. The diagram is parsed into editable Mermaid below.",
+ )
+ else:
+ uploaded_dfd_image = None
+ st.caption(
+ "Image parsing requires a vision-capable model "
+ "(OpenAI GPT-4/5, Anthropic Claude, or Google Gemini)."
+ )
+
+ # --- Section 2: Handle Generation --- #
+
+ # API keys live in st.session_state once the sidebar has run (loaded from
+ # .env in load_env_variables and/or typed into the password field). The
+ # bare names openai_api_key / anthropic_api_key etc. are only ever local
+ # to load_env_variables, so always go through session_state here.
+ _dfd_api_key = st.session_state.get(
+ {
+ "OpenAI API": "openai_api_key",
+ "Anthropic API": "anthropic_api_key",
+ "Google AI API": "google_api_key",
+ "Mistral API": "mistral_api_key",
+ "Groq API": "groq_api_key",
+ }.get(model_provider, ""),
+ "",
+ )
+
+ if dfd_generate_button and st.session_state.get("app_input"):
+ dfd_prompt = create_dfd_prompt(
+ app_type,
+ authentication,
+ internet_facing,
+ sensitive_data,
+ st.session_state["app_input"],
+ )
+ if model_provider != "LM Studio Server" and not _dfd_api_key:
+ st.error(
+ f"Please enter your {model_provider} key in the sidebar (or set it in "
+ "your .env file) before generating a DFD."
+ )
+ else:
+ with st.spinner("Generating Data Flow Diagram..."):
+ try:
+ if model_provider == "OpenAI API":
+ dfd_mermaid = get_dfd_openai(_dfd_api_key, selected_model, dfd_prompt)
+ elif model_provider == "Anthropic API":
+ dfd_mermaid = get_dfd_anthropic(_dfd_api_key, selected_model, dfd_prompt)
+ elif model_provider == "Google AI API":
+ dfd_mermaid = get_dfd_google(_dfd_api_key, selected_model, dfd_prompt)
+ elif model_provider == "Mistral API":
+ dfd_mermaid = get_dfd_mistral(_dfd_api_key, selected_model, dfd_prompt)
+ elif model_provider == "Groq API":
+ dfd_mermaid = get_dfd_groq(_dfd_api_key, selected_model, dfd_prompt)
+ elif model_provider == "LM Studio Server":
+ dfd_mermaid = get_dfd_lm_studio(
+ st.session_state["lm_studio_endpoint"],
+ selected_model,
+ dfd_prompt,
+ st.session_state.get("lm_studio_api_key", ""),
+ )
+ else:
+ dfd_mermaid = ""
+ # Generation invalidates any previous confirmation — the user
+ # needs to re-tick "Use this DFD" to opt back in.
+ st.session_state["dfd_mermaid"] = dfd_mermaid
+ st.session_state.pop("confirmed_dfd", None)
+ except Exception as e:
+ st.error(f"Error generating DFD: {e}")
+ elif dfd_generate_button and not st.session_state.get("app_input"):
+ st.error("Please enter your application details on the Threat Model tab first.")
+
+ # --- Section 3: Handle Image Parsing --- #
+
+ if uploaded_dfd_image is not None:
+ # Re-parse on every change of the upload — the file_uploader returns
+ # the same object across reruns, so we key on the file name + size.
+ upload_key = f"{uploaded_dfd_image.name}:{uploaded_dfd_image.size}"
+ if st.session_state.get("dfd_last_upload_key") != upload_key:
+ if not _dfd_api_key:
+ st.error(
+ f"Please enter your {model_provider} key in the sidebar before "
+ "uploading a DFD image."
+ )
+ else:
+ with st.spinner("Parsing DFD image..."):
+ try:
+ image_bytes = uploaded_dfd_image.read()
+ base64_image = base64.b64encode(image_bytes).decode("utf-8")
+ media_type = (
+ "image/png" if uploaded_dfd_image.type == "image/png" else "image/jpeg"
+ )
+ if model_provider == "OpenAI API":
+ dfd_mermaid = get_dfd_from_image_openai(
+ _dfd_api_key, selected_model, base64_image
+ )
+ elif model_provider == "Anthropic API":
+ dfd_mermaid = get_dfd_from_image_anthropic(
+ _dfd_api_key, selected_model, base64_image, media_type
+ )
+ elif model_provider == "Google AI API":
+ dfd_mermaid = get_dfd_from_image_google(
+ _dfd_api_key, selected_model, base64_image
+ )
+ else:
+ dfd_mermaid = ""
+ st.session_state["dfd_mermaid"] = dfd_mermaid
+ st.session_state["dfd_last_upload_key"] = upload_key
+ st.session_state.pop("confirmed_dfd", None)
+ except Exception as e:
+ st.error(f"Error parsing DFD image: {e}")
+
+ # --- Section 4: Editor + Preview --- #
+
+ st.markdown("---")
+ st.markdown("**Mermaid source** (edit freely — the preview updates on rerun):")
+ dfd_editor_value = st.text_area(
+ "DFD Mermaid code",
+ value=st.session_state.get("dfd_mermaid", ""),
+ height=260,
+ key="dfd_editor",
+ label_visibility="collapsed",
+ placeholder="No DFD yet. Use 'Generate DFD' or upload an image to populate this editor.",
+ )
+ # Keep dfd_mermaid in sync with the editor so it survives reruns.
+ st.session_state["dfd_mermaid"] = dfd_editor_value
+
+ if dfd_editor_value.strip():
+ st.markdown("**Diagram preview:**")
+ try:
+ mermaid(dfd_editor_value)
+ except Exception as e:
+ st.warning(f"Could not render preview: {e}")
+
+ # --- Section 5: Confirm + Download --- #
+
+ confirmed = st.checkbox(
+ "Use this DFD for the threat model",
+ value=bool(st.session_state.get("confirmed_dfd")),
+ help=(
+ "When ticked, this DFD is included in the Threat Model and Attack Tree "
+ "prompts so the analysis aligns with the diagram."
+ ),
+ key="dfd_confirm_checkbox",
+ )
+ # Keep confirmed_dfd in lockstep with the checkbox AND the editor.
+ if confirmed:
+ st.session_state["confirmed_dfd"] = dfd_editor_value
+ st.success(
+ "DFD confirmed — it will be used as the system model in the next "
+ "Threat Model and Attack Tree runs."
+ )
+ else:
+ st.session_state.pop("confirmed_dfd", None)
+ st.caption(
+ "Not in use. Tick the checkbox above to feed this DFD into the next analysis."
+ )
+
+ dfd_dl_col1, dfd_dl_col2, _ = st.columns([1, 1, 3])
+ with dfd_dl_col1:
+ st.download_button(
+ label="Download Diagram Code",
+ data=dfd_editor_value,
+ file_name="data_flow_diagram.md",
+ mime="text/plain",
+ help="Download the Mermaid source for this DFD.",
+ )
+ with dfd_dl_col2:
+ st.link_button("Open Mermaid Live", "https://mermaid.live")
+
+
# ------------------ Mitigations Generation ------------------ #
with tab3:
diff --git a/stride_gpt/agent/html_report.py b/stride_gpt/agent/html_report.py
index 3657c21..ca980b6 100644
--- a/stride_gpt/agent/html_report.py
+++ b/stride_gpt/agent/html_report.py
@@ -61,6 +61,7 @@ def render_html(report: AnalysisReport) -> str:
"generated_at": datetime.now(timezone.utc).isoformat(),
"target": report.plan.target_path,
"overview": report.plan.overall_description,
+ "data_flow_diagram": report.data_flow_diagram,
"subsystems": [
{
"name": f.subsystem,
@@ -86,6 +87,7 @@ def render_html_from_json(data: dict[str, Any]) -> str:
target_name = Path(target).name or target or "(unknown target)"
generated_at = _format_generated_at(data.get("generated_at", ""))
overview = (data.get("overview") or "").strip()
+ dfd_mermaid = (data.get("data_flow_diagram") or "").strip()
subsystems = data.get("subsystems") or []
cross_cutting = data.get("cross_cutting_threats") or []
metadata = data.get("metadata") or {}
@@ -106,6 +108,9 @@ def render_html_from_json(data: dict[str, Any]) -> str:
if overview:
parts.append(_render_overview(overview))
+ if dfd_mermaid:
+ parts.append(_render_dfd(dfd_mermaid))
+
for sub in subsystems:
parts.append(_render_subsystem(sub))
@@ -137,6 +142,10 @@ def _scaffold(*, target_name: str, body: str) -> str:
{body}
+