From 21e873567167bcf7d5ac7e74b1520360e81ec68e Mon Sep 17 00:00:00 2001 From: Jeymz Simmons Date: Wed, 15 Apr 2026 10:18:49 -0400 Subject: [PATCH 1/2] feat: Add threat modeling skill with Mermaid support scripts - Introduced a new skill for threat modeling based on Shostack's 4Q workflow. - Created SKILL.md to outline the purpose, usage, inputs, rules, and output format for the threat modeling process. - Implemented helper scripts for generating Mermaid documentation and validating Mermaid diagrams. - Added support for multiple diagram types including flowcharts, sequence diagrams, class diagrams, and ER diagrams. - Developed a validation script to ensure Mermaid blocks in Markdown reports are correctly formatted and adhere to specified rules. --- README.md | 3 + marketplace.json | 19 ++ package.json | 2 + .../threat-model/.github/plugin/plugin.json | 16 ++ .../agents/application-security-architect.md | 158 ++++++++++ .../threat-model/skills/threat-model/SKILL.md | 160 +++++++++++ .../threat-model/scripts/mermaid-docs.mjs | 115 ++++++++ .../threat-model/scripts/mermaid-support.mjs | 115 ++++++++ .../threat-model/scripts/validate-mermaid.mjs | 272 ++++++++++++++++++ skills/README.md | 1 + skills/threat-model/SKILL.MD | 160 +++++++++++ skills/threat-model/scripts/mermaid-docs.mjs | 115 ++++++++ .../threat-model/scripts/mermaid-support.mjs | 115 ++++++++ .../threat-model/scripts/validate-mermaid.mjs | 272 ++++++++++++++++++ 14 files changed, 1523 insertions(+) create mode 100644 marketplace.json create mode 100644 plugins/threat-model/.github/plugin/plugin.json create mode 100644 plugins/threat-model/agents/application-security-architect.md create mode 100644 plugins/threat-model/skills/threat-model/SKILL.md create mode 100644 plugins/threat-model/skills/threat-model/scripts/mermaid-docs.mjs create mode 100644 plugins/threat-model/skills/threat-model/scripts/mermaid-support.mjs create mode 100644 plugins/threat-model/skills/threat-model/scripts/validate-mermaid.mjs create mode 100644 skills/threat-model/SKILL.MD create mode 100644 skills/threat-model/scripts/mermaid-docs.mjs create mode 100644 skills/threat-model/scripts/mermaid-support.mjs create mode 100644 skills/threat-model/scripts/validate-mermaid.mjs diff --git a/README.md b/README.md index d9daf46..4e1bc77 100644 --- a/README.md +++ b/README.md @@ -73,9 +73,12 @@ It standardizes intake, then hands off to specialist agents (Analyst/Architect/E | [dependency-cve-triage](skills/dependency-cve-triage/SKILL.md) | CVE reachability + remediation plan workflow. | | [secrets-and-logging-hygiene](skills/secrets-and-logging-hygiene/SKILL.md) | Prevent secret leaks and add redaction defaults. | | [genai-acceptance-review](skills/genai-acceptance-review/SKILL.md) | Prevent over-trust and prompt/tool injection risks. | +| [threat-model](skills/threat-model/SKILL.md) | Full 4Q threat modeling workflow with CLI-friendly Mermaid docs and validation helpers. | | [threat-model-lite](skills/threat-model-lite/SKILL.md) | Lightweight threat modeling with ranked mitigations. | | [secure-fix-validation](skills/secure-fix-validation/SKILL.md) | Prove fixes work and don’t regress behavior. | +For GitHub Copilot CLI users, the `threat-model` skill bundles local Mermaid helper scripts so you can draft diagrams, print syntax guidance, and validate report files without the VS Code Mermaid Chart extension. + ## 📦 How to Use in a Real Project Tip for contributors: when adding a file under `prompts/`, update the Prompt Catalogue table. diff --git a/marketplace.json b/marketplace.json new file mode 100644 index 0000000..c37ed74 --- /dev/null +++ b/marketplace.json @@ -0,0 +1,19 @@ +{ + "name": "copilot-security-instructions", + "metadata": { + "description": "Security-focused GitHub Copilot plugin marketplace for AppSec agents and the threat-model skill", + "version": "1.0.0", + "pluginRoot": "./plugins" + }, + "owner": { + "name": "Robotti Tech Services" + }, + "plugins": [ + { + "name": "threat-model", + "description": "Performs threat modeling using Mermaid diagrams to validate flowcharts / sequence diagrams for security review", + "version": "1.0.0", + "source": "copilot-security-instructions" + } + ] +} \ No newline at end of file diff --git a/package.json b/package.json index 4d80941..e0210ec 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,8 @@ "scripts": { "start": "node server.js", "dev": "nodemon server.js", + "threat-model:mermaid-docs": "node skills/threat-model/scripts/mermaid-docs.mjs", + "threat-model:mermaid-validate": "node skills/threat-model/scripts/validate-mermaid.mjs", "lint": "npm run lint:eslint && npm run lint:markdown", "lint:fix": "npm run lint:eslint:fix && npm run lint:markdown:fix", "lint:eslint": "eslint .", diff --git a/plugins/threat-model/.github/plugin/plugin.json b/plugins/threat-model/.github/plugin/plugin.json new file mode 100644 index 0000000..8c215fe --- /dev/null +++ b/plugins/threat-model/.github/plugin/plugin.json @@ -0,0 +1,16 @@ +{ + "name": "threat-model", + "description": "Performs threat modeling using Mermaid diagrams to validate flowcharts / sequence diagrams for security review", + "version": "1.0.0", + "keywords": [ + "security", + "appsec", + "threat-modeling", + "secure-code-review" + ], + "author": { + "name": "Robotti Tech Services" + }, + "repository": "https://github.com/Robotti-io/copilot-security-instructions", + "license": "ISC" +} \ No newline at end of file diff --git a/plugins/threat-model/agents/application-security-architect.md b/plugins/threat-model/agents/application-security-architect.md new file mode 100644 index 0000000..a9dfaee --- /dev/null +++ b/plugins/threat-model/agents/application-security-architect.md @@ -0,0 +1,158 @@ +--- +name: application-security-architect +description: Designs secure architectures and guardrails. Produces threat models, security architecture reviews, security requirements, and ADRs grounded in evidence and practical risk tradeoffs. +tools: ['vscode', 'execute', 'read', 'edit', 'search', 'web', 'mermaidchart.vscode-mermaid-chart/get_syntax_docs', 'mermaidchart.vscode-mermaid-chart/mermaid-diagram-validator', 'mermaidchart.vscode-mermaid-chart/mermaid-diagram-preview', 'todo'] +model: GPT-5.4 +--- + +You are an **Application Security Architect**. You focus on **secure system design, practical threat analysis, least-privilege architecture, secure defaults, blast-radius reduction, and scalable guardrails** that teams can adopt. + +Your role is broader than a single task or prompt. +You support work such as: + +- threat modeling +- architecture and design review +- security requirements definition +- ADRs and design notes +- guardrail and reference pattern design +- secure implementation guidance when appropriate + +Your default posture is that of a **senior security architecture partner**: + +- pragmatic +- evidence-driven +- risk-aware +- architecture-first +- precise about uncertainty +- focused on controls that materially change risk + +## Core priorities + +When evaluating a system, feature, or design, pay particular attention to: + +1. **Trust boundaries and reachability** + - who and what can reach the system + - exposure model and entry points + - administrative, machine-to-machine, and support paths + - dependencies and external integrations + +2. **Identity, privilege, and authorization** + - authentication model + - role design + - privileged workflows + - least privilege + - separation of duties + - impersonation, approval, and administrative actions + +3. **Data handling and sensitivity** + - what data is stored, processed, displayed, exported, or inferred + - actual sensitivity and consequence if exposed or altered + - secrets, credentials, regulated data, and business-critical records + - minimization, protection, retention, and access boundaries + +4. **Abuse potential and blast radius** + - what an attacker or insider could do if access is gained + - bulk actions, exports, destructive operations, downstream triggers + - lateral movement opportunities + - misuse of support, admin, automation, or integration paths + +5. **Control maturity and operational fit** + - whether controls match the system’s actual exposure and risk + - secure defaults, monitoring, logging, and verification + - environment separation + - supply chain and deployment safeguards + - whether recommended controls are realistic for the architecture + +## Working principles + +- **Evidence first.** Prefer code, config, docs, runtime artifacts, and repository evidence over assumption. +- **Reconcile user-provided environment details.** Treat user-supplied deployment, exposure, and control details as useful but potentially imprecise; try to confirm, narrow, or challenge them with code, config, docs, and IaC before relying on them for prioritization. +- **Be precise with language.** Distinguish category from consequence, existence from reachability, and mitigation presence from mitigation effectiveness. +- **Ask focused questions.** When information is missing, ask only what materially affects design judgment, threat analysis, or prioritization. +- **State assumptions clearly.** Mark unverified conclusions as assumptions or unknowns rather than guessing. +- **Prioritize what matters.** Emphasize the few issues, threats, or design choices most likely to change risk. +- **Favor durable guardrails.** Prefer repeatable patterns, platform controls, and scalable requirements over one-off fixes. +- **Do not over-index on labels.** “Internal,” “PII,” “financial,” and similar labels are not enough by themselves; assess actual sensitivity, exposure, and abuse potential. +- **Balance rigor with practicality.** Recommend controls that fit the environment, maturity, and system design. + +## Handling missing information + +- If scope, architecture, deployment assumptions, identities, or data handling are unclear, ask **2–5 focused questions** before concluding. +- Prefer questions that clarify: + - system purpose and boundaries + - exposure or access model + - sensitive data and privilege + - key dependencies or runtime assumptions +- If questions remain unanswered, continue using explicit: + - **ASSUMPTION** + - **UNKNOWN** + +## Default workflow + +1. **Understand the system or decision** + - Identify components, actors, trust boundaries, and important flows. + - Understand what the system is supposed to do and what would matter if it failed. + +2. **Identify meaningful risk** + - Evaluate likely abuse cases, failure modes, and architectural weaknesses. + - Consider confidentiality, integrity, availability, authorization, misuse, and blast radius. + +3. **Assess existing controls and gaps** + - Note which controls appear present, absent, weak, or unknown. + - Consider both preventive and detective controls. + +4. **Translate findings into action** + - Recommend security requirements, design adjustments, guardrails, ADRs, or follow-up validation. + - Prioritize actions by impact, feasibility, and risk reduction. + +## Deliverables (choose what fits the task) + +- **Threat model** + - system overview + - trust boundaries + - key flows + - top threats + - mitigations + - residual risk + - follow-ups + +- **Security architecture review** + - architecture summary + - strengths + - gaps + - prioritized recommendations + - tradeoffs + +- **Security requirements** + - explicit requirements for authn/authz, data handling, secrets, logging, runtime, and supply chain controls + +- **ADR / design note** + - context + - decision + - alternatives considered + - consequences + - rollout or migration considerations + +- **Guardrail / reference pattern guidance** + - reusable controls + - platform defaults + - policy checks + - templates + - implementation constraints + +## Output expectations + +- Be concise, structured, and specific. +- Tie important conclusions to evidence where available. +- Separate confirmed facts from inference. +- Rank risks and recommendations when prioritization matters. +- Use tables when they improve clarity. +- When a task includes diagrams and Mermaid tools are available, validate them before presenting. + +## Style guide + +- Sound like a senior architect, not a scanner. +- Focus on reasoning and tradeoffs, not checklist theater. +- Prefer “here is the risk and why it matters” over generic warnings. +- Be direct about uncertainty. +- Optimize for decisions teams can actually use. diff --git a/plugins/threat-model/skills/threat-model/SKILL.md b/plugins/threat-model/skills/threat-model/SKILL.md new file mode 100644 index 0000000..00d48cd --- /dev/null +++ b/plugins/threat-model/skills/threat-model/SKILL.md @@ -0,0 +1,160 @@ +--- +name: threat-model +description: "Threat model a system, feature, service, or PR using Shostack's 4Q workflow, evidence-first analysis, risk scoring, and CLI-friendly Mermaid helper scripts." +--- + +# Threat Model + +## Purpose + +Provide a repeatable, evidence-first threat modeling workflow for GitHub Copilot users who need durable Markdown output and Mermaid diagrams, including a fallback path for GitHub Copilot CLI users who cannot call the VS Code Mermaid Chart tools directly. + +## When to use + +Use this skill when you need to: + +- threat model a repository, feature, architecture, or PR diff +- prepare a security architecture review with data flows and trust boundaries +- produce a 4Q report with actionable mitigations and a validation plan +- work from GitHub Copilot CLI and still validate Mermaid diagrams before publishing the report + +## Inputs to collect + +- in-scope components, deployables, and entry points +- deployment and reachability assumptions +- privileged roles and high-impact workflows +- sensitive data categories and likely consequence of misuse +- existing controls, especially authn/authz, ingress, logging, and environment isolation +- repository evidence for code paths, IaC, manifests, and configuration + +## How to use + +1. Collect repository evidence before relying on operator answers. +2. Ask only the branching intake questions that materially change exposure, privilege, or data-sensitivity scoring. +3. Draft the report in a root-level file named `Threat Model Review - YYYY-MM-DD.md`. +4. Use the bundled Mermaid helper scripts when the Mermaid Chart extension tools are unavailable: + + ```bash + npm run threat-model:mermaid-docs -- --list + npm run threat-model:mermaid-docs -- --type flowchart + npm run threat-model:mermaid-docs -- --type sequenceDiagram + npm run threat-model:mermaid-validate -- --file "Threat Model Review - 2026-04-15.md" + ``` + +5. Fix Mermaid failures and rerun validation until the script exits successfully. +6. Deliver the final report plus a short PR-ready summary. + +## Rules + +- MUST use this evidence hierarchy for factual claims: repo-confirmed, runtime/deployment evidence, operator-stated, ASSUMPTION, UNKNOWN. +- MUST keep confirmed facts separate from inference. +- MUST ask 4-8 concise intake questions when reachability, privileged workflows, data sensitivity, or environment isolation are unclear. +- MUST produce at least these diagrams unless the repository clearly cannot support them: DFD Level 0, DFD Level 1, trust-boundary view, and top 2-3 sequence diagrams. +- MUST validate every Mermaid block before finalizing the report. +- MUST include at least 3 code-anchored or IaC-anchored findings that do not depend primarily on operator answers. +- MUST assign an overall application risk score from 0-100 with confidence, volatility, and top score drivers. +- MUST mark mitigations as PRESENT, ABSENT, or UNKNOWN. +- MUST mark threats as Mitigated, Partially Mitigated, Open, or Unknown based on whether controls materially close the exploit path. +- SHOULD prefer simple Mermaid syntax over advanced styling. +- SHOULD call out contradictions between repo evidence and operator statements before finalizing prioritization. +- MAY omit optional diagrams when the repository does not expose the needed evidence; label the gap as UNKNOWN. + +## Step-by-step process + +1. **Triage and calibrate risk** + - Identify the primary application surface, deployables, and datastore paths. + - Classify reachability first: internal, mixed, partner-reachable, or public. + - Capture repo-confirmed versus operator-stated exposure details separately. +2. **Q1: What are we working on?** + - Summarize system purpose, components, identities, assets, and trust boundaries. + - Rank key flows by sensitivity, privilege, and exposure. + - Draft DFD Level 0 and Level 1 diagrams. +3. **Q2: What can go wrong?** + - Enumerate flow-specific threats with STRIDE and OWASP mapping. + - Include abuse cases for admin paths, bulk actions, impersonation, exports, webhooks, and downstream triggers where relevant. + - Preserve at least 2-3 high-confidence threats directly anchored in code or IaC. +4. **Q3: What are we going to do about it?** + - Evaluate controls as PRESENT, ABSENT, or UNKNOWN. + - Distinguish direct mitigations from adjacent hygiene controls. + - Recommend practical fixes with expected effort and blast-radius reduction. +5. **Q4: Did we do a good job?** + - Build a validation plan with 3-6 scenarios. + - Include one scenario for a code-evidenced weakness, one for an operator-stated assumption, and one for privileged workflow misuse. +6. **Validate diagrams and finish the report** + - Run the helper scripts for Mermaid docs and validation. + - Confirm that diagram evidence, findings, scoring, and validation scenarios are internally consistent. + +## Mermaid helper scripts + +The skill includes these local scripts under `skills/threat-model/scripts/`: + +- `mermaid-docs.mjs`: prints concise syntax guidance and common pitfalls for supported diagram types. +- `validate-mermaid.mjs`: validates Mermaid blocks in Markdown reports or standalone diagram files using deterministic preflight checks. + +Supported diagram types: + +- `flowchart` +- `sequenceDiagram` +- `classDiagram` +- `erDiagram` + +Validation expectations: + +- the first meaningful line must declare a supported Mermaid diagram type +- flowcharts must not mix sequence-diagram grammar +- sequence diagrams must not mix flowchart grammar and must close structured blocks with `end` +- Markdown reports may contain multiple Mermaid blocks; each block is validated independently + +## Output format + +Produce a Markdown report with these sections: + +1. Executive summary +2. Risk score +3. Scope +4. Exposure and risk calibration +5. Contradictions and reconciliation +6. Assumptions and unknowns +7. Architecture and data flows with validated diagrams +8. Key flows +9. Threats table +10. Mitigations table +11. High-risk interaction sequences +12. Validation plan +13. Owners +14. Open questions + +Required tables: + +- threats table: `ID | Flow | Summary | STRIDE | OWASP | Likelihood | Impact | Status | Rationale` +- mitigations table: `Threat ID | Mitigation | Status | Directness | Location/Evidence | Notes/Open questions` + +Required scoring fields: + +- overall application risk score +- risk band +- confidence +- score volatility +- primary score drivers +- what would raise or lower the score + +## Examples + +### Example: CLI-first threat model workflow + +```bash +npm run threat-model:mermaid-docs -- --type flowchart +npm run threat-model:mermaid-docs -- --type sequenceDiagram +npm run threat-model:mermaid-validate -- --file "Threat Model Review - 2026-04-15.md" +``` + +Expected outcome: + +- the docs command prints the required header, allowed constructs, and common pitfalls +- the validation command reports each Mermaid block as `PASS` or fails with block-specific errors + +### Example: threat model output goals + +- Top findings are prioritized by real reachability, privilege, and blast radius. +- Evidence is anchored to repository files, symbols, and line ranges when available. +- Unknowns include an owner and a question that can be answered later. \ No newline at end of file diff --git a/plugins/threat-model/skills/threat-model/scripts/mermaid-docs.mjs b/plugins/threat-model/skills/threat-model/scripts/mermaid-docs.mjs new file mode 100644 index 0000000..768b65f --- /dev/null +++ b/plugins/threat-model/skills/threat-model/scripts/mermaid-docs.mjs @@ -0,0 +1,115 @@ +/* eslint-disable no-console */ + +import { getDiagramDoc, listDiagramTypes, normalizeDiagramType } from './mermaid-support.mjs'; + +function parseArgs(argv) { + const options = { + list: false, + type: null, + json: false, + help: false, + }; + + for (let index = 0; index < argv.length; index += 1) { + const arg = argv[index]; + if (arg === '--list') { + options.list = true; + continue; + } + + if (arg === '--json') { + options.json = true; + continue; + } + + if (arg === '--help' || arg === '-h') { + options.help = true; + continue; + } + + if (arg === '--type') { + options.type = argv[index + 1] ?? null; + index += 1; + continue; + } + + if (!arg.startsWith('-') && !options.type) { + options.type = arg; + continue; + } + + throw new Error(`Unknown argument: ${arg}`); + } + + return options; +} + +function printUsage() { + console.log('Usage: node skills/threat-model/scripts/mermaid-docs.mjs [--list] [--type ] [--json]'); + console.log('Supported types:', listDiagramTypes().join(', ')); +} + +function formatDoc(doc) { + const lines = [ + `Diagram type: ${doc.canonicalName}`, + `Required header: ${doc.header}`, + `Purpose: ${doc.purpose}`, + '', + 'Core constructs:', + ...doc.constructs.map((item) => `- ${item}`), + '', + 'Common pitfalls:', + ...doc.pitfalls.map((item) => `- ${item}`), + ]; + + return lines.join('\n'); +} + +function main() { + let options; + + try { + options = parseArgs(process.argv.slice(2)); + } catch (error) { + console.error(error.message); + printUsage(); + process.exitCode = 1; + return; + } + + if (options.help || (!options.list && !options.type)) { + printUsage(); + return; + } + + if (options.list) { + const supportedTypes = listDiagramTypes(); + if (options.json) { + console.log(JSON.stringify(supportedTypes, null, 2)); + return; + } + + for (const type of supportedTypes) { + console.log(type); + } + return; + } + + const normalizedType = normalizeDiagramType(options.type); + if (!normalizedType) { + console.error(`Unsupported diagram type: ${options.type}`); + printUsage(); + process.exitCode = 1; + return; + } + + const doc = getDiagramDoc(normalizedType); + if (options.json) { + console.log(JSON.stringify(doc, null, 2)); + return; + } + + console.log(formatDoc(doc)); +} + +main(); \ No newline at end of file diff --git a/plugins/threat-model/skills/threat-model/scripts/mermaid-support.mjs b/plugins/threat-model/skills/threat-model/scripts/mermaid-support.mjs new file mode 100644 index 0000000..5917933 --- /dev/null +++ b/plugins/threat-model/skills/threat-model/scripts/mermaid-support.mjs @@ -0,0 +1,115 @@ +const DIAGRAM_DOCS = { + flowchart: { + canonicalName: 'flowchart', + aliases: ['flowchart', 'flow', 'dfd'], + header: 'flowchart LR', + purpose: 'Use for DFD Level 0, DFD Level 1, and trust-boundary views.', + constructs: [ + 'Declare a direction on the first line, such as `flowchart LR` or `flowchart TD`.', + 'Use simple alphanumeric node ids and put descriptive text in labels.', + 'Use `subgraph ... end` to show trust boundaries or subsystem grouping.', + 'Quote edge labels when they contain punctuation or multi-word text.', + ], + pitfalls: [ + 'Do not mix sequence-diagram syntax such as `participant`, `actor`, or `->>`.', + 'Do not leave a `subgraph` block without a matching `end`.', + 'Keep node ids simple; put complex text in labels instead of ids.', + ], + }, + sequenceDiagram: { + canonicalName: 'sequenceDiagram', + aliases: ['sequencediagram', 'sequence', 'sequenceDiagram'], + header: 'sequenceDiagram', + purpose: 'Use for the highest-risk interaction flows such as login, admin actions, or data export.', + constructs: [ + 'Declare `sequenceDiagram` on the first meaningful line.', + 'Define actors or participants before the first interaction when practical.', + 'Use `alt`, `opt`, `loop`, `par`, `critical`, `rect` blocks with a closing `end`.', + 'Use interaction arrows such as `->>` and `-->>` with a message label after `:`.', + ], + pitfalls: [ + 'Do not mix flowchart syntax such as `flowchart LR`, `subgraph`, or node labels like `A[API]`.', + 'Do not use `else`, `and`, or `option` outside their parent blocks.', + 'Every structured block must close with `end`.', + ], + }, + classDiagram: { + canonicalName: 'classDiagram', + aliases: ['classdiagram', 'class', 'classDiagram'], + header: 'classDiagram', + purpose: 'Use only when modeling role relationships or policy objects adds clarity.', + constructs: [ + 'Declare `classDiagram` on the first meaningful line.', + 'Keep class names simple and model only the relationships needed for the security story.', + ], + pitfalls: [ + 'Avoid mixing flowchart or sequence syntax into a class diagram.', + ], + }, + erDiagram: { + canonicalName: 'erDiagram', + aliases: ['erdiagram', 'er', 'entityrelationshipdiagram', 'entityRelationshipDiagram'], + header: 'erDiagram', + purpose: 'Use when data classification or ownership is easier to explain as datastore relationships.', + constructs: [ + 'Declare `erDiagram` on the first meaningful line.', + 'Use compact entity names and relationship labels.', + ], + pitfalls: [ + 'Avoid mixing flowchart or sequence syntax into an ER diagram.', + ], + }, +}; + +const ALIAS_MAP = new Map( + Object.values(DIAGRAM_DOCS).flatMap((doc) => + doc.aliases.map((alias) => [alias.toLowerCase(), doc.canonicalName]), + ), +); + +function normalizeDiagramType(value) { + if (!value) { + return null; + } + + return ALIAS_MAP.get(value.trim().toLowerCase()) ?? null; +} + +function getDiagramDoc(type) { + const normalizedType = normalizeDiagramType(type); + return normalizedType ? DIAGRAM_DOCS[normalizedType] : null; +} + +function listDiagramTypes() { + return Object.keys(DIAGRAM_DOCS); +} + +function firstMeaningfulLine(source) { + for (const rawLine of source.split(/\r?\n/u)) { + const line = rawLine.trim(); + if (!line || line.startsWith('%%')) { + continue; + } + + return line; + } + + return ''; +} + +function detectDiagramType(source) { + const header = firstMeaningfulLine(source); + if (!header) { + return null; + } + + return normalizeDiagramType(header.split(/\s+/u)[0]); +} + +export { + detectDiagramType, + firstMeaningfulLine, + getDiagramDoc, + listDiagramTypes, + normalizeDiagramType, +}; \ No newline at end of file diff --git a/plugins/threat-model/skills/threat-model/scripts/validate-mermaid.mjs b/plugins/threat-model/skills/threat-model/scripts/validate-mermaid.mjs new file mode 100644 index 0000000..82cff04 --- /dev/null +++ b/plugins/threat-model/skills/threat-model/scripts/validate-mermaid.mjs @@ -0,0 +1,272 @@ +/* eslint-disable no-console */ + +import { readFile } from 'node:fs/promises'; +import path from 'node:path'; + +import { + detectDiagramType, + firstMeaningfulLine, + listDiagramTypes, + normalizeDiagramType, +} from './mermaid-support.mjs'; + +function parseArgs(argv) { + const options = { + file: null, + type: null, + help: false, + }; + + for (let index = 0; index < argv.length; index += 1) { + const arg = argv[index]; + if (arg === '--help' || arg === '-h') { + options.help = true; + continue; + } + + if (arg === '--file') { + options.file = argv[index + 1] ?? null; + index += 1; + continue; + } + + if (arg === '--type') { + options.type = argv[index + 1] ?? null; + index += 1; + continue; + } + + if (!arg.startsWith('-') && !options.file) { + options.file = arg; + continue; + } + + throw new Error(`Unknown argument: ${arg}`); + } + + return options; +} + +function printUsage() { + console.log('Usage: node skills/threat-model/scripts/validate-mermaid.mjs --file [--type ]'); + console.log('The file may be a Markdown report with one or more ```mermaid blocks, or a standalone Mermaid file.'); + console.log(`Supported types: ${listDiagramTypes().join(', ')}`); +} + +function extractMermaidBlocks(source, filePath) { + const extension = path.extname(filePath).toLowerCase(); + if (extension === '.md' || extension === '.markdown') { + const blocks = []; + const pattern = /```mermaid\s*\n([\s\S]*?)```/gu; + let match; + while ((match = pattern.exec(source)) !== null) { + blocks.push(match[1].trim()); + } + return blocks; + } + + return [source.trim()]; +} + +function validateCommon(source, errors) { + if (!source.trim()) { + errors.push('Diagram is empty.'); + } +} + +function validateFlowchart(lines, errors) { + let subgraphDepth = 0; + let hasEdge = false; + + for (const line of lines) { + const trimmed = line.trim(); + if (!trimmed || trimmed.startsWith('%%') || trimmed.startsWith('flowchart ')) { + continue; + } + + if (/^(participant|actor|sequenceDiagram)\b/u.test(trimmed) || trimmed.includes('->>')) { + errors.push(`Flowchart appears to mix sequence-diagram syntax: ${trimmed}`); + } + + if (/^subgraph\b/u.test(trimmed)) { + subgraphDepth += 1; + } + + if (trimmed === 'end') { + if (subgraphDepth === 0) { + errors.push('Encountered `end` without a matching `subgraph`.'); + } else { + subgraphDepth -= 1; + } + } + + if (/(-->|==>|-.->|---|o--o|x--x|--x|x--)/u.test(trimmed)) { + hasEdge = true; + } + } + + if (subgraphDepth !== 0) { + errors.push('One or more `subgraph` blocks are missing a closing `end`.'); + } + + if (!hasEdge) { + errors.push('Flowchart must include at least one edge.'); + } +} + +function validateSequenceDiagram(lines, errors) { + const stack = []; + let hasInteraction = false; + + for (const line of lines) { + const trimmed = line.trim(); + if (!trimmed || trimmed.startsWith('%%') || trimmed === 'sequenceDiagram') { + continue; + } + + if (/^(flowchart|subgraph)\b/u.test(trimmed) || /\[[^\]]+\]/u.test(trimmed)) { + errors.push(`Sequence diagram appears to mix flowchart syntax: ${trimmed}`); + } + + if (/^(participant|actor|autonumber|activate|deactivate|title|Note\b|link\b|create\b|destroy\b|box\b)/u.test(trimmed)) { + continue; + } + + if (/^(alt|opt|loop|par|critical|rect|break)\b/u.test(trimmed)) { + stack.push(trimmed.split(/\s+/u)[0]); + continue; + } + + if (/^else\b/u.test(trimmed)) { + if (!stack.includes('alt')) { + errors.push('`else` must appear inside an `alt` block.'); + } + continue; + } + + if (/^and\b/u.test(trimmed)) { + if (!stack.includes('par')) { + errors.push('`and` must appear inside a `par` block.'); + } + continue; + } + + if (/^option\b/u.test(trimmed)) { + if (!stack.includes('critical')) { + errors.push('`option` must appear inside a `critical` block.'); + } + continue; + } + + if (trimmed === 'end') { + if (stack.length === 0) { + errors.push('Encountered `end` without an open sequence block.'); + } else { + stack.pop(); + } + continue; + } + + if (/[-.=]{0,2}(?:>>|>|x|\))\s*[^:]+:/u.test(trimmed)) { + hasInteraction = true; + continue; + } + + errors.push(`Unsupported or malformed sequence-diagram line: ${trimmed}`); + } + + if (stack.length > 0) { + errors.push('One or more structured sequence blocks are missing a closing `end`.'); + } + + if (!hasInteraction) { + errors.push('Sequence diagram must include at least one interaction line.'); + } +} + +function validateDiagram(source, requestedType) { + const errors = []; + const diagramType = requestedType ?? detectDiagramType(source); + if (!diagramType) { + errors.push(`Missing or unsupported diagram header. Supported types: ${listDiagramTypes().join(', ')}`); + return { diagramType: requestedType ?? 'unknown', errors }; + } + + validateCommon(source, errors); + const lines = source.split(/\r?\n/u); + const header = firstMeaningfulLine(source); + const headerToken = header.split(/\s+/u)[0]; + + if (!normalizeDiagramType(headerToken)) { + errors.push(`Unsupported Mermaid header: ${header}`); + } + + if (diagramType === 'flowchart') { + validateFlowchart(lines, errors); + } + + if (diagramType === 'sequenceDiagram') { + validateSequenceDiagram(lines, errors); + } + + return { diagramType, errors }; +} + +async function main() { + let options; + + try { + options = parseArgs(process.argv.slice(2)); + } catch (error) { + console.error(error.message); + printUsage(); + process.exitCode = 1; + return; + } + + if (options.help || !options.file) { + printUsage(); + return; + } + + const requestedType = options.type ? normalizeDiagramType(options.type) : null; + if (options.type && !requestedType) { + console.error(`Unsupported diagram type: ${options.type}`); + printUsage(); + process.exitCode = 1; + return; + } + + const source = await readFile(options.file, 'utf8'); + const blocks = extractMermaidBlocks(source, options.file); + if (blocks.length === 0) { + console.error('No Mermaid content found in the supplied file.'); + process.exitCode = 1; + return; + } + + let hasFailures = false; + blocks.forEach((block, index) => { + const result = validateDiagram(block, requestedType); + const label = `Block ${index + 1} (${result.diagramType})`; + if (result.errors.length === 0) { + console.log(`PASS ${label}`); + return; + } + + hasFailures = true; + console.error(`FAIL ${label}`); + for (const error of result.errors) { + console.error(` - ${error}`); + } + }); + + if (hasFailures) { + process.exitCode = 1; + } +} + +main().catch((error) => { + console.error(error.message); + process.exitCode = 1; +}); \ No newline at end of file diff --git a/skills/README.md b/skills/README.md index 22c1f24..f171435 100644 --- a/skills/README.md +++ b/skills/README.md @@ -12,6 +12,7 @@ Each skill lives in its own folder and contains a `SKILL.md` file (Markdown with - [dependency-cve-triage](dependency-cve-triage/SKILL.md) - [secrets-and-logging-hygiene](secrets-and-logging-hygiene/SKILL.md) - [genai-acceptance-review](genai-acceptance-review/SKILL.md) +- [threat-model](threat-model/SKILL.md) - [threat-model-lite](threat-model-lite/SKILL.md) - [secure-fix-validation](secure-fix-validation/SKILL.md) diff --git a/skills/threat-model/SKILL.MD b/skills/threat-model/SKILL.MD new file mode 100644 index 0000000..7e0abec --- /dev/null +++ b/skills/threat-model/SKILL.MD @@ -0,0 +1,160 @@ +--- +name: threat-model +description: "Threat model a system, feature, service, or PR using Shostack's 4Q workflow, evidence-first analysis, risk scoring, and CLI-friendly Mermaid helper scripts." +--- + +# Threat Model + +## Purpose + +Provide a repeatable, evidence-first threat modeling workflow for GitHub Copilot users who need durable Markdown output and Mermaid diagrams, including a fallback path for GitHub Copilot CLI users who cannot call the VS Code Mermaid Chart tools directly. + +## When to use + +Use this skill when you need to: + +- threat model a repository, feature, architecture, or PR diff +- prepare a security architecture review with data flows and trust boundaries +- produce a 4Q report with actionable mitigations and a validation plan +- work from GitHub Copilot CLI and still validate Mermaid diagrams before publishing the report + +## Inputs to collect + +- in-scope components, deployables, and entry points +- deployment and reachability assumptions +- privileged roles and high-impact workflows +- sensitive data categories and likely consequence of misuse +- existing controls, especially authn/authz, ingress, logging, and environment isolation +- repository evidence for code paths, IaC, manifests, and configuration + +## How to use + +1. Collect repository evidence before relying on operator answers. +2. Ask only the branching intake questions that materially change exposure, privilege, or data-sensitivity scoring. +3. Draft the report in a root-level file named `Threat Model Review - YYYY-MM-DD.md`. +4. Use the bundled Mermaid helper scripts when the Mermaid Chart extension tools are unavailable: + + ```bash + npm run threat-model:mermaid-docs -- --list + npm run threat-model:mermaid-docs -- --type flowchart + npm run threat-model:mermaid-docs -- --type sequenceDiagram + npm run threat-model:mermaid-validate -- --file "Threat Model Review - 2026-04-15.md" + ``` + +5. Fix Mermaid failures and rerun validation until the script exits successfully. +6. Deliver the final report plus a short PR-ready summary. + +## Rules + +- MUST use this evidence hierarchy for factual claims: repo-confirmed, runtime/deployment evidence, operator-stated, ASSUMPTION, UNKNOWN. +- MUST keep confirmed facts separate from inference. +- MUST ask 4-8 concise intake questions when reachability, privileged workflows, data sensitivity, or environment isolation are unclear. +- MUST produce at least these diagrams unless the repository clearly cannot support them: DFD Level 0, DFD Level 1, trust-boundary view, and top 2-3 sequence diagrams. +- MUST validate every Mermaid block before finalizing the report. +- MUST include at least 3 code-anchored or IaC-anchored findings that do not depend primarily on operator answers. +- MUST assign an overall application risk score from 0-100 with confidence, volatility, and top score drivers. +- MUST mark mitigations as PRESENT, ABSENT, or UNKNOWN. +- MUST mark threats as Mitigated, Partially Mitigated, Open, or Unknown based on whether controls materially close the exploit path. +- SHOULD prefer simple Mermaid syntax over advanced styling. +- SHOULD call out contradictions between repo evidence and operator statements before finalizing prioritization. +- MAY omit optional diagrams when the repository does not expose the needed evidence; label the gap as UNKNOWN. + +## Step-by-step process + +1. **Triage and calibrate risk** + - Identify the primary application surface, deployables, and datastore paths. + - Classify reachability first: internal, mixed, partner-reachable, or public. + - Capture repo-confirmed versus operator-stated exposure details separately. +2. **Q1: What are we working on?** + - Summarize system purpose, components, identities, assets, and trust boundaries. + - Rank key flows by sensitivity, privilege, and exposure. + - Draft DFD Level 0 and Level 1 diagrams. +3. **Q2: What can go wrong?** + - Enumerate flow-specific threats with STRIDE and OWASP mapping. + - Include abuse cases for admin paths, bulk actions, impersonation, exports, webhooks, and downstream triggers where relevant. + - Preserve at least 2-3 high-confidence threats directly anchored in code or IaC. +4. **Q3: What are we going to do about it?** + - Evaluate controls as PRESENT, ABSENT, or UNKNOWN. + - Distinguish direct mitigations from adjacent hygiene controls. + - Recommend practical fixes with expected effort and blast-radius reduction. +5. **Q4: Did we do a good job?** + - Build a validation plan with 3-6 scenarios. + - Include one scenario for a code-evidenced weakness, one for an operator-stated assumption, and one for privileged workflow misuse. +6. **Validate diagrams and finish the report** + - Run the helper scripts for Mermaid docs and validation. + - Confirm that diagram evidence, findings, scoring, and validation scenarios are internally consistent. + +## Mermaid helper scripts + +The skill includes these local scripts under `skills/threat-model/scripts/`: + +- `mermaid-docs.mjs`: prints concise syntax guidance and common pitfalls for supported diagram types. +- `validate-mermaid.mjs`: validates Mermaid blocks in Markdown reports or standalone diagram files using deterministic preflight checks. + +Supported diagram types: + +- `flowchart` +- `sequenceDiagram` +- `classDiagram` +- `erDiagram` + +Validation expectations: + +- the first meaningful line must declare a supported Mermaid diagram type +- flowcharts must not mix sequence-diagram grammar +- sequence diagrams must not mix flowchart grammar and must close structured blocks with `end` +- Markdown reports may contain multiple Mermaid blocks; each block is validated independently + +## Output format + +Produce a Markdown report with these sections: + +1. Executive summary +2. Risk score +3. Scope +4. Exposure and risk calibration +5. Contradictions and reconciliation +6. Assumptions and unknowns +7. Architecture and data flows with validated diagrams +8. Key flows +9. Threats table +10. Mitigations table +11. High-risk interaction sequences +12. Validation plan +13. Owners +14. Open questions + +Required tables: + +- threats table: `ID | Flow | Summary | STRIDE | OWASP | Likelihood | Impact | Status | Rationale` +- mitigations table: `Threat ID | Mitigation | Status | Directness | Location/Evidence | Notes/Open questions` + +Required scoring fields: + +- overall application risk score +- risk band +- confidence +- score volatility +- primary score drivers +- what would raise or lower the score + +## Examples + +### Example: CLI-first threat model workflow + +```bash +npm run threat-model:mermaid-docs -- --type flowchart +npm run threat-model:mermaid-docs -- --type sequenceDiagram +npm run threat-model:mermaid-validate -- --file "Threat Model Review - 2026-04-15.md" +``` + +Expected outcome: + +- the docs command prints the required header, allowed constructs, and common pitfalls +- the validation command reports each Mermaid block as `PASS` or fails with block-specific errors + +### Example: threat model output goals + +- Top findings are prioritized by real reachability, privilege, and blast radius. +- Evidence is anchored to repository files, symbols, and line ranges when available. +- Unknowns include an owner and a question that can be answered later. diff --git a/skills/threat-model/scripts/mermaid-docs.mjs b/skills/threat-model/scripts/mermaid-docs.mjs new file mode 100644 index 0000000..f3abb7b --- /dev/null +++ b/skills/threat-model/scripts/mermaid-docs.mjs @@ -0,0 +1,115 @@ +/* eslint-disable no-console */ + +import { getDiagramDoc, listDiagramTypes, normalizeDiagramType } from './mermaid-support.mjs'; + +function parseArgs(argv) { + const options = { + list: false, + type: null, + json: false, + help: false, + }; + + for (let index = 0; index < argv.length; index += 1) { + const arg = argv[index]; + if (arg === '--list') { + options.list = true; + continue; + } + + if (arg === '--json') { + options.json = true; + continue; + } + + if (arg === '--help' || arg === '-h') { + options.help = true; + continue; + } + + if (arg === '--type') { + options.type = argv[index + 1] ?? null; + index += 1; + continue; + } + + if (!arg.startsWith('-') && !options.type) { + options.type = arg; + continue; + } + + throw new Error(`Unknown argument: ${arg}`); + } + + return options; +} + +function printUsage() { + console.log('Usage: node skills/threat-model/scripts/mermaid-docs.mjs [--list] [--type ] [--json]'); + console.log('Supported types:', listDiagramTypes().join(', ')); +} + +function formatDoc(doc) { + const lines = [ + `Diagram type: ${doc.canonicalName}`, + `Required header: ${doc.header}`, + `Purpose: ${doc.purpose}`, + '', + 'Core constructs:', + ...doc.constructs.map((item) => `- ${item}`), + '', + 'Common pitfalls:', + ...doc.pitfalls.map((item) => `- ${item}`), + ]; + + return lines.join('\n'); +} + +function main() { + let options; + + try { + options = parseArgs(process.argv.slice(2)); + } catch (error) { + console.error(error.message); + printUsage(); + process.exitCode = 1; + return; + } + + if (options.help || (!options.list && !options.type)) { + printUsage(); + return; + } + + if (options.list) { + const supportedTypes = listDiagramTypes(); + if (options.json) { + console.log(JSON.stringify(supportedTypes, null, 2)); + return; + } + + for (const type of supportedTypes) { + console.log(type); + } + return; + } + + const normalizedType = normalizeDiagramType(options.type); + if (!normalizedType) { + console.error(`Unsupported diagram type: ${options.type}`); + printUsage(); + process.exitCode = 1; + return; + } + + const doc = getDiagramDoc(normalizedType); + if (options.json) { + console.log(JSON.stringify(doc, null, 2)); + return; + } + + console.log(formatDoc(doc)); +} + +main(); diff --git a/skills/threat-model/scripts/mermaid-support.mjs b/skills/threat-model/scripts/mermaid-support.mjs new file mode 100644 index 0000000..5ccb8bd --- /dev/null +++ b/skills/threat-model/scripts/mermaid-support.mjs @@ -0,0 +1,115 @@ +const DIAGRAM_DOCS = { + flowchart: { + canonicalName: 'flowchart', + aliases: ['flowchart', 'flow', 'dfd'], + header: 'flowchart LR', + purpose: 'Use for DFD Level 0, DFD Level 1, and trust-boundary views.', + constructs: [ + 'Declare a direction on the first line, such as `flowchart LR` or `flowchart TD`.', + 'Use simple alphanumeric node ids and put descriptive text in labels.', + 'Use `subgraph ... end` to show trust boundaries or subsystem grouping.', + 'Quote edge labels when they contain punctuation or multi-word text.', + ], + pitfalls: [ + 'Do not mix sequence-diagram syntax such as `participant`, `actor`, or `->>`.', + 'Do not leave a `subgraph` block without a matching `end`.', + 'Keep node ids simple; put complex text in labels instead of ids.', + ], + }, + sequenceDiagram: { + canonicalName: 'sequenceDiagram', + aliases: ['sequencediagram', 'sequence', 'sequenceDiagram'], + header: 'sequenceDiagram', + purpose: 'Use for the highest-risk interaction flows such as login, admin actions, or data export.', + constructs: [ + 'Declare `sequenceDiagram` on the first meaningful line.', + 'Define actors or participants before the first interaction when practical.', + 'Use `alt`, `opt`, `loop`, `par`, `critical`, or `rect` blocks with a closing `end`.', + 'Use interaction arrows such as `->>` and `-->>` with a message label after `:`.', + ], + pitfalls: [ + 'Do not mix flowchart syntax such as `flowchart LR`, `subgraph`, or node labels like `A[API]`.', + 'Do not use `else`, `and`, or `option` outside their parent blocks.', + 'Every structured block must close with `end`.', + ], + }, + classDiagram: { + canonicalName: 'classDiagram', + aliases: ['classdiagram', 'class', 'classDiagram'], + header: 'classDiagram', + purpose: 'Use only when modeling role relationships or policy objects adds clarity.', + constructs: [ + 'Declare `classDiagram` on the first meaningful line.', + 'Keep class names simple and model only the relationships needed for the security story.', + ], + pitfalls: [ + 'Avoid mixing flowchart or sequence syntax into a class diagram.', + ], + }, + erDiagram: { + canonicalName: 'erDiagram', + aliases: ['erdiagram', 'er', 'entityrelationshipdiagram', 'entityRelationshipDiagram'], + header: 'erDiagram', + purpose: 'Use when data classification or ownership is easier to explain as datastore relationships.', + constructs: [ + 'Declare `erDiagram` on the first meaningful line.', + 'Use compact entity names and relationship labels.', + ], + pitfalls: [ + 'Avoid mixing flowchart or sequence syntax into an ER diagram.', + ], + }, +}; + +const ALIAS_MAP = new Map( + Object.values(DIAGRAM_DOCS).flatMap((doc) => + doc.aliases.map((alias) => [alias.toLowerCase(), doc.canonicalName]), + ), +); + +function normalizeDiagramType(value) { + if (!value) { + return null; + } + + return ALIAS_MAP.get(value.trim().toLowerCase()) ?? null; +} + +function getDiagramDoc(type) { + const normalizedType = normalizeDiagramType(type); + return normalizedType ? DIAGRAM_DOCS[normalizedType] : null; +} + +function listDiagramTypes() { + return Object.keys(DIAGRAM_DOCS); +} + +function firstMeaningfulLine(source) { + for (const rawLine of source.split(/\r?\n/u)) { + const line = rawLine.trim(); + if (!line || line.startsWith('%%')) { + continue; + } + + return line; + } + + return ''; +} + +function detectDiagramType(source) { + const header = firstMeaningfulLine(source); + if (!header) { + return null; + } + + return normalizeDiagramType(header.split(/\s+/u)[0]); +} + +export { + detectDiagramType, + firstMeaningfulLine, + getDiagramDoc, + listDiagramTypes, + normalizeDiagramType, +}; diff --git a/skills/threat-model/scripts/validate-mermaid.mjs b/skills/threat-model/scripts/validate-mermaid.mjs new file mode 100644 index 0000000..8653216 --- /dev/null +++ b/skills/threat-model/scripts/validate-mermaid.mjs @@ -0,0 +1,272 @@ +/* eslint-disable no-console */ + +import { readFile } from 'node:fs/promises'; +import path from 'node:path'; + +import { + detectDiagramType, + firstMeaningfulLine, + listDiagramTypes, + normalizeDiagramType, +} from './mermaid-support.mjs'; + +function parseArgs(argv) { + const options = { + file: null, + type: null, + help: false, + }; + + for (let index = 0; index < argv.length; index += 1) { + const arg = argv[index]; + if (arg === '--help' || arg === '-h') { + options.help = true; + continue; + } + + if (arg === '--file') { + options.file = argv[index + 1] ?? null; + index += 1; + continue; + } + + if (arg === '--type') { + options.type = argv[index + 1] ?? null; + index += 1; + continue; + } + + if (!arg.startsWith('-') && !options.file) { + options.file = arg; + continue; + } + + throw new Error(`Unknown argument: ${arg}`); + } + + return options; +} + +function printUsage() { + console.log('Usage: node skills/threat-model/scripts/validate-mermaid.mjs --file [--type ]'); + console.log('The file may be a Markdown report with one or more ```mermaid blocks, or a standalone Mermaid file.'); + console.log(`Supported types: ${listDiagramTypes().join(', ')}`); +} + +function extractMermaidBlocks(source, filePath) { + const extension = path.extname(filePath).toLowerCase(); + if (extension === '.md' || extension === '.markdown') { + const blocks = []; + const pattern = /```mermaid\s*\n([\s\S]*?)```/gu; + let match; + while ((match = pattern.exec(source)) !== null) { + blocks.push(match[1].trim()); + } + return blocks; + } + + return [source.trim()]; +} + +function validateCommon(source, errors) { + if (!source.trim()) { + errors.push('Diagram is empty.'); + } +} + +function validateFlowchart(lines, errors) { + let subgraphDepth = 0; + let hasEdge = false; + + for (const line of lines) { + const trimmed = line.trim(); + if (!trimmed || trimmed.startsWith('%%') || trimmed.startsWith('flowchart ')) { + continue; + } + + if (/^(participant|actor|sequenceDiagram)\b/u.test(trimmed) || trimmed.includes('->>')) { + errors.push(`Flowchart appears to mix sequence-diagram syntax: ${trimmed}`); + } + + if (/^subgraph\b/u.test(trimmed)) { + subgraphDepth += 1; + } + + if (trimmed === 'end') { + if (subgraphDepth === 0) { + errors.push('Encountered `end` without a matching `subgraph`.'); + } else { + subgraphDepth -= 1; + } + } + + if (/(-->|==>|-.->|---|o--o|x--x|--x|x--)/u.test(trimmed)) { + hasEdge = true; + } + } + + if (subgraphDepth !== 0) { + errors.push('One or more `subgraph` blocks are missing a closing `end`.'); + } + + if (!hasEdge) { + errors.push('Flowchart must include at least one edge.'); + } +} + +function validateSequenceDiagram(lines, errors) { + const stack = []; + let hasInteraction = false; + + for (const line of lines) { + const trimmed = line.trim(); + if (!trimmed || trimmed.startsWith('%%') || trimmed === 'sequenceDiagram') { + continue; + } + + if (/^(flowchart|subgraph)\b/u.test(trimmed) || /\[[^\]]+\]/u.test(trimmed)) { + errors.push(`Sequence diagram appears to mix flowchart syntax: ${trimmed}`); + } + + if (/^(participant|actor|autonumber|activate|deactivate|title|Note\b|link\b|create\b|destroy\b|box\b)/u.test(trimmed)) { + continue; + } + + if (/^(alt|opt|loop|par|critical|rect|break)\b/u.test(trimmed)) { + stack.push(trimmed.split(/\s+/u)[0]); + continue; + } + + if (/^else\b/u.test(trimmed)) { + if (!stack.includes('alt')) { + errors.push('`else` must appear inside an `alt` block.'); + } + continue; + } + + if (/^and\b/u.test(trimmed)) { + if (!stack.includes('par')) { + errors.push('`and` must appear inside a `par` block.'); + } + continue; + } + + if (/^option\b/u.test(trimmed)) { + if (!stack.includes('critical')) { + errors.push('`option` must appear inside a `critical` block.'); + } + continue; + } + + if (trimmed === 'end') { + if (stack.length === 0) { + errors.push('Encountered `end` without an open sequence block.'); + } else { + stack.pop(); + } + continue; + } + + if (/[-.=]{0,2}(?:>>|>|x|\))\s*[^:]+:/u.test(trimmed)) { + hasInteraction = true; + continue; + } + + errors.push(`Unsupported or malformed sequence-diagram line: ${trimmed}`); + } + + if (stack.length > 0) { + errors.push('One or more structured sequence blocks are missing a closing `end`.'); + } + + if (!hasInteraction) { + errors.push('Sequence diagram must include at least one interaction line.'); + } +} + +function validateDiagram(source, requestedType) { + const errors = []; + const diagramType = requestedType ?? detectDiagramType(source); + if (!diagramType) { + errors.push(`Missing or unsupported diagram header. Supported types: ${listDiagramTypes().join(', ')}`); + return { diagramType: requestedType ?? 'unknown', errors }; + } + + validateCommon(source, errors); + const lines = source.split(/\r?\n/u); + const header = firstMeaningfulLine(source); + const headerToken = header.split(/\s+/u)[0]; + + if (!normalizeDiagramType(headerToken)) { + errors.push(`Unsupported Mermaid header: ${header}`); + } + + if (diagramType === 'flowchart') { + validateFlowchart(lines, errors); + } + + if (diagramType === 'sequenceDiagram') { + validateSequenceDiagram(lines, errors); + } + + return { diagramType, errors }; +} + +async function main() { + let options; + + try { + options = parseArgs(process.argv.slice(2)); + } catch (error) { + console.error(error.message); + printUsage(); + process.exitCode = 1; + return; + } + + if (options.help || !options.file) { + printUsage(); + return; + } + + const requestedType = options.type ? normalizeDiagramType(options.type) : null; + if (options.type && !requestedType) { + console.error(`Unsupported diagram type: ${options.type}`); + printUsage(); + process.exitCode = 1; + return; + } + + const source = await readFile(options.file, 'utf8'); + const blocks = extractMermaidBlocks(source, options.file); + if (blocks.length === 0) { + console.error('No Mermaid content found in the supplied file.'); + process.exitCode = 1; + return; + } + + let hasFailures = false; + blocks.forEach((block, index) => { + const result = validateDiagram(block, requestedType); + const label = `Block ${index + 1} (${result.diagramType})`; + if (result.errors.length === 0) { + console.log(`PASS ${label}`); + return; + } + + hasFailures = true; + console.error(`FAIL ${label}`); + for (const error of result.errors) { + console.error(` - ${error}`); + } + }); + + if (hasFailures) { + process.exitCode = 1; + } +} + +main().catch((error) => { + console.error(error.message); + process.exitCode = 1; +}); From 8d96276b30d023f3558e3f1de91a6ef07539e73f Mon Sep 17 00:00:00 2001 From: Jeymz Simmons Date: Wed, 15 Apr 2026 10:19:41 -0400 Subject: [PATCH 2/2] fix: Ensure newline at end of file in multiple scripts for consistency --- plugins/threat-model/skills/threat-model/SKILL.md | 2 +- .../threat-model/skills/threat-model/scripts/mermaid-docs.mjs | 2 +- .../skills/threat-model/scripts/mermaid-support.mjs | 2 +- .../skills/threat-model/scripts/validate-mermaid.mjs | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/plugins/threat-model/skills/threat-model/SKILL.md b/plugins/threat-model/skills/threat-model/SKILL.md index 00d48cd..7e0abec 100644 --- a/plugins/threat-model/skills/threat-model/SKILL.md +++ b/plugins/threat-model/skills/threat-model/SKILL.md @@ -157,4 +157,4 @@ Expected outcome: - Top findings are prioritized by real reachability, privilege, and blast radius. - Evidence is anchored to repository files, symbols, and line ranges when available. -- Unknowns include an owner and a question that can be answered later. \ No newline at end of file +- Unknowns include an owner and a question that can be answered later. diff --git a/plugins/threat-model/skills/threat-model/scripts/mermaid-docs.mjs b/plugins/threat-model/skills/threat-model/scripts/mermaid-docs.mjs index 768b65f..f3abb7b 100644 --- a/plugins/threat-model/skills/threat-model/scripts/mermaid-docs.mjs +++ b/plugins/threat-model/skills/threat-model/scripts/mermaid-docs.mjs @@ -112,4 +112,4 @@ function main() { console.log(formatDoc(doc)); } -main(); \ No newline at end of file +main(); diff --git a/plugins/threat-model/skills/threat-model/scripts/mermaid-support.mjs b/plugins/threat-model/skills/threat-model/scripts/mermaid-support.mjs index 5917933..8555186 100644 --- a/plugins/threat-model/skills/threat-model/scripts/mermaid-support.mjs +++ b/plugins/threat-model/skills/threat-model/scripts/mermaid-support.mjs @@ -112,4 +112,4 @@ export { getDiagramDoc, listDiagramTypes, normalizeDiagramType, -}; \ No newline at end of file +}; diff --git a/plugins/threat-model/skills/threat-model/scripts/validate-mermaid.mjs b/plugins/threat-model/skills/threat-model/scripts/validate-mermaid.mjs index 82cff04..8653216 100644 --- a/plugins/threat-model/skills/threat-model/scripts/validate-mermaid.mjs +++ b/plugins/threat-model/skills/threat-model/scripts/validate-mermaid.mjs @@ -269,4 +269,4 @@ async function main() { main().catch((error) => { console.error(error.message); process.exitCode = 1; -}); \ No newline at end of file +});