Generated banner for Security Planning
Generated logo for Security Planning

Security Planning

MicrosoftOfficialPublished by Microsoft

  • Skill
  • Tool integration
  • Security
  • MIT
  • Medium risk
  • Has scripts

From the author

Security planning and plan-drift analysis for STRIDE, standards, controls, backlog handoff, current findings, and TM7 generation.
— the description by Microsoft

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

  1. Download the skill as a ZIP:

    https://autogineer.com/agent-skills/microsoft/security-planning/download
  2. Unzip 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 raiEnabled is 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.

  1. Resolve and extract the baseline through drift-input-contracts.md.

  2. Normalize caller-supplied audit, diff, or Code Review findings without treating plan-mode evidence as repository state.

  3. Filter default exclusions, then classify only conclusions supported by drift-comparison-model.md.

  4. Render the direct, Security Reviewer, Security Planner, or Code Review adaptation from drift-report-contract.md.

  5. 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.md

To 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-evidence

Resolve <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.

ReferenceTopic
references/00-index.mdNavigation catalog and consolidated attribution
references/operational-buckets.mdOperational bucket definitions, GS overlay, and classification guidance
references/stride-model.mdSTRIDE methodology, AI extensions, risk matrix, and data-flow analysis
references/standards-cross-reference.mdBucket-to-standards mapping table and component mapping output format
references/nist-control-families.mdNIST 800-53 priority tiers and NIST AI RMF subcategory mappings
references/backlog-formats.mdSecurity-specific prioritization and RAI work item categories
references/data-classification.mdPublic-safe data-classification taxonomy, tiers/categories/retention, and schema mapping
references/threat-model-review.mdThreat-model completeness checklist, PASS/INCOMPLETE verdict, and gap list
references/drift-input-contracts.mdSecurity Planner baseline extraction, normalized current-finding forms, evidence scope, and drift handling
references/drift-comparison-model.mdExclusions, evidence preconditions, five plan-drift categories, matching order, and recommendation-only handoffs
references/drift-report-contract.mdCanonical body and direct, Security Reviewer, Security Planner, and Code Review destination adaptations
references/drift-worked-examples.mdSynthetic complete, incomplete, malformed, diff-scoped, exclusion, and caller-regression behavior scenarios
references/tm7-generation-workflow.mdHuman-in-the-loop contract for TM7 generation: authorship confirmation, native feedback-loop operator safety, and layout overlay promotion
references/tm7-generation.mdTM7 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:

ResourceUse
scripts/generate_tm7.pyRun to build a .tm7 from a threat-model spec
scripts/generate_markdown.pyRun to render the same spec as a markdown report
scripts/generate_tb7.pyRun to emit a template file
scripts/validate_tm7_with_tmt.pyRun for native Windows TMT validation and the opt-in feedback loop
scripts/Deserialize-Tm7.ps1Run to check round-trip fidelity against the tool's own assemblies
assets/schemas/tm7-layout-overlay.schema.jsonRead as the layout overlay schema
assets/schemas/tm7-agent-review-request.schema.jsonRead as the agent visual-review request schema
assets/schemas/tm7-visual-feedback-manifest.schema.jsonRead as the evidence manifest schema
templates/threat-model-spec-example.yamlCopy 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

scripts/tm7_threat_contract.py

#!/usr/bin/env python3
# Copyright (c) 2026 Microsoft Corporation. All rights reserved.
# SPDX-License-Identifier: MIT
"""Validate and serialize the TM7 threat-instance DataContract."""

from __future__ import annotations

import hashlib
import re
import threading
import uuid
from collections import Counter
from collections.abc import Iterator
from contextlib import contextmanager
from pathlib import Path
from typing import Any
from xml.etree import ElementTree as ET

from defusedxml import ElementTree as DefusedET

ABSTRACT_NS = "http://schemas.datacontract.org/2004/07/ThreatModeling.Model.Abstracts"
ARRAYS_NS = "http://schemas.microsoft.com/2003/10/Serialization/Arrays"
KNOWLEDGE_NS = "http://schemas.datacontract.org/2004/07/ThreatModeling.KnowledgeBase"
MODEL_NS = "http://schemas.datacontract.org/2004/07/ThreatModeling.Model"
SERIALIZATION_NS = "http://schemas.microsoft.com/2003/10/Serialization/"
XSD_NS = "http://www.w3.org/2001/XMLSchema"
XSI_NS = "http://www.w3.org/2001/XMLSchema-instance"

# TMT reads prefixes positionally in places, so a written model must carry the
# exact prefix bindings below rather than the ns0/ns1 names ElementTree invents.
TM7_NAMESPACE_PREFIXES: tuple[tuple[str, str], ...] = (
    ("", MODEL_NS),
    ("a", ARRAYS_NS),
    ("b", KNOWLEDGE_NS),
    ("d2p1", ABSTRACT_NS),
    ("i", XSI_NS),
    ("z", SERIALIZATION_NS),
)

# Input ceilings are derived from the largest supported committed inputs plus
# generous headroom: the largest spec fixture is about 81 KiB and the largest
# generated model is about 1.7 MiB. Bounds are enforced before parsing so an
# oversized document cannot be expanded in memory first.
MAX_SPEC_BYTES = 8 * 1024 * 1024
MAX_MODEL_BYTES = 64 * 1024 * 1024

# TMT stores ThreatInstance.Id as a signed 32-bit integer and treats 0 as the
# unassigned sentinel, so derived identifiers occupy the closed interval
# [1, 2147483647]. The identifier is a fixed standard-library digest over the
# UTF-8 bytes of the exact normalized source id, which keeps a threat's visible
# id constant across insertion, reordering, and separate processes.
THREAT_ID_DIGEST = "sha256"
THREAT_ID_MIN = 1
THREAT_ID_MAX = 2147483647

THREAT_MEMBER_ORDER = [
    "ChangedBy",
    "DrawingSurfaceGuid",
    "FlowGuid",
    "Id",
    "InteractionKey",
    "InteractionString",
    "ModifiedAt",
    "Priority",
    "Properties",
    "SourceGuid",
    "State",
    "StateInformation",
    "TargetGuid",
    "Title",
    "TypeId",
    "Upgraded",
    "UserThreatCategory",
    "UserThreatDescription",
    "UserThreatShortDescription",
    "Wide",
]
THREAT_PROPERTY_KEYS = [
    "Title",
    "UserThreatCategory",
    "UserThreatShortDescription",
    "UserThreatDescription",
    "InteractionString",
    "PossibleMitigations",
    "Priority",
    "SDLPhase",
]
STATE_ALIASES = {
    "Accepted (Outside Control)": "NotApplicable",
    "Accepted with Documentation": "NotApplicable",
    "AutoGenerated": "AutoGenerated",
    "Migrated": "Migrated",
    "Mitigated": "Mitigated",
    "Mitigated (Copilot Controls)": "Mitigated",
    "Mitigated by Design": "Mitigated",
    "Mitigated with Documentation": "Mitigated",
    "NeedsInvestigation": "NeedsInvestigation",
    "Needs-Investigation": "NeedsInvestigation",
    "NeedsMitigation": "NeedsMitigation",
    "NotApplicable": "NotApplicable",
    "Not-Applicable-False-Positive": "NotApplicable",
    "Open": "NeedsInvestigation",
    "Partially Mitigated": "NeedsMitigation",
    "Partially Mitigated with Documentation": "NeedsMitigation",
}
NULL_GUID = "00000000-0000-0000-0000-000000000000"


class UnsafeXmlError(ValueError):
    """Raised when XML input declares a DTD or entities, or cannot be parsed."""


class InputTooLargeError(ValueError):
    """Raised when an untrusted input exceeds its documented size ceiling."""


# ElementTree resolves prefixes through the process-global ``ET._namespace_map``
# and offers no per-document alternative, so every TM7 write passes through this
# single boundary. The lock serializes concurrent writers so one cannot observe
# another's prefixes, and the snapshot restores the caller's registry on every
# exit path including an exception.
_NAMESPACE_REGISTRY_LOCK = threading.Lock()


@contextmanager
def tm7_serialization_namespaces(root: ET.Element) -> Iterator[None]:
    """Bind TM7 prefixes and the root xsd attribute for one serialization.

    Args:
        root: Model root element that carries the ``xmlns:c`` schema binding.

    Yields:
        None, while the TM7 prefix bindings are active.
    """
    with _NAMESPACE_REGISTRY_LOCK:
        previous_map = ET._namespace_map.copy()
        try:
            for prefix, uri in TM7_NAMESPACE_PREFIXES:
                ET.register_namespace(prefix, uri)
            root.set("xmlns:c", XSD_NS)
            yield
        finally:
            root.attrib.pop("xmlns:c", None)
            ET._namespace_map.clear()
            ET._namespace_map.update(previous_map)


def read_bounded_bytes(path: Path, max_bytes: int) -> bytes:
    """Read a file only when it fits inside its documented size ceiling.

    The size is checked before any content is loaded, so an oversized document
    is rejected without being expanded in memory.

    Args:
        path: File to read.
        max_bytes: Maximum accepted size in bytes.

    Returns:
        Raw file bytes.

    Raises:
        InputTooLargeError: If the file exceeds ``max_bytes``.
        OSError: If the file cannot be inspected or read.
    """
    size = path.stat().st_size
    if size > max_bytes:
        raise InputTooLargeError(
            f"{path} is {size} bytes, which exceeds the {max_bytes} byte limit"
        )
    return path.read_bytes()


def contains_doctype_or_entity(data: bytes) -> bool:
    """Return True when the payload declares a DTD or entity in any encoding.

    A byte-level scan for ``<!DOCTYPE`` only matches UTF-8. The same document
    encoded as UTF-16 interleaves NUL bytes, so the marker never appears and
    the check silently passes. The payload is therefore decoded through the
    encodings XML permits before scanning.
    """
    for encoding in ("utf-8", "utf-16", "utf-16-le", "utf-16-be", "utf-32"):
        try:
            text = data.decode(encoding)
        except (UnicodeDecodeError, LookupError, ValueError):
            continue
        upper = text.upper()
        if "<!DOCTYPE" in upper or "<!ENTITY" in upper:
            return True
    return False


def parse_hardened_xml_bytes(data: bytes) -> ET.Element:
    """Parse XML bytes under one fail-closed policy shared by every reader.

    Named for its byte input so it cannot be confused with
    ``generate_tm7.parse_hardened_xml``, which takes a path and returns a
    parsed model payload rather than an element.

    Every entry point rejects DTD and entity declarations in any supported
    encoding, parses through defusedxml, which is a required runtime
    dependency, and converts parser and defusedxml exceptions alike into
    ``UnsafeXmlError``. Callers translate that into their own domain error so
    an unsafe document never surfaces as an unhandled traceback.

    Args:
        data: Raw XML bytes from an untrusted source.

    Returns:
        Parsed root element.

    Raises:
        UnsafeXmlError: If the payload declares a DTD or entity, or cannot be
            parsed safely.
    """
    if contains_doctype_or_entity(data):
        raise UnsafeXmlError("Refusing to parse XML with DTD or entity declarations")
    try:
        return DefusedET.fromstring(data)
    except ET.ParseError as exc:
        raise UnsafeXmlError(f"Unable to parse XML: {exc}") from exc
    except Exception as exc:  # defusedxml raises its own DTD/entity errors
        raise UnsafeXmlError(f"Refusing to parse XML: {exc}") from exc


class ThreatContractError(ValueError):
    """Raised when a threat-instance payload violates the TM7 contract."""


def _normalize_text(value: Any) -> str:
    return "" if value is None else str(value).strip()


def _coerce_list(value: Any) -> list[Any]:
    if value is None:
        return []
    return value if isinstance(value, list) else [value]


def _local_name(tag: str) -> str:
    return tag.rsplit("}", 1)[-1]


def _node_text(node: ET.Element | None) -> str:
    return "" if node is None or node.text is None else node.text.strip()


def _optional_node_text(node: ET.Element | None) -> str | None:
    return None if node is None or node.text is None else node.text.strip()


def _flow_reference(value: Any) -> str:
    if isinstance(value, dict):
        return _normalize_text(value.get("id"))
    return _normalize_text(value)


def normalize_state(value: Any) -> str:
    """Translate a source state into a TMT 7.3.51110.1 enum name."""
    state = _normalize_text(value) or "Open"
    try:
        return STATE_ALIASES[state]
    except KeyError as exc:
        raise ThreatContractError(f"unsupported threat state {state}") from exc


def build_entry_key(
    type_id: str,
    source_guid: str,
    flow_guid: str,
    target_guid: str,
) -> str:
    """Build the fixture-confirmed threat dictionary key."""
    return f"{type_id}{source_guid}{flow_guid}{target_guid}"


def build_interaction_key(
    source_guid: str,
    flow_guid: str,
    target_guid: str,
) -> str:
    """Build the fixture-confirmed colon-delimited interaction key."""
    return f"{source_guid}:{flow_guid}:{target_guid}"


def build_threat_instance_properties(
    threat: dict[str, Any],
) -> list[tuple[str, str]]:
    """Build the eight fixture-confirmed threat property entries."""
    supplied = threat.get("properties")
    supplied = supplied if isinstance(supplied, dict) else {}
    return [
        ("Title", _normalize_text(supplied.get("Title") or threat.get("title"))),
        (
            "UserThreatCategory",
            _normalize_text(
                supplied.get("UserThreatCategory") or threat.get("category")
            ),
        ),
        (
            "UserThreatShortDescription",
            _normalize_text(
                supplied.get("UserThreatShortDescription") or threat.get("description")
            ),
        ),
        (
            "UserThreatDescription",
            _normalize_text(
                supplied.get("UserThreatDescription") or threat.get("description")
            ),
        ),
        (
            "InteractionString",
            _normalize_text(
                supplied.get("InteractionString")
                or threat.get("interaction_string")
                or threat.get("interaction_ref")
            ),
        ),
        (
            "PossibleMitigations",
            _normalize_text(
                supplied.get("PossibleMitigations") or threat.get("mitigations")
            ),
        ),
        ("Priority", _normalize_text(supplied.get("Priority")) or "High"),
        ("SDLPhase", _normalize_text(supplied.get("SDLPhase")) or "Design"),
    ]


def _make_guid(identifier: str) -> str:
    """Create a deterministic GUID from a stable identifier.

    Lives here alongside the other identity builders because both generators
    mint GUIDs that must agree for the same identifier. Two independent copies
    of a deterministic identity function is a correctness hazard, not just
    duplication: a divergence would silently renumber every element.
    """
    return str(uuid.uuid5(uuid.NAMESPACE_URL, identifier))


def build_custom_threat_type_id(source_id: str) -> str:
    """Build the deterministic ThreatType identifier for a custom spec threat.

    Both TM7 producers emit the same DataContract, so slug derivation must be
    identical. Runs of characters outside ``a-z0-9`` collapse to a single
    hyphen, which keeps identifiers stable when punctuation abuts a hyphen.
    """
    normalized = _normalize_text(source_id) or "spec-threat"
    slug = (re.sub(r"[^a-z0-9]+", "-", normalized.lower()).strip("-") or "spec-threat")[
        :24
    ]
    digest = hashlib.sha256(normalized.encode("utf-8")).hexdigest()[:16].upper()
    return f"THC-{slug}-{digest}"


def build_mitigation_text(spec: dict[str, Any], threat: dict[str, Any]) -> str:
    """Resolve a threat's declared mitigation identifiers into display text."""
    mitigations = {
        _normalize_text(item.get("id")): item
        for item in _coerce_list(spec.get("mitigations"))
        if isinstance(item, dict) and item.get("id")
    }
    values: list[str] = []
    for mitigation_id in _coerce_list(threat.get("mitigation_ids")):
        mitigation = mitigations.get(_normalize_text(mitigation_id))
        if not isinstance(mitigation, dict):
            continue
        value = _normalize_text(mitigation.get("description") or mitigation.get("name"))
        if value:
            values.append(value)
    return "; ".join(dict.fromkeys(values))


def _surface_metadata(spec: dict[str, Any]) -> list[dict[str, Any]]:
    representations = spec.get("representations")
    if not isinstance(representations, dict):
        return []
    surfaces: list[dict[str, Any]] = []
    for group in (
        "context_diagrams",
        "functional_scenarios",
        "operational_views",
    ):
        for surface in _coerce_list(representations.get(group)):
            if not isinstance(surface, dict):
                continue
            members = surface.get("components") or surface.get("elements") or []
            surfaces.append(
                {
                    "id": _normalize_text(surface.get("id")),
                    "members": {
                        _normalize_text(member.get("id"))
                        for member in members
                        if isinstance(member, dict) and member.get("id")
                    },
                    "flow_ids": {
                        _flow_reference(flow_id)
                        for flow_id in _coerce_list(surface.get("flows"))
                        if _flow_reference(flow_id)
                    },
                }
            )
    return surfaces


def _placement_override(threat: dict[str, Any]) -> tuple[bool, str | None]:
    override = threat.get("placement_override")
    if override is None:
        return False, None
    if not isinstance(override, dict):
        return False, None
    rationale = _normalize_text(override.get("rationale"))
    reviewed = override.get("reviewed")
    if reviewed is True and rationale:
        return True, rationale
    return False, rationale or None


def _surface_matchers(
    spec: dict[str, Any],
    interaction_ref: str,
    source_ref: str,
    target_ref: str,
    threat_target: str,
) -> list[dict[str, Any]]:
    endpoint_targets = {source_ref, target_ref}
    return [
        surface
        for surface in _surface_metadata(spec)
        if interaction_ref in surface["flow_ids"]
        and endpoint_targets.issubset(surface["members"])
        and threat_target in surface["members"]
    ]


def collect_mapping_failures(
    spec: dict[str, Any],
    *,
    expected_count: int | None = None,
) -> list[str]:
    """Collect deterministic source-threat-to-flow topology failures."""
    if not isinstance(spec, dict):
        return ["spec root must be a mapping"]

    raw_flows = _coerce_list(spec.get("data_flows"))
    flow_counts = Counter(
        _normalize_text(flow.get("id"))
        for flow in raw_flows
        if isinstance(flow, dict) and flow.get("id")
    )
    flows = {
        _normalize_text(flow.get("id")): flow
        for flow in raw_flows
        if isinstance(flow, dict) and flow.get("id")
    }
    failures: list[tuple[str, str]] = []
    seen_ids: set[str] = set()

    for ordinal, threat in enumerate(_coerce_list(spec.get("threats")), start=1):
        if not isinstance(threat, dict):
            failures.append((f"~{ordinal:04d}", "threat entry is not a mapping"))
            continue
        threat_id = _normalize_text(threat.get("id"))
        sort_id = threat_id or f"~{ordinal:04d}"
        if not threat_id:
            failures.append((sort_id, "missing threat id"))
            continue
        if threat_id in seen_ids:
            failures.append((sort_id, f"{threat_id}: duplicate threat id"))
            continue
        seen_ids.add(threat_id)

        interaction_ref = _normalize_text(threat.get("interaction_ref"))
        if not interaction_ref:
            failures.append((sort_id, f"{threat_id}: missing interaction_ref"))
            continue
        if flow_counts[interaction_ref] > 1:
            failures.append(
                (sort_id, f"{threat_id}: ambiguous interaction_ref {interaction_ref}")
            )
            continue
        flow = flows.get(interaction_ref)
        if flow is None:
            failures.append(
                (sort_id, f"{threat_id}: unknown interaction_ref {interaction_ref}")
            )
            continue

        source_ref = _normalize_text(flow.get("source_ref"))
        target_ref = _normalize_text(flow.get("target_ref"))
        threat_target = _normalize_text(threat.get("target_ref"))
        if not source_ref or not target_ref:
            failures.append(
                (sort_id, f"{threat_id}: null-linked interaction_ref {interaction_ref}")
            )
            continue
        if not threat_target:
            failures.append((sort_id, f"{threat_id}: missing target_ref"))
            continue

        endpoint_targets = {source_ref, target_ref}
        if threat_target not in endpoint_targets:
            override_is_valid, _ = _placement_override(threat)
            if not override_is_valid:
                failures.append(
                    (
                        sort_id,
                        f"{threat_id}: semantic target {threat_target} is not an "
                        f"endpoint of interaction_ref {interaction_ref} and "
                        "placement_override is required",
                    )
                )
                continue

        matching_surfaces = _surface_matchers(
            spec,
            interaction_ref,
            source_ref,
            target_ref,
            threat_target,
        )
        if len(matching_surfaces) != 1:
            failures.append(
                (
                    sort_id,
                    f"{threat_id}: interaction_ref {interaction_ref} and target_ref "
                    f"{threat_target} resolve to {len(matching_surfaces)} surfaces",
                )
            )

    if expected_count is not None and len(seen_ids) != expected_count:
        failures.append(
            (
                "~~count",
                f"expected {expected_count} unique threat ids, found {len(seen_ids)}",
            )
        )
    return [message for _, message in sorted(failures)]


def collect_gap_citation_failures(
    spec: dict[str, Any],
    registers: dict[str, set[str]],
) -> list[str]:
    """Collect gap citations that do not resolve against a per-skill register.

    Gap ids are scoped per skill and may repeat across skills, so ``G-INF-1``
    names a different weakness in each file that declares it. The citable
    identity is therefore the ``(source_skill, gap_id)`` pair, and a citation
    that omits ``source_skill`` is unresolvable rather than merely incomplete.

    ``registers`` maps a skill name to the gap ids that skill declares.
    """
    if not isinstance(spec, dict):
        return ["spec root must be a mapping"]

    failures: list[tuple[str, str]] = []
    for ordinal, threat in enumerate(_coerce_list(spec.get("threats")), start=1):
        if not isinstance(threat, dict):
            continue
        threat_id = _normalize_text(threat.get("id"))
        sort_id = threat_id or f"~{ordinal:04d}"
        properties = threat.get("properties")
        if not isinstance(properties, dict):
            continue
        gap_ids = [
            _normalize_text(gap) for gap in _coerce_list(properties.get("gap_ids"))
        ]
        gap_ids = [gap for gap in gap_ids if gap]
        if not gap_ids:
            continue

        source_skill = _normalize_text(properties.get("source_skill"))
        if not source_skill:
            failures.append(
                (
                    sort_id,
                    f"{threat_id}: cites {', '.join(sorted(gap_ids))} without a "
                    "source_skill, so the gap ids cannot be resolved",
                )
            )
            continue

        register = registers.get(source_skill)
        if register is None:
            failures.append(
                (
                    sort_id,
                    f"{threat_id}: source_skill {source_skill} has no gap register",
                )
            )
            continue

        for gap_id in sorted(set(gap_ids)):
            if gap_id not in register:
                failures.append(
                    (
                        f"{sort_id}:{gap_id}",
                        f"{threat_id}: {gap_id} is not declared by {source_skill}",
                    )
                )
    return [message for _, message in sorted(failures)]


def _coerce_authored_base_index(authored_base: dict[str, Any]) -> dict[str, Any]:
    """Normalize the one supported authored-base index shape.

    ``populate_tm7_threats._build_authored_base_index`` is the single producer.
    Each surface entry carries the declared surface ``id`` it was resolved to,
    the authored ``guid``, and an ``elements`` map keyed by declared element id.
    """
    return {
        "connectors": authored_base.get("connectors") or {},
        "surfaces": authored_base.get("surfaces") or [],
    }


def _index_authored_surfaces(
    authored_surfaces: Any,
) -> tuple[dict[str, dict[str, Any]], list[str]]:
    """Index authored surfaces by declared identity and report duplicates.

    Authored surfaces that resolve to no declared surface carry an empty id and
    are ignored; a base model may legitimately contain extra diagrams.
    """
    by_id: dict[str, dict[str, Any]] = {}
    duplicates: set[str] = set()
    for entry in _coerce_list(authored_surfaces):
        if not isinstance(entry, dict):
            continue
        surface_id = _normalize_text(entry.get("id"))
        if not surface_id:
            continue
        if surface_id in by_id:
            duplicates.add(surface_id)
            continue
        by_id[surface_id] = entry
    failures = [
        f"authored-base surface {surface_id} is declared more than once"
        for surface_id in sorted(duplicates)
    ]
    return by_id, failures


def reconcile_authored_base(
    spec: dict[str, Any],
    authored_base: dict[str, Any] | None,
) -> list[str]:
    """Reconcile a spec against an authored base model for connector placement."""
    if not isinstance(spec, dict):
        return ["spec root must be a mapping"]
    if authored_base is None:
        return []
    if not isinstance(authored_base, dict):
        return ["authored base must be an index mapping"]

    index = _coerce_authored_base_index(authored_base)
    connectors = index.get("connectors") or {}
    authored_surfaces, failures = _index_authored_surfaces(index.get("surfaces"))
    declared_surfaces = _surface_metadata(spec)
    flows = {
        _normalize_text(flow.get("id")): flow
        for flow in _coerce_list(spec.get("data_flows"))
        if isinstance(flow, dict) and flow.get("id")
    }

    for threat in _coerce_list(spec.get("threats")):
        if not isinstance(threat, dict):
            continue
        threat_id = _normalize_text(threat.get("id")) or "~threat"
        interaction_ref = _normalize_text(threat.get("interaction_ref"))
        if not interaction_ref:
            continue
        flow = flows.get(interaction_ref)
        if flow is None:
            continue
        source_ref = _normalize_text(flow.get("source_ref"))
        target_ref = _normalize_text(flow.get("target_ref"))
        expected_matches = [
            surface
            for surface in declared_surfaces
            if interaction_ref in surface["flow_ids"]
            and {source_ref, target_ref}.issubset(surface["members"])
        ]
        if len(expected_matches) != 1:
            failures.append(
                f"{threat_id}: authored-base expected surface for "
                f"{interaction_ref} is ambiguous"
            )
            continue
        expected_surface_id = _normalize_text(expected_matches[0].get("id"))
        authored_surface = authored_surfaces.get(expected_surface_id)
        if authored_surface is None:
            failures.append(
                f"{threat_id}: authored-base surface "
                f"{expected_surface_id or '~unidentified'} for {interaction_ref} "
                "is absent"
            )
            continue
        expected_surface_guid = _normalize_text(authored_surface.get("guid"))

        raw_connectors = connectors.get(interaction_ref)
        candidates = (
            raw_connectors
            if isinstance(raw_connectors, list)
            else [raw_connectors]
            if isinstance(raw_connectors, dict)
            else []
        )
        connector = next(
            (
                candidate
                for candidate in candidates
                if _normalize_text(candidate.get("drawing_surface_guid"))
                == expected_surface_guid
            ),
            None,
        )
        if connector is None:
            if candidates:
                failures.append(
                    f"{threat_id}: authored-base surface mismatch for {interaction_ref}"
                )
            else:
                failures.append(
                    f"{threat_id}: authored-base connector {interaction_ref} is absent"
                )
            continue
        guid_values = [
            expected_surface_guid,
            _normalize_text(connector.get("flow_guid")),
            _normalize_text(connector.get("source_guid")),
            _normalize_text(connector.get("target_guid")),
        ]
        if any(not value or value == NULL_GUID for value in guid_values):
            failures.append(
                f"{threat_id}: authored-base connector {interaction_ref} has null GUIDs"
            )
            continue

        surface_elements = authored_surface.get("elements") or {}
        endpoint_guid_map = {
            source_ref: surface_elements.get(source_ref),
            target_ref: surface_elements.get(target_ref),
        }
        if not all(endpoint_guid_map.values()):
            failures.append(
                f"{threat_id}: authored-base connector {interaction_ref} "
                "endpoint identity is unavailable"
            )
            continue
        if (
            connector.get("source_guid") != endpoint_guid_map[source_ref]
            or connector.get("target_guid") != endpoint_guid_map[target_ref]
        ):
            failures.append(
                f"{threat_id}: authored-base connector {interaction_ref} "
                "endpoint identity mismatch"
            )

    return failures


def validate_serializable_threat_node(threat_node: ET.Element) -> None:
    """Validate the exact member ordering used by the TM7 threat DataContract."""
    member_order = [_local_name(child.tag) for child in threat_node]
    if member_order != THREAT_MEMBER_ORDER:
        raise ThreatContractError(
            f"threat member order {member_order} does not match the contract"
        )


def validate_threat_instance(
    threat: dict[str, Any],
    *,
    type_ids: set[str] | None = None,
) -> None:
    """Validate one fully resolved threat-instance payload."""
    if not isinstance(threat, dict):
        raise ThreatContractError("threat payload must be a mapping")
    threat_id = threat.get("id")
    if (
        not isinstance(threat_id, int)
        or isinstance(threat_id, bool)
        or not THREAT_ID_MIN <= threat_id <= THREAT_ID_MAX
    ):
        raise ThreatContractError(
            f"threat id must be an integer in [{THREAT_ID_MIN}, {THREAT_ID_MAX}]"
        )

    if not _normalize_text(threat.get("interaction_ref")):
        raise ThreatContractError("threat interaction_ref is required")

    type_id = _normalize_text(threat.get("type_id"))
    if not type_id:
        raise ThreatContractError("threat type_id is required")
    if type_ids is not None and type_id not in type_ids:
        raise ThreatContractError(
            f"threat type_id {type_id} is not embedded in KnowledgeBase"
        )

    guids = [
        _normalize_text(threat.get(name))
        for name in (
            "drawing_surface_guid",
            "source_guid",
            "flow_guid",
            "target_guid",
        )
    ]
    if any(not guid or guid == NULL_GUID for guid in guids):
        raise ThreatContractError("threat guid members must be non-null")

    expected_interaction = build_interaction_key(*guids[1:])
    if _normalize_text(threat.get("interaction_key")) != expected_interaction:
        raise ThreatContractError("interaction_key does not match the GUID tuple")
    expected_dictionary = build_entry_key(type_id, *guids[1:])
    if _normalize_text(threat.get("dictionary_key")) != expected_dictionary:
        raise ThreatContractError("dictionary_key does not match the threat contract")
    normalize_state(threat.get("state"))


def derive_threat_numeric_id(source_id: str) -> int:
    """Derive the stable TMT-visible numeric id for a normalized source id."""
    if not source_id:
        raise ThreatContractError("source threat id is required to derive an id")
    digest = hashlib.new(THREAT_ID_DIGEST, source_id.encode("utf-8")).digest()
    span = THREAT_ID_MAX - THREAT_ID_MIN + 1
    return THREAT_ID_MIN + int.from_bytes(digest, "big") % span


def prepare_threat_instances(
    threats: list[dict[str, Any]],
    *,
    type_ids: set[str] | None = None,
) -> list[dict[str, Any]]:
    """Assign stable numeric IDs and validate collection-wide uniqueness."""
    prepared: list[dict[str, Any]] = []
    seen_source_ids: set[str] = set()
    seen_keys: set[str] = set()
    numeric_owners: dict[int, str] = {}
    for threat in sorted(
        threats, key=lambda item: _normalize_text(item.get("source_id"))
    ):
        source_id = _normalize_text(threat.get("source_id"))
        if not source_id or source_id in seen_source_ids:
            raise ThreatContractError(
                f"duplicate or missing source threat id {source_id}"
            )
        seen_source_ids.add(source_id)
        numeric_id = derive_threat_numeric_id(source_id)
        collided_with = numeric_owners.get(numeric_id)
        if collided_with is not None:
            raise ThreatContractError(
                f"threat id collision: {collided_with} and {source_id} both "
                f"derive numeric id {numeric_id}"
            )
        numeric_owners[numeric_id] = source_id
        resolved = {
            **threat,
            "id": numeric_id,
            "state": normalize_state(threat.get("state")),
        }
        validate_threat_instance(resolved, type_ids=type_ids)
        dictionary_key = _normalize_text(resolved.get("dictionary_key"))
        if dictionary_key in seen_keys:
            raise ThreatContractError(f"duplicate dictionary key for {source_id}")
        seen_keys.add(dictionary_key)
        prepared.append(resolved)
    return prepared


def _add_value(parent: ET.Element, name: str, value: str | None) -> ET.Element:
    node = ET.SubElement(parent, f"{{{KNOWLEDGE_NS}}}{name}")
    if value is None:
        node.set(f"{{{XSI_NS}}}nil", "true")
    else:
        node.text = value
    return node


def serialize_threat_instance(threat: dict[str, Any]) -> ET.Element:
    """Serialize one validated threat value in exact DataContract member order."""
    validate_threat_instance(threat)
    value = ET.Element(f"{{{ARRAYS_NS}}}Value")
    _add_value(value, "ChangedBy", None)
    _add_value(value, "DrawingSurfaceGuid", threat["drawing_surface_guid"])
    _add_value(value, "FlowGuid", threat["flow_guid"])
    _add_value(value, "Id", str(threat["id"]))
    _add_value(value, "InteractionKey", threat["interaction_key"])
    _add_value(value, "InteractionString", None)
    _add_value(value, "ModifiedAt", "0001-01-01T00:00:00")
    _add_value(value, "Priority", "High")
    properties = _add_value(value, "Properties", "")
    properties.text = None
    for property_key, property_value in build_threat_instance_properties(threat):
        item = ET.SubElement(properties, f"{{{ARRAYS_NS}}}KeyValueOfstringstring")
        ET.SubElement(item, f"{{{ARRAYS_NS}}}Key").text = property_key
        ET.SubElement(item, f"{{{ARRAYS_NS}}}Value").text = property_value
    _add_value(value, "SourceGuid", threat["source_guid"])
    _add_value(value, "State", threat["state"])
    _add_value(value, "StateInformation", None)
    _add_value(value, "TargetGuid", threat["target_guid"])
    _add_value(value, "Title", None)
    _add_value(value, "TypeId", threat["type_id"])
    _add_value(value, "Upgraded", "false")
    _add_value(value, "UserThreatCategory", None)
    _add_value(value, "UserThreatDescription", None)
    _add_value(value, "UserThreatShortDescription", None)
    _add_value(value, "Wide", "false")
    return value


def serialize_threat_instances(
    root: ET.Element,
    threats: list[dict[str, Any]],
    *,
    type_ids: set[str] | None = None,
) -> list[dict[str, Any]]:
    """Replace the root ThreatInstances member with validated entries."""
    prepared = prepare_threat_instances(threats, type_ids=type_ids)
    existing = next(
        (child for child in root if _local_name(child.tag) == "ThreatInstances"),
        None,
    )
    if existing is None:
        threat_instances = ET.Element("ThreatInstances")
        insert_at = next(
            (
                index
                for index, child in enumerate(root)
                if _local_name(child.tag) == "ThreatGenerationEnabled"
            ),
            len(root),
        )
        root.insert(insert_at, threat_instances)
    else:
        threat_instances = existing
        threat_instances.clear()

    for threat in prepared:
        item = ET.SubElement(
            threat_instances,
            f"{{{ARRAYS_NS}}}KeyValueOfstringThreatpc_P0_PhOB",
        )
        ET.SubElement(item, f"{{{ARRAYS_NS}}}Key").text = threat["dictionary_key"]
        item.append(serialize_threat_instance(threat))
    return prepared


def extract_serializable_threat(threat_node: ET.Element) -> dict[str, Any]:
    """Extract a serialized threat value for contract regression assertions."""
    validate_serializable_threat_node(threat_node)
    child_map = {_local_name(child.tag): child for child in threat_node}
    properties: dict[str, str] = {}
    properties_node = child_map.get("Properties")
    if properties_node is not None:
        for item in properties_node.findall("{*}KeyValueOfstringstring"):
            properties[_node_text(item.find("{*}Key"))] = _node_text(
                item.find("{*}Value")
            )
    content_fields = [
        "Title",
        "UserThreatCategory",
        "UserThreatShortDescription",
        "UserThreatDescription",
        "InteractionString",
        "StateInformation",
    ]
    return {
        "member_order": [_local_name(child.tag) for child in threat_node],
        "id": _node_text(child_map.get("Id")),
        "type_id": _node_text(child_map.get("TypeId")),
        "drawing_surface_guid": _node_text(child_map.get("DrawingSurfaceGuid")),
        "source_guid": _node_text(child_map.get("SourceGuid")),
        "flow_guid": _node_text(child_map.get("FlowGuid")),
        "target_guid": _node_text(child_map.get("TargetGuid")),
        "interaction_key": _node_text(child_map.get("InteractionKey")),
        "state": _node_text(child_map.get("State")),
        "properties": properties,
        "top_level_content": {
            field: _optional_node_text(child_map.get(field)) for field in content_fields
        },
    }


def validate_serialized_threat_entry(
    entry: ET.Element,
    *,
    type_ids: set[str] | None = None,
) -> dict[str, Any]:
    """Validate one fixture-shaped threat dictionary entry."""
    key_node = entry.find("{*}Key")
    value_node = entry.find("{*}Value")
    if key_node is None or value_node is None:
        raise ThreatContractError("threat entry must contain Key and Value")

    extracted = extract_serializable_threat(value_node)
    if extracted["member_order"] != THREAT_MEMBER_ORDER:
        raise ThreatContractError("threat members are not in DataContract order")

    threat_id = str(extracted["id"])
    if not threat_id.isdigit() or int(threat_id) <= 0:
        raise ThreatContractError("threat id must be a positive integer")

    type_id = str(extracted["type_id"])
    if not type_id:
        raise ThreatContractError("threat type_id is required")
    if type_ids is not None and type_id not in type_ids:
        raise ThreatContractError(
            f"threat type_id {type_id} is not embedded in KnowledgeBase"
        )

    guids = [
        str(extracted[name])
        for name in (
            "drawing_surface_guid",
            "source_guid",
            "flow_guid",
            "target_guid",
        )
    ]
    if any(not guid or guid == NULL_GUID for guid in guids):
        raise ThreatContractError("threat guid members must be non-null")

    expected_interaction = build_interaction_key(*guids[1:])
    if extracted["interaction_key"] != expected_interaction:
        raise ThreatContractError("interaction_key does not match the GUID tuple")
    expected_key = build_entry_key(type_id, *guids[1:])
    if _node_text(key_node) != expected_key:
        raise ThreatContractError("dictionary_key does not match the threat contract")

    if list(extracted["properties"]) != THREAT_PROPERTY_KEYS:
        raise ThreatContractError("threat properties do not match fixture order")
    if str(extracted["state"]) not in set(STATE_ALIASES.values()):
        raise ThreatContractError(f"unsupported threat state {extracted['state']}")
    if any(value is not None for value in extracted["top_level_content"].values()):
        raise ThreatContractError("top-level threat content fields must be nil")
    return extracted


def validate_serialized_threat_entries(
    threat_instances: ET.Element,
    *,
    type_ids: set[str] | None = None,
) -> list[dict[str, Any]]:
    """Validate all serialized entries and collection-wide uniqueness."""
    extracted_entries: list[dict[str, Any]] = []
    seen_ids: set[str] = set()
    seen_keys: set[str] = set()
    for entry in threat_instances.findall("{*}KeyValueOfstringThreatpc_P0_PhOB"):
        extracted = validate_serialized_threat_entry(entry, type_ids=type_ids)
        threat_id = str(extracted["id"])
        key = _node_text(entry.find("{*}Key"))
        if threat_id in seen_ids:
            raise ThreatContractError(f"duplicate numeric threat id {threat_id}")
        if key in seen_keys:
            raise ThreatContractError("duplicate threat dictionary key")
        seen_ids.add(threat_id)
        seen_keys.add(key)
        extracted_entries.append(extracted)
    return extracted_entries

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 or Microsoft.

Download ZIP

The ZIP holds the skill’s files, its licence files and an ATTRIBUTION.md. Download security-planning.zip