Security Planning
MicrosoftOfficialPublished by Microsoft
- Skill
- Tool integration
- Security
- MIT
- Medium risk
- Has scripts
AutoGineer summary
Written by AutoGineer from the skill’s own files, not by the author
Security Planning provides a structured framework for managing application security throughout the development lifecycle. It includes methodologies for STRIDE threat analysis, NIST control mapping, and data classification. The skill supports plan-drift analysis to correlate security baselines with current findings, and provides robust workflows for generating and validating Microsoft Threat Modeling Tool (TM7) files, including an optional Windows-native feedback loop for visual model validation.
What it does
- Classifies application components into operational security buckets.
- Performs STRIDE-based threat analysis with support for AI-specific extensions.
- Maps security findings to industry standards and NIST control families.
- Generates security-focused backlog items for implementation handoff.
- Analyzes plan-drift by comparing security baselines against current repository findings.
- Automates the generation of TM7 threat models and associated markdown reports.
When to use it
- When performing initial security planning or threat modeling for an application.
- When you need to reconcile an existing security plan with current audit or code review findings.
- When preparing security-related work items for a development backlog.
- When generating or updating TM7 threat model files for compliance or audit workflows.
Advantages
- Provides a consistent, repeatable methodology for security planning and threat modeling.
- Reduces manual effort in mapping security findings to regulatory standards.
- Enables automated detection of drift between planned security controls and actual implementation.
- Supports both portable markdown reporting and native integration with the Microsoft Threat Modeling Tool.
Usage examples
- “Analyze my application components and generate a security backlog based on the current STRIDE findings.”
- The agent will classify the components into operational buckets, perform a STRIDE analysis, and output a prioritized list of security work items formatted for your backlog.
- “Compare my existing security baseline with these new code review findings to identify control drift.”
- The agent will correlate the findings, filter out excluded paths, and produce a report detailing validated controls, residual risks, and any identified control drift.
- “Generate a TM7 threat model file from my current YAML threat-model specification.”
- The agent will process the specification and output a .tm7 file compatible with the Microsoft Threat Modeling Tool, along with a corresponding markdown report.
Install
Download the skill as a ZIP:
https://mp-staging.autogineer.com/agent-skills/microsoft/security-planning/downloadUnzip it into the cross-client skills folder of your project:
mkdir -p .agents/skills && unzip security-planning.zip -d .agents/skills
Documentation by Microsoft
Security Planning
This skill packages durable security-planning and plan-drift analysis used by Security Planner, Security Reviewer, and Code Review: operational bucket guidance, STRIDE analysis patterns, standards cross-references, NIST control-family references, security-specific backlog formats, and correlation of a Security Planner baseline with current security findings.
When to use
Use this skill when you need to:
Classify application components into the operational security buckets used during planning.
Evaluate threats with STRIDE-based analysis, including AI-specific extensions when
raiEnabledis true.Map bucket findings to standards references and control families without re-embedding long standard tables.
Derive security-specific backlog priorities and RAI work item categories for Phase 5 handoff.
Correlate a Security Planner baseline with caller-supplied current findings to identify validated controls, control drift, residual planned risks, newly introduced threats, and obsolete plan items.
Generate a dual-output TM7 model plus markdown report from a YAML/JSON threat-model spec for human-reviewed audit workflows.
Security plan drift analysis
Use the drift capability when a Security Planner baseline and current-state security findings already exist. It correlates supplied evidence only: it does not scan, profile, select security skills, verify findings, re-rate severity, invoke another agent, or modify source, plan, reviewer, backlog, or state artifacts.
Resolve and extract the baseline through drift-input-contracts.md.
Normalize caller-supplied audit, diff, or Code Review findings without treating plan-mode evidence as repository state.
Filter default exclusions, then classify only conclusions supported by drift-comparison-model.md.
Render the direct, Security Reviewer, Security Planner, or Code Review adaptation from drift-report-contract.md.
Use drift-worked-examples.md for deterministic behavior scenarios and expected category suppression.
Before full conversational or durable drift output, display the Security Planning CAUTION block in this skill verbatim. Report the default exclusions below in the canonical body's Current repository evidence section. A bounded Code Review section may point to these blocks because its containing review already carries the Code-Review disclaimer.
Default exclusions in effect. Planning and agent-customization artifacts are excluded from drift findings:
Paths:
.copilot-tracking/**,docs/planning/**,docs/adrs/**,.github/agents/**,.github/prompts/**,.github/instructions/**,.github/skills/**File globs:
*.prompt.md,*.agent.md,*.instructions.md,SKILL.mdTo override, pass
scope=explicitly. Overlapping user scope wins and is reported as a warning.
A write-capable direct caller may create one sequence-safe report under .copilot-tracking/security-audits/<project-slug>/. Security Reviewer and Security Planner render in conversation. Code Review renders a bounded section and optional security_plan_drift field. Every destination keeps inputs read-only and recommends follow-up without dispatch.
Stop the affected conclusion when a baseline is incomplete, a required fact is unresolved, evidence scope does not cover the claim, or input format drift prevents exclusions. Report suppressed categories as insufficient evidence rather than zero findings. Stop the entire correlation when no baseline resolves or no finding location is available to enforce exclusions.
TM7 generation workflow
When the user asks for a TM7 threat model, the runtime can generate a .tm7 file and a matching markdown report from the same spec. The generator supports the pre-populated-comprehensive and diagram-only-defer-to-tmt modes and can update an existing model with --update. Use the generate_tm7.py and generate_markdown.py entry points with --template to select a profile.
The .tm7 output mirrors the Microsoft Threat Modeling Tool's real SerializableModelData DataContract and deserializes cleanly under the tool's own DataContractSerializer. Fidelity is validated against the tool's own assemblies by scripts/Deserialize-Tm7.ps1, which the pytest suite runs when the tool is installed and skips cleanly otherwise. See references/tm7-generation.md for the verified contract.
Native TM7 visual feedback workflow
The skill also supports an opt-in, Windows-local feedback loop for the native Microsoft Threat Modeling Tool UI. The feature is off by default. The generator and standard validator keep their existing portable behavior when the feedback flags are absent. Native feedback is enabled only when validate_tm7_with_tmt.py is run with --feedback-loop, --spec, and --overlay-output; optional --overlay-input, --max-iterations, and --require-feedback-evidence refine the execution contract.
The workflow requires Microsoft Threat Modeling Tool 7.3.51110.1, a Windows desktop session, and UI Automation access. The loop is bounded to a baseline run plus at most three refinement iterations, and defaults to a baseline plus one. A single run holds the mouse and keyboard continuously across the baseline and every refinement iteration; control returns only at the release notice. Replaying a corrected overlay is a new run and a new takeover. Exit codes, stop reasons, and the discovery-failure rules are defined in the generation reference.
The harness controls TMT windows and may open, close, and reopen the app for save/reopen validation. It emits a start notice before automation begins, progress updates for the baseline and each refinement candidate, and a release notice when the loop completes or aborts so the operator knows when control is returned. These are notices only; the harness does not block on operator acknowledgment. The agent-facing lockout and release obligations that surround a run are owned by references/tm7-generation-workflow.md.
The loop records one evidence bundle per run under the requested evidence directory, with per-iteration screenshots, UI Automation snapshots, summaries, and candidate models. A run that captured at least one surface and either passed the automated gates or stopped for layout exhaustion also emits agent-review-request.json at the bundle root. The overlay payload remains in approval_state: pending, and no runtime path or flag auto-promotes it to approved or rewrites the canonical baseline.
Scoring keeps deterministic geometry gates separate from advisory screenshot heuristics. Screenshot heuristics are not a semantic approval signal.
A Windows-native example uses the skill's locked Windows dependency group:
uv run --project "<security-planning-skill-root>" --group windows \
python "<security-planning-skill-root>/scripts/validate_tm7_with_tmt.py" model.tm7 \
--evidence-dir ./artifacts/feedback \
--feedback-loop \
--spec ./specs/model.yaml \
--overlay-output ./artifacts/feedback/overlay.json \
--max-iterations 3 \
--require-feedback-evidenceResolve <security-planning-skill-root> from the loaded skill location before running the command.
The overlay contract is versioned and deterministic. It carries layout intent in named rule collections and is invalidated unless its full fingerprint block matches, so a stale overlay is rejected rather than replayed onto a changed model.
See references/tm7-generation.md for the full CLI surface, exit codes, stop reasons, geometry thresholds, the evidence-bundle layout, the overlay fingerprint contract, and the operator runbook covering prerequisites, abort, recovery, and rollback.
Agent-assisted visual review
Some layout defects never reach a metric. TM7 persists no connector label geometry and UI Automation exposes no label element, so label collisions, unreadable label text, and visual crowding are invisible to the deterministic gates. An agent that reads the rendered screenshots can see them and author corrections into the overlay the harness publishes.
This review is a default step of every feedback-loop run, not an opt-in extra. A run that captured at least one surface and either passed the automated gates or stopped for layout exhaustion emits agent-review-request.json at the evidence root, carrying per surface the screenshot and UI Automation paths, the node and zone rectangles, the connector handle points, and the predicted label rectangles in model coordinates, plus the coordinate-translation constants and the port convention. The agent reviews from that payload rather than hand-parsing UI Automation trees. automated-ready-pending-human means the automated gates passed and the result awaits review; it is not an approval. The remedy remains documented rather than proven: it has not yet been demonstrated end to end on a real defect.
On the success path the published overlay addresses every captured surface, so a correction can be authored for any of them. When the run found no automated correction, that overlay is the seed shape, carrying an overlay-seed- identifier and empty rule collections. A layout-exhaustion stop publishes the same seed shape; a correctness or environment stop publishes nothing at all.
See references/tm7-generation.md for the protocol, the request payload, the accepted rule fields, the coordinate translation, and the constraints that bound an agent-authored overlay.
[!CAUTION] Disclaimer: This agent is an assistive tool only. It does not provide legal, regulatory, or compliance advice and does not replace professional security review boards, penetration testing teams, compliance auditors, legal counsel, or other qualified human reviewers. The output consists of suggested actions and considerations to support a user's own internal security review and decision‑making. All security plans, threat models, security models, and mitigation recommendations generated by this tool must be independently reviewed and validated by appropriate security and compliance reviewers before use. Outputs from this tool do not constitute security approval, compliance certification, or regulatory sign‑off.
The human-in-the-loop contract governing authorship confirmation, native feedback-loop operator safety, and layout overlay promotion is owned by references/tm7-generation-workflow.md. This skill owns the mechanics only.
Skill layout
Load the reference file that matches the phase or topic you need.
| Reference | Topic |
|---|---|
| references/00-index.md | Navigation catalog and consolidated attribution |
| references/operational-buckets.md | Operational bucket definitions, GS overlay, and classification guidance |
| references/stride-model.md | STRIDE methodology, AI extensions, risk matrix, and data-flow analysis |
| references/standards-cross-reference.md | Bucket-to-standards mapping table and component mapping output format |
| references/nist-control-families.md | NIST 800-53 priority tiers and NIST AI RMF subcategory mappings |
| references/backlog-formats.md | Security-specific prioritization and RAI work item categories |
| references/data-classification.md | Public-safe data-classification taxonomy, tiers/categories/retention, and schema mapping |
| references/threat-model-review.md | Threat-model completeness checklist, PASS/INCOMPLETE verdict, and gap list |
| references/drift-input-contracts.md | Security Planner baseline extraction, normalized current-finding forms, evidence scope, and drift handling |
| references/drift-comparison-model.md | Exclusions, evidence preconditions, five plan-drift categories, matching order, and recommendation-only handoffs |
| references/drift-report-contract.md | Canonical body and direct, Security Reviewer, Security Planner, and Code Review destination adaptations |
| references/drift-worked-examples.md | Synthetic complete, incomplete, malformed, diff-scoped, exclusion, and caller-regression behavior scenarios |
| references/tm7-generation-workflow.md | Human-in-the-loop contract for TM7 generation: authorship confirmation, native feedback-loop operator safety, and layout overlay promotion |
| references/tm7-generation.md | TM7 input schema, dual-output generation contract, profile mapping, emission contract, native feedback loop, overlay and fingerprint contract, and operator runbook |
Bundled executable and data resources:
| Resource | Use |
|---|---|
scripts/generate_tm7.py | Run to build a .tm7 from a threat-model spec |
scripts/generate_markdown.py | Run to render the same spec as a markdown report |
scripts/generate_tb7.py | Run to emit a template file |
scripts/validate_tm7_with_tmt.py | Run for native Windows TMT validation and the opt-in feedback loop |
scripts/Deserialize-Tm7.ps1 | Run to check round-trip fidelity against the tool's own assemblies |
assets/schemas/tm7-layout-overlay.schema.json | Read as the layout overlay schema |
assets/schemas/tm7-agent-review-request.schema.json | Read as the agent visual-review request schema |
assets/schemas/tm7-visual-feedback-manifest.schema.json | Read as the evidence manifest schema |
templates/threat-model-spec-example.yaml | Copy as the starting point for a new spec |
The skill ships public defaults for the taxonomy and the completeness checklist. Organization-specific internal details such as internal data-type taxonomies, internal auth service names, and internal review-gate steps are supplied through a private config overlay referenced by state.overlayConfigPath and are never embedded in the public skill.
Attribution
The durable reference content in this skill is organized by reference file and summarized in references/00-index.md. See that index for the consolidated attribution and delegation notes.
Files
- ATTRIBUTION.mdAdded by AutoGineer847 B
- LICENSELicence file added by AutoGineer1.1 KiB
- README.md5.3 KiB
- SECURITY.md24.9 KiB
- SKILL.md16.9 KiB
- assets/schemas/tm7-agent-review-request.schema.json4.6 KiB
- assets/schemas/tm7-layout-overlay.schema.json6.8 KiB
- assets/schemas/tm7-visual-feedback-manifest.schema.json8.3 KiB
- assets/template-profiles/LICENSE-MIT-REFERENCE.txt366 B
- assets/template-profiles/README.md795 B
- assets/template-profiles/sdl_core_generic.yaml324 B
- assets/templates/LICENSE1.1 KiB
- assets/templates/default-kb.xml1.0 MiB
- assets/templates/default.tb7375.2 KiB
- pyproject.toml1.0 KiB
- references/00-index.md6.7 KiB
- references/backlog-formats.md2.0 KiB
- references/data-classification.md5.3 KiB
- references/drift-comparison-model.md8.0 KiB
- references/drift-input-contracts.md7.0 KiB
- references/drift-report-contract.md5.9 KiB
- references/drift-worked-examples.md11.5 KiB
- references/nist-control-families.md1.1 KiB
- references/operational-buckets.md2.9 KiB
- references/standards-cross-reference.md1.5 KiB
- references/stride-model.md4.0 KiB
- references/threat-model-review.md6.1 KiB
- references/tm7-generation-workflow.md3.9 KiB
- references/tm7-generation.md59.0 KiB
- scripts/Deserialize-Tm7.ps1Script7.8 KiB
- scripts/generate_markdown.pyScript8.0 KiB
- scripts/generate_tb7.pyScript11.6 KiB
- scripts/generate_tm7.pyScript256.5 KiB
- scripts/populate_tm7_threats.pyScript24.9 KiB
- scripts/tm7_threat_contract.pyScript38.3 KiB
- scripts/tm7_visual_feedback.pyScript119.6 KiB
- scripts/validate_gap_citations.pyScript2.5 KiB
- scripts/validate_tm7_with_tmt.pyScript262.9 KiB
- templates/threat-model-spec-example.yaml5.8 KiB
- tests/corpus/minimal-spec.yaml111 B
- tests/corpus/minimal-tm7.xml17 B
- tests/fixtures/README.md3.2 KiB
- tests/fixtures/comprehensive-spec.yaml78.5 KiB
- tests/fixtures/expected.tm71.1 MiB
- tests/fixtures/tmp-sample.tm71.1 MiB
- tests/fixtures/tmt-reference-threats.tm71.3 MiB
- tests/fixtures/tmt-reference.tm71.2 MiB
- tests/fixtures/visual-feedback/valid-manifest.json921 B
- tests/fixtures/visual-feedback/valid-overlay.yaml1.4 KiB
- tests/fuzz_harness.pyScript3.0 KiB
- tests/test_generate_tb7.pyScript11.9 KiB
- tests/test_generate_tm7.pyScript189.8 KiB
- tests/test_populate_tm7_threats.pyScript13.5 KiB
- tests/test_tm7_threat_contract.pyScript36.4 KiB
- tests/test_tm7_visual_feedback.pyScript59.5 KiB
- tests/test_validate_tm7_with_tmt.pyScript208.7 KiB
- uv.lock100.6 KiB
scripts/populate_tm7_threats.py
#!/usr/bin/env python3
# Copyright (c) 2026 Microsoft Corporation. All rights reserved.
# SPDX-License-Identifier: MIT
"""Populate a TM7 model with explicitly mapped threat instances."""
from __future__ import annotations
import argparse
import hashlib
import json
import os
import sys
import tempfile
import uuid
from pathlib import Path
from typing import Any
from xml.etree import ElementTree as ET
import yaml
from tm7_threat_contract import (
KNOWLEDGE_NS,
MODEL_NS,
XSD_NS,
ThreatContractError,
UnsafeXmlError,
_local_name,
_normalize_text,
build_custom_threat_type_id,
build_entry_key,
build_interaction_key,
build_mitigation_text,
collect_mapping_failures,
parse_hardened_xml_bytes,
prepare_threat_instances,
reconcile_authored_base,
serialize_threat_instances,
tm7_serialization_namespaces,
)
EXIT_SUCCESS = 0
EXIT_FAILURE = 1
# SIGINT convention: 128 + signal number.
EXIT_INTERRUPTED = 130
# Namespace constants are re-exported from the shared contract module so callers
# and tests can read them from this entry point.
__all__ = ["KNOWLEDGE_NS", "MODEL_NS", "XSD_NS"]
class GenerationError(Exception):
"""Raised when input validation or population fails."""
def _load_yaml_or_json(path: Path) -> dict[str, Any]:
if not path.is_file():
raise GenerationError(f"Spec file not found: {path}")
try:
with path.open("r", encoding="utf-8") as handle:
loaded = (
json.load(handle)
if path.suffix.lower() == ".json"
else yaml.safe_load(handle) or {}
)
except (json.JSONDecodeError, yaml.YAMLError, UnicodeDecodeError) as exc:
raise GenerationError(f"Unable to parse spec: {exc}") from exc
if not isinstance(loaded, dict):
raise GenerationError("Spec root must be an object")
return loaded
def _parse_model(path: Path) -> ET.Element:
if not path.is_file():
raise GenerationError(f"TM7 model not found: {path}")
try:
return parse_hardened_xml_bytes(path.read_bytes())
except UnsafeXmlError as exc:
raise GenerationError(f"Unable to parse TM7 model: {exc}") from exc
def _direct_text(element: ET.Element, name: str) -> str:
return next(
(
_normalize_text(child.text)
for child in element
if _local_name(child.tag) == name
),
"",
)
def _display_name(element: ET.Element) -> str:
properties = next(
(child for child in element if _local_name(child.tag) == "Properties"),
None,
)
if properties is None:
return ""
for attribute in properties:
display_name = _direct_text(attribute, "DisplayName")
if display_name == "Name":
return _direct_text(attribute, "Value")
return ""
def _canonical_hash(element: ET.Element | None) -> str:
if element is None:
return hashlib.sha256(b"").hexdigest()
return hashlib.sha256(
ET.tostring(element, encoding="utf-8", xml_declaration=False)
).hexdigest()
def _hash_semantic_subtrees(root: ET.Element) -> dict[str, str]:
drawing_surface_list = next(
(child for child in root if _local_name(child.tag) == "DrawingSurfaceList"),
None,
)
knowledge_base = next(
(child for child in root if _local_name(child.tag) == "KnowledgeBase"),
None,
)
return {
"drawing_surface_list": _canonical_hash(drawing_surface_list),
"knowledge_base": _canonical_hash(knowledge_base),
}
def _find_child(root: ET.Element, name: str) -> ET.Element | None:
return next(
(child for child in root if _local_name(child.tag) == name),
None,
)
def _find_generation_enabled(root: ET.Element) -> bool:
node = _find_child(root, "ThreatGenerationEnabled")
if node is None:
return False
return _normalize_text(node.text).lower() == "true"
def _index_model(root: ET.Element) -> dict[str, Any]:
"""Index the embedded KnowledgeBase threat types of a model."""
type_ids: set[str] = set()
type_ids_by_title: dict[str, str] = {}
duplicate_titles: set[str] = set()
for threat_type in root.findall(".//{*}ThreatType"):
type_id = _direct_text(threat_type, "Id")
if type_id:
type_ids.add(type_id)
title = _direct_text(threat_type, "ShortTitle")
if not type_id or not title:
continue
if title in type_ids_by_title:
duplicate_titles.add(title)
type_ids_by_title[title] = type_id
for title in duplicate_titles:
type_ids_by_title.pop(title, None)
return {
"type_ids": type_ids,
"type_ids_by_title": type_ids_by_title,
}
def _build_authored_base_index(
spec: dict[str, Any],
model_root: ET.Element,
) -> dict[str, Any]:
"""Index an authored base model against the spec by stable surface identity.
An authored drawing surface is bound to its declared surface through the
deterministic surface GUID, never through list position, so reordering the
authored surfaces cannot rebind connectors to the wrong diagram. Authored
surfaces that match no declared surface are indexed with an empty id and are
ignored by reconciliation.
"""
index = _index_model(model_root)
connectors: dict[str, list[dict[str, str]]] = {}
surfaces: list[dict[str, Any]] = []
components = {
_normalize_text(component.get("id")): component
for component in spec.get("components") or []
if isinstance(component, dict) and component.get("id")
}
declared_by_guid: dict[str, dict[str, Any]] = {}
representations = spec.get("representations") or {}
for group in (
"context_diagrams",
"functional_scenarios",
"operational_views",
):
for declared in representations.get(group) or []:
if not isinstance(declared, dict):
continue
declared_id = _normalize_text(declared.get("id"))
if not declared_id:
raise GenerationError(
"Authored-base reconciliation failed:\n"
"a declared surface is missing a stable id"
)
expected_guid = _deterministic_guid(f"surface:{declared_id}")
if expected_guid in declared_by_guid:
raise GenerationError(
"Authored-base reconciliation failed:\n"
f"declared surface {declared_id} is defined more than once"
)
declared_by_guid[expected_guid] = declared
for surface in model_root.findall("{*}DrawingSurfaceList/{*}DrawingSurfaceModel"):
surface_guid = _direct_text(surface, "Guid")
declared_surface = declared_by_guid.get(surface_guid, {})
declared_entries = (
declared_surface.get("components") or declared_surface.get("elements") or []
)
expected_names: dict[str, str] = {}
for entry in declared_entries:
if not isinstance(entry, dict):
continue
element_id = _normalize_text(entry.get("id"))
component = components.get(element_id, {})
expected_names[element_id] = _normalize_text(
entry.get("name") or component.get("name") or element_id
)
actual_elements_by_name: dict[str, str] = {}
borders = next(
(child for child in surface if _local_name(child.tag) == "Borders"),
None,
)
if borders is not None:
for entry in borders:
value = next(
(child for child in entry if _local_name(child.tag) == "Value"),
None,
)
if value is None:
continue
element_guid = _direct_text(value, "Guid")
element_name = _display_name(value)
if element_name and element_guid:
actual_elements_by_name[element_name] = element_guid
surface_elements = {
element_id: actual_elements_by_name[name]
for element_id, name in expected_names.items()
if name in actual_elements_by_name
}
surface_flow_ids: dict[tuple[str, str], str] = {}
for flow in spec.get("data_flows") or []:
if not isinstance(flow, dict):
continue
flow_id = _normalize_text(flow.get("id"))
source_ref = _normalize_text(flow.get("source_ref"))
target_ref = _normalize_text(flow.get("target_ref"))
if not flow_id or not source_ref or not target_ref:
continue
source_guid = surface_elements.get(source_ref)
target_guid = surface_elements.get(target_ref)
if source_guid and target_guid:
surface_flow_ids[(source_guid, target_guid)] = flow_id
surfaces.append(
{
"id": _normalize_text(declared_surface.get("id")),
"guid": surface_guid,
"elements": surface_elements,
}
)
lines = next(
(child for child in surface if _local_name(child.tag) == "Lines"),
None,
)
if lines is None:
continue
for entry in lines:
value = next(
(child for child in entry if _local_name(child.tag) == "Value"),
None,
)
if value is None:
continue
flow_name = _display_name(value)
if not flow_name:
continue
connector = {
"surface_id": _normalize_text(declared_surface.get("id")),
"drawing_surface_guid": surface_guid,
"flow_guid": _direct_text(value, "Guid"),
"source_guid": _direct_text(value, "SourceGuid"),
"target_guid": _direct_text(value, "TargetGuid"),
"interaction_string": flow_name,
}
flow_id = surface_flow_ids.get(
(
connector["source_guid"],
connector["target_guid"],
)
)
connectors.setdefault(flow_id or flow_name, []).append(connector)
return {
"surfaces": surfaces,
"connectors": connectors,
"type_ids": index["type_ids"],
"type_ids_by_title": index["type_ids_by_title"],
}
def _resolve_type_id(
spec_threat: dict[str, Any],
model_type_ids: set[str],
type_ids_by_title: dict[str, str],
) -> str:
source_id = _normalize_text(spec_threat.get("id"))
if source_id in model_type_ids:
return source_id
title_match = type_ids_by_title.get(_normalize_text(spec_threat.get("title")))
if title_match:
return title_match
return build_custom_threat_type_id(source_id)
def _mitigation_text(spec: dict[str, Any], threat: dict[str, Any]) -> str:
return build_mitigation_text(spec, threat)
def _canonical_surface(spec: dict[str, Any], threat: dict[str, Any]) -> str:
"""Return the declared surface id that carries a threat, or an empty string."""
representations = spec.get("representations") or {}
for group in (
"context_diagrams",
"functional_scenarios",
"operational_views",
):
for surface in representations.get(group) or []:
if not isinstance(surface, dict):
continue
members = surface.get("components") or surface.get("elements") or []
member_ids = {
_normalize_text(member.get("id"))
for member in members
if isinstance(member, dict)
}
flow_ids = {
_normalize_text(item.get("id"))
if isinstance(item, dict)
else _normalize_text(item)
for item in surface.get("flows") or []
}
if (
_normalize_text(threat.get("target_ref")) in member_ids
and _normalize_text(threat.get("interaction_ref")) in flow_ids
):
return _normalize_text(surface.get("id"))
return ""
def _deterministic_guid(value: str) -> str:
return str(uuid.uuid5(uuid.NAMESPACE_URL, value))
def _build_threat_instances(
spec: dict[str, Any],
model_root: ET.Element,
*,
allow_missing_connectors: bool,
authored_base_index: dict[str, Any] | None = None,
) -> tuple[list[dict[str, Any]], set[str], list[dict[str, Any]]]:
index = _index_model(model_root)
resolved: list[dict[str, Any]] = []
diagnostics: list[dict[str, Any]] = []
failures: list[str] = []
type_ids = index["type_ids"]
authored_base_index = authored_base_index or _build_authored_base_index(
spec, model_root
)
for threat in spec.get("threats") or []:
if not isinstance(threat, dict):
continue
source_id = _normalize_text(threat.get("id"))
interaction_ref = _normalize_text(threat.get("interaction_ref"))
title = _normalize_text(threat.get("title"))
surface_id = _canonical_surface(spec, threat)
interaction = None
if surface_id:
matching_connectors = authored_base_index.get("connectors", {}).get(
interaction_ref, []
)
if isinstance(matching_connectors, list):
interaction = next(
(
value
for value in matching_connectors
if _normalize_text(value.get("surface_id")) == surface_id
),
None,
)
if interaction is None and allow_missing_connectors:
flow = next(
(
item
for item in spec.get("data_flows") or []
if isinstance(item, dict)
and _normalize_text(item.get("id")) == interaction_ref
),
{},
)
interaction = {
"drawing_surface_guid": _deterministic_guid(
f"surface:{surface_id}"
),
"flow_guid": _deterministic_guid(f"{surface_id}:{interaction_ref}"),
"source_guid": _deterministic_guid(
f"{surface_id}:{_normalize_text(flow.get('source_ref'))}"
),
"target_guid": _deterministic_guid(
f"{surface_id}:{_normalize_text(flow.get('target_ref'))}"
),
"interaction_string": _normalize_text(
flow.get("display_label") or flow.get("id")
),
}
type_id = _resolve_type_id(
threat,
type_ids,
index["type_ids_by_title"],
)
if type_id not in type_ids:
failures.append(
f"{source_id}: embedded type {type_id} is absent from KnowledgeBase"
)
continue
if interaction is None:
failures.append(f"{source_id}: connector {interaction_ref} is absent")
continue
source_guid = interaction["source_guid"]
flow_guid = interaction["flow_guid"]
target_guid = interaction["target_guid"]
resolved.append(
{
"source_id": source_id,
"target_ref": _normalize_text(threat.get("target_ref")),
"interaction_ref": interaction_ref,
"interaction_string": _normalize_text(
interaction.get("interaction_string") or interaction_ref
),
"title": title,
"description": _normalize_text(threat.get("description")) or title,
"category": _normalize_text(threat.get("category")),
"state": _normalize_text(threat.get("state")) or "Open",
"mitigations": _mitigation_text(spec, threat),
"type_id": type_id,
"drawing_surface_guid": interaction["drawing_surface_guid"],
"source_guid": source_guid,
"flow_guid": flow_guid,
"target_guid": target_guid,
"interaction_key": build_interaction_key(
source_guid, flow_guid, target_guid
),
"dictionary_key": build_entry_key(
type_id, source_guid, flow_guid, target_guid
),
}
)
diagnostics.append(
{
"source_id": source_id,
"interaction_ref": interaction_ref,
"type_id": type_id,
"drawing_surface_guid": interaction["drawing_surface_guid"],
"source_guid": source_guid,
"flow_guid": flow_guid,
"target_guid": target_guid,
}
)
if failures:
raise GenerationError(
"Threat resolution failed:\n" + "\n".join(sorted(failures))
)
return resolved, type_ids, diagnostics
def _set_generation_enabled(root: ET.Element, enabled: bool) -> None:
node = _find_child(root, "ThreatGenerationEnabled")
if node is None:
node = ET.SubElement(root, "ThreatGenerationEnabled")
node.text = "true" if enabled else "false"
def _write_model(root: ET.Element, destination: Path) -> Path:
destination.parent.mkdir(parents=True, exist_ok=True)
handle = tempfile.NamedTemporaryFile(
mode="wb",
dir=str(destination.parent),
prefix=f"{destination.name}.",
suffix=".tmp",
delete=False,
)
temp_path = Path(handle.name)
try:
with tm7_serialization_namespaces(root):
with handle:
ET.ElementTree(root).write(
handle,
encoding="utf-8",
xml_declaration=True,
)
return temp_path
except Exception:
temp_path.unlink(missing_ok=True)
raise
def _sha256_file(path: Path) -> str:
digest = hashlib.sha256()
with path.open("rb") as handle:
for chunk in iter(lambda: handle.read(1024 * 1024), b""):
digest.update(chunk)
return digest.hexdigest()
def populate_tm7_threats(
spec_path: str | Path,
model_path: str | Path,
output_path: str | Path | None = None,
*,
generation_state: bool | None = None,
expected_threat_count: int | None = None,
) -> dict[str, Any]:
"""Resolve and optionally write explicit threats into an existing model.
``expected_threat_count`` is an optional caller assertion. It defaults to
``None`` so any internally consistent spec is accepted; a fixed count would
reject every model of a different size.
"""
spec = _load_yaml_or_json(Path(spec_path))
mapping_failures = collect_mapping_failures(
spec,
expected_count=expected_threat_count,
)
if mapping_failures:
raise GenerationError(
"Threat mapping contract validation failed:\n" + "\n".join(mapping_failures)
)
model_path = Path(model_path)
destination = Path(output_path) if output_path is not None else None
if destination is not None and destination.resolve() == model_path.resolve():
raise GenerationError(
f"Refusing to overwrite the immutable base model at {model_path}"
)
if destination is not None and generation_state is None:
generation_state = False
base_sha256 = _sha256_file(model_path)
model_root = _parse_model(model_path)
before_hashes = _hash_semantic_subtrees(model_root)
authored_base_index = None
if output_path is not None:
authored_base_index = _build_authored_base_index(spec, model_root)
authored_base_failures = reconcile_authored_base(spec, authored_base_index)
if authored_base_failures:
raise GenerationError(
"Authored-base reconciliation failed:\n"
+ "\n".join(authored_base_failures)
)
threats, type_ids, diagnostics = _build_threat_instances(
spec,
model_root,
allow_missing_connectors=output_path is None,
authored_base_index=authored_base_index,
)
try:
prepared = prepare_threat_instances(threats, type_ids=type_ids)
except ThreatContractError as exc:
raise GenerationError(str(exc)) from exc
selected_state = (
_find_generation_enabled(model_root)
if generation_state is None
else generation_state
)
serialize_threat_instances(model_root, threats, type_ids=type_ids)
_set_generation_enabled(model_root, selected_state)
after_hashes = _hash_semantic_subtrees(model_root)
if before_hashes != after_hashes:
raise GenerationError(
"Threat population mutated the DrawingSurfaceList or KnowledgeBase subtree"
)
if _sha256_file(model_path) != base_sha256:
raise GenerationError("The immutable base model changed during population")
referenced_type_ids = {item["type_id"] for item in prepared}
result = {
"ThreatGenerationEnabled": selected_state,
"ThreatInstances": prepared,
"KnowledgeBaseTypeIds": sorted(type_ids),
"counts": {
"threat_instances": len(prepared),
"custom_types": len(referenced_type_ids),
"source_mappings": len(diagnostics),
},
"hashes": {
"drawing_surface_list_before": before_hashes["drawing_surface_list"],
"drawing_surface_list_after": after_hashes["drawing_surface_list"],
"knowledge_base_before": before_hashes["knowledge_base"],
"knowledge_base_after": after_hashes["knowledge_base"],
"base_sha256_before": base_sha256,
"base_sha256_after": _sha256_file(model_path),
},
"state": {
"generation_state": selected_state,
"source_path": str(model_path),
"output_path": str(output_path) if output_path is not None else None,
"in_place": False,
},
"diagnostics": {
"mapping_failures": mapping_failures,
"mappings": diagnostics,
},
}
if output_path is None:
return result
assert destination is not None
temp_path = _write_model(model_root, destination)
try:
written_root = _parse_model(temp_path)
written_hashes = _hash_semantic_subtrees(written_root)
if written_hashes != after_hashes:
raise GenerationError("Post-write semantic hashes did not validate")
written_state = _find_generation_enabled(written_root)
written_instances = len(
written_root.findall(
"{*}ThreatInstances/{*}KeyValueOfstringThreatpc_P0_PhOB"
)
)
if written_state != selected_state or written_instances != len(prepared):
raise GenerationError("Post-write validation failed")
os.replace(temp_path, destination)
except Exception:
temp_path.unlink(missing_ok=True)
raise
result["state"]["output_path"] = str(destination)
result["state"]["in_place"] = False
return result
def create_parser() -> argparse.ArgumentParser:
"""Create the population command-line parser."""
parser = argparse.ArgumentParser(
description="Populate a TM7 file with explicit threats"
)
parser.add_argument("spec", type=Path, help="Input threat-model spec")
parser.add_argument("model", type=Path, help="Base TM7 model")
parser.add_argument("-o", "--output", type=Path, default=None)
parser.add_argument(
"--generation-state",
choices=["true", "false"],
default=None,
help="Explicitly set ThreatGenerationEnabled in the output",
)
parser.add_argument(
"--expected-threat-count",
type=int,
default=None,
help=(
"Assert the spec declares exactly this many unique threat ids. "
"Omit to accept any internally consistent count."
),
)
return parser
def main() -> int:
"""Run the TM7 population command."""
args = create_parser().parse_args()
try:
generation_state = None
if args.generation_state is not None:
generation_state = args.generation_state.lower() == "true"
result = populate_tm7_threats(
args.spec,
args.model,
args.output,
generation_state=generation_state,
expected_threat_count=args.expected_threat_count,
)
except GenerationError as exc:
print(str(exc), file=sys.stderr)
return EXIT_FAILURE
except KeyboardInterrupt:
print("\nInterrupted by user", file=sys.stderr)
return EXIT_INTERRUPTED
except BrokenPipeError:
sys.stderr.close()
return EXIT_FAILURE
print(
f"Populated {len(result['ThreatInstances'])} threat instances with "
f"ThreatGenerationEnabled={result['ThreatGenerationEnabled']}"
)
return EXIT_SUCCESS
if __name__ == "__main__":
raise SystemExit(main())
Requirements and compatibility
Written for: GitHub Copilot, Claude Code
Works with: Claude Code, Claude apps, OpenAI Codex, GitHub Copilot, Cursor, Gemini CLI, Kiro, OpenCode, Goose, Amp, Roo Code, JetBrains Junie, Factory Droid, Devin (ex-Windsurf), Mistral Vibe, OpenHands, Letta
“Works with” means the tool reads the open Agent Skills format; a skill written for one tool may use features others lack.
Needs: Claude Code frontmatter, Needs Python, Needs Node.js
The author says: Generation requires Python 3.11+ and uv. The native TM7 feedback loop additionally requires Windows with an interactive desktop session and the pinned Microsoft Threat Modeling Tool 7.3.51110.1.
Security
What our scanner noticed· Medium risk
- Large base64-encoded blobs
- Scripts that your agent may run
This skill includes scripts that your agent may run. Read them before you use it.
A static scan is not a guarantee. Skills are third-party content: review a skill before you let an agent use it.
Attribution and licence
- Published by
- Microsoft
- Publisher
- Microsoft (Official)
- Source
- microsoft/hve-core · .github/skills/project-planning/security-planning · commit f425e94
- Licence
- MIT License (redistribution allowed)
- LICENSE (added by AutoGineer from LICENSE)
- ATTRIBUTION.md (written by AutoGineer)
- Synced
AutoGineer is not affiliated with or endorsed by Microsoft.
Download ZIP
The ZIP holds the skill’s files, its licence files and an ATTRIBUTION.md. Download security-planning.zip