From 261193c9f3bac57a76c6d894bd054cbd69505f1e Mon Sep 17 00:00:00 2001 From: Christy Jones Date: Wed, 3 Jun 2026 14:58:58 -0400 Subject: [PATCH 1/6] Add public workflow review toolkit scaffold Create a self-contained public toolkit so workflow review and remediation can be shared through docs, a CLI, Cursor wrappers, and an MCP entry point without depending on the private skills repo. Co-authored-by: Cursor --- README.md | 21 +- Toolkit/README.md | 71 +++++++ Toolkit/cli/README.md | 84 ++++++++ Toolkit/cli/pyproject.toml | 22 ++ Toolkit/cli/src/workflow_review/__init__.py | 5 + Toolkit/cli/src/workflow_review/__main__.py | 5 + Toolkit/cli/src/workflow_review/checklist.py | 42 ++++ Toolkit/cli/src/workflow_review/cli.py | 136 +++++++++++++ Toolkit/cli/src/workflow_review/enumerate.py | 188 +++++++++++++++++ Toolkit/cli/src/workflow_review/mcp_server.py | 191 ++++++++++++++++++ .../cli/src/workflow_review/remediation.py | 96 +++++++++ Toolkit/cli/src/workflow_review/review.py | 51 +++++ .../cli/tests/fixtures/sample_workflow.json | 22 ++ Toolkit/cli/tests/test_enumerate.py | 28 +++ Toolkit/cursor/README.md | 28 +++ .../cursor/exchange-workflow-review/SKILL.md | 51 +++++ .../exchange-workflow-review/reference.md | 18 ++ Toolkit/cursor/install_cursor_skills.sh | 33 +++ .../workflow-review-remediation/SKILL.md | 48 +++++ .../workflow-review-remediation/reference.md | 17 ++ Toolkit/examples/README.md | 25 +++ Toolkit/examples/review-output.md | 30 +++ Toolkit/mcp/README.md | 32 +++ Toolkit/mcp/cursor.example.json | 16 ++ Toolkit/remediation/README.md | 44 ++++ Toolkit/remediation/reference.md | 68 +++++++ Toolkit/review/README.md | 36 ++++ Toolkit/review/reference.md | 62 ++++++ WorkflowsSubmissionProcess.md | 6 + 29 files changed, 1471 insertions(+), 5 deletions(-) create mode 100644 Toolkit/README.md create mode 100644 Toolkit/cli/README.md create mode 100644 Toolkit/cli/pyproject.toml create mode 100644 Toolkit/cli/src/workflow_review/__init__.py create mode 100644 Toolkit/cli/src/workflow_review/__main__.py create mode 100644 Toolkit/cli/src/workflow_review/checklist.py create mode 100644 Toolkit/cli/src/workflow_review/cli.py create mode 100644 Toolkit/cli/src/workflow_review/enumerate.py create mode 100644 Toolkit/cli/src/workflow_review/mcp_server.py create mode 100644 Toolkit/cli/src/workflow_review/remediation.py create mode 100644 Toolkit/cli/src/workflow_review/review.py create mode 100644 Toolkit/cli/tests/fixtures/sample_workflow.json create mode 100644 Toolkit/cli/tests/test_enumerate.py create mode 100644 Toolkit/cursor/README.md create mode 100644 Toolkit/cursor/exchange-workflow-review/SKILL.md create mode 100644 Toolkit/cursor/exchange-workflow-review/reference.md create mode 100755 Toolkit/cursor/install_cursor_skills.sh create mode 100644 Toolkit/cursor/workflow-review-remediation/SKILL.md create mode 100644 Toolkit/cursor/workflow-review-remediation/reference.md create mode 100644 Toolkit/examples/README.md create mode 100644 Toolkit/examples/review-output.md create mode 100644 Toolkit/mcp/README.md create mode 100644 Toolkit/mcp/cursor.example.json create mode 100644 Toolkit/remediation/README.md create mode 100644 Toolkit/remediation/reference.md create mode 100644 Toolkit/review/README.md create mode 100644 Toolkit/review/reference.md diff --git a/README.md b/README.md index f12897b..f451d6e 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Cisco Workflows Automation -This repository contains automation workflows organized by target platform and functionality as described in the following repository structure: +This repository contains automation workflows organized by target platform and functionality, plus a public toolkit for reviewing and improving workflow exports before submission. ## Repository Structure @@ -11,15 +11,26 @@ CiscoWorkflowsAutomation/ ├── CONTRIBUTING.md # Contribution guidelines and procedures ├── README.md # This file - repository overview ├── AI/ # AI-powered automation workflows and integrations -├── Catalyst Center/ # Cisco Catalyst Center automation workflows -├── Cross Domain/ # Multi-platform and cross-domain integration workflows -├── Learning Series/ # Educational workflows and training materials +├── Catalyst_Center/ # Cisco Catalyst Center automation workflows +├── Cross_Domain/ # Multi-platform and cross-domain integration workflows +├── Learning_Series/ # Educational workflows and training materials ├── LICENSE # Repository license terms ├── Meraki/ # Cisco Meraki automation workflows and integrations -├── Restful WebService/ # RESTful API integration workflows and examples +├── Restful_WebService/ # RESTful API integration workflows and examples +├── Toolkit/ # Public review, remediation, CLI, MCP, and Cursor helpers └── SECURITY.md # Security policies and reporting procedures ``` +## Public Toolkit + +The `Toolkit/` area provides reusable helpers for the workflow review checklist in this repository. It is intended for: + +- Cursor and VS Code users who want installable skill wrappers +- solution engineers or contributors using another LLM +- external contributors who want a documented CLI and MCP-based path + +See `Toolkit/README.md` for the audience guide and `WorkflowReviewChecklist.md` for the canonical review contract. + ## Contributing We welcome contributions to expand and improve the automation workflows and supporting utilities. Please review our contribution guidelines: diff --git a/Toolkit/README.md b/Toolkit/README.md new file mode 100644 index 0000000..fd5d598 --- /dev/null +++ b/Toolkit/README.md @@ -0,0 +1,71 @@ +# Workflow Toolkit + +This toolkit turns the public workflow review checklist in this repository into reusable review and remediation paths for multiple audiences. + +The canonical review contract remains the root-level `WorkflowReviewChecklist.md`. Everything in `Toolkit/` is designed to help contributors apply that checklist more consistently. + +## Who It Is For + +### Cursor and VS Code users + +Use `Toolkit/cursor/` to install thin Cursor skill wrappers that call the shared CLI. + +### Other LLM users + +Use `Toolkit/review/` and `Toolkit/remediation/` as copyable playbooks, then run the CLI in `Toolkit/cli/` for deterministic enumeration and checklist resolution. + +### External contributors + +Start with: + +1. `WorkflowReviewChecklist.md` +2. `Toolkit/review/README.md` +3. `Toolkit/cli/README.md` + +That path does not require Cursor. + +## Layout + +```text +Toolkit/ +├── README.md +├── cli/ # Layer 1: shared Python CLI and core helpers +├── cursor/ # Layer 2: thin Cursor skill wrappers +├── examples/ # Example commands and sample output +├── mcp/ # Layer 3: stdio MCP guidance +├── remediation/ # Public remediation guidance and mode reference +└── review/ # Public review guidance and checklist companion +``` + +## Layering + +- Layer 1: the CLI is the shared core for enumeration, checklist resolution, review preparation, and remediation planning. +- Layer 2: Cursor skills stay thin and delegate deterministic work to the CLI. +- Layer 3: the MCP server wraps the same core instead of re-implementing logic. + +## Quick Start + +### Review a workflow export with the CLI + +```bash +cd Toolkit/cli +python3 -m workflow_review inspect-workflow-export "/path/to/workflow.json" +python3 -m workflow_review prepare-review "/path/to/workflow.json" --json +``` + +### Install the public Cursor wrappers + +```bash +bash Toolkit/cursor/install_cursor_skills.sh +``` + +### Run the stdio MCP scaffold + +```bash +cd Toolkit/cli +python3 -m workflow_review.mcp_server +``` + +## Scope + +This public toolkit intentionally focuses on shareable workflow review and remediation content. Internal Jira/reporting skills and local scratch exports should remain outside this repo unless they are scrubbed and clearly reusable. diff --git a/Toolkit/cli/README.md b/Toolkit/cli/README.md new file mode 100644 index 0000000..98459fd --- /dev/null +++ b/Toolkit/cli/README.md @@ -0,0 +1,84 @@ +# Workflow Review CLI + +This directory contains the Layer 1 core for the public toolkit. + +The initial CLI focuses on deterministic tasks that are useful across Cursor, other LLMs, and MCP-capable clients: + +- inspect a workflow export and list its parent and embedded workflows +- validate that an export can be parsed +- resolve the canonical checklist path +- prepare a structured review brief +- prepare a structured remediation plan + +## Install + +From this directory: + +```bash +python3 -m pip install -e . +``` + +If you do not want to install the package, you can run it in-place: + +```bash +PYTHONPATH=src python3 -m workflow_review inspect-workflow-export "/path/to/workflow.json" +``` + +## Commands + +### Enumerate workflows + +```bash +PYTHONPATH=src python3 -m workflow_review inspect-workflow-export "/path/to/workflow.json" +PYTHONPATH=src python3 -m workflow_review inspect-workflow-export "/path/to/workflow.json" --json +``` + +The legacy `enumerate` subcommand still works as an alias, but `inspect-workflow-export` is the clearer public-facing name. + +### Validate exports + +```bash +PYTHONPATH=src python3 -m workflow_review validate "/path/to/workflow.json" +``` + +### Resolve the checklist + +```bash +PYTHONPATH=src python3 -m workflow_review checklist +PYTHONPATH=src python3 -m workflow_review checklist --show +``` + +### Prepare a review run + +```bash +PYTHONPATH=src python3 -m workflow_review prepare-review "/path/to/workflow.json" --json +``` + +### Prepare a remediation plan + +```bash +PYTHONPATH=src python3 -m workflow_review plan-remediation "/path/to/workflow.json" \ + --mode fix-high-only \ + --safety ask-before-major-change \ + --json +``` + +## Checklist Resolution + +The CLI resolves the checklist in this order: + +1. `--checklist /path/to/file.md` +2. `WORKFLOW_REVIEW_CHECKLIST=/path/to/file.md` +3. the repo-root `WorkflowReviewChecklist.md` + +That keeps the root file canonical while still allowing external users to point at another checked-in copy if needed. + +## MCP + +The stdio MCP scaffold is implemented in the same package and exposed through: + +```bash +PYTHONPATH=src python3 -m workflow_review.mcp_server +``` + +See `../mcp/README.md` for client configuration examples. diff --git a/Toolkit/cli/pyproject.toml b/Toolkit/cli/pyproject.toml new file mode 100644 index 0000000..a4c76ba --- /dev/null +++ b/Toolkit/cli/pyproject.toml @@ -0,0 +1,22 @@ +[build-system] +requires = ["hatchling>=1.25.0"] +build-backend = "hatchling.build" + +[project] +name = "cisco-workflow-review" +version = "0.1.0" +description = "Public CLI and MCP helpers for Cisco workflow review and remediation." +readme = "README.md" +requires-python = ">=3.9" +license = { text = "Cisco Sample Code License" } +authors = [ + { name = "Cisco Workflows Automation DevNet Team" } +] +dependencies = [] + +[project.scripts] +workflow-review = "workflow_review.cli:main" +workflow-review-mcp = "workflow_review.mcp_server:main" + +[tool.hatch.build.targets.wheel] +packages = ["src/workflow_review"] diff --git a/Toolkit/cli/src/workflow_review/__init__.py b/Toolkit/cli/src/workflow_review/__init__.py new file mode 100644 index 0000000..e753d7c --- /dev/null +++ b/Toolkit/cli/src/workflow_review/__init__.py @@ -0,0 +1,5 @@ +"""Shared workflow review helpers for the public toolkit.""" + +__all__ = ["__version__"] + +__version__ = "0.1.0" diff --git a/Toolkit/cli/src/workflow_review/__main__.py b/Toolkit/cli/src/workflow_review/__main__.py new file mode 100644 index 0000000..df699c3 --- /dev/null +++ b/Toolkit/cli/src/workflow_review/__main__.py @@ -0,0 +1,5 @@ +from workflow_review.cli import main + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/Toolkit/cli/src/workflow_review/checklist.py b/Toolkit/cli/src/workflow_review/checklist.py new file mode 100644 index 0000000..be81d54 --- /dev/null +++ b/Toolkit/cli/src/workflow_review/checklist.py @@ -0,0 +1,42 @@ +from __future__ import annotations + +import os +from pathlib import Path + + +CHECKLIST_ENV_VAR = "WORKFLOW_REVIEW_CHECKLIST" +CHECKLIST_FILENAME = "WorkflowReviewChecklist.md" + + +def _candidate_repo_roots() -> list[Path]: + current = Path(__file__).resolve() + return list(current.parents) + + +def resolve_checklist_path(explicit_path: str | None = None) -> Path: + candidates: list[Path] = [] + + if explicit_path: + candidates.append(Path(explicit_path).expanduser().resolve()) + + env_value = os.getenv(CHECKLIST_ENV_VAR) + if env_value: + candidates.append(Path(env_value).expanduser().resolve()) + + for parent in _candidate_repo_roots(): + candidates.append(parent / CHECKLIST_FILENAME) + + for candidate in candidates: + if candidate.exists() and candidate.is_file(): + return candidate + + searched = ", ".join(str(path) for path in candidates[:5]) + raise FileNotFoundError( + f"Could not resolve {CHECKLIST_FILENAME}. Checked explicit path, " + f"{CHECKLIST_ENV_VAR}, and repo-relative candidates such as: {searched}" + ) + + +def read_checklist(explicit_path: str | None = None) -> tuple[Path, str]: + checklist_path = resolve_checklist_path(explicit_path=explicit_path) + return checklist_path, checklist_path.read_text(encoding="utf-8") diff --git a/Toolkit/cli/src/workflow_review/cli.py b/Toolkit/cli/src/workflow_review/cli.py new file mode 100644 index 0000000..1f96122 --- /dev/null +++ b/Toolkit/cli/src/workflow_review/cli.py @@ -0,0 +1,136 @@ +from __future__ import annotations + +import argparse +import json +import sys +from typing import Any + +from workflow_review.checklist import read_checklist, resolve_checklist_path +from workflow_review.enumerate import enumerate_workflows, render_enumeration_text +from workflow_review.remediation import ( + list_remediation_modes, + list_safety_modes, + plan_remediation, +) +from workflow_review.review import prepare_review + + +def build_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser( + description="Workflow review and remediation helpers for Cisco workflow exports." + ) + subparsers = parser.add_subparsers(dest="command", required=True) + + inspect_parser = subparsers.add_parser( + "inspect-workflow-export", + help="Inspect a workflow export and list the parent and embedded workflows it contains.", + ) + inspect_parser.add_argument("workflow_json") + inspect_parser.add_argument("--json", action="store_true", dest="json_output") + + enumerate_parser = subparsers.add_parser( + "enumerate", + help="Alias for inspect-workflow-export.", + ) + enumerate_parser.add_argument("workflow_json") + enumerate_parser.add_argument("--json", action="store_true", dest="json_output") + + validate_parser = subparsers.add_parser( + "validate", + help="Validate that a workflow export can be parsed and enumerated.", + ) + validate_parser.add_argument("workflow_json") + + checklist_parser = subparsers.add_parser( + "checklist", + help="Resolve or show the canonical workflow review checklist.", + ) + checklist_parser.add_argument("--checklist") + checklist_parser.add_argument("--show", action="store_true") + + review_parser = subparsers.add_parser( + "prepare-review", + help="Prepare a structured review brief for a workflow export.", + ) + review_parser.add_argument("workflow_json") + review_parser.add_argument("--checklist") + review_parser.add_argument("--priority-focus") + review_parser.add_argument("--severity-threshold") + review_parser.add_argument("--json", action="store_true", dest="json_output") + + remediation_parser = subparsers.add_parser( + "plan-remediation", + help="Prepare a structured remediation plan without applying edits.", + ) + remediation_parser.add_argument("workflow_json") + remediation_parser.add_argument("--mode", required=True, choices=sorted(list_remediation_modes())) + remediation_parser.add_argument("--safety", required=True, choices=sorted(list_safety_modes())) + remediation_parser.add_argument("--findings") + remediation_parser.add_argument("--priority-focus") + remediation_parser.add_argument("--json", action="store_true", dest="json_output") + + return parser + + +def _print_payload(payload: Any, json_output: bool) -> int: + if json_output: + print(json.dumps(payload, indent=2)) + elif isinstance(payload, str): + print(payload) + else: + print(json.dumps(payload, indent=2)) + return 0 + + +def main(argv: list[str] | None = None) -> int: + parser = build_parser() + args = parser.parse_args(argv) + + try: + if args.command in {"inspect-workflow-export", "enumerate"}: + result = enumerate_workflows(args.workflow_json) + if args.json_output: + return _print_payload(result, json_output=True) + return _print_payload(render_enumeration_text(result), json_output=False) + + if args.command == "validate": + result = enumerate_workflows(args.workflow_json) + print( + f"Validated {result['workflow_count']} workflows in {result['file']}" + ) + return 0 + + if args.command == "checklist": + checklist_path = resolve_checklist_path(args.checklist) + if args.show: + _, checklist_text = read_checklist(args.checklist) + print(checklist_text) + else: + print(checklist_path) + return 0 + + if args.command == "prepare-review": + result = prepare_review( + workflow_path=args.workflow_json, + checklist_path=args.checklist, + priority_focus=args.priority_focus, + severity_threshold=args.severity_threshold, + ) + return _print_payload(result, json_output=args.json_output) + + if args.command == "plan-remediation": + result = plan_remediation( + workflow_path=args.workflow_json, + remediation_mode=args.mode, + safety_mode=args.safety, + findings_path=args.findings, + priority_focus=args.priority_focus, + ) + return _print_payload(result, json_output=args.json_output) + + except (FileNotFoundError, ValueError) as exc: + print(str(exc), file=sys.stderr) + return 1 + + parser.print_help() + return 1 diff --git a/Toolkit/cli/src/workflow_review/enumerate.py b/Toolkit/cli/src/workflow_review/enumerate.py new file mode 100644 index 0000000..f1aa03b --- /dev/null +++ b/Toolkit/cli/src/workflow_review/enumerate.py @@ -0,0 +1,188 @@ +from __future__ import annotations + +import json +import re +from pathlib import Path +from typing import Any + + +def load_json_file(path: Path) -> tuple[str, Any]: + if not path.exists(): + raise FileNotFoundError(f"Workflow export not found: {path}") + if not path.is_file(): + raise ValueError(f"Workflow export is not a file: {path}") + + text = path.read_text(encoding="utf-8") + try: + data = json.loads(text) + except json.JSONDecodeError as exc: + raise ValueError( + f"Invalid JSON in {path}: line {exc.lineno}, column {exc.colno}: {exc.msg}" + ) from exc + + return text, data + + +def iter_workflows(node: Any, path: list[Any] | None = None) -> list[dict[str, Any]]: + if path is None: + path = [] + + workflows: list[dict[str, Any]] = [] + + if isinstance(node, dict): + if node.get("object_type") == "definition_workflow": + workflows.append({"path": list(path), "workflow": node}) + + for key, value in node.items(): + workflows.extend(iter_workflows(value, [*path, key])) + elif isinstance(node, list): + for index, value in enumerate(node): + workflows.extend(iter_workflows(value, [*path, index])) + + return workflows + + +def is_simple_identifier(value: str) -> bool: + return bool(re.fullmatch(r"[A-Za-z_][A-Za-z0-9_]*", value)) + + +def format_path(path: list[Any]) -> str: + result = "$" + for part in path: + if isinstance(part, int): + result += f"[{part}]" + elif is_simple_identifier(part): + result += f".{part}" + else: + escaped = str(part).replace("\\", "\\\\").replace('"', '\\"') + result += f'["{escaped}"]' + return result + + +def find_start_line(text_lines: list[str], unique_name: str | None) -> int | None: + if not unique_name: + return None + + needle = f'"unique_name": "{unique_name}"' + for index, line in enumerate(text_lines, start=1): + if needle in line: + return index + return None + + +def classify_workflows(workflows: list[dict[str, Any]]) -> None: + parent_assigned = False + + for item in workflows: + path = item["path"] + if path == ["workflow"] and not parent_assigned: + item["workflow_type"] = "parent" + parent_assigned = True + else: + item["workflow_type"] = "embedded" + + if not parent_assigned and workflows: + workflows[0]["workflow_type"] = "parent" + + +def enrich_line_ranges(text: str, workflows: list[dict[str, Any]]) -> None: + text_lines = text.splitlines() + total_lines = len(text_lines) + + for item in workflows: + workflow = item["workflow"] + item["start_line"] = find_start_line(text_lines, workflow.get("unique_name")) + item["end_line"] = None + + indexed = sorted( + ( + (item["start_line"], index) + for index, item in enumerate(workflows) + if item["start_line"] is not None + ), + key=lambda pair: pair[0], + ) + + for position, (_, index) in enumerate(indexed): + next_start = indexed[position + 1][0] if position + 1 < len(indexed) else None + workflows[index]["end_line"] = (next_start - 1) if next_start else total_lines + + +def summarize_workflows(workflows: list[dict[str, Any]]) -> list[dict[str, Any]]: + summary: list[dict[str, Any]] = [] + + for item in workflows: + workflow = item["workflow"] + properties = workflow.get("properties", {}) + name = ( + workflow.get("title") + or properties.get("display_name") + or workflow.get("name") + or workflow.get("unique_name") + or "Unnamed Workflow" + ) + summary.append( + { + "name": name, + "unique_name": workflow.get("unique_name"), + "workflow_type": item["workflow_type"], + "path": format_path(item["path"]), + "start_line": item["start_line"], + "end_line": item["end_line"], + } + ) + + return summary + + +def enumerate_workflows(path: str | Path) -> dict[str, Any]: + workflow_path = Path(path).expanduser().resolve() + text, data = load_json_file(workflow_path) + workflows = iter_workflows(data) + if not workflows: + raise ValueError("No definition_workflow objects found in the supplied JSON export.") + + classify_workflows(workflows) + enrich_line_ranges(text, workflows) + summary = summarize_workflows(workflows) + parent_count = sum(1 for item in summary if item["workflow_type"] == "parent") + embedded_count = sum(1 for item in summary if item["workflow_type"] == "embedded") + + return { + "file": str(workflow_path), + "workflow_count": len(summary), + "parent_count": parent_count, + "embedded_count": embedded_count, + "workflows": summary, + } + + +def render_enumeration_text(result: dict[str, Any]) -> str: + lines = [ + "## Workflow Enumeration", + f"File: {result['file']}", + ( + f"Count: {result['workflow_count']} workflows found " + f"({result['parent_count']} parent + {result['embedded_count']} embedded)" + ), + "", + ] + + for index, item in enumerate(result["workflows"], start=1): + if item["start_line"] and item["end_line"]: + line_range = f"L{item['start_line']}-L{item['end_line']}" + else: + line_range = "unavailable" + + lines.extend( + [ + f"{index}. {item['name']}", + f" Type: {item['workflow_type']}", + f" Unique name: {item['unique_name'] or 'missing'}", + f" Path: {item['path']}", + f" Line range: {line_range}", + "", + ] + ) + + return "\n".join(lines).rstrip() diff --git a/Toolkit/cli/src/workflow_review/mcp_server.py b/Toolkit/cli/src/workflow_review/mcp_server.py new file mode 100644 index 0000000..e647e22 --- /dev/null +++ b/Toolkit/cli/src/workflow_review/mcp_server.py @@ -0,0 +1,191 @@ +from __future__ import annotations + +import json +import sys +from typing import Any + +from workflow_review.checklist import resolve_checklist_path +from workflow_review.enumerate import enumerate_workflows +from workflow_review.remediation import list_remediation_modes, list_safety_modes, plan_remediation +from workflow_review.review import prepare_review + + +PROTOCOL_VERSION = "2024-11-05" + + +TOOLS = [ + { + "name": "workflow_inspect_export", + "description": "Inspect a workflow export and list the parent workflow plus any embedded subworkflows.", + "inputSchema": { + "type": "object", + "required": ["workflow_path"], + "properties": { + "workflow_path": {"type": "string"}, + }, + }, + }, + { + "name": "workflow_resolve_checklist", + "description": "Resolve the canonical workflow review checklist path.", + "inputSchema": { + "type": "object", + "properties": { + "checklist_path": {"type": "string"}, + }, + }, + }, + { + "name": "workflow_prepare_review", + "description": "Prepare a structured review brief for a workflow export without performing the full LLM review.", + "inputSchema": { + "type": "object", + "required": ["workflow_path"], + "properties": { + "workflow_path": {"type": "string"}, + "checklist_path": {"type": "string"}, + "priority_focus": {"type": "string"}, + "severity_threshold": {"type": "string"}, + }, + }, + }, + { + "name": "workflow_plan_remediation", + "description": "Prepare a remediation plan without writing to disk.", + "inputSchema": { + "type": "object", + "required": ["workflow_path", "mode", "safety"], + "properties": { + "workflow_path": {"type": "string"}, + "mode": {"type": "string", "enum": sorted(list_remediation_modes())}, + "safety": {"type": "string", "enum": sorted(list_safety_modes())}, + "findings_path": {"type": "string"}, + "priority_focus": {"type": "string"}, + }, + }, + }, +] + + +def _write_message(message: dict[str, Any]) -> None: + payload = json.dumps(message).encode("utf-8") + sys.stdout.write(f"Content-Length: {len(payload)}\r\n\r\n") + sys.stdout.flush() + sys.stdout.buffer.write(payload) + sys.stdout.buffer.flush() + + +def _read_message() -> dict[str, Any] | None: + headers: dict[str, str] = {} + while True: + line = sys.stdin.buffer.readline() + if not line: + return None + if line == b"\r\n": + break + name, value = line.decode("utf-8").split(":", 1) + headers[name.strip().lower()] = value.strip() + + content_length = int(headers.get("content-length", "0")) + if content_length <= 0: + return None + + payload = sys.stdin.buffer.read(content_length) + return json.loads(payload.decode("utf-8")) + + +def _result_content(payload: Any) -> dict[str, Any]: + return { + "content": [{"type": "text", "text": json.dumps(payload, indent=2)}], + "structuredContent": payload, + } + + +def _handle_tool_call(name: str, arguments: dict[str, Any]) -> dict[str, Any]: + if name in {"workflow_inspect_export", "workflow_enumerate"}: + return _result_content(enumerate_workflows(arguments["workflow_path"])) + if name == "workflow_resolve_checklist": + checklist_path = resolve_checklist_path(arguments.get("checklist_path")) + return _result_content({"checklist_path": str(checklist_path)}) + if name == "workflow_prepare_review": + return _result_content( + prepare_review( + workflow_path=arguments["workflow_path"], + checklist_path=arguments.get("checklist_path"), + priority_focus=arguments.get("priority_focus"), + severity_threshold=arguments.get("severity_threshold"), + ) + ) + if name == "workflow_plan_remediation": + return _result_content( + plan_remediation( + workflow_path=arguments["workflow_path"], + remediation_mode=arguments["mode"], + safety_mode=arguments["safety"], + findings_path=arguments.get("findings_path"), + priority_focus=arguments.get("priority_focus"), + ) + ) + raise ValueError(f"Unknown tool: {name}") + + +def _success(message_id: Any, result: dict[str, Any]) -> dict[str, Any]: + return {"jsonrpc": "2.0", "id": message_id, "result": result} + + +def _error(message_id: Any, code: int, text: str) -> dict[str, Any]: + return { + "jsonrpc": "2.0", + "id": message_id, + "error": {"code": code, "message": text}, + } + + +def handle_message(message: dict[str, Any]) -> dict[str, Any] | None: + method = message.get("method") + message_id = message.get("id") + params = message.get("params", {}) + + if method == "initialize": + return _success( + message_id, + { + "protocolVersion": PROTOCOL_VERSION, + "serverInfo": {"name": "cisco-workflow-review", "version": "0.1.0"}, + "capabilities": {"tools": {}}, + }, + ) + + if method == "notifications/initialized": + return None + + if method == "tools/list": + return _success(message_id, {"tools": TOOLS}) + + if method == "tools/call": + try: + result = _handle_tool_call(params["name"], params.get("arguments", {})) + return _success(message_id, result) + except Exception as exc: # pragma: no cover - best-effort server path + return _error(message_id, -32000, str(exc)) + + if method == "ping": + return _success(message_id, {}) + + if message_id is not None: + return _error(message_id, -32601, f"Unsupported method: {method}") + return None + + +def main() -> int: + while True: + message = _read_message() + if message is None: + return 0 + response = handle_message(message) + if response is not None: + _write_message(response) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/Toolkit/cli/src/workflow_review/remediation.py b/Toolkit/cli/src/workflow_review/remediation.py new file mode 100644 index 0000000..60f3f2a --- /dev/null +++ b/Toolkit/cli/src/workflow_review/remediation.py @@ -0,0 +1,96 @@ +from __future__ import annotations + +from pathlib import Path +from typing import Any + +from workflow_review.enumerate import enumerate_workflows + + +REMEDIATION_MODES = { + "fix-all": "Apply all approved findings that are safe to fix now.", + "fix-high-and-medium": "Apply approved high and medium findings.", + "fix-high-only": "Apply only approved high-severity findings.", + "fix-low-only": "Apply low-risk cleanup and readability fixes only.", + "proposal-only": "Plan the work without editing files.", + "improve-activity-descriptions": "Improve activity titles and descriptions only.", + "improve-workflow-description": "Improve the main workflow description only.", +} + +SAFETY_MODES = { + "update-in-place": "Edit the current file directly.", + "propose-copy": "Create a sibling proposed file and keep the original unchanged.", + "ask-before-major-change": "Apply safe fixes, but stop before structural changes.", +} + +LOW_RISK_MODES = { + "fix-low-only", + "proposal-only", + "improve-activity-descriptions", + "improve-workflow-description", +} + + +def list_remediation_modes() -> dict[str, str]: + return dict(REMEDIATION_MODES) + + +def list_safety_modes() -> dict[str, str]: + return dict(SAFETY_MODES) + + +def major_change_risk(remediation_mode: str) -> bool: + return remediation_mode not in LOW_RISK_MODES + + +def plan_remediation( + workflow_path: str, + remediation_mode: str, + safety_mode: str, + findings_path: str | None = None, + priority_focus: str | None = None, +) -> dict[str, Any]: + if remediation_mode not in REMEDIATION_MODES: + raise ValueError(f"Unsupported remediation mode: {remediation_mode}") + if safety_mode not in SAFETY_MODES: + raise ValueError(f"Unsupported safety mode: {safety_mode}") + + workflow_path = str(Path(workflow_path).expanduser().resolve()) + findings_path = ( + str(Path(findings_path).expanduser().resolve()) if findings_path else None + ) + enumeration = enumerate_workflows(workflow_path) + requires_major_change_review = major_change_risk(remediation_mode) + + if remediation_mode in {"improve-activity-descriptions", "improve-workflow-description"}: + planned_fixes = [ + "Limit changes to user-facing descriptions and readability improvements.", + "Preserve workflow logic, outputs, categories, and targets.", + ] + elif remediation_mode == "proposal-only": + planned_fixes = ["Generate a remediation plan only. No file edits should be applied."] + else: + planned_fixes = [ + "Review approved findings against the selected severity scope.", + "Patch the current workflow incrementally rather than rewriting it.", + "Re-run review after changes and report fixed, remaining, and new issues.", + ] + + approval_required = safety_mode == "ask-before-major-change" and requires_major_change_review + + return { + "workflow_path": workflow_path, + "findings_path": findings_path, + "priority_focus": priority_focus, + "remediation_mode": remediation_mode, + "remediation_mode_description": REMEDIATION_MODES[remediation_mode], + "safety_mode": safety_mode, + "safety_mode_description": SAFETY_MODES[safety_mode], + "major_change_possible": requires_major_change_review, + "approval_required": approval_required, + "enumeration": enumeration, + "planned_fixes": planned_fixes, + "deferred": [ + "Do not apply unrelated cleanup outside the selected remediation scope.", + "Do not change workflow identity fields unless explicitly approved.", + ], + } diff --git a/Toolkit/cli/src/workflow_review/review.py b/Toolkit/cli/src/workflow_review/review.py new file mode 100644 index 0000000..d5b575c --- /dev/null +++ b/Toolkit/cli/src/workflow_review/review.py @@ -0,0 +1,51 @@ +from __future__ import annotations + +from pathlib import Path +from typing import Any + +from workflow_review.checklist import resolve_checklist_path +from workflow_review.enumerate import enumerate_workflows + + +def prepare_review( + workflow_path: str, + checklist_path: str | None = None, + priority_focus: str | None = None, + severity_threshold: str | None = None, +) -> dict[str, Any]: + workflow_path = str(Path(workflow_path).expanduser().resolve()) + enumeration = enumerate_workflows(workflow_path) + + resolved_checklist = None + checklist_error = None + try: + resolved_checklist = str(resolve_checklist_path(checklist_path)) + except FileNotFoundError as exc: + checklist_error = str(exc) + + return { + "workflow_path": workflow_path, + "checklist_path": resolved_checklist, + "checklist_error": checklist_error, + "priority_focus": priority_focus, + "severity_threshold": severity_threshold, + "enumeration": enumeration, + "review_contract": { + "sequence": [ + "Enumerate all workflows before review.", + "Review the parent workflow first.", + "Review each embedded workflow across all 7 checklist categories.", + "Lead with findings ordered by severity.", + "Finish with an overall assessment and next steps.", + ], + "categories": [ + "Inputs & Parameters", + "Targets & Target Groups", + "Atomics & API Usage", + "Groups & Categories", + "Logic & Flow", + "Error Handling", + "Essential Hygiene & Security", + ], + }, + } diff --git a/Toolkit/cli/tests/fixtures/sample_workflow.json b/Toolkit/cli/tests/fixtures/sample_workflow.json new file mode 100644 index 0000000..73e451d --- /dev/null +++ b/Toolkit/cli/tests/fixtures/sample_workflow.json @@ -0,0 +1,22 @@ +{ + "workflow": { + "object_type": "definition_workflow", + "unique_name": "definition_workflow_parent", + "name": "Parent Workflow", + "title": "Parent Workflow", + "properties": { + "display_name": "Parent Workflow" + }, + "embedded": [ + { + "object_type": "definition_workflow", + "unique_name": "definition_workflow_child", + "name": "Child Workflow", + "title": "Child Workflow", + "properties": { + "display_name": "Child Workflow" + } + } + ] + } +} diff --git a/Toolkit/cli/tests/test_enumerate.py b/Toolkit/cli/tests/test_enumerate.py new file mode 100644 index 0000000..21d1814 --- /dev/null +++ b/Toolkit/cli/tests/test_enumerate.py @@ -0,0 +1,28 @@ +from __future__ import annotations + +import json +import unittest +from pathlib import Path + +from workflow_review.enumerate import enumerate_workflows + + +FIXTURE_PATH = Path(__file__).parent / "fixtures" / "sample_workflow.json" + + +class EnumerateWorkflowTests(unittest.TestCase): + def test_enumerates_parent_and_embedded_workflows(self) -> None: + result = enumerate_workflows(FIXTURE_PATH) + self.assertEqual(result["workflow_count"], 2) + self.assertEqual(result["parent_count"], 1) + self.assertEqual(result["embedded_count"], 1) + self.assertEqual(result["workflows"][0]["workflow_type"], "parent") + self.assertEqual(result["workflows"][1]["workflow_type"], "embedded") + + def test_fixture_is_valid_json(self) -> None: + data = json.loads(FIXTURE_PATH.read_text(encoding="utf-8")) + self.assertIn("workflow", data) + + +if __name__ == "__main__": + unittest.main() diff --git a/Toolkit/cursor/README.md b/Toolkit/cursor/README.md new file mode 100644 index 0000000..b68b2bd --- /dev/null +++ b/Toolkit/cursor/README.md @@ -0,0 +1,28 @@ +# Cursor Wrappers + +This directory contains thin Cursor wrappers for the public toolkit. + +The wrappers intentionally delegate deterministic work to the shared CLI in `../cli/` and keep only the agent-facing orchestration in `SKILL.md`. + +## Install + +From the repository root: + +```bash +bash Toolkit/cursor/install_cursor_skills.sh +``` + +This installs the public workflow-review skills into `~/.cursor/skills`. + +## Included wrappers + +- `exchange-workflow-review` +- `workflow-review-remediation` + +## Design rule + +If a change can live in the CLI, it should live in the CLI. The Cursor wrappers should stay focused on: + +- collecting inputs +- choosing the right CLI calls +- presenting the result in a reviewer-friendly format diff --git a/Toolkit/cursor/exchange-workflow-review/SKILL.md b/Toolkit/cursor/exchange-workflow-review/SKILL.md new file mode 100644 index 0000000..c1c2d13 --- /dev/null +++ b/Toolkit/cursor/exchange-workflow-review/SKILL.md @@ -0,0 +1,51 @@ +--- +name: exchange-workflow-review +description: Review exported Cisco workflow JSON against the public Workflow Review Checklist, inspect the export to list parent and embedded subworkflows first, and produce severity-ranked findings plus an overall readiness assessment. +--- + +# Exchange Workflow Review + +## When to use + +- The user provides a workflow export JSON file. +- The user wants Exchange review, standards validation, or approval-readiness feedback. +- The user wants findings aligned to the public workflow review checklist. + +## Inputs to collect + +- Workflow export JSON path +- Optional priority focus: `Security`, `Performance`, or `Maintainability` +- Optional severity threshold: `Critical`, `High`, `Medium`, or `Low` + +## Required flow + +1. Use the canonical checklist at the repo root when available: `WorkflowReviewChecklist.md`. +2. Resolve the real skill directory and the repo root from this skill's file location. Do not assume the current working directory is the repo. +3. Run the shared CLI first to inspect the workflow export and establish review scope: + ```bash + PYTHONPATH="/Toolkit/cli/src" python3 -m workflow_review inspect-workflow-export "/path/to/workflow.json" + ``` +4. Prepare the review brief: + ```bash + PYTHONPATH="/Toolkit/cli/src" python3 -m workflow_review prepare-review "/path/to/workflow.json" --json + ``` +5. Present the enumeration list before deeper review. +6. Review one workflow at a time across all 7 checklist categories. +7. Lead with findings ordered by severity, then finish with the overall assessment and top improvements. + +## Review rules + +- Do not skip embedded subworkflows. +- Prefer user-facing names over raw internal IDs. +- Use `reference.md` for the local review sequence and severity guidance. +- Keep the checklist as the source of truth if there is any conflict. + +## Output sections + +- Enumeration +- Critical issues +- High priority issues +- Medium priority issues +- Low priority issues +- Workflow-by-workflow notes +- Overall assessment diff --git a/Toolkit/cursor/exchange-workflow-review/reference.md b/Toolkit/cursor/exchange-workflow-review/reference.md new file mode 100644 index 0000000..3a27763 --- /dev/null +++ b/Toolkit/cursor/exchange-workflow-review/reference.md @@ -0,0 +1,18 @@ +# Cursor Review Wrapper Reference + +Use this wrapper with the shared CLI in `Toolkit/cli/`. + +## Minimum command set + +```bash +PYTHONPATH="/Toolkit/cli/src" python3 -m workflow_review inspect-workflow-export "/path/to/workflow.json" +PYTHONPATH="/Toolkit/cli/src" python3 -m workflow_review prepare-review "/path/to/workflow.json" --json +``` + +## Reminder + +- Enumerate first. +- Review parent workflow first. +- Cover all embedded workflows. +- Lead with findings. +- Keep the root `WorkflowReviewChecklist.md` canonical. diff --git a/Toolkit/cursor/install_cursor_skills.sh b/Toolkit/cursor/install_cursor_skills.sh new file mode 100755 index 0000000..1bc9296 --- /dev/null +++ b/Toolkit/cursor/install_cursor_skills.sh @@ -0,0 +1,33 @@ +#!/usr/bin/env bash +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +SKILLS_SRC_DIR="$REPO_ROOT/Toolkit/cursor" +CURSOR_SKILLS_DIR="${HOME}/.cursor/skills" + +mkdir -p "$CURSOR_SKILLS_DIR" + +installed=0 + +for skill_dir in "$SKILLS_SRC_DIR"/*; do + [ -d "$skill_dir" ] || continue + [ -f "$skill_dir/SKILL.md" ] || continue + + skill_name="$(basename "$skill_dir")" + target_link="$CURSOR_SKILLS_DIR/$skill_name" + + if [ -L "$target_link" ] || [ -e "$target_link" ]; then + rm -rf "$target_link" + fi + + ln -s "$skill_dir" "$target_link" + printf 'Installed skill: %s -> %s\n' "$skill_name" "$target_link" + installed=$((installed + 1)) +done + +if [ "$installed" -eq 0 ]; then + printf 'No skill directories with SKILL.md were found under %s\n' "$SKILLS_SRC_DIR" >&2 + exit 1 +fi + +printf '\nInstalled %d skill(s) into %s\n' "$installed" "$CURSOR_SKILLS_DIR" diff --git a/Toolkit/cursor/workflow-review-remediation/SKILL.md b/Toolkit/cursor/workflow-review-remediation/SKILL.md new file mode 100644 index 0000000..100cfb7 --- /dev/null +++ b/Toolkit/cursor/workflow-review-remediation/SKILL.md @@ -0,0 +1,48 @@ +--- +name: workflow-review-remediation +description: Apply approved workflow review fixes safely by building a remediation plan, honoring remediation and safety modes, and preserving workflow identity unless the user explicitly approves structural changes. +disable-model-invocation: true +--- + +# Workflow Review Remediation + +## When to use + +- The user wants to apply workflow review feedback instead of only reading findings. +- The user asks to fix checklist issues by severity. +- The user wants description-only cleanup or a guarded remediation plan. + +## Inputs to collect + +- Workflow export JSON path +- Remediation mode +- Safety mode +- Optional findings source +- Optional priority focus + +## Required flow + +1. Reuse an approved findings set when available. Otherwise run the review wrapper first. +2. Resolve the real skill directory and repo root from this skill's file location. Do not assume the current working directory is the repo. +3. Enumerate the workflow scope before editing: + ```bash + PYTHONPATH="/Toolkit/cli/src" python3 -m workflow_review inspect-workflow-export "/path/to/workflow.json" --json + ``` +4. Build the remediation plan first: + ```bash + PYTHONPATH="/Toolkit/cli/src" python3 -m workflow_review plan-remediation "/path/to/workflow.json" --mode fix-high-only --safety ask-before-major-change --json + ``` +5. Apply the selected safety mode exactly. +6. Re-review after edits and report fixed, remaining, and new issues. + +## Guardrails + +- No rewrite-by-default. +- Preserve `name`, `title`, `unique_name`, and core intent unless the user explicitly approves otherwise. +- Keep changes inside the selected remediation mode. +- Stop if JSON validity or post-edit review fails. +- Description-only modes must remain non-structural. + +## Additional reference + +Use `reference.md` for mode definitions and the major-change rules. diff --git a/Toolkit/cursor/workflow-review-remediation/reference.md b/Toolkit/cursor/workflow-review-remediation/reference.md new file mode 100644 index 0000000..a20cf7c --- /dev/null +++ b/Toolkit/cursor/workflow-review-remediation/reference.md @@ -0,0 +1,17 @@ +# Cursor Remediation Wrapper Reference + +Use this wrapper with the shared CLI in `Toolkit/cli/`. + +## Minimum command set + +```bash +PYTHONPATH="/Toolkit/cli/src" python3 -m workflow_review inspect-workflow-export "/path/to/workflow.json" --json +PYTHONPATH="/Toolkit/cli/src" python3 -m workflow_review plan-remediation "/path/to/workflow.json" --mode fix-high-only --safety ask-before-major-change --json +``` + +## Key rules + +- Reuse approved findings when possible. +- Stay inside the chosen remediation scope. +- Treat branch, output, target, and category changes as potentially major. +- Re-review after edits. diff --git a/Toolkit/examples/README.md b/Toolkit/examples/README.md new file mode 100644 index 0000000..e244327 --- /dev/null +++ b/Toolkit/examples/README.md @@ -0,0 +1,25 @@ +# Toolkit Examples + +This directory shows how the public toolkit is intended to be used without requiring a private repo or internal-only tooling. + +## Review example + +From `Toolkit/cli`: + +```bash +PYTHONPATH=src python3 -m workflow_review inspect-workflow-export "../../Meraki/CheckAvailableFirmwareForNetwork__definition_workflow_02M30GYJJSYJL0wQPPnkQgIcavBkG6796mF/definition_workflow_02M30GYJJSYJL0wQPPnkQgIcavBkG6796mF.json" +PYTHONPATH=src python3 -m workflow_review prepare-review "../../Meraki/CheckAvailableFirmwareForNetwork__definition_workflow_02M30GYJJSYJL0wQPPnkQgIcavBkG6796mF/definition_workflow_02M30GYJJSYJL0wQPPnkQgIcavBkG6796mF.json" --json +``` + +## Remediation planning example + +```bash +PYTHONPATH=src python3 -m workflow_review plan-remediation "../../Meraki/CheckAvailableFirmwareForNetwork__definition_workflow_02M30GYJJSYJL0wQPPnkQgIcavBkG6796mF/definition_workflow_02M30GYJJSYJL0wQPPnkQgIcavBkG6796mF.json" \ + --mode fix-low-only \ + --safety ask-before-major-change \ + --json +``` + +## Sample output + +See `review-output.md` for the intended structure of a reviewer-facing summary. diff --git a/Toolkit/examples/review-output.md b/Toolkit/examples/review-output.md new file mode 100644 index 0000000..8fa3ae0 --- /dev/null +++ b/Toolkit/examples/review-output.md @@ -0,0 +1,30 @@ +# Sample Review Output + +```markdown +## Workflow Review Results + +### Enumeration +- Workflow 1: Parent Workflow +- Workflow 2: Embedded Workflow + +### High Priority Issues +- Activity "Submit API Request" does not surface failure details in the workflow outputs. + +### Low Priority Issues +- The main workflow description is too thin for Exchange reviewers. + +### Workflow-by-Workflow Notes +#### Parent Workflow +- Error Handling: Add explicit success and failure output updates. + +#### Embedded Workflow +- Essential Hygiene & Security: Replace a real org ID default with a placeholder. + +### Overall Assessment +- Score: 7/10 +- Approval readiness: approve with suggestions +- Top improvements: + - improve output handling + - remove production-like defaults + - strengthen reviewer-facing descriptions +``` diff --git a/Toolkit/mcp/README.md b/Toolkit/mcp/README.md new file mode 100644 index 0000000..3a6d744 --- /dev/null +++ b/Toolkit/mcp/README.md @@ -0,0 +1,32 @@ +# Workflow Review MCP + +This directory documents the Layer 3 stdio MCP path for the public toolkit. + +The MCP server is intentionally thin. It wraps the same core package used by the CLI instead of re-implementing workflow review logic. + +## Current tools + +- `workflow_inspect_export` +- `workflow_resolve_checklist` +- `workflow_prepare_review` +- `workflow_plan_remediation` + +These tools are read-only planning helpers in the first public slice. + +## Run the stdio server + +```bash +cd Toolkit/cli +PYTHONPATH=src python3 -m workflow_review.mcp_server +``` + +## Example Cursor MCP config + +See `cursor.example.json`. + +## Design intent + +- Local stdio transport first +- Shared core with the CLI +- Narrow tools with explicit scope +- No file-writing remediation in the initial public scaffold diff --git a/Toolkit/mcp/cursor.example.json b/Toolkit/mcp/cursor.example.json new file mode 100644 index 0000000..72993cd --- /dev/null +++ b/Toolkit/mcp/cursor.example.json @@ -0,0 +1,16 @@ +{ + "mcpServers": { + "cisco-workflow-review": { + "command": "python3", + "args": [ + "-m", + "workflow_review.mcp_server" + ], + "cwd": "/path/to/CiscoWorkflowsAutomation/Toolkit/cli", + "env": { + "PYTHONPATH": "/path/to/CiscoWorkflowsAutomation/Toolkit/cli/src", + "WORKFLOW_REVIEW_CHECKLIST": "/path/to/CiscoWorkflowsAutomation/WorkflowReviewChecklist.md" + } + } + } +} diff --git a/Toolkit/remediation/README.md b/Toolkit/remediation/README.md new file mode 100644 index 0000000..27b0eb9 --- /dev/null +++ b/Toolkit/remediation/README.md @@ -0,0 +1,44 @@ +# Workflow Remediation Guide + +Use this guide after a review has identified accepted findings to fix. + +The remediation path in this public toolkit is intentionally conservative: + +- review first +- scope fixes explicitly +- preserve workflow identity +- avoid rewrite-by-default +- stop before major structural edits unless the safety mode allows them + +## Planning first + +The initial public CLI prepares remediation plans without applying edits: + +```bash +cd Toolkit/cli +PYTHONPATH=src python3 -m workflow_review plan-remediation "/path/to/workflow.json" \ + --mode fix-high-only \ + --safety ask-before-major-change \ + --json +``` + +That plan can then drive: + +- a human review pass +- a Cursor skill invocation +- or a future automated remediation engine + +## Modes + +See `reference.md` for: + +- remediation modes such as `fix-all` or `improve-workflow-description` +- safety modes such as `update-in-place` or `ask-before-major-change` +- the public definition of a major change + +## Guardrails + +- Patch existing workflows instead of regenerating them. +- Keep changes inside the approved remediation scope. +- Preserve `name`, `title`, `unique_name`, and core workflow intent unless explicitly approved otherwise. +- Re-review after edits and report fixed, remaining, and new issues. diff --git a/Toolkit/remediation/reference.md b/Toolkit/remediation/reference.md new file mode 100644 index 0000000..0a327f5 --- /dev/null +++ b/Toolkit/remediation/reference.md @@ -0,0 +1,68 @@ +# Workflow Remediation Reference + +## Prerequisite + +Use workflow review first unless you already have an approved findings list. + +## Remediation modes + +### `fix-all` +Apply all approved findings that fit the selected safety mode. + +### `fix-high-and-medium` +Apply approved `High` and `Medium` findings. + +### `fix-high-only` +Apply approved `High` findings only. + +### `fix-low-only` +Apply low-risk cleanup and readability fixes only. + +### `proposal-only` +Produce a concrete plan without editing files. + +### `improve-activity-descriptions` +Improve activity titles and descriptions only. This must remain non-structural. + +### `improve-workflow-description` +Improve only the main workflow description. This must remain non-structural. + +## Safety modes + +### `update-in-place` +Edit the current workflow file directly. + +### `propose-copy` +Create a sibling proposed file and leave the original unchanged. + +### `ask-before-major-change` +Apply safe fixes directly, but stop before structural changes. + +## Major change definition + +Treat the remediation as a major change when it would: + +- add or remove branches +- add or remove outputs +- add or remove major logic blocks +- materially change target behavior +- materially change categories +- convert a non-atomic workflow into an atomic workflow +- redesign the workflow rather than patching it + +## Non-negotiable guardrails + +- No rewrite-by-default. +- Preserve workflow identity. +- Stay inside the selected remediation scope. +- Re-review after edits. + +## Useful command + +```bash +cd Toolkit/cli +PYTHONPATH=src python3 -m workflow_review plan-remediation "/path/to/workflow.json" \ + --mode fix-high-only \ + --safety ask-before-major-change \ + --json +``` diff --git a/Toolkit/review/README.md b/Toolkit/review/README.md new file mode 100644 index 0000000..e418260 --- /dev/null +++ b/Toolkit/review/README.md @@ -0,0 +1,36 @@ +# Workflow Review Guide + +Use this guide when you want to review a workflow export against the canonical `WorkflowReviewChecklist.md`. + +The recommended flow is: + +1. Enumerate the workflow scope first. +2. Review the parent workflow. +3. Review each embedded workflow. +4. Aggregate findings by severity. +5. End with an approval-readiness summary and next actions. + +## CLI-first path + +```bash +cd Toolkit/cli +PYTHONPATH=src python3 -m workflow_review inspect-workflow-export "/path/to/workflow.json" +PYTHONPATH=src python3 -m workflow_review prepare-review "/path/to/workflow.json" --json +``` + +The `prepare-review` output is intentionally structured so it can be handed to another LLM or used by a thin Cursor or MCP wrapper. + +## Manual/LLM path + +If you are using another LLM directly: + +1. Attach `WorkflowReviewChecklist.md` +2. Attach the workflow JSON export +3. Use the reference in `reference.md` to keep the review sequence and output format consistent + +## Expectations + +- Do not skip embedded subworkflows. +- Keep the public checklist as the source of truth. +- Lead with findings, sorted by severity. +- Use user-facing names instead of raw internal IDs whenever possible. diff --git a/Toolkit/review/reference.md b/Toolkit/review/reference.md new file mode 100644 index 0000000..d9c4f9a --- /dev/null +++ b/Toolkit/review/reference.md @@ -0,0 +1,62 @@ +# Workflow Review Reference + +## Source of truth + +The canonical review standard is the root-level `WorkflowReviewChecklist.md` in this repository. + +Use that file whenever it is available. This reference is the lightweight companion for tools and wrappers in `Toolkit/`. + +## Mandatory sequence + +1. Enumerate all workflows in the export first. +2. Present the parent workflow and every embedded workflow before deeper review. +3. Review one workflow at a time across all 7 checklist categories. +4. Lead with findings ordered by severity. +5. End with an overall assessment and next actions. + +## Checklist categories + +1. Inputs & Parameters +2. Targets & Target Groups +3. Atomics & API Usage +4. Groups & Categories +5. Logic & Flow +6. Error Handling +7. Essential Hygiene & Security + +## Reporting rules + +- Use user-facing labels whenever possible. +- Avoid raw internal IDs in reviewer-facing comments. +- For each issue include: + - location + - problem + - recommended fix + - severity +- Reviews are incomplete until embedded workflows are covered too. + +## Severity guidance + +- `High`: correctness, hidden failure, security, or likely approval blocker +- `Medium`: meaningfully important but not a blocker +- `Low`: cleanup, readability, consistency, and maintainability + +## Review output shape + +Suggested sections: + +- Enumeration +- Critical issues +- High priority issues +- Medium priority issues +- Low priority issues +- Workflow-by-workflow notes +- Overall assessment + +## Useful commands + +```bash +cd Toolkit/cli +PYTHONPATH=src python3 -m workflow_review inspect-workflow-export "/path/to/workflow.json" +PYTHONPATH=src python3 -m workflow_review prepare-review "/path/to/workflow.json" --json +``` diff --git a/WorkflowsSubmissionProcess.md b/WorkflowsSubmissionProcess.md index 495f7b3..bad61fe 100644 --- a/WorkflowsSubmissionProcess.md +++ b/WorkflowsSubmissionProcess.md @@ -6,6 +6,12 @@ A Markdown file has been created and checked into the Public Repo which contains criterial for Workflow review prior to submission. This file can be provided to your LLM of choice as well as the JSON payload representing your workflow. https://github.com/CiscoDevNet/CiscoWorkflowsAutomation/blob/main/WorkflowReviewChecklist.md +The public `Toolkit/` directory in this repository now provides three ways to apply that checklist: + +- `Toolkit/cli/` for command-line review helpers +- `Toolkit/cursor/` for installable Cursor wrappers +- `Toolkit/mcp/` for a thin stdio MCP path + The LLM will apply the critera against your workflow highlighting: - Workflow Inputs & Parameters - Targets & Target Groups From 934a83e23ae539fb8adc192148f7baf44f09bbde Mon Sep 17 00:00:00 2001 From: Christy Jones Date: Wed, 10 Jun 2026 11:52:28 -0400 Subject: [PATCH 2/6] Package the workflow review standard and simplify MCP setup. Make the review tooling installable without repo-relative paths by bundling the checklist, exposing the installed MCP entrypoint, and updating the docs and examples to use the review-first flow. Co-authored-by: Cursor --- Toolkit/README.md | 14 +- Toolkit/cli/README.md | 11 +- Toolkit/cli/pyproject.toml | 3 + Toolkit/cli/src/workflow_review/checklist.py | 22 ++ .../data/WorkflowReviewChecklist.md | 226 ++++++++++++++++++ Toolkit/cli/src/workflow_review/mcp_server.py | 46 +++- .../cursor/exchange-workflow-review/SKILL.md | 29 +-- .../exchange-workflow-review/reference.md | 3 +- Toolkit/mcp/README.md | 75 +++++- Toolkit/mcp/cursor.example.json | 5 + Toolkit/mcp/install_mcp.sh | 30 +++ Toolkit/review/README.md | 15 +- Toolkit/review/reference.md | 9 +- 13 files changed, 439 insertions(+), 49 deletions(-) create mode 100644 Toolkit/cli/src/workflow_review/data/WorkflowReviewChecklist.md create mode 100755 Toolkit/mcp/install_mcp.sh diff --git a/Toolkit/README.md b/Toolkit/README.md index fd5d598..4195de2 100644 --- a/Toolkit/README.md +++ b/Toolkit/README.md @@ -1,8 +1,8 @@ # Workflow Toolkit -This toolkit turns the public workflow review checklist in this repository into reusable review and remediation paths for multiple audiences. +This toolkit turns the internal workflow review standard into reusable review and remediation paths for the team. -The canonical review contract remains the root-level `WorkflowReviewChecklist.md`. Everything in `Toolkit/` is designed to help contributors apply that checklist more consistently. +The packaged internal review standard is the default source for review and remediation helpers. Everything in `Toolkit/` is designed to help contributors apply that standard more consistently, while still allowing repo-based development overrides when needed. ## Who It Is For @@ -39,7 +39,7 @@ Toolkit/ ## Layering -- Layer 1: the CLI is the shared core for enumeration, checklist resolution, review preparation, and remediation planning. +- Layer 1: the CLI is the shared core for enumeration, checklist resolution, review preparation with remediation suggestions, and remediation planning. - Layer 2: Cursor skills stay thin and delegate deterministic work to the CLI. - Layer 3: the MCP server wraps the same core instead of re-implementing logic. @@ -59,6 +59,12 @@ python3 -m workflow_review prepare-review "/path/to/workflow.json" --json bash Toolkit/cursor/install_cursor_skills.sh ``` +### Install the workflow review MCP + +```bash +bash Toolkit/mcp/install_mcp.sh +``` + ### Run the stdio MCP scaffold ```bash @@ -66,6 +72,8 @@ cd Toolkit/cli python3 -m workflow_review.mcp_server ``` +For installed-client usage, see `Toolkit/mcp/README.md` for Cursor, VS Code, and generic stdio MCP examples that point at `workflow-review-mcp` directly and start with `review`; `inspect_export` is available as an advanced helper. + ## Scope This public toolkit intentionally focuses on shareable workflow review and remediation content. Internal Jira/reporting skills and local scratch exports should remain outside this repo unless they are scrubbed and clearly reusable. diff --git a/Toolkit/cli/README.md b/Toolkit/cli/README.md index 98459fd..42270de 100644 --- a/Toolkit/cli/README.md +++ b/Toolkit/cli/README.md @@ -48,6 +48,8 @@ PYTHONPATH=src python3 -m workflow_review checklist PYTHONPATH=src python3 -m workflow_review checklist --show ``` +When installed, the CLI resolves the packaged internal review standard automatically if neither `--checklist` nor `WORKFLOW_REVIEW_CHECKLIST` is set. + ### Prepare a review run ```bash @@ -69,16 +71,19 @@ The CLI resolves the checklist in this order: 1. `--checklist /path/to/file.md` 2. `WORKFLOW_REVIEW_CHECKLIST=/path/to/file.md` -3. the repo-root `WorkflowReviewChecklist.md` +3. the packaged internal review standard +4. the repo-root `WorkflowReviewChecklist.md` -That keeps the root file canonical while still allowing external users to point at another checked-in copy if needed. +That keeps the packaged internal standard available by default while still allowing the team to point at another checked-in copy during development. ## MCP The stdio MCP scaffold is implemented in the same package and exposed through: ```bash -PYTHONPATH=src python3 -m workflow_review.mcp_server +workflow-review-mcp ``` +For the easiest MCP setup, run `bash Toolkit/mcp/install_mcp.sh` from the repo root. It installs the package and prints the exact command path to use. + See `../mcp/README.md` for client configuration examples. diff --git a/Toolkit/cli/pyproject.toml b/Toolkit/cli/pyproject.toml index a4c76ba..1a54cc2 100644 --- a/Toolkit/cli/pyproject.toml +++ b/Toolkit/cli/pyproject.toml @@ -20,3 +20,6 @@ workflow-review-mcp = "workflow_review.mcp_server:main" [tool.hatch.build.targets.wheel] packages = ["src/workflow_review"] + +[tool.hatch.build.targets.wheel.force-include] +"../../WorkflowReviewChecklist.md" = "src/workflow_review/data/WorkflowReviewChecklist.md" diff --git a/Toolkit/cli/src/workflow_review/checklist.py b/Toolkit/cli/src/workflow_review/checklist.py index be81d54..9a47ef1 100644 --- a/Toolkit/cli/src/workflow_review/checklist.py +++ b/Toolkit/cli/src/workflow_review/checklist.py @@ -2,10 +2,14 @@ import os from pathlib import Path +from tempfile import NamedTemporaryFile +from importlib import resources CHECKLIST_ENV_VAR = "WORKFLOW_REVIEW_CHECKLIST" CHECKLIST_FILENAME = "WorkflowReviewChecklist.md" +PACKAGE_CHECKLIST_RESOURCE = "data/WorkflowReviewChecklist.md" +_PACKAGED_CHECKLIST_PATH: Path | None = None def _candidate_repo_roots() -> list[Path]: @@ -30,6 +34,10 @@ def resolve_checklist_path(explicit_path: str | None = None) -> Path: if candidate.exists() and candidate.is_file(): return candidate + package_resource = resources.files("workflow_review").joinpath(PACKAGE_CHECKLIST_RESOURCE) + if package_resource.is_file(): + return _materialize_packaged_checklist(package_resource) + searched = ", ".join(str(path) for path in candidates[:5]) raise FileNotFoundError( f"Could not resolve {CHECKLIST_FILENAME}. Checked explicit path, " @@ -40,3 +48,17 @@ def resolve_checklist_path(explicit_path: str | None = None) -> Path: def read_checklist(explicit_path: str | None = None) -> tuple[Path, str]: checklist_path = resolve_checklist_path(explicit_path=explicit_path) return checklist_path, checklist_path.read_text(encoding="utf-8") + + +def _materialize_packaged_checklist(package_resource: resources.abc.Traversable) -> Path: + global _PACKAGED_CHECKLIST_PATH + if _PACKAGED_CHECKLIST_PATH and _PACKAGED_CHECKLIST_PATH.exists(): + return _PACKAGED_CHECKLIST_PATH + + with resources.as_file(package_resource) as checklist_path: + source_path = Path(checklist_path) + with NamedTemporaryFile("w", suffix=".md", delete=False, encoding="utf-8") as handle: + handle.write(source_path.read_text(encoding="utf-8")) + _PACKAGED_CHECKLIST_PATH = Path(handle.name) + + return _PACKAGED_CHECKLIST_PATH diff --git a/Toolkit/cli/src/workflow_review/data/WorkflowReviewChecklist.md b/Toolkit/cli/src/workflow_review/data/WorkflowReviewChecklist.md new file mode 100644 index 0000000..c5c93e5 --- /dev/null +++ b/Toolkit/cli/src/workflow_review/data/WorkflowReviewChecklist.md @@ -0,0 +1,226 @@ +# Workflow Review Checklist + +This checklist is designed for LLM agents to systematically review JSON workflow files in the CiscoWorkflowsAutomation repository. Each item includes specific JSON elements to inspect and validation criteria for automated analysis. + +## 1. Workflow Inputs & Parameters + +**JSON Elements to Inspect:** `definition_workflow.properties.input_groups`, `definition_workflow.properties.inputs` + +- [ ] **Input Validation**: **If inputs exist**, check each input in `inputs` array has `name`, `type`, `description`, and appropriate `required` flag - **Note**: Workflows with no inputs are valid for automated/scheduled workflows +- [ ] **Secure String Review**: For inputs with `type: "SecureString"`, verify `name` is descriptive and `scope` is documented in description +- [ ] **Standard Outputs Present**: Verify outputs include `Result`, `Status Code`, `Status Message`, `Error Message` in `outputs` array +- [ ] **Default Values Safety**: Scan `default_value` fields for production data (real IPs, device serials, org IDs, passwords) - should be generic placeholders only +- [ ] **Input Redundancy**: Identify duplicate or similar inputs that could be consolidated +- [ ] **Variable Naming Convention**: + - Workflow variables (user-facing): Human-readable, capitalized (e.g., "Device ID", "Sort By Date") + - Code activity outputs: camelCase (e.g., "deviceID", "sortByDate") + - **Note**: System-generated `unique_name` fields use random alphanumeric patterns - this is expected XDR behavior +- [ ] **Prefix Check**: Ensure variable names don't use unnecessary "Input -" or "Output -" prefixes + +## 2. Targets & Target Groups + +**JSON Elements to Inspect:** `definition_workflow.properties.targets`, `definition_workflow.properties.target_groups` + +- [ ] **Hardcoded Target Analysis**: Check if `targets` array contains specific target references - validate if justified for scheduled/webhook workflows +- [ ] **Target Customization**: Verify `targets` can be overridden during installation (not hardcoded in workflow logic) - **Note**: Some workflows legitimately require specific target groups for proper operation +- [ ] **Target Group Genericity**: In `target_groups`, check `matching_conditions` don't contain environment-specific values (specific hostnames, IPs, user IDs) unless justified +- [ ] **Unused Target Cleanup**: Cross-reference targets/target_groups with actual usage in workflow activities + +## 3. Atomics & API Usage + +**JSON Elements to Inspect:** `definition_workflow.activities`, activity `type` fields, `api_requests` + +- [ ] **New Atomic Justification**: Identify activities with `type: "atomic_workflow"` - check if description explains why new atomic was needed +- [ ] **Generic API Usage**: Look for activities with `type: "web_service_request"` - verify if atomic alternative exists and document rationale +- [ ] **API Request Documentation**: For generic API calls, ensure future improvement notes are in activity description + +## 4. Groups & Categories + +**JSON Elements to Inspect:** `definition_workflow.properties.groups`, `definition_workflow.properties.categories` + +- [ ] **Group Functionality**: Review `groups` array for clear functional purpose vs. existing groups +- [ ] **Category Appropriateness**: Check that `categories` array contains relevant classifications for the workflow's purpose and domain +- [ ] **Unnecessary Category Creation**: Flag if new categories are being created when existing ones would be more appropriate (check against repository's existing category taxonomy) +- [ ] **Missing Categories**: Verify workflow isn't missing obvious category classifications that would help with organization and discoverability +- [ ] **Orphaned Elements**: Identify groups/categories defined but not referenced in workflow activities + +## 5. Logic & Flow + +**JSON Elements to Inspect:** `definition_workflow.activities`, activity relationships, conditional logic + +- [ ] **Group Activity Usage**: Verify major workflow sections use `type: "group"` activities with descriptive `name` and `description` +- [ ] **Validation Steps**: After API calls or sub-workflows, check for success/failure validation activities +- [ ] **Continue on Failure**: For activities that should continue on error, verify `continue_on_failure: true` is set appropriately +- [ ] **Idempotency Implementation**: **CRITICAL** - For CREATE/DELETE operations: + - CREATE: Look for existence checks before creation (conditional logic, error handling for "already exists") + - DELETE: Look for existence validation before deletion (handle "not found" gracefully) + - Verify error handling allows multiple executions without failure + - **Note**: Some operations may be inherently idempotent or designed for single execution - verify if multiple runs are expected +- [ ] **Loop Exit Conditions**: In loop activities, verify `break_conditions` or `max_iterations` prevent infinite execution +- [ ] **Modularity Assessment**: Identify repeated logic blocks that could be extracted to sub-workflows + +## 6. Error Handling + +**JSON Elements to Inspect:** Error handling activities, output variable assignments, `continue_on_failure` settings + +- [ ] **Explicit Success/Failure Paths**: Verify both success and failure branches update `Result`, `Status Code`, `Status Message`, `Error Message` outputs +- [ ] **Error Aggregation**: Check that errors from sub-workflows and API calls are captured and surfaced in outputs +- [ ] **Error Message Population**: Ensure failure paths set meaningful values in `Error Message` output +- [ ] **Timeout Implementation**: For long-running operations, verify `timeout` settings and retry logic exist + +## 7. Essential Hygiene & Security + +**JSON Elements to Inspect:** All string values, variable assignments, activity configurations + +- [ ] **Sensitive Data Scan**: Search for passwords, API keys, tokens in plain text - should use `SecureString` variables instead +- [ ] **Description Quality**: Check `definition_workflow.properties.description` includes purpose, prerequisites, limitations, dependencies +- [ ] **Placeholder Data**: Scan for sample/test data in variable defaults, API endpoints, or documentation +- [ ] **Naming Consistency**: Verify consistent naming patterns across inputs, outputs, groups, categories, activities +- [ ] **Cleanup Verification**: Ensure no unused variables, groups, categories, or debug artifacts remain +- [ ] **Grammar and Spelling**: Validate text fields for typos and grammatical errors + +## LLM Review Instructions + +### Prompt Template: + +``` +Context: You are a workflow validator, checking the quality of the Workflow before it can be approved. + +Objective: Validate the Workflow with the Workflow-Review-Checklist items so that there is consistency and quality in approved workflows. Provide overall assessment score. + +Workflow: [Workflow Name] + +MANDATORY MULTI-PHASE REVIEW PROCESS: + +PHASE 1 - ENUMERATION (Complete this FIRST): +- Identify and list ALL workflows in the JSON file +- For each workflow, document: name, type (parent/embedded), line range +- Present the enumeration list before proceeding +- Count: X workflows found (1 parent + Y embedded subworkflows) + +PHASE 2 - DETAILED REVIEW (One workflow at a time): +- Review each workflow individually against all 7 checklist categories +- Complete all categories for Workflow N before starting Workflow N+1 +- Report findings for each workflow separately + +PHASE 3 - AGGREGATE ASSESSMENT: +- Compile findings across all workflows +- Provide overall quality score and recommendations + +Audience: The feedback will be for workflow developers with technical expertise seeking approval of their workflows. Suggest improvements for the findings. + +Style: Provide a bulleted list of issues for each Checklist Category. + +Tone: Crisp, not too verbose. +``` + +**Optional Enhancements:** +- `File Path: [path/to/workflow.json]` - Helps LLM understand file context +- `Priority Focus: [Security/Performance/Maintainability]` - When specific concerns exist +- `Severity Threshold: [Critical/High/Medium/Low]` - To filter noise for mature workflows + +### Analysis Process: +1. **Parse Full JSON Structure**: Load the workflow JSON and identify the parent workflow plus all embedded subworkflows in the file. +2. **Enumerate Review Scope - MANDATORY FIRST STEP**: + - Build a complete list of ALL workflows to review BEFORE starting validation + - For each workflow, document: name, unique_name, line range, and type (parent/subworkflow) + - Present this enumeration to the reviewer/user for confirmation + - DO NOT proceed to validation until enumeration is complete and confirmed +3. **Systematic Validation - One Workflow at a Time**: + - Review workflows sequentially (parent first, then each subworkflow) + - For EACH workflow, apply ALL 7 checklist categories completely + - Mark each workflow as complete individually before moving to the next + - Report findings for each workflow separately, not in aggregate +4. **Pattern Recognition**: Look for anti-patterns such as hardcoded values, missing error handling, weak idempotency, and security vulnerabilities across parent and embedded workflows. +5. **Cross-Reference Analysis**: Verify consistency between definitions and actual usage within each workflow, and between parent/subworkflow interfaces. +6. **Report Findings**: For each failed check, provide: + - **Specific Location**: Clearly identify the workflow element (e.g., "Workflow Variable 'Variable Name'", "Activity 'Activity Title'", "Workflow's main 'Variables' section"). If a specific property within an element is relevant, include it (e.g., "Description of Activity 'Activity Title'"). + - Description of the problem + - Recommended fix or improvement + - Severity level (Critical, High, Medium, Low) + +### Quality Score Guidelines: + +- **9-10**: Excellent - Minor improvements only +- **7-8**: Good - Few moderate issues to address +- **5-6**: Acceptable - Several issues requiring attention +- **3-4**: Needs Work - Multiple critical/high issues +- **1-2**: Major Revision Required - Fundamental problems + +### Common JSON Patterns to Flag: + +- **Security Issues**: Plain text secrets, production URLs/IPs in defaults +- **Reliability Issues**: Missing error handling, no idempotency for CREATE/DELETE (when multiple executions expected) +- **Maintainability Issues**: Inconsistent naming, missing descriptions, redundant elements +- **Usability Issues**: Confusing variable names, missing standard outputs + +### Important Notes for Reviewers: + +- **Definition - Embedded Subworkflow**: Any workflow logic invoked from the parent workflow (for example, activities with `type: "atomic_workflow"` or equivalent nested workflow references in the same JSON context). +- **Default Review Scope**: Comprehensive reviews must include the parent workflow and all embedded subworkflows within the JSON file. +- **Coverage Expectation**: Do not mark a review complete until all embedded subworkflows have been validated against the checklist. +- **No Shortcuts**: Do not summarize or skip embedded subworkflow reviews. Each must be read and validated completely. +- **Sequential Completion**: Do not mark multiple review categories as "complete" simultaneously without actual validation. +- **Evidence Required**: For each finding, cite specific workflow name, activity name, or variable that demonstrates the issue. +- **System-Generated IDs**: Don't flag random alphanumeric `unique_name` fields - these are XDR-generated +- **Target Groups**: Some workflows legitimately require specific target configurations +- **Idempotency**: Not all workflows need to be idempotent - consider the use case +- **Context Matters**: Always consider the workflow's intended purpose and deployment scenario + +### Pre-Submission Verification: + +Before submitting your review, confirm: +- [ ] Enumerated all workflows in the file (parent + embedded subworkflows) +- [ ] Read the full definition of each enumerated workflow +- [ ] Applied all 7 checklist categories to each workflow +- [ ] Reported workflow-specific findings (not just parent workflow issues) +- [ ] Did not batch-complete validation steps without thorough review + +### Expected Output Format: + +``` +## Workflow Review Results + +### ✅ Passed Checks: [X/Y] +### ⚠️ Issues Found: [Count by severity] + +#### Critical Issues: +- [Issue description using user-friendly field names and business terminology with specific location and fix recommendation] + +#### High Priority Issues: +- [Issue description using user-friendly field names and business terminology with specific location and fix recommendation] + +#### Medium Priority Issues: +- [Issue description using user-friendly field names and business terminology with specific location and fix recommendation] + +#### Low Priority Issues: +- [Issue description using user-friendly field names and business terminology with specific location and fix recommendation] + +### Summary: +[Overall assessment and key recommendations using terminology end users understand] + +### Example Issue Descriptions: +- Instead of: "Variable 'hostIPAddress' in definition_workflow.properties.inputs lacks proper naming convention" +- Use: "Input field 'Server IP Address' should use a more user-friendly display name" + +- Instead of: "Missing continue_on_failure flag in activity uuid-1234" +- Use: "Step 'Device Registration' needs error handling to continue if the device already exists" + +- Instead of: "SecureString variable 'authToken' missing scope documentation" +- Use: "Password field 'API Authentication Token' needs usage description for end users" + +**- For Location Clarity:** +- Instead of: "Location: `definition_workflow.variables`" +- Use: "Location: Workflow's main `Variables` section." + +- Instead of: "Location: `variable_workflow_02O427U6E0UKK4pPkgaKcbm9H3Y1Oy4eUhF`" +- Use: "Location: Output variable `Output - Filtered JSON` in the Workflow's `Variables` section." + +- Instead of: "Location: `definition_activity_02O427U9YGQD32G3ACkUh3TgKs9D2TyfwVd`" +- Use: "Location: Activity `Check the HTTP Request for Errors`." + +- Instead of: "Location: `definition_activity_02O427U95N2YX0AbAR87yYHC6aDfWC3Ky8f.properties.description`" +- Use: "Location: Description of activity `JSON Placeholder Web Request`." +--- + +*This checklist enables systematic, automated review of workflow JSON files to ensure quality, security, and consistency across the CiscoWorkflowsAutomation repository.* diff --git a/Toolkit/cli/src/workflow_review/mcp_server.py b/Toolkit/cli/src/workflow_review/mcp_server.py index e647e22..c36f6fb 100644 --- a/Toolkit/cli/src/workflow_review/mcp_server.py +++ b/Toolkit/cli/src/workflow_review/mcp_server.py @@ -12,11 +12,17 @@ PROTOCOL_VERSION = "2024-11-05" +CAPABILITIES = { + "tools": {"listChanged": False}, + "resources": {"subscribe": False, "listChanged": False}, + "prompts": {"listChanged": False}, +} + TOOLS = [ { - "name": "workflow_inspect_export", - "description": "Inspect a workflow export and list the parent workflow plus any embedded subworkflows.", + "name": "inspect_export", + "description": "Advanced helper that lists the parent workflow and any embedded subworkflows in a JSON export.", "inputSchema": { "type": "object", "required": ["workflow_path"], @@ -26,8 +32,8 @@ }, }, { - "name": "workflow_resolve_checklist", - "description": "Resolve the canonical workflow review checklist path.", + "name": "load_checklist", + "description": "Resolve and load the internal review standard used by the workflow review toolkit.", "inputSchema": { "type": "object", "properties": { @@ -36,8 +42,8 @@ }, }, { - "name": "workflow_prepare_review", - "description": "Prepare a structured review brief for a workflow export without performing the full LLM review.", + "name": "review", + "description": "Review a workflow export end to end: enumerate workflows first, then return the review brief that leads into findings, severity, and remediation suggestions.", "inputSchema": { "type": "object", "required": ["workflow_path"], @@ -50,7 +56,7 @@ }, }, { - "name": "workflow_plan_remediation", + "name": "plan_remediation", "description": "Prepare a remediation plan without writing to disk.", "inputSchema": { "type": "object", @@ -102,12 +108,17 @@ def _result_content(payload: Any) -> dict[str, Any]: def _handle_tool_call(name: str, arguments: dict[str, Any]) -> dict[str, Any]: - if name in {"workflow_inspect_export", "workflow_enumerate"}: + if name == "inspect_export": return _result_content(enumerate_workflows(arguments["workflow_path"])) - if name == "workflow_resolve_checklist": + if name == "load_checklist": checklist_path = resolve_checklist_path(arguments.get("checklist_path")) - return _result_content({"checklist_path": str(checklist_path)}) - if name == "workflow_prepare_review": + return _result_content( + { + "checklist_path": str(checklist_path), + "description": "Internal review standard used by the workflow review toolkit.", + } + ) + if name == "review": return _result_content( prepare_review( workflow_path=arguments["workflow_path"], @@ -116,7 +127,7 @@ def _handle_tool_call(name: str, arguments: dict[str, Any]) -> dict[str, Any]: severity_threshold=arguments.get("severity_threshold"), ) ) - if name == "workflow_plan_remediation": + if name == "plan_remediation": return _result_content( plan_remediation( workflow_path=arguments["workflow_path"], @@ -152,7 +163,7 @@ def handle_message(message: dict[str, Any]) -> dict[str, Any] | None: { "protocolVersion": PROTOCOL_VERSION, "serverInfo": {"name": "cisco-workflow-review", "version": "0.1.0"}, - "capabilities": {"tools": {}}, + "capabilities": CAPABILITIES, }, ) @@ -162,6 +173,15 @@ def handle_message(message: dict[str, Any]) -> dict[str, Any] | None: if method == "tools/list": return _success(message_id, {"tools": TOOLS}) + if method == "resources/list": + return _success(message_id, {"resources": []}) + + if method == "resources/templates/list": + return _success(message_id, {"resourceTemplates": []}) + + if method == "prompts/list": + return _success(message_id, {"prompts": []}) + if method == "tools/call": try: result = _handle_tool_call(params["name"], params.get("arguments", {})) diff --git a/Toolkit/cursor/exchange-workflow-review/SKILL.md b/Toolkit/cursor/exchange-workflow-review/SKILL.md index c1c2d13..994ce2d 100644 --- a/Toolkit/cursor/exchange-workflow-review/SKILL.md +++ b/Toolkit/cursor/exchange-workflow-review/SKILL.md @@ -1,6 +1,6 @@ --- name: exchange-workflow-review -description: Review exported Cisco workflow JSON against the public Workflow Review Checklist, inspect the export to list parent and embedded subworkflows first, and produce severity-ranked findings plus an overall readiness assessment. +description: Review exported Cisco workflow JSON against the internal workflow review standard, enumerate the export first, and produce severity-ranked findings plus remediation suggestions and an overall readiness assessment. --- # Exchange Workflow Review @@ -9,36 +9,37 @@ description: Review exported Cisco workflow JSON against the public Workflow Rev - The user provides a workflow export JSON file. - The user wants Exchange review, standards validation, or approval-readiness feedback. -- The user wants findings aligned to the public workflow review checklist. +- The user wants findings aligned to the internal workflow review standard. + +## Start here prompts + +- Review this workflow export. +- Review this file against the internal review standard. +- Review this export and include remediation suggestions. ## Inputs to collect - Workflow export JSON path -- Optional priority focus: `Security`, `Performance`, or `Maintainability` -- Optional severity threshold: `Critical`, `High`, `Medium`, or `Low` ## Required flow -1. Use the canonical checklist at the repo root when available: `WorkflowReviewChecklist.md`. +1. Use the packaged internal review standard when available. Fall back to the repo-root `WorkflowReviewChecklist.md` only for team development overrides. 2. Resolve the real skill directory and the repo root from this skill's file location. Do not assume the current working directory is the repo. -3. Run the shared CLI first to inspect the workflow export and establish review scope: - ```bash - PYTHONPATH="/Toolkit/cli/src" python3 -m workflow_review inspect-workflow-export "/path/to/workflow.json" - ``` -4. Prepare the review brief: +3. Run the shared CLI first to review the export and establish scope: + - The review flow should enumerate first and return remediation suggestions in the first pass. ```bash PYTHONPATH="/Toolkit/cli/src" python3 -m workflow_review prepare-review "/path/to/workflow.json" --json ``` -5. Present the enumeration list before deeper review. -6. Review one workflow at a time across all 7 checklist categories. -7. Lead with findings ordered by severity, then finish with the overall assessment and top improvements. +4. Present the enumeration list before deeper review. +5. Review one workflow at a time across all 7 checklist categories. +6. Lead with findings ordered by severity, then finish with the overall assessment, top improvements, and remediation suggestions. ## Review rules - Do not skip embedded subworkflows. - Prefer user-facing names over raw internal IDs. - Use `reference.md` for the local review sequence and severity guidance. -- Keep the checklist as the source of truth if there is any conflict. +- Keep the internal review standard as the source of truth if there is any conflict. ## Output sections diff --git a/Toolkit/cursor/exchange-workflow-review/reference.md b/Toolkit/cursor/exchange-workflow-review/reference.md index 3a27763..d46980b 100644 --- a/Toolkit/cursor/exchange-workflow-review/reference.md +++ b/Toolkit/cursor/exchange-workflow-review/reference.md @@ -15,4 +15,5 @@ PYTHONPATH="/Toolkit/cli/src" python3 -m workflow_review prepare-revi - Review parent workflow first. - Cover all embedded workflows. - Lead with findings. -- Keep the root `WorkflowReviewChecklist.md` canonical. +- Include remediation suggestions in the first pass output. +- Keep the packaged internal review standard canonical, with repo-root overrides only for team development. diff --git a/Toolkit/mcp/README.md b/Toolkit/mcp/README.md index 3a6d744..47f9c42 100644 --- a/Toolkit/mcp/README.md +++ b/Toolkit/mcp/README.md @@ -6,23 +6,82 @@ The MCP server is intentionally thin. It wraps the same core package used by the ## Current tools -- `workflow_inspect_export` -- `workflow_resolve_checklist` -- `workflow_prepare_review` -- `workflow_plan_remediation` +- `review` +- `load_checklist` +- `plan_remediation` +- `inspect_export` (advanced helper, optional) These tools are read-only planning helpers in the first public slice. ## Run the stdio server ```bash -cd Toolkit/cli -PYTHONPATH=src python3 -m workflow_review.mcp_server +workflow-review-mcp ``` -## Example Cursor MCP config +## One-time install -See `cursor.example.json`. +For the easiest setup, run: + +```bash +bash Toolkit/mcp/install_mcp.sh +``` + +That installs the package and prints the exact `workflow-review-mcp` path you can use in Cursor, VS Code, or any stdio MCP client. + +## Start here + +Use one of these simple prompts to kick off the review: + +- "Review this workflow export." +- "Review this export and tell me what needs improvement." +- "Review this file against the internal review standard." +- "Review this export and include remediation suggestions." + +### Cursor + +Use `review` as the starting action. Cursor will enumerate the export first, then return findings and remediation suggestions. + +See `cursor.example.json`. It includes both: + +- an installed-package configuration that uses `workflow-review-mcp` +- a dev-only fallback that still uses `python3 -m workflow_review.mcp_server` + +### VS Code + +If your MCP client supports stdio servers, use the installed command directly and start with `review`: + +```json +{ + "mcpServers": { + "cisco-workflow-review": { + "command": "/Users/christyaajones/Library/Python/3.9/bin/workflow-review-mcp", + "args": [], + "env": {} + } + } +} +``` + +If your VS Code setup uses a different Python environment, point `command` at that environment’s installed `workflow-review-mcp` binary instead. The install helper prints the exact path for you. + +### Generic MCP client + +Any stdio-capable MCP client can use the same installed binary and start with `review`: + +```json +{ + "mcpServers": { + "cisco-workflow-review": { + "command": "workflow-review-mcp", + "args": [], + "env": {} + } + } +} +``` + +If the command is not on `PATH`, replace it with the full path to the installed script. ## Design intent diff --git a/Toolkit/mcp/cursor.example.json b/Toolkit/mcp/cursor.example.json index 72993cd..bfffb7d 100644 --- a/Toolkit/mcp/cursor.example.json +++ b/Toolkit/mcp/cursor.example.json @@ -1,6 +1,11 @@ { "mcpServers": { "cisco-workflow-review": { + "command": "workflow-review-mcp", + "args": [], + "env": {} + }, + "cisco-workflow-review-dev": { "command": "python3", "args": [ "-m", diff --git a/Toolkit/mcp/install_mcp.sh b/Toolkit/mcp/install_mcp.sh new file mode 100755 index 0000000..b28d749 --- /dev/null +++ b/Toolkit/mcp/install_mcp.sh @@ -0,0 +1,30 @@ +#!/usr/bin/env bash +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +CLI_DIR="$REPO_ROOT/Toolkit/cli" + +printf 'Installing workflow review MCP package...\n' +python3 -m pip install -e "$CLI_DIR" + +SCRIPT_PATH="$(python3 - <<'PY' +import site +from pathlib import Path + +user_base = Path(site.getuserbase()) +print(user_base / "bin" / "workflow-review-mcp") +PY +)" + +cat < Date: Fri, 12 Jun 2026 17:30:17 -0400 Subject: [PATCH 3/6] feat(CDA-4127): add structured firewall rule backups Capture pre-change firewall rules as JSON large-string output for each modified network while leaving the add/remove behavior unchanged. Co-authored-by: Cursor --- ...w_02S5ARIDA6X2F2WjfxedD9dBIamPXcTcUFO.json | 1889 +++++++++++++++++ 1 file changed, 1889 insertions(+) create mode 100644 Meraki/Bulk L3 Firewall Rule Manager__definition_workflow_02S5ARIDA6X2F2WjfxedD9dBIamPXcTcUFO/definition_workflow_02S5ARIDA6X2F2WjfxedD9dBIamPXcTcUFO.json diff --git a/Meraki/Bulk L3 Firewall Rule Manager__definition_workflow_02S5ARIDA6X2F2WjfxedD9dBIamPXcTcUFO/definition_workflow_02S5ARIDA6X2F2WjfxedD9dBIamPXcTcUFO.json b/Meraki/Bulk L3 Firewall Rule Manager__definition_workflow_02S5ARIDA6X2F2WjfxedD9dBIamPXcTcUFO/definition_workflow_02S5ARIDA6X2F2WjfxedD9dBIamPXcTcUFO.json new file mode 100644 index 0000000..03b9a8f --- /dev/null +++ b/Meraki/Bulk L3 Firewall Rule Manager__definition_workflow_02S5ARIDA6X2F2WjfxedD9dBIamPXcTcUFO/definition_workflow_02S5ARIDA6X2F2WjfxedD9dBIamPXcTcUFO.json @@ -0,0 +1,1889 @@ +{ + "workflow": { + "unique_name": "definition_workflow_02S5ARIDA6X2F2WjfxedD9dBIamPXcTcUFO", + "name": "Bulk L3 Firewall Rule Manager", + "title": "Bulk L3 Firewall Rule Manager", + "type": "generic.workflow", + "base_type": "workflow", + "variables": [ + { + "schema_id": "datatype.integer", + "properties": { + "value": 0, + "scope": "output", + "name": "Status Code", + "type": "datatype.integer", + "description": "The HTTP status code of the workflow execution.", + "is_required": false, + "display_on_wizard": false, + "is_invisible": false, + "variable_string_format": "" + }, + "unique_name": "variable_workflow_02S5ARIDRCJP865BBJDArddRLud9wIFN54Z", + "object_type": "variable_workflow" + }, + { + "schema_id": "datatype.string", + "properties": { + "value": "", + "scope": "local", + "name": "Temp Message", + "type": "datatype.string", + "description": "Temporary variable used to accumulate and format output messages from multiple network processing operations.", + "is_required": false, + "display_on_wizard": false, + "is_invisible": false, + "variable_string_format": "text" + }, + "unique_name": "variable_workflow_02S5ARIDREUYJ6FyD70PIRCUKxNM0mfNibU", + "object_type": "variable_workflow" + }, + { + "schema_id": "datatype.large_string", + "properties": { + "value": "", + "scope": "local", + "name": "Temp Firewall Rules Backup", + "type": "datatype.large_string", + "description": "Accumulates per-network firewall backup JSON objects until the final summary step serializes them into a JSON array.", + "is_required": false, + "display_on_wizard": false, + "is_invisible": false, + "variable_string_format": "" + }, + "unique_name": "variable_workflow_02S5FWBACKUPACC01", + "object_type": "variable_workflow" + }, + { + "schema_id": "datatype.integer", + "properties": { + "value": 0, + "scope": "input", + "name": "Position of the Rule", + "type": "datatype.integer", + "description": "0-based insertion position used only when adding a rule. Ignored for remove operations.", + "is_required": false, + "display_on_wizard": false, + "is_invisible": false, + "variable_string_format": "" + }, + "unique_name": "variable_workflow_02S5G3TQZRE3Q5yzsq9fvf6r7AYaQRWeq2A", + "object_type": "variable_workflow" + }, + { + "schema_id": "datatype.string", + "properties": { + "value": "", + "scope": "output", + "name": "Result", + "type": "datatype.string", + "description": "Summary of firewall rule management results across processed networks, including counts and per-organization outcomes.", + "is_required": false, + "display_on_wizard": false, + "is_invisible": false, + "variable_string_format": "text" + }, + "unique_name": "variable_workflow_02S5ARIDREDIL3gQ7VYT5lEBbCCpINTpxcZ", + "object_type": "variable_workflow" + }, + { + "schema_id": "datatype.large_string", + "properties": { + "value": "", + "scope": "output", + "name": "Firewall Rules Backup", + "type": "datatype.large_string", + "description": "JSON array of pre-change firewall rule backups for modified networks. Each entry preserves organization, network, timestamp, current rules, and requested rule payload.", + "is_required": false, + "display_on_wizard": false, + "is_invisible": false, + "variable_string_format": "" + }, + "unique_name": "variable_workflow_02S5FWBACKUPOUT01", + "object_type": "variable_workflow" + }, + { + "schema_id": "datatype.boolean", + "properties": { + "value": true, + "scope": "input", + "name": "Add Rule (true) or Remove Rule (false)", + "type": "datatype.boolean", + "description": "Operation to perform: Set to true to ADD the firewall rule to matching networks, or false to REMOVE matching firewall rules from matching networks.", + "is_required": false, + "display_on_wizard": false, + "is_invisible": false, + "variable_string_format": "" + }, + "unique_name": "variable_workflow_02S5AU0MI637M65d8mb4PWMvGVAhEcjeN4K", + "object_type": "variable_workflow" + }, + { + "schema_id": "variable_type_array_01JhSTW61I3ZU2IfL7dwQox83eFDzE1qUiA", + "properties": { + "value": [], + "scope": "input", + "name": "Network Tags to Search", + "type": "datatype.array", + "description": "Provide a list of network tags to search. Only networks in the user's organization with at least one matching tag from this list will be included. Note: The filtering is case-sensitive.\nExample:\n[\n \"example-tag\", \"another-tag\"\n]", + "is_required": true, + "display_on_wizard": false, + "is_invisible": false, + "variable_string_format": "" + }, + "unique_name": "variable_workflow_02S5ARIDRC9NE16NkQ6YrYWAtehxAZUuSbJ", + "object_type": "variable_workflow" + }, + { + "schema_id": "datatype.string", + "properties": { + "value": "", + "scope": "output", + "name": "Error Message", + "type": "datatype.string", + "description": "Detailed error message providing specific information about workflow failures including validation errors, API failures, network discovery issues, or firewall rule management problems for troubleshooting and resolution.", + "is_required": false, + "display_on_wizard": false, + "is_invisible": false, + "variable_string_format": "text" + }, + "unique_name": "variable_workflow_02S5ARIDRDHDR0xGcPb1HbVjBK9LjCSoM4N", + "object_type": "variable_workflow" + }, + { + "schema_id": "datatype.string", + "properties": { + "value": "", + "scope": "output", + "name": "Status Message", + "type": "datatype.string", + "description": "Overall execution status message indicating successful completion or high-level error information for the bulk firewall rule management operation.", + "is_required": false, + "display_on_wizard": false, + "is_invisible": false, + "variable_string_format": "text" + }, + "unique_name": "variable_workflow_02S5ARIDRDZJU5ULuZ73m7nrVQyiDaH61Ca", + "object_type": "variable_workflow" + }, + { + "schema_id": "datatype.string", + "properties": { + "value": "", + "scope": "input", + "name": "L3 Firewall Rule Definition", + "type": "datatype.string", + "description": "Complete L3 firewall rule definition in JSON format. Specify all rule properties including policy (allow/deny), protocol, source/destination CIDRs and ports, comment, and syslog settings. This rule is applied when adding a rule and used as the match criteria when removing.", + "is_required": true, + "display_on_wizard": false, + "is_invisible": false, + "variable_string_format": "json" + }, + "unique_name": "variable_workflow_02S5ARIDRD1EG0MlQerAT6HCVXWc6bL2H30", + "object_type": "variable_workflow" + } + ], + "properties": { + "atomic": { + "is_atomic": false + }, + "automation_rules": { + "type": [] + }, + "delete_workflow_instance": false, + "description": "This workflow manages (adds or removes) L3 firewall rules across multiple Meraki MX networks based on network tags. It automatically discovers networks with matching tags and applies the specified firewall rule configuration.\n\nThe workflow accepts a complete L3 firewall rule definition (including policy, protocol, source/destination CIDRs, ports, etc.) and can either add it to or remove it from all matching networks.\n\nPrerequisites:\n- User must have network admin privileges for target organizations\n- Target networks must exist, have specified tags, and support MX appliances\n- Valid L3 firewall rule definition in JSON format\n\nFeatures:\n- Add or remove firewall rules based on boolean flag\n- Position-based insertion for add operations\n- Automatic network discovery by tags\n- Bulk firewall rule management\n- Deterministic result summarization\n\nLimitations:\n- Maximum 500 matching networks per run\n- Case-sensitive tag matching\n- Rule definition must follow Meraki L3 firewall rule schema\n\nCaptures machine-readable JSON backups of pre-change firewall rules for each network that is actually modified.", + "display_name": "Bulk L3 Firewall Rule Manager", + "runtime_user": { + "override_target_runtime_user": false, + "specify_on_workflow_start": false, + "target_default": true + }, + "target": { + "target_type": "meraki.endpoint", + "specify_on_workflow_start": true + } + }, + "object_type": "definition_workflow", + "actions": [ + { + "unique_name": "definition_activity_02S5ARIXC38F81iknkzC4HkubqPwzyYxRpo", + "name": "Group", + "title": "Input Validation", + "type": "logic.group", + "base_type": "activity", + "properties": { + "continue_on_failure": false, + "description": "Validate input parameters to ensure they meet required format and constraints.", + "display_as_suggestion": false, + "display_name": "Input Validation", + "skip_execution": false + }, + "object_type": "definition_activity", + "actions": [ + { + "unique_name": "definition_activity_02S5ARJ135IBZ4r71D4TkjD00ScssYMiFaw", + "name": "Parallel Block", + "title": "Validate Required Inputs", + "type": "logic.parallel", + "base_type": "activity", + "properties": { + "continue_on_failure": false, + "description": "Validate all required input parameters in parallel to ensure efficient error detection.", + "display_name": "Validate Inputs", + "skip_execution": false + }, + "object_type": "definition_activity", + "blocks": [ + { + "unique_name": "definition_activity_02S5ARJ2W869J59dILQzBKZc65FExKuuMYT", + "name": "Parallel Branch", + "title": "Check Tags are provided", + "type": "logic.parallel_block", + "base_type": "activity", + "properties": { + "continue_on_failure": false, + "display_name": "Tags Provided?", + "skip_execution": false + }, + "object_type": "definition_activity", + "actions": [ + { + "unique_name": "definition_activity_02S5ARJ4V4SOZ4cPyw5eH2KCOMQf86hs09M", + "name": "Execute Python Script", + "title": "Validate Network Tags Array", + "type": "python3.script", + "base_type": "activity", + "properties": { + "action_timeout": 180, + "continue_on_failure": false, + "description": "Validates that the network tags array is not empty and contains valid tag values for network discovery.", + "display_name": "Parse Tags Array", + "script": "import sys\nimport json\n\n# Get the network tags input\ntags_input = sys.argv[1]\n\n# Check if array is empty\nif tags_input == \"[]\":\n isValid = False\n errorMessage = \"Network tags array is empty. At least one tag must be provided.\"\nelse:\n isValid = True\n errorMessage = \"\"", + "script_arguments": [ + "$workflow.definition_workflow_02S5ARIDA6X2F2WjfxedD9dBIamPXcTcUFO.input.variable_workflow_02S5ARIDRC9NE16NkQ6YrYWAtehxAZUuSbJ$" + ], + "script_queries": [ + { + "script_query": "isValid", + "script_query_name": "isValid", + "script_query_type": "boolean" + }, + { + "script_query": "errorMessage", + "script_query_name": "errorMessage", + "script_query_type": "string" + } + ], + "skip_execution": false + }, + "object_type": "definition_activity" + }, + { + "unique_name": "definition_activity_02S5ARJ6KSYXO3Oa5Px3Nqdf08KCJjqwMs8", + "name": "Condition Block", + "title": "Has Tags?", + "type": "logic.if_else", + "base_type": "activity", + "properties": { + "conditions": [], + "continue_on_failure": false, + "display_name": "Valid Format?", + "skip_execution": false + }, + "object_type": "definition_activity", + "blocks": [ + { + "unique_name": "definition_activity_02S5ARJ8EA6UN5xFEwI1fdw8vj44UHenHg3", + "name": "Condition Branch", + "title": "No Tags Provided", + "type": "logic.condition_block", + "base_type": "activity", + "properties": { + "condition": { + "left_operand": "$activity.definition_activity_02S5ARJ4V4SOZ4cPyw5eH2KCOMQf86hs09M.output.script_queries.isValid$", + "operator": "eq", + "right_operand": false + }, + "continue_on_failure": false, + "description": "Handle validation failure when no network tags are provided by the user.", + "display_name": "Empty/Invalid", + "skip_execution": false + }, + "object_type": "definition_activity", + "actions": [ + { + "unique_name": "definition_activity_02S5ARJAQTO8R3KvbLTiAjRju5Jy1Mw9j0D", + "name": "Set Variables", + "title": "Set Tag Validation Error", + "type": "core.set_multiple_variables", + "base_type": "activity", + "properties": { + "continue_on_failure": false, + "description": "Set error output variables when network tags validation fails.", + "display_name": "Set Output Variables", + "skip_execution": false, + "variables_to_update": [ + { + "variable_to_update": "$workflow.definition_workflow_02S5ARIDA6X2F2WjfxedD9dBIamPXcTcUFO.output.variable_workflow_02S5ARIDRDHDR0xGcPb1HbVjBK9LjCSoM4N$", + "variable_value_new": "$workflow.definition_workflow_02S5ARIDA6X2F2WjfxedD9dBIamPXcTcUFO.output.variable_workflow_02S5ARIDRDHDR0xGcPb1HbVjBK9LjCSoM4N$\n$activity.definition_activity_02S5ARJ4V4SOZ4cPyw5eH2KCOMQf86hs09M.output.script_queries.errorMessage$" + }, + { + "variable_to_update": "$workflow.definition_workflow_02S5ARIDA6X2F2WjfxedD9dBIamPXcTcUFO.output.variable_workflow_02S5ARIDRDZJU5ULuZ73m7nrVQyiDaH61Ca$", + "variable_value_new": "Failed" + }, + { + "variable_to_update": "$workflow.definition_workflow_02S5ARIDA6X2F2WjfxedD9dBIamPXcTcUFO.output.variable_workflow_02S5ARIDRCJP865BBJDArddRLud9wIFN54Z$", + "variable_value_new": "400" + } + ] + }, + "object_type": "definition_activity" + }, + { + "unique_name": "definition_activity_02S5ARJCKRHTF7KJdtOrdUMKjaMq2n4CuCK", + "name": "Completed", + "title": "End on Tag Validation Error", + "type": "logic.completed", + "base_type": "activity", + "properties": { + "completion_type": "failed-completed", + "continue_on_failure": false, + "description": "Terminate workflow execution when network tags validation fails.", + "display_name": "Exit Workflow", + "result_message": "Network tags validation failed", + "skip_execution": false + }, + "object_type": "definition_activity" + } + ] + } + ] + } + ] + }, + { + "unique_name": "definition_activity_02S5ARJ2W869J59dILQzBKZc65FExKuuMYU", + "name": "Parallel Branch", + "title": "Validate L3 Firewall Rule JSON", + "type": "logic.parallel_block", + "base_type": "activity", + "properties": { + "continue_on_failure": false, + "display_name": "Rule Valid JSON?", + "skip_execution": false + }, + "object_type": "definition_activity", + "actions": [ + { + "unique_name": "definition_activity_02S5ARJ4V4SOZ4cPyw5eH2KCOMQf86hs09N", + "name": "Execute Python Script", + "title": "Validate L3 Rule JSON Format", + "type": "python3.script", + "base_type": "activity", + "properties": { + "action_timeout": 180, + "continue_on_failure": false, + "description": "Validates that the L3 Firewall Rule Definition is valid JSON and contains required fields for Meraki L3 firewall rules. Automatically sets optional fields to 'any' if not provided.", + "display_name": "Validate Rule JSON", + "script": "import sys\nimport json\n\n# Get the rule definition input\nrule_input = sys.argv[1]\n\ntry:\n # Parse JSON\n rule = json.loads(rule_input)\n \n # Check required fields\n required_fields = ['policy', 'protocol', 'destCidr']\n missing_fields = [field for field in required_fields if field not in rule]\n \n if missing_fields:\n isValid = False\n errorMessage = f\"L3 Firewall Rule Definition is missing required fields: {', '.join(missing_fields)}\"\n normalizedRule = \"\"\n elif rule.get('policy') not in ['allow', 'deny']:\n isValid = False\n errorMessage = \"L3 Firewall Rule 'policy' must be either 'allow' or 'deny'\"\n normalizedRule = \"\"\n else:\n # Set optional fields to Meraki-compatible defaults if not provided\n rule.setdefault('srcCidr', 'any')\n rule.setdefault('srcPort', 'any')\n rule.setdefault('destPort', 'any')\n rule.setdefault('comment', '')\n rule.setdefault('syslogEnabled', False)\n \n isValid = True\n errorMessage = \"\"\n normalizedRule = json.dumps(rule)\nexcept json.JSONDecodeError as e:\n isValid = False\n errorMessage = f\"L3 Firewall Rule Definition is not valid JSON: {str(e)}\"\n normalizedRule = \"\"", + "script_arguments": [ + "$workflow.definition_workflow_02S5ARIDA6X2F2WjfxedD9dBIamPXcTcUFO.input.variable_workflow_02S5ARIDRD1EG0MlQerAT6HCVXWc6bL2H30$" + ], + "script_queries": [ + { + "script_query": "isValid", + "script_query_name": "isValid", + "script_query_type": "boolean" + }, + { + "script_query": "errorMessage", + "script_query_name": "errorMessage", + "script_query_type": "string" + }, + { + "script_query": "normalizedRule", + "script_query_name": "normalizedRule", + "script_query_type": "string" + } + ], + "skip_execution": false + }, + "object_type": "definition_activity" + }, + { + "unique_name": "definition_activity_02S5ARJ6KSYXO3Oa5Px3Nqdf08KCJjqwMs9", + "name": "Condition Block", + "title": "Valid Rule JSON?", + "type": "logic.if_else", + "base_type": "activity", + "properties": { + "conditions": [], + "continue_on_failure": false, + "display_name": "Valid JSON?", + "skip_execution": false + }, + "object_type": "definition_activity", + "blocks": [ + { + "unique_name": "definition_activity_02S5ARJ8EA6UN5xFEwI1fdw8vj44UHenHg4", + "name": "Condition Branch", + "title": "Invalid JSON", + "type": "logic.condition_block", + "base_type": "activity", + "properties": { + "condition": { + "left_operand": "$activity.definition_activity_02S5ARJ4V4SOZ4cPyw5eH2KCOMQf86hs09N.output.script_queries.isValid$", + "operator": "eq", + "right_operand": false + }, + "continue_on_failure": false, + "description": "Handle validation failure when L3 Firewall Rule JSON is invalid.", + "display_name": "Invalid", + "skip_execution": false + }, + "object_type": "definition_activity", + "actions": [ + { + "unique_name": "definition_activity_02S5ARJAQTO8R3KvbLTiAjRju5Jy1Mw9j0E", + "name": "Set Variables", + "title": "Set Rule Validation Error", + "type": "core.set_multiple_variables", + "base_type": "activity", + "properties": { + "continue_on_failure": false, + "description": "Set error output variables when L3 rule validation fails.", + "display_name": "Set Output Variables", + "skip_execution": false, + "variables_to_update": [ + { + "variable_to_update": "$workflow.definition_workflow_02S5ARIDA6X2F2WjfxedD9dBIamPXcTcUFO.output.variable_workflow_02S5ARIDRDHDR0xGcPb1HbVjBK9LjCSoM4N$", + "variable_value_new": "$workflow.definition_workflow_02S5ARIDA6X2F2WjfxedD9dBIamPXcTcUFO.output.variable_workflow_02S5ARIDRDHDR0xGcPb1HbVjBK9LjCSoM4N$\n$activity.definition_activity_02S5ARJ4V4SOZ4cPyw5eH2KCOMQf86hs09N.output.script_queries.errorMessage$" + }, + { + "variable_to_update": "$workflow.definition_workflow_02S5ARIDA6X2F2WjfxedD9dBIamPXcTcUFO.output.variable_workflow_02S5ARIDRDZJU5ULuZ73m7nrVQyiDaH61Ca$", + "variable_value_new": "Failed" + }, + { + "variable_to_update": "$workflow.definition_workflow_02S5ARIDA6X2F2WjfxedD9dBIamPXcTcUFO.output.variable_workflow_02S5ARIDRCJP865BBJDArddRLud9wIFN54Z$", + "variable_value_new": "400" + } + ] + }, + "object_type": "definition_activity" + }, + { + "unique_name": "definition_activity_02S5ARJCKRHTF7KJdtOrdUMKjaMq2n4CuCL", + "name": "Completed", + "title": "End on Rule Validation Error", + "type": "logic.completed", + "base_type": "activity", + "properties": { + "completion_type": "failed-completed", + "continue_on_failure": false, + "description": "Terminate workflow execution when L3 rule validation fails.", + "display_name": "Exit Workflow", + "result_message": "L3 Firewall Rule validation failed", + "skip_execution": false + }, + "object_type": "definition_activity" + } + ] + } + ] + } + ] + } + ] + } + ] + }, + { + "unique_name": "definition_activity_02S5ARJRK3ZX11h8MpSLFPrj5J8SNUhyogH", + "name": "Group", + "title": "Find Organizations", + "type": "logic.group", + "base_type": "activity", + "properties": { + "continue_on_failure": false, + "description": "Retrieve all organizations accessible to the authenticated API key and identify those containing networks with the specified tags for firewall rule management.", + "display_as_suggestion": false, + "display_name": "Find Organizations", + "skip_execution": false + }, + "object_type": "definition_activity", + "actions": [ + { + "unique_name": "definition_activity_02S5ARJVIG3V40czX2BiBnKSbDepGORUD4V", + "name": "Get Organizations", + "title": "Get Organizations", + "type": "workflow.atomic_workflow", + "base_type": "subworkflow", + "properties": { + "continue_on_failure": true, + "description": "Retrieve a list of organizations that the authenticated user has access to within their Meraki account. Error handling in the subsequent condition block validates both empty results and operation failures.", + "display_name": "Get Organizations", + "input": { + "variable_workflow_02LXK9KUOD0MH4qj4j062vm12gYmFb9y5N0": 100, + "variable_workflow_02LXKBG6HGMSD1YojnHQ3bM2cT6mP8A02A9": "", + "variable_workflow_02LXKCCX4AREX6odSHkvY5oECunJtqcLahK": "" + }, + "runtime_user": { + "target_default": true + }, + "skip_execution": false, + "target": { + "target_type": "meraki.endpoint", + "use_workflow_target": true + }, + "workflow_id": "definition_workflow_02LV5GIDUTY3X0WhuidM5ZWhkjL8wQCGAPq", + "workflow_name": "Meraki - Get Organizations" + }, + "object_type": "definition_activity" + }, + { + "unique_name": "definition_activity_02S5ARJXNKKXP4qXXqcA7hzO2HjXuCBDyNE", + "name": "Condition Block", + "title": "No Organizations Found?", + "type": "logic.if_else", + "base_type": "activity", + "properties": { + "conditions": [], + "continue_on_failure": false, + "description": "Validate that organizations are available before proceeding with network discovery and firewall rule management.", + "display_name": "Orgs Available?", + "skip_execution": false + }, + "object_type": "definition_activity", + "blocks": [ + { + "unique_name": "definition_activity_02S5ARJZQMMXO2vRZqaSnAnf6ywYtPvquYR", + "name": "Condition Branch", + "title": "No Organizations Available", + "type": "logic.condition_block", + "base_type": "activity", + "properties": { + "condition": { + "left_operand": { + "left_operand": "$activity.definition_activity_02S5ARJVIG3V40czX2BiBnKSbDepGORUD4V.output.variable_workflow_02LXKZA6725S35QBWMZCg1AlZnT11mbSFQi$", + "operator": "eq", + "right_operand": "[]" + }, + "operator": "or", + "right_operand": { + "left_operand": "$activity.definition_activity_02S5ARJVIG3V40czX2BiBnKSbDepGORUD4V.output.succeeded$", + "operator": "eq", + "right_operand": false + } + }, + "continue_on_failure": false, + "description": "Handle scenarios when organizations cannot be retrieved: either no organizations are accessible to the authenticated API key, or the Get Organizations API call failed due to authentication, network, or other API errors.", + "display_name": "None Available", + "skip_execution": false + }, + "object_type": "definition_activity", + "actions": [ + { + "unique_name": "definition_activity_02S5ARK2N8MXG5T9auuUmdm5aAC0osjleT2", + "name": "Set Variables", + "title": "Set No Organizations Error", + "type": "core.set_multiple_variables", + "base_type": "activity", + "properties": { + "continue_on_failure": false, + "description": "Set error output variables when no organizations are accessible.", + "display_name": "Set Output Variables", + "skip_execution": false, + "variables_to_update": [ + { + "variable_to_update": "$workflow.definition_workflow_02S5ARIDA6X2F2WjfxedD9dBIamPXcTcUFO.output.variable_workflow_02S5ARIDRDHDR0xGcPb1HbVjBK9LjCSoM4N$", + "variable_value_new": "No organizations found or accessible with the provided API key" + }, + { + "variable_to_update": "$workflow.definition_workflow_02S5ARIDA6X2F2WjfxedD9dBIamPXcTcUFO.output.variable_workflow_02S5ARIDRDZJU5ULuZ73m7nrVQyiDaH61Ca$", + "variable_value_new": "Failed" + }, + { + "variable_to_update": "$workflow.definition_workflow_02S5ARIDA6X2F2WjfxedD9dBIamPXcTcUFO.output.variable_workflow_02S5ARIDRCJP865BBJDArddRLud9wIFN54Z$", + "variable_value_new": "404" + } + ] + }, + "object_type": "definition_activity" + }, + { + "unique_name": "definition_activity_02S5ARK481DR81EoAvrJxq81YiOMEijgcCL", + "name": "Completed", + "title": "End on No Organizations", + "type": "logic.completed", + "base_type": "activity", + "properties": { + "completion_type": "failed-completed", + "continue_on_failure": false, + "description": "Terminate workflow execution when no organizations are accessible.", + "display_name": "Exit Workflow", + "result_message": "No organizations accessible", + "skip_execution": false + }, + "object_type": "definition_activity" + } + ] + } + ] + } + ] + }, + { + "unique_name": "definition_activity_02S5ARK5SHPLN0EZzo3MQpylUlIa96JIGad", + "name": "Group", + "title": "Deploy Rules", + "type": "logic.group", + "base_type": "activity", + "properties": { + "continue_on_failure": false, + "description": "Iterate through organizations to find networks with specified tags and manage (add or remove) the specified firewall rule across all matching networks.", + "display_as_suggestion": false, + "display_name": "Deploy Rules", + "skip_execution": false + }, + "object_type": "definition_activity", + "actions": [ + { + "unique_name": "definition_activity_02S5ARK8QSPWZ7Jh9gyjFhZyuC3wiWODqus", + "name": "Read Table from JSON", + "title": "Read Orgs from JSON", + "type": "corejava.read_table_from_json", + "base_type": "activity", + "properties": { + "action_timeout": 180, + "continue_on_failure": false, + "description": "Parse the JSON response containing organizations to extract organization IDs and names for network discovery.", + "display_name": "Parse Organizations", + "input_json": "$activity.definition_activity_02S5ARJVIG3V40czX2BiBnKSbDepGORUD4V.output.variable_workflow_02LXKZA6725S35QBWMZCg1AlZnT11mbSFQi$", + "jsonpath_query": "$.*", + "persist_output": true, + "populate_columns": false, + "skip_execution": false, + "table_columns": [ + { + "column_name": "id", + "column_type": "string" + }, + { + "column_name": "name", + "column_type": "string" + } + ] + }, + "object_type": "definition_activity" + }, + { + "unique_name": "definition_activity_02S5ARKJA9KBA7OPfzGcU4k88ADpw1nirdx", + "name": "For Each", + "title": "For Each Org", + "type": "logic.for_each", + "base_type": "activity", + "properties": { + "continue_on_failure": false, + "description": "Iterate through each organization to discover networks with matching tags for firewall rule management.", + "display_name": "Each Org", + "skip_execution": false, + "source_array": "$activity.definition_activity_02S5ARK8QSPWZ7Jh9gyjFhZyuC3wiWODqus.output.read_table_from_json$" + }, + "object_type": "definition_activity", + "actions": [ + { + "unique_name": "definition_activity_02S5ARKO577963d6OVPRDxzdiqGB9v4Hqta", + "name": "Meraki - Get Organization Networks", + "title": "Meraki - Get Organization Networks", + "type": "workflow.atomic_workflow", + "base_type": "subworkflow", + "properties": { + "continue_on_failure": true, + "description": "List networks in the current organization that match the specified tags. In bulk operations across multiple organizations, if one organization fails (due to API limits, permissions, or temporary issues), the workflow continues processing other organizations to maximize coverage.", + "display_name": "Meraki - Get Organization Networks", + "input": { + "variable_workflow_02L9TZH6C81473IDtFQ0NIzW3jmXrgchtN9": "$activity.definition_activity_02S5ARKJA9KBA7OPfzGcU4k88ADpw1nirdx.input.source_array[@].id$", + "variable_workflow_02LEUL5GOHN9T53T7jFQx3T2Yo04p5Xuz0r": false, + "variable_workflow_02LNK5WW0RD2A20mG7v1G7gsyE2N2RrKdQd": "", + "variable_workflow_02LNK5WW0RTCB1AXbOwJOBVWvUx1wdyKtkt": "withAnyTags", + "variable_workflow_02LNK5WW0S5E21Q9biFSXTiLbHr7cWP2K7y": "false", + "variable_workflow_02LNK5WW0SE2H1zmchhUHyfX7L0XGvKmB7W": 500, + "variable_workflow_02LNK5WW0SM7R5SjIvnnytN6vYzR7wfDHeE": "$workflow.definition_workflow_02S5ARIDA6X2F2WjfxedD9dBIamPXcTcUFO.input.variable_workflow_02S5ARIDRC9NE16NkQ6YrYWAtehxAZUuSbJ$", + "variable_workflow_02LNK5WW0STMX38BXiY2IvPQoHQliJHSCgE": [ + "appliance" + ], + "variable_workflow_02LNK5WW0T0NX26NyApdlTn355uYJwcQGqW": "", + "variable_workflow_02LNK5WW0T92U1HJd1sxzFrmVvwxy7NVaQn": "" + }, + "runtime_user": { + "target_default": true + }, + "skip_execution": false, + "target": { + "target_type": "meraki.endpoint", + "use_workflow_target": true + }, + "workflow_id": "definition_workflow_02L9TZH5W95870XebPF2g8kSyIYcawUvljX", + "workflow_name": "Meraki - Get Organization Networks" + }, + "object_type": "definition_activity" + }, + { + "unique_name": "definition_activity_02S5ARKQN81231fuSvPzIMP2nBbxGQhpoCI", + "name": "Condition Block", + "title": "Has Networks matching Tag?", + "type": "logic.if_else", + "base_type": "activity", + "properties": { + "conditions": [], + "continue_on_failure": false, + "description": "Validate that networks with matching tags exist in the current organization and that the Get Organization Networks API call succeeded before proceeding with firewall rule management.", + "display_name": "Networks Found?", + "skip_execution": false + }, + "object_type": "definition_activity", + "blocks": [ + { + "unique_name": "definition_activity_02S5ARKSTHBI53aIVuRLapgbTGkVnFa71Ko", + "name": "Condition Branch", + "title": "Yes", + "type": "logic.condition_block", + "base_type": "activity", + "properties": { + "condition": { + "left_operand": { + "left_operand": "$activity.definition_activity_02S5ARKO577963d6OVPRDxzdiqGB9v4Hqta.output.variable_workflow_02L9TZH6C8OZI7c9t9Z0h0JHAbqCc7LuTGT$", + "operator": "ne", + "right_operand": "[]" + }, + "operator": "and", + "right_operand": { + "left_operand": "$activity.definition_activity_02S5ARKO577963d6OVPRDxzdiqGB9v4Hqta.output.succeeded$", + "operator": "eq", + "right_operand": true + } + }, + "continue_on_failure": false, + "display_name": "Yes", + "skip_execution": false + }, + "object_type": "definition_activity", + "actions": [ + { + "unique_name": "definition_activity_02S5ARKVB9RHW2pnxv5ZFOgh83ouCvN7fHo", + "name": "Set Variables", + "title": "Set Success Variables", + "type": "core.set_multiple_variables", + "base_type": "activity", + "properties": { + "continue_on_failure": false, + "description": "Append organization processing results to the output message accumulator.", + "display_name": "Set Output Variables", + "skip_execution": false, + "variables_to_update": [ + { + "variable_to_update": "$workflow.definition_workflow_02S5ARIDA6X2F2WjfxedD9dBIamPXcTcUFO.local.variable_workflow_02S5ARIDREUYJ6FyD70PIRCUKxNM0mfNibU$", + "variable_value_new": "$workflow.definition_workflow_02S5ARIDA6X2F2WjfxedD9dBIamPXcTcUFO.local.variable_workflow_02S5ARIDREUYJ6FyD70PIRCUKxNM0mfNibU$\nResults for Organization: $activity.definition_activity_02S5ARKJA9KBA7OPfzGcU4k88ADpw1nirdx.input.source_array[@].name$" + } + ] + }, + "object_type": "definition_activity" + }, + { + "unique_name": "definition_activity_02S5ARKX8EDGY7iedfOFWnrvD9Gphc4RyLf", + "name": "Read Table from JSON", + "title": "Read Networks from JSON", + "type": "corejava.read_table_from_json", + "base_type": "activity", + "properties": { + "action_timeout": 180, + "continue_on_failure": false, + "description": "Parse the JSON response containing networks to extract network IDs and names for iterating through each network to apply firewall rule changes.", + "display_name": "Read Networks from JSON", + "input_json": "$activity.definition_activity_02S5ARKO577963d6OVPRDxzdiqGB9v4Hqta.output.variable_workflow_02L9TZH6C8OZI7c9t9Z0h0JHAbqCc7LuTGT$", + "jsonpath_query": "$.*", + "persist_output": true, + "populate_columns": false, + "skip_execution": false, + "table_columns": [ + { + "column_name": "id", + "column_type": "string" + }, + { + "column_name": "name", + "column_type": "string" + } + ] + }, + "object_type": "definition_activity" + }, + { + "unique_name": "definition_activity_02S5ARL6XX1WB6ZttXbUrzj37A2AgujqTbp", + "name": "For Each", + "title": "For Each Network", + "type": "logic.for_each", + "base_type": "activity", + "properties": { + "continue_on_failure": false, + "description": "Iterate through each discovered network to manage firewall rules for all matching networks in the current organization.", + "display_name": "For Each Network", + "skip_execution": false, + "source_array": "$activity.definition_activity_02S5ARKX8EDGY7iedfOFWnrvD9Gphc4RyLf.output.read_table_from_json$" + }, + "object_type": "definition_activity", + "actions": [ + { + "unique_name": "definition_activity_02S5ARLAZG1240oBSeGAtTPqPy45xfQckZe", + "name": "Meraki - Manage MX L3 Firewall Rules", + "title": "Meraki - Manage MX L3 Firewall Rules", + "type": "workflow.sub_workflow", + "base_type": "subworkflow", + "properties": { + "continue_on_failure": true, + "description": "Add or remove the specified L3 firewall rule on this network based on the Add/Remove flag. In bulk operations across multiple networks, individual network failures do not stop processing of remaining networks - this maximizes coverage even when some networks have issues (API limits, insufficient permissions, configuration conflicts, etc.).", + "display_name": "Meraki - Manage MX L3 Firewall Rules", + "input": { + "variable_workflow_02Q2QBASBNJPU4Jb0bAtz75rsEjBfxWl1zC": "$activity.definition_activity_02S5ARL6XX1WB6ZttXbUrzj37A2AgujqTbp.input.source_array[@].id$", + "variable_workflow_02Q2QE6HSDSEX1fzaLABhotUMJPPr6qdkNX": "$activity.definition_activity_02S5ARJ4V4SOZ4cPyw5eH2KCOMQf86hs09N.output.script_queries.normalizedRule$", + "variable_workflow_02Q2QEL1AO2C12qBxR6feTHxGJnyV0DnqKJ": "$workflow.definition_workflow_02S5ARIDA6X2F2WjfxedD9dBIamPXcTcUFO.input.variable_workflow_02S5AU0MI637M65d8mb4PWMvGVAhEcjeN4K$", + "variable_workflow_02Q2RX032F5FC0xIG13ihAQLIaoxWflVFkr": "$workflow.definition_workflow_02S5ARIDA6X2F2WjfxedD9dBIamPXcTcUFO.input.variable_workflow_02S5G3TQZRE3Q5yzsq9fvf6r7AYaQRWeq2A$", + "variable_workflow_02Q2FWBKORGID001": "$activity.definition_activity_02S5ARKJA9KBA7OPfzGcU4k88ADpw1nirdx.input.source_array[@].id$", + "variable_workflow_02Q2FWBKORGNAME01": "$activity.definition_activity_02S5ARKJA9KBA7OPfzGcU4k88ADpw1nirdx.input.source_array[@].name$", + "variable_workflow_02Q2FWBKNETNAME01": "$activity.definition_activity_02S5ARL6XX1WB6ZttXbUrzj37A2AgujqTbp.input.source_array[@].name$" + }, + "runtime_user": { + "target_default": true + }, + "skip_execution": false, + "target": { + "target_type": "meraki.endpoint", + "use_workflow_target": true + }, + "workflow_id": "definition_workflow_02Q2Q77WT8QTE4UkIuKUVeySa9Viy5opNWU", + "workflow_name": "Meraki - Manage MX L3 Firewall Rules" + }, + "object_type": "definition_activity" + }, + { + "unique_name": "definition_activity_02S5ARLE9IBX42GGlU2sppMN1uUxkvSLxA7", + "name": "Set Variables", + "title": "Append Network Results", + "type": "core.set_multiple_variables", + "base_type": "activity", + "properties": { + "continue_on_failure": false, + "description": "Append network firewall rule management results to the output message accumulator.", + "display_name": "Set Output Variables", + "skip_execution": false, + "variables_to_update": [ + { + "variable_to_update": "$workflow.definition_workflow_02S5ARIDA6X2F2WjfxedD9dBIamPXcTcUFO.local.variable_workflow_02S5ARIDREUYJ6FyD70PIRCUKxNM0mfNibU$", + "variable_value_new": "$workflow.definition_workflow_02S5ARIDA6X2F2WjfxedD9dBIamPXcTcUFO.local.variable_workflow_02S5ARIDREUYJ6FyD70PIRCUKxNM0mfNibU$\nNetwork: $activity.definition_activity_02S5ARL6XX1WB6ZttXbUrzj37A2AgujqTbp.input.source_array[@].name$ $activity.definition_activity_02S5ARLAZG1240oBSeGAtTPqPy45xfQckZe.output.variable_workflow_02Q2Q77XBFC8H025dryrtHYnRJPx1un4EfB$ $activity.definition_activity_02S5ARLAZG1240oBSeGAtTPqPy45xfQckZe.output.variable_workflow_02Q2Q77XBFIRE6QjQkXlVEVTnhSUvp49dFB$" + }, + { + "variable_to_update": "$workflow.definition_workflow_02S5ARIDA6X2F2WjfxedD9dBIamPXcTcUFO.local.variable_workflow_02S5FWBACKUPACC01$", + "variable_value_new": "$workflow.definition_workflow_02S5ARIDA6X2F2WjfxedD9dBIamPXcTcUFO.local.variable_workflow_02S5FWBACKUPACC01$\n$activity.definition_activity_02S5ARLAZG1240oBSeGAtTPqPy45xfQckZe.output.variable_workflow_02Q2FWBACKUPOUT01$" + } + ] + }, + "object_type": "definition_activity" + } + ] + } + ] + }, + { + "unique_name": "definition_activity_02S5ARLO3IUUX3qKNoJGDgtHVgovpHXhwE0", + "name": "Condition Branch", + "title": "No", + "type": "logic.condition_block", + "base_type": "activity", + "properties": { + "condition": { + "left_operand": { + "left_operand": "$activity.definition_activity_02S5ARKO577963d6OVPRDxzdiqGB9v4Hqta.output.variable_workflow_02L9TZH6C8OZI7c9t9Z0h0JHAbqCc7LuTGT$", + "operator": "eq", + "right_operand": "[]" + }, + "operator": "or", + "right_operand": { + "left_operand": "$activity.definition_activity_02S5ARKO577963d6OVPRDxzdiqGB9v4Hqta.output.succeeded$", + "operator": "ne", + "right_operand": true + } + }, + "continue_on_failure": false, + "display_name": "No", + "skip_execution": false + }, + "object_type": "definition_activity", + "actions": [ + { + "unique_name": "definition_activity_02S5ARLQZHV8V4dz8JyVogyc5GklYZimorl", + "name": "Set Variables", + "title": "Set Error Variables", + "type": "core.set_multiple_variables", + "base_type": "activity", + "properties": { + "continue_on_failure": false, + "description": "Append organization error details to the output message accumulator when no matching networks are found.", + "display_name": "Set Output Variables", + "skip_execution": false, + "variables_to_update": [ + { + "variable_to_update": "$workflow.definition_workflow_02S5ARIDA6X2F2WjfxedD9dBIamPXcTcUFO.local.variable_workflow_02S5ARIDREUYJ6FyD70PIRCUKxNM0mfNibU$", + "variable_value_new": "$workflow.definition_workflow_02S5ARIDA6X2F2WjfxedD9dBIamPXcTcUFO.local.variable_workflow_02S5ARIDREUYJ6FyD70PIRCUKxNM0mfNibU$\nOrganization: $activity.definition_activity_02S5ARKJA9KBA7OPfzGcU4k88ADpw1nirdx.input.source_array[@].name$ has no matching tags." + } + ] + }, + "object_type": "definition_activity" + } + ] + } + ] + } + ] + } + ] + }, + { + "unique_name": "definition_activity_02S5ARLSZ10EN5T98yvN0VcPSzrNSQ45oqi", + "name": "Group", + "title": "Summarize Outputs", + "type": "logic.group", + "base_type": "activity", + "properties": { + "continue_on_failure": false, + "description": "Generate a deterministic summary of firewall rule management results for administrator review.", + "display_as_suggestion": false, + "display_name": "Summarize Outputs", + "skip_execution": false + }, + "object_type": "definition_activity", + "actions": [ + { + "unique_name": "definition_activity_02S5ARLW1R1XY6IvpJdWTvcL0jz25NO2Pgf", + "name": "Execute Python Script", + "title": "Summarize Results", + "type": "python3.script", + "base_type": "activity", + "properties": { + "action_timeout": 180, + "continue_on_failure": false, + "description": "Generate a deterministic summary of firewall rule management results and derive final workflow status outputs.", + "display_name": "Summarize Results", + "script": "import json\nimport sys\n\ninput_text = sys.argv[1] if len(sys.argv) > 1 else \"\"\nraw_tags_input = sys.argv[2] if len(sys.argv) > 2 else \"[]\"\nraw_add_input = sys.argv[3] if len(sys.argv) > 3 else \"true\"\nraw_backup_input = sys.argv[4] if len(sys.argv) > 4 else \"\"\n\n\ndef normalize_text(text):\n return text.replace('\\r\\n', '\\n').replace('\\\\n', '\\n')\n\n\ndef format_tags(raw_tags):\n try:\n tags = json.loads(raw_tags)\n except (json.JSONDecodeError, TypeError):\n return \"Invalid tags format\"\n\n if isinstance(tags, list) and tags:\n return \", \".join(str(tag) for tag in tags)\n return \"No tags provided\"\n\n\ndef operation_label(raw_value):\n return \"Add\" if str(raw_value).lower() == \"true\" else \"Remove\"\n\n\ndef parse_network_line(line):\n body = line[len(\"Network:\"):].strip()\n markers = [\n \" Firewall updated successfully.\",\n \" Firewall rule skipped\",\n \" Error:\",\n \" Validation Error:\",\n ]\n matches = [(body.find(marker), marker) for marker in markers if body.find(marker) > 0]\n if not matches:\n return body, \"unknown\", body\n\n idx, marker = min(matches, key=lambda item: item[0])\n network_name = body[:idx].strip()\n detail = body[idx:].strip()\n\n if marker == \" Firewall updated successfully.\":\n category = \"success\"\n elif marker == \" Firewall rule skipped\":\n category = \"skipped\"\n else:\n category = \"failed\"\n\n return network_name, category, detail\n\n\ndef parse_results(text):\n parsed = {\n \"successful_networks\": [],\n \"skipped_networks\": [],\n \"failed_networks\": [],\n }\n current_org = None\n\n for raw_line in normalize_text(text).splitlines():\n line = raw_line.strip()\n if not line:\n continue\n if line.startswith(\"Results for Organization:\"):\n current_org = line.split(\"Results for Organization:\", 1)[1].strip()\n continue\n if line.startswith(\"Organization:\") and \"has no matching tags.\" in line:\n continue\n if line.startswith(\"Network:\") and current_org:\n network_name, category, detail = parse_network_line(line)\n payload = {\n \"organization\": current_org,\n \"network\": network_name,\n \"detail\": detail,\n }\n if category == \"success\":\n parsed[\"successful_networks\"].append(payload)\n elif category == \"skipped\":\n parsed[\"skipped_networks\"].append(payload)\n else:\n parsed[\"failed_networks\"].append(payload)\n\n return parsed\n\n\ndef parse_backup_entries(text):\n entries = []\n for raw_line in normalize_text(text).splitlines():\n line = raw_line.strip()\n if not line:\n continue\n try:\n payload = json.loads(line)\n except (json.JSONDecodeError, TypeError):\n continue\n if isinstance(payload, dict):\n entries.append(payload)\n return entries\n\n\ndef append_grouped(lines, heading, items):\n if not items:\n return\n lines.append(\"\")\n lines.append(heading)\n current_org = None\n for item in items:\n if current_org != item['organization']:\n current_org = item['organization']\n lines.append(f\"{current_org}:\")\n lines.append(f\" - {item['network']} - {item['detail']}\")\n\n\ndef build_summary(parsed, tags_string, op_label, backup_entries):\n organizations = {item['organization'] for key in parsed for item in parsed[key]}\n success_count = len(parsed['successful_networks'])\n skipped_count = len(parsed['skipped_networks'])\n failed_count = len(parsed['failed_networks'])\n\n lines = [\n \"Bulk L3 Firewall Rule Management Summary:\",\n f\"Operation: {op_label}\",\n f\"Target Tags: {tags_string}\",\n f\"Organizations with matching networks: {len(organizations)}\",\n f\"Successful operations: {success_count}\",\n f\"Skipped operations: {skipped_count}\",\n f\"Failed operations: {failed_count}\",\n f\"Backup entries captured: {len(backup_entries)}\",\n ]\n\n append_grouped(lines, \"Successful Operations by Organization:\", parsed['successful_networks'])\n append_grouped(lines, \"Skipped Operations by Organization:\", parsed['skipped_networks'])\n append_grouped(lines, \"Failed Operations by Organization:\", parsed['failed_networks'])\n return \"\\n\".join(lines)\n\n\nop_label = operation_label(raw_add_input)\ntags_string = format_tags(raw_tags_input)\nparsed_results = parse_results(input_text)\nbackup_entries = parse_backup_entries(raw_backup_input)\nsummary_text = build_summary(parsed_results, tags_string, op_label, backup_entries)\nbackup_json = json.dumps(backup_entries, separators=(\",\", \":\"))\n\nsuccess_count = len(parsed_results['successful_networks'])\nskipped_count = len(parsed_results['skipped_networks'])\nfailed_count = len(parsed_results['failed_networks'])\ntotal_count = success_count + skipped_count + failed_count\n\nif total_count == 0:\n final_status_code = 404\n final_status_message = \"Not Found\"\n final_error_message = \"No matching networks were found for the provided tags.\"\nelif failed_count > 0 and (success_count > 0 or skipped_count > 0):\n final_status_code = 207\n final_status_message = \"Partial Success\"\n final_error_message = f\"{failed_count} network operation(s) failed. Review Result for details.\"\nelif failed_count > 0:\n final_status_code = 500\n final_status_message = \"Failed\"\n final_error_message = f\"{failed_count} network operation(s) failed. Review Result for details.\"\nelif skipped_count > 0 and success_count == 0:\n final_status_code = 200\n final_status_message = \"No Changes\"\n final_error_message = \"\"\nelse:\n final_status_code = 200\n final_status_message = \"Success\"\n final_error_message = \"\"\n", + "script_arguments": [ + "$workflow.definition_workflow_02S5ARIDA6X2F2WjfxedD9dBIamPXcTcUFO.local.variable_workflow_02S5ARIDREUYJ6FyD70PIRCUKxNM0mfNibU$", + "$workflow.definition_workflow_02S5ARIDA6X2F2WjfxedD9dBIamPXcTcUFO.input.variable_workflow_02S5ARIDRC9NE16NkQ6YrYWAtehxAZUuSbJ$", + "$workflow.definition_workflow_02S5ARIDA6X2F2WjfxedD9dBIamPXcTcUFO.input.variable_workflow_02S5AU0MI637M65d8mb4PWMvGVAhEcjeN4K$", + "$workflow.definition_workflow_02S5ARIDA6X2F2WjfxedD9dBIamPXcTcUFO.local.variable_workflow_02S5FWBACKUPACC01$" + ], + "script_queries": [ + { + "script_query": "summary_text", + "script_query_name": "summary_text", + "script_query_type": "string" + }, + { + "script_query": "final_status_code", + "script_query_name": "final_status_code", + "script_query_type": "integer" + }, + { + "script_query": "final_status_message", + "script_query_name": "final_status_message", + "script_query_type": "string" + }, + { + "script_query": "final_error_message", + "script_query_name": "final_error_message", + "script_query_type": "string" + }, + { + "script_query": "backup_json", + "script_query_name": "firewall_rules_backup_json", + "script_query_type": "string" + } + ], + "skip_execution": false + }, + "object_type": "definition_activity" + }, + { + "unique_name": "definition_activity_02S5ARLW1R1XY6IvpJdWTvcL0jz25NO2Pgh", + "name": "Set Variables", + "title": "Set Final Outputs", + "type": "core.set_multiple_variables", + "base_type": "activity", + "properties": { + "continue_on_failure": false, + "description": "Set the final result summary, status message, status code, error message, and firewall backup JSON array.", + "display_name": "Set Final Outputs", + "skip_execution": false, + "variables_to_update": [ + { + "variable_to_update": "$workflow.definition_workflow_02S5ARIDA6X2F2WjfxedD9dBIamPXcTcUFO.output.variable_workflow_02S5ARIDREDIL3gQ7VYT5lEBbCCpINTpxcZ$", + "variable_value_new": "$activity.definition_activity_02S5ARLW1R1XY6IvpJdWTvcL0jz25NO2Pgf.output.script_queries.summary_text$" + }, + { + "variable_to_update": "$workflow.definition_workflow_02S5ARIDA6X2F2WjfxedD9dBIamPXcTcUFO.output.variable_workflow_02S5ARIDRDZJU5ULuZ73m7nrVQyiDaH61Ca$", + "variable_value_new": "$activity.definition_activity_02S5ARLW1R1XY6IvpJdWTvcL0jz25NO2Pgf.output.script_queries.final_status_message$" + }, + { + "variable_to_update": "$workflow.definition_workflow_02S5ARIDA6X2F2WjfxedD9dBIamPXcTcUFO.output.variable_workflow_02S5ARIDRCJP865BBJDArddRLud9wIFN54Z$", + "variable_value_new": "$activity.definition_activity_02S5ARLW1R1XY6IvpJdWTvcL0jz25NO2Pgf.output.script_queries.final_status_code$" + }, + { + "variable_to_update": "$workflow.definition_workflow_02S5ARIDA6X2F2WjfxedD9dBIamPXcTcUFO.output.variable_workflow_02S5ARIDRDHDR0xGcPb1HbVjBK9LjCSoM4N$", + "variable_value_new": "$activity.definition_activity_02S5ARLW1R1XY6IvpJdWTvcL0jz25NO2Pgf.output.script_queries.final_error_message$" + }, + { + "variable_to_update": "$workflow.definition_workflow_02S5ARIDA6X2F2WjfxedD9dBIamPXcTcUFO.output.variable_workflow_02S5FWBACKUPOUT01$", + "variable_value_new": "$activity.definition_activity_02S5ARLW1R1XY6IvpJdWTvcL0jz25NO2Pgf.output.script_queries.firewall_rules_backup_json$" + } + ] + }, + "object_type": "definition_activity" + } + ] + } + ], + "categories": [ + "category_1BMfMXSnJMyt5Ihqi7rWJr5N8cf" + ] + }, + "subworkflows": [ + { + "workflow": { + "unique_name": "definition_workflow_02Q2Q77WT8QTE4UkIuKUVeySa9Viy5opNWU", + "name": "Meraki - Manage MX L3 Firewall Rules", + "title": "Meraki - Manage MX L3 Firewall Rules", + "type": "generic.workflow", + "base_type": "workflow", + "variables": [ + { + "schema_id": "datatype.string", + "properties": { + "value": "", + "scope": "input", + "name": "Network ID", + "type": "datatype.string", + "description": "Meraki network identifier where firewall rules will be managed. Format: N_XXXXXXXXXX. Must be a valid MX network with Layer 3 firewall capabilities. Found in Meraki Dashboard under Network-wide > General.", + "is_required": true, + "display_on_wizard": false, + "is_invisible": false, + "variable_string_format": "text" + }, + "unique_name": "variable_workflow_02Q2QBASBNJPU4Jb0bAtz75rsEjBfxWl1zC", + "object_type": "variable_workflow" + }, + { + "schema_id": "datatype.string", + "properties": { + "value": "", + "scope": "input", + "name": "Organization ID", + "type": "datatype.string", + "description": "Organization identifier associated with the network being modified.", + "is_required": true, + "display_on_wizard": false, + "is_invisible": false, + "variable_string_format": "text" + }, + "unique_name": "variable_workflow_02Q2FWBKORGID001", + "object_type": "variable_workflow" + }, + { + "schema_id": "datatype.string", + "properties": { + "value": "", + "scope": "input", + "name": "Organization Name", + "type": "datatype.string", + "description": "Organization display name associated with the network being modified.", + "is_required": true, + "display_on_wizard": false, + "is_invisible": false, + "variable_string_format": "text" + }, + "unique_name": "variable_workflow_02Q2FWBKORGNAME01", + "object_type": "variable_workflow" + }, + { + "schema_id": "datatype.string", + "properties": { + "value": "", + "scope": "input", + "name": "Network Name", + "type": "datatype.string", + "description": "Network display name associated with the network being modified.", + "is_required": true, + "display_on_wizard": false, + "is_invisible": false, + "variable_string_format": "text" + }, + "unique_name": "variable_workflow_02Q2FWBKNETNAME01", + "object_type": "variable_workflow" + }, + { + "schema_id": "datatype.string", + "properties": { + "value": "", + "scope": "output", + "name": "Status Message", + "type": "datatype.string", + "description": "Brief status message indicating workflow outcome. Examples: 'Success: Firewall rule operation completed', 'Validation Error: Invalid policy value', 'Error: GET operation failed'. Useful for quick status checks.", + "is_required": false, + "display_on_wizard": false, + "is_invisible": false, + "variable_string_format": "text" + }, + "unique_name": "variable_workflow_02Q2Q77XBFC8H025dryrtHYnRJPx1un4EfB", + "object_type": "variable_workflow" + }, + { + "schema_id": "datatype.string", + "properties": { + "value": "", + "scope": "output", + "name": "Error Message", + "type": "datatype.string", + "description": "Detailed error message when workflow fails. Includes specific validation errors, API failures, or processing issues to help troubleshoot problems.", + "is_required": false, + "display_on_wizard": false, + "is_invisible": false, + "variable_string_format": "text" + }, + "unique_name": "variable_workflow_02Q2Q77XBFIRE6QjQkXlVEVTnhSUvp49dFB", + "object_type": "variable_workflow" + }, + { + "schema_id": "datatype.string", + "properties": { + "value": "", + "scope": "output", + "name": "Result", + "type": "datatype.string", + "description": "Comprehensive summary of firewall rule operation including network ID, operation performed, rule details, and success/failure status. Useful for logging and monitoring workflow executions.", + "is_required": false, + "display_on_wizard": false, + "is_invisible": false, + "variable_string_format": "text" + }, + "unique_name": "variable_workflow_02Q2Q77XBG9322NBbswlydiy7ZEAmNzlTX0", + "object_type": "variable_workflow" + }, + { + "schema_id": "datatype.large_string", + "properties": { + "value": "", + "scope": "output", + "name": "Firewall Rules Backup", + "type": "datatype.large_string", + "description": "JSON snapshot of the pre-change firewall rules for a network that is actually updated. Empty when no change is applied.", + "is_required": false, + "display_on_wizard": false, + "is_invisible": false, + "variable_string_format": "" + }, + "unique_name": "variable_workflow_02Q2FWBACKUPOUT01", + "object_type": "variable_workflow" + }, + { + "schema_id": "datatype.string", + "properties": { + "value": "", + "scope": "input", + "name": "Rule", + "type": "datatype.string", + "description": "JSON object defining the firewall rule to add. When removing, the same rule fields are used as the match criteria for rules to remove.", + "is_required": true, + "display_on_wizard": false, + "is_invisible": false, + "variable_string_format": "json" + }, + "unique_name": "variable_workflow_02Q2QE6HSDSEX1fzaLABhotUMJPPr6qdkNX", + "object_type": "variable_workflow" + }, + { + "schema_id": "datatype.integer", + "properties": { + "value": 0, + "scope": "output", + "name": "Status Code", + "type": "datatype.integer", + "description": "HTTP status code returned by the Meraki API operations. Common values: 200 (success), 400 (validation error), 401 (unauthorized), 404 (network not found), 429 (rate limited).", + "is_required": false, + "display_on_wizard": false, + "is_invisible": false, + "variable_string_format": "" + }, + "unique_name": "variable_workflow_02Q2Q77XBFP4H7gfjwt0uyrKj8h9WQVEUQm", + "object_type": "variable_workflow" + }, + { + "schema_id": "datatype.integer", + "properties": { + "value": 0, + "scope": "input", + "name": "Position", + "type": "datatype.integer", + "description": "Rule insertion position in firewall list (0-based index). Used only when adding a rule. Ignored during remove operations.", + "is_required": false, + "display_on_wizard": false, + "is_invisible": false, + "variable_string_format": "" + }, + "unique_name": "variable_workflow_02Q2RX032F5FC0xIG13ihAQLIaoxWflVFkr", + "object_type": "variable_workflow" + }, + { + "schema_id": "datatype.boolean", + "properties": { + "value": true, + "scope": "input", + "name": "Add or Remove Rule", + "type": "datatype.boolean", + "description": "Operation mode: true = Add new firewall rule at the specified position, false = Remove matching firewall rules. Position is ignored when removing.", + "is_required": false, + "display_on_wizard": false, + "is_invisible": false, + "variable_string_format": "" + }, + "unique_name": "variable_workflow_02Q2QEL1AO2C12qBxR6feTHxGJnyV0DnqKJ", + "object_type": "variable_workflow" + } + ], + "properties": { + "atomic": { + "is_atomic": false + }, + "automation_rules": { + "type": [] + }, + "delete_workflow_instance": false, + "description": "Manages Meraki MX Layer 3 outbound firewall rules with add/remove operations and position-based insertion. Includes validation, duplicate detection, and error handling.\n\nFeatures:\n- Add new firewall rules at the specified position (0=first/highest priority, 1=second, etc.)\n- Remove matching firewall rules using the rule's traffic key fields: policy, protocol, srcPort, srcCidr, destPort, and destCidr\n- Preserve the provided comment on add operations\n- Validate required rule fields: policy, protocol, and destCidr\n- Apply Meraki-compatible defaults for optional fields when omitted: srcCidr=any, srcPort=any, destPort=any, comment=\"\", syslogEnabled=false\n- Error handling for GET/UPDATE operations\n- Comprehensive output variables\n- Position validation with automatic boundary adjustment for add operations only\n\nEmits a JSON snapshot of the pre-change firewall rule state for every network update that is actually applied.", + "display_name": "Meraki - Manage MX L3 Firewall Rules", + "runtime_user": { + "target_default": true + }, + "target": { + "target_type": "meraki.endpoint", + "specify_on_workflow_start": true + } + }, + "object_type": "definition_workflow", + "actions": [ + { + "unique_name": "definition_activity_02Q2RQABK55WX3su2ycT7dg2xZjh86uCPDE", + "name": "Group", + "title": "Input Validation & Requirements Check", + "type": "logic.group", + "base_type": "activity", + "properties": { + "continue_on_failure": false, + "description": "Comprehensive validation of all input parameters before processing firewall rules. Validates JSON structure, required fields, policy values, and data types to ensure safe and reliable firewall rule operations.", + "display_name": "Input Validation & Requirements Check", + "skip_execution": false + }, + "object_type": "definition_activity", + "actions": [ + { + "unique_name": "definition_activity_02Q2Q77ZB7LYU259f0SCVlJBZfjGrUaXvEG", + "name": "JSONPath Query", + "title": "Validate Rule JSON Structure", + "type": "corejava.jsonpathquery", + "base_type": "activity", + "properties": { + "action_timeout": 180, + "continue_on_failure": true, + "description": "Validate that the Rule input contains all required firewall rule fields and can be parsed as valid JSON", + "display_name": "Validate Rule JSON Structure", + "input_json": "$workflow.definition_workflow_02Q2Q77WT8QTE4UkIuKUVeySa9Viy5opNWU.input.variable_workflow_02Q2QE6HSDSEX1fzaLABhotUMJPPr6qdkNX$", + "jsonpath_queries": [ + { + "jsonpath_query": "$.policy", + "jsonpath_query_name": "policy", + "jsonpath_query_type": "string" + }, + { + "jsonpath_query": "$.protocol", + "jsonpath_query_name": "protocol", + "jsonpath_query_type": "string" + }, + { + "jsonpath_query": "$.destPort", + "jsonpath_query_name": "destPort", + "jsonpath_query_type": "string" + }, + { + "jsonpath_query": "$.destCidr", + "jsonpath_query_name": "destCidr", + "jsonpath_query_type": "string" + }, + { + "jsonpath_query": "$.srcPort", + "jsonpath_query_name": "srcPort", + "jsonpath_query_type": "string" + }, + { + "jsonpath_query": "$.srcCidr", + "jsonpath_query_name": "srcCidr", + "jsonpath_query_type": "string" + } + ], + "skip_execution": false + }, + "object_type": "definition_activity" + }, + { + "unique_name": "definition_activity_02Q2ROPCRNOG915LCfZBLVjbez3kON0u37H", + "name": "Condition Block", + "title": "Check JSON Parsing Success", + "type": "logic.if_else", + "base_type": "activity", + "properties": { + "continue_on_failure": false, + "description": "Check if the JSONPath query succeeded, indicating valid JSON structure. If failed, terminate with validation error.", + "display_name": "Check JSON Parsing Success", + "skip_execution": false + }, + "object_type": "definition_activity", + "blocks": [ + { + "unique_name": "definition_activity_02Q2ROPDE0QA6053P9xEh0yZXXO32bput3C", + "name": "Condition Branch", + "title": "JSON Parsing Failed", + "type": "logic.condition_block", + "base_type": "activity", + "properties": { + "condition": { + "left_operand": "$activity.definition_activity_02Q2Q77ZB7LYU259f0SCVlJBZfjGrUaXvEG.output.succeeded$", + "operator": "eq", + "right_operand": false + }, + "continue_on_failure": false, + "description": "Triggered when Rule input contains invalid JSON or missing required fields that prevent JSONPath parsing.", + "display_name": "JSON Parsing Failed", + "skip_execution": false + }, + "object_type": "definition_activity", + "actions": [ + { + "unique_name": "definition_activity_02Q2ROPEDARFA7PBhcwETk5Q5OwqM19ULKI", + "name": "Set Variables", + "title": "Set JSON Validation Error Details", + "type": "core.set_multiple_variables", + "base_type": "activity", + "properties": { + "continue_on_failure": false, + "description": "Populate output variables with JSON validation error details including missing fields or malformed JSON structure.", + "display_name": "Set JSON Validation Error Details", + "skip_execution": false, + "variables_to_update": [ + { + "variable_to_update": "$workflow.definition_workflow_02Q2Q77WT8QTE4UkIuKUVeySa9Viy5opNWU.output.variable_workflow_02Q2Q77XBFIRE6QjQkXlVEVTnhSUvp49dFB$", + "variable_value_new": "Invalid Rule JSON format or missing required fields. Required fields: policy, protocol, destPort, destCidr, srcPort, srcCidr. Error: $activity.definition_activity_02Q2Q77ZB7LYU259f0SCVlJBZfjGrUaXvEG.output.error.message$" + }, + { + "variable_to_update": "$workflow.definition_workflow_02Q2Q77WT8QTE4UkIuKUVeySa9Viy5opNWU.output.variable_workflow_02Q2Q77XBFP4H7gfjwt0uyrKj8h9WQVEUQm$", + "variable_value_new": 400 + }, + { + "variable_to_update": "$workflow.definition_workflow_02Q2Q77WT8QTE4UkIuKUVeySa9Viy5opNWU.output.variable_workflow_02Q2Q77XBFC8H025dryrtHYnRJPx1un4EfB$", + "variable_value_new": "Validation Error: Invalid JSON format" + }, + { + "variable_to_update": "$workflow.definition_workflow_02Q2Q77WT8QTE4UkIuKUVeySa9Viy5opNWU.output.variable_workflow_02Q2Q77XBG9322NBbswlydiy7ZEAmNzlTX0$", + "variable_value_new": "Validation failed: Invalid Rule JSON format or missing required fields." + } + ] + }, + "object_type": "definition_activity" + }, + { + "unique_name": "definition_activity_02Q2ROPF58RDD0sjSO9bGp4DzjUweeagNBs", + "name": "Completed", + "title": "Terminate - Invalid JSON Format", + "type": "logic.completed", + "base_type": "activity", + "properties": { + "completion_type": "failed-completed", + "continue_on_failure": false, + "description": "Terminate workflow execution due to invalid JSON format or missing required fields in Rule input.", + "display_name": "Terminate - Invalid JSON Format", + "result_message": "Rule JSON validation failed: Invalid format or missing required fields", + "skip_execution": false + }, + "object_type": "definition_activity" + } + ] + } + ] + }, + { + "unique_name": "definition_activity_02Q2ROPFQD0MP5NtfWODy25UtjDGNrYW0hq", + "name": "Condition Block", + "title": "Validate Policy Values (Allow/Deny)", + "type": "logic.if_else", + "base_type": "activity", + "properties": { + "continue_on_failure": false, + "description": "Validate policy field contains only valid firewall values: 'allow' or 'deny'. Invalid values cause workflow failure with error details.", + "display_name": "Validate Policy Values (Allow/Deny)", + "skip_execution": false + }, + "object_type": "definition_activity", + "blocks": [ + { + "unique_name": "definition_activity_02Q2ROPGDCJ174fSZlvWUnNzi7auvuxBMET", + "name": "Condition Branch", + "title": "Policy Value Not Allow/Deny", + "type": "logic.condition_block", + "base_type": "activity", + "properties": { + "condition": { + "left_operand": "$activity.definition_activity_02Q2Q77ZB7LYU259f0SCVlJBZfjGrUaXvEG.output.jsonpath_queries.policy$", + "operator": "mregex", + "right_operand": "^(?!allow$|deny$).*" + }, + "continue_on_failure": false, + "description": "Triggered when policy field contains invalid values. Only 'allow' and 'deny' are permitted for Meraki firewall rules.", + "display_name": "Policy Value Not Allow/Deny", + "skip_execution": false + }, + "object_type": "definition_activity", + "actions": [ + { + "unique_name": "definition_activity_02Q2ROPHD68UM2MuwS8gNyZOmq30fRAqwoi", + "name": "Set Variables", + "title": "Set Policy Validation Error Details", + "type": "core.set_multiple_variables", + "base_type": "activity", + "properties": { + "continue_on_failure": false, + "description": "Populate output variables with detailed policy validation error info including invalid value and expected values.", + "display_name": "Set Policy Validation Error Details", + "skip_execution": false, + "variables_to_update": [ + { + "variable_to_update": "$workflow.definition_workflow_02Q2Q77WT8QTE4UkIuKUVeySa9Viy5opNWU.output.variable_workflow_02Q2Q77XBFIRE6QjQkXlVEVTnhSUvp49dFB$", + "variable_value_new": "Invalid policy value: '$activity.definition_activity_02Q2Q77ZB7LYU259f0SCVlJBZfjGrUaXvEG.output.jsonpath_queries.policy$'. Must be 'allow' or 'deny'." + }, + { + "variable_to_update": "$workflow.definition_workflow_02Q2Q77WT8QTE4UkIuKUVeySa9Viy5opNWU.output.variable_workflow_02Q2Q77XBFP4H7gfjwt0uyrKj8h9WQVEUQm$", + "variable_value_new": 400 + }, + { + "variable_to_update": "$workflow.definition_workflow_02Q2Q77WT8QTE4UkIuKUVeySa9Viy5opNWU.output.variable_workflow_02Q2Q77XBFC8H025dryrtHYnRJPx1un4EfB$", + "variable_value_new": "Validation Error: Invalid policy value" + }, + { + "variable_to_update": "$workflow.definition_workflow_02Q2Q77WT8QTE4UkIuKUVeySa9Viy5opNWU.output.variable_workflow_02Q2Q77XBG9322NBbswlydiy7ZEAmNzlTX0$", + "variable_value_new": "Validation failed: Invalid policy value in Rule input. Expected 'allow' or 'deny'." + } + ] + }, + "object_type": "definition_activity" + }, + { + "unique_name": "definition_activity_02Q2ROPI5T6467Yhe0ppqwtt53CJcawngSs", + "name": "Completed", + "title": "Terminate - Invalid Policy Value", + "type": "logic.completed", + "base_type": "activity", + "properties": { + "completion_type": "failed-completed", + "continue_on_failure": false, + "description": "Terminate workflow execution due to invalid policy value in Rule input. Completes with failure status and detailed error info.", + "display_name": "Terminate - Invalid Policy Value", + "result_message": "Policy validation failed: '$activity.definition_activity_02Q2Q77ZB7LYU259f0SCVlJBZfjGrUaXvEG.output.jsonpath_queries.policy$' is not a valid policy value", + "skip_execution": false + }, + "object_type": "definition_activity" + } + ] + } + ] + } + ] + }, + { + "unique_name": "definition_activity_02Q2Q8UZC4U2B23Y8N5JEgL2bSMSXmATZA0", + "name": "Group", + "title": "Firewall Rules Processing", + "type": "logic.group", + "base_type": "activity", + "properties": { + "continue_on_failure": false, + "description": "Main processing group that retrieves current firewall rules, modifies them based on Add/Remove input, and updates the network configuration.", + "display_name": "Firewall Rules Processing", + "skip_execution": false + }, + "object_type": "definition_activity", + "actions": [ + { + "unique_name": "definition_activity_02Q2Q78SQ9K1F74GEslWWM4grvtwqaSkPgS", + "name": "Meraki - Get Network Appliance Firewall L3 Firewall Rules", + "title": "Meraki - Get Network Appliance Firewall L3 Firewall Rules", + "type": "workflow.atomic_workflow", + "base_type": "subworkflow", + "properties": { + "continue_on_failure": true, + "description": "API endpoint is used to retrieve the Layer 3 (L3) firewall rules configured on a network appliance. Layer 3 firewall rules control traffic based on IP addresses, protocols, and ports, allowing administrators to manage traffic flows and secure the network.", + "display_name": "Meraki - Get Network Appliance Firewall L3 Firewall Rules", + "input": { + "variable_workflow_02MN4XD4FIP4W3FWhabwFykP3DiQaZHmfTR": "$workflow.definition_workflow_02Q2Q77WT8QTE4UkIuKUVeySa9Viy5opNWU.input.variable_workflow_02Q2QBASBNJPU4Jb0bAtz75rsEjBfxWl1zC$" + }, + "runtime_user": { + "target_default": true + }, + "skip_execution": false, + "target": { + "target_type": "meraki.endpoint", + "use_workflow_target": true + }, + "workflow_id": "definition_workflow_02MN4XD4FINLC4h2i95QOdYECcfArPzx1eY", + "workflow_name": "Meraki - Get Network Appliance Firewall L3 Firewall Rules" + }, + "object_type": "definition_activity" + }, + { + "unique_name": "definition_activity_02Q2ROPL8N11N31v9pX6ZYgIkG2awCfZHl3", + "name": "Condition Block", + "title": "Handle GET Firewall Rules Failure", + "type": "logic.if_else", + "base_type": "activity", + "properties": { + "continue_on_failure": false, + "description": "Check if GET firewall rules API failed and handle errors gracefully by setting output variables and terminating execution.", + "display_name": "Handle GET Firewall Rules Failure", + "skip_execution": false + }, + "object_type": "definition_activity", + "blocks": [ + { + "unique_name": "definition_activity_02Q2ROPLUOOLF1nU43WStZiBIunZTq5BEz7", + "name": "Condition Branch", + "title": "GET API Operation Failed", + "type": "logic.condition_block", + "base_type": "activity", + "properties": { + "condition": { + "left_operand": "$activity.definition_activity_02Q2Q78SQ9K1F74GEslWWM4grvtwqaSkPgS.output.succeeded$", + "operator": "eq", + "right_operand": false + }, + "continue_on_failure": false, + "description": "Triggered when GET firewall rules API fails due to network issues, authentication problems, invalid Network ID, or API errors.", + "display_name": "GET API Operation Failed", + "skip_execution": false + }, + "object_type": "definition_activity", + "actions": [ + { + "unique_name": "definition_activity_02Q2ROPN2DMDO1r53CLaUG1gxNRIUpUEghK", + "name": "Set Variables", + "title": "Set GET Failure Error Details", + "type": "core.set_multiple_variables", + "base_type": "activity", + "properties": { + "continue_on_failure": false, + "description": "Populate output variables with comprehensive error info from failed GET operation including Network ID, error message, and HTTP status.", + "display_name": "Set GET Failure Error Details", + "skip_execution": false, + "variables_to_update": [ + { + "variable_to_update": "$workflow.definition_workflow_02Q2Q77WT8QTE4UkIuKUVeySa9Viy5opNWU.output.variable_workflow_02Q2Q77XBFIRE6QjQkXlVEVTnhSUvp49dFB$", + "variable_value_new": "Failed to retrieve firewall rules for Network ID: $workflow.definition_workflow_02Q2Q77WT8QTE4UkIuKUVeySa9Viy5opNWU.input.variable_workflow_02Q2QBASBNJPU4Jb0bAtz75rsEjBfxWl1zC$. Error: $activity.definition_activity_02Q2Q78SQ9K1F74GEslWWM4grvtwqaSkPgS.output.error.message$" + }, + { + "variable_to_update": "$workflow.definition_workflow_02Q2Q77WT8QTE4UkIuKUVeySa9Viy5opNWU.output.variable_workflow_02Q2Q77XBFP4H7gfjwt0uyrKj8h9WQVEUQm$", + "variable_value_new": "$activity.definition_activity_02Q2Q78SQ9K1F74GEslWWM4grvtwqaSkPgS.output.variable_workflow_02MN4XD4FIQOG0udjI82Gn6l19oPOym5CTt$" + }, + { + "variable_to_update": "$workflow.definition_workflow_02Q2Q77WT8QTE4UkIuKUVeySa9Viy5opNWU.output.variable_workflow_02Q2Q77XBFC8H025dryrtHYnRJPx1un4EfB$", + "variable_value_new": "Error: GET operation failed" + }, + { + "variable_to_update": "$workflow.definition_workflow_02Q2Q77WT8QTE4UkIuKUVeySa9Viy5opNWU.output.variable_workflow_02Q2Q77XBG9322NBbswlydiy7ZEAmNzlTX0$", + "variable_value_new": "Failed to retrieve or modify firewall rules due to GET operation failure." + } + ] + }, + "object_type": "definition_activity" + }, + { + "unique_name": "definition_activity_02Q2ROPO3VBO62oFYc1RUo0hxo4eSOh6pED", + "name": "Completed", + "title": "Terminate - GET Operation Failed", + "type": "logic.completed", + "base_type": "activity", + "properties": { + "completion_type": "succeeded", + "continue_on_failure": false, + "description": "Gracefully terminate workflow when GET firewall rules fails. Completes with success status but comprehensive error details in outputs.", + "display_name": "Terminate - GET Operation Failed", + "result_message": "Workflow completed with GET error: $activity.definition_activity_02Q2Q78SQ9K1F74GEslWWM4grvtwqaSkPgS.output.error.message$", + "skip_execution": false + }, + "object_type": "definition_activity" + } + ] + } + ] + }, + { + "unique_name": "definition_activity_02Q2Q78U5XHWB0bAe6yWQZWyl3qO6LgFkPw", + "name": "JSONPath Query", + "title": "Extract Rules Array from Response", + "type": "corejava.jsonpathquery", + "base_type": "activity", + "properties": { + "action_timeout": 180, + "continue_on_failure": false, + "description": "Extract the 'rules' array from the GET firewall rules API response to prepare for rule modification.", + "display_name": "Extract Rules Array from Response", + "input_json": "$activity.definition_activity_02Q2Q78SQ9K1F74GEslWWM4grvtwqaSkPgS.output.variable_workflow_02MN4XD4FIS801gUrtzo8CEOhwoP6kzmgvY$", + "jsonpath_queries": [ + { + "jsonpath_query": "$.rules", + "jsonpath_query_name": "rules", + "jsonpath_query_type": "string" + } + ], + "skip_execution": false + }, + "object_type": "definition_activity" + }, + { + "unique_name": "definition_activity_02Q2Q78VGCHXX73AyhxI9mrF6jQtpZEEOHQ", + "name": "Execute Python Script", + "title": "Add or Remove Firewall Rule", + "type": "python3.script", + "base_type": "activity", + "properties": { + "action_timeout": 180, + "continue_on_failure": false, + "description": "Process firewall rules based on Add/Remove boolean and Position. Add: check for an existing rule with matching traffic key fields, then insert at the requested position if no duplicate exists. Remove: delete rules that match those same traffic key fields; position is ignored.", + "display_name": "Add or Remove Firewall Rule", + "script": "from datetime import datetime, timezone\nimport sys, json\n\nexistingRules = sys.argv[1] # This is already the rules array, not full response\nruleInput = sys.argv[2]\naddOrRemove = sys.argv[3].lower() == 'true' # true = add, false = remove\nposition = int(sys.argv[4]) # position to insert rule\norganizationId = sys.argv[5]\norganizationName = sys.argv[6]\nnetworkId = sys.argv[7]\nnetworkName = sys.argv[8]\n\n# Parse inputs - existingRules is already just the rules array\nrulesArray = json.loads(existingRules)\nnewRule = json.loads(ruleInput)\n\n# Filter out the default rule from existing rules (API update should not include it)\n# Default rule format: comment=\"Default rule\", policy=\"allow\", protocol=\"Any/any\", all ports/CIDRs=\"Any/any\"\nuser_rules = []\nfor rule in rulesArray:\n is_default_rule = (\n rule.get('comment', '') == 'Default rule' and\n rule.get('policy', '').lower() == 'allow' and\n rule.get('protocol', '').lower() == 'any' and\n rule.get('destCidr', '').lower() == 'any' and\n rule.get('srcCidr', '').lower() == 'any' and\n rule.get('destPort', '').lower() == 'any' and\n rule.get('srcPort', '').lower() == 'any'\n )\n if not is_default_rule:\n user_rules.append(rule)\n\n\ndef build_backup_snapshot():\n backup_payload = {\n 'organizationId': organizationId,\n 'organizationName': organizationName,\n 'networkId': networkId,\n 'networkName': networkName,\n 'operation': 'add' if addOrRemove else 'remove',\n 'timestamp': datetime.now(timezone.utc).isoformat().replace('+00:00', 'Z'),\n 'currentFirewallRules': rulesArray,\n 'requestedRulePayload': newRule,\n }\n return json.dumps(backup_payload, separators=(',', ':'))\n\nbackupJson = ''\n\nif addOrRemove:\n # Add rule - first check for duplicates against ALL existing rules (including default), then insert at specified position\n # Use comment as provided in input (or empty string if not provided)\n if 'comment' not in newRule:\n newRule['comment'] = \"\"\n\n # Define the key fields to check for duplicates (exclude comment and syslogEnabled)\n key_fields = ['policy', 'protocol', 'destPort', 'destCidr', 'srcPort', 'srcCidr']\n\n # Check if a rule with the same key fields already exists in ALL rules (including default rule)\n # Use case-insensitive comparison for field values\n duplicate_found = False\n for existing_rule in rulesArray: # Check against ALL rules, not just user_rules\n if all(existing_rule.get(field, '').lower() == newRule.get(field, '').lower() for field in key_fields):\n duplicate_found = True\n break\n\n if duplicate_found:\n # Don't add duplicate rule\n result = f\"Duplicate rule detected.\"\n newRules = json.dumps(user_rules) # Return unchanged user rules (no default rule)\n skipUpdate = True\n else:\n backupJson = build_backup_snapshot()\n # No duplicate found, proceed with adding the rule\n # Ensure position is within valid range for user rules\n max_position = len(user_rules)\n if position > max_position:\n position = max_position # Insert at end if position too high\n elif position < 0:\n position = 0 # Insert at beginning if position negative\n\n user_rules.insert(position, newRule)\n result = f\"Rule added successfully at position {position} (no duplicate found)\"\n newRules = json.dumps(user_rules) # Return user rules without default rule\n skipUpdate = False\nelse:\n # Remove rule - find and remove matching rules by comparing key fields among user rules\n original_count = len(user_rules)\n\n # Define the key fields to match for rule identification\n key_fields = ['policy', 'protocol', 'destPort', 'destCidr', 'srcPort', 'srcCidr']\n\n # Filter out rules that match ALL key fields of the input rule (case-insensitive)\n filtered_rules = [rule for rule in user_rules\n if not all(rule.get(field, '').lower() == newRule.get(field, '').lower() for field in key_fields)]\n\n user_rules = filtered_rules\n removed_count = original_count - len(user_rules)\n\n if removed_count == 0:\n result = f\"No matching rule found to remove\"\n skipUpdate = True # No changes to make\n elif removed_count == 1:\n backupJson = build_backup_snapshot()\n result = f\"Successfully removed 1 matching rule\"\n skipUpdate = False # Changes made, need to update\n else:\n # This shouldn't happen in normal scenarios but handle it defensively\n backupJson = build_backup_snapshot()\n result = f\"Warning: Removed {removed_count} identical rules (unexpected)\"\n skipUpdate = False # Changes made, need to update\n\n # Output user rules array without default rule\n newRules = json.dumps(user_rules)\n\nprint(newRules)\n", + "script_arguments": [ + "$activity.definition_activity_02Q2Q78U5XHWB0bAe6yWQZWyl3qO6LgFkPw.output.jsonpath_queries.rules$", + "$workflow.definition_workflow_02Q2Q77WT8QTE4UkIuKUVeySa9Viy5opNWU.input.variable_workflow_02Q2QE6HSDSEX1fzaLABhotUMJPPr6qdkNX$", + "$workflow.definition_workflow_02Q2Q77WT8QTE4UkIuKUVeySa9Viy5opNWU.input.variable_workflow_02Q2QEL1AO2C12qBxR6feTHxGJnyV0DnqKJ$", + "$workflow.definition_workflow_02Q2Q77WT8QTE4UkIuKUVeySa9Viy5opNWU.input.variable_workflow_02Q2RX032F5FC0xIG13ihAQLIaoxWflVFkr$", + "$workflow.definition_workflow_02Q2Q77WT8QTE4UkIuKUVeySa9Viy5opNWU.input.variable_workflow_02Q2FWBKORGID001$", + "$workflow.definition_workflow_02Q2Q77WT8QTE4UkIuKUVeySa9Viy5opNWU.input.variable_workflow_02Q2FWBKORGNAME01$", + "$workflow.definition_workflow_02Q2Q77WT8QTE4UkIuKUVeySa9Viy5opNWU.input.variable_workflow_02Q2QBASBNJPU4Jb0bAtz75rsEjBfxWl1zC$", + "$workflow.definition_workflow_02Q2Q77WT8QTE4UkIuKUVeySa9Viy5opNWU.input.variable_workflow_02Q2FWBKNETNAME01$" + ], + "script_queries": [ + { + "script_query": "newRules", + "script_query_name": "newRules", + "script_query_type": "string" + }, + { + "script_query": "result", + "script_query_name": "operationResult", + "script_query_type": "string" + }, + { + "script_query": "skipUpdate", + "script_query_name": "skipUpdate", + "script_query_type": "boolean" + }, + { + "script_query": "backupJson", + "script_query_name": "firewallRulesBackup", + "script_query_type": "string" + } + ], + "skip_execution": false + }, + "object_type": "definition_activity" + }, + { + "unique_name": "definition_activity_02Q2Q7CHB8K1E3dxs6m7n9QCDFhHj2LaP89", + "name": "Condition Block", + "title": "Should Update Firewall Rules?", + "type": "logic.if_else", + "base_type": "activity", + "properties": { + "continue_on_failure": false, + "description": "Check if firewall rules should be updated based on duplicate detection results. Skip API call if no changes are needed.", + "display_name": "Should Update Firewall Rules?", + "skip_execution": false + }, + "object_type": "definition_activity", + "blocks": [ + { + "unique_name": "definition_activity_02Q2Q7CHI5T4F2wn8k1x0N9bCzRgHm3VqPe", + "name": "Condition Branch", + "title": "Yes - Update Required", + "type": "logic.condition_block", + "base_type": "activity", + "properties": { + "condition": { + "left_operand": "$activity.definition_activity_02Q2Q78VGCHXX73AyhxI9mrF6jQtpZEEOHQ.output.script_queries.skipUpdate$", + "operator": "eq", + "right_operand": false + }, + "continue_on_failure": false, + "description": "Proceed with Meraki API update when changes were made to the rules array.", + "display_name": "Yes - Update Required", + "skip_execution": false + }, + "object_type": "definition_activity", + "actions": [ + { + "unique_name": "definition_activity_02Q2Q78XAX8TO45HOyCGANT6Xz0IjI86LZo", + "name": "Meraki - Update Network Appliance Firewall L3 Firewall Rules", + "title": "Meraki - Update Network Appliance Firewall L3 Firewall Rules", + "type": "workflow.atomic_workflow", + "base_type": "subworkflow", + "properties": { + "continue_on_failure": false, + "description": "Update the L3 firewall rules of an MX network", + "display_name": "Meraki - Update Network Appliance Firewall L3 Firewall Rules", + "input": { + "variable_workflow_02OARCAWCPEKW6FIUW4231asrsW5zdZSuRt": "$workflow.definition_workflow_02Q2Q77WT8QTE4UkIuKUVeySa9Viy5opNWU.input.variable_workflow_02Q2QBASBNJPU4Jb0bAtz75rsEjBfxWl1zC$", + "variable_workflow_02OARCAWCPFCO3SCyjYumx4q9hDvyXUUjZd": "$activity.definition_activity_02Q2Q78VGCHXX73AyhxI9mrF6jQtpZEEOHQ.output.script_queries.newRules$", + "variable_workflow_02OARCAWCPG4G4L7GFWJjCfqcwghsizomSK": "" + }, + "runtime_user": { + "target_default": true + }, + "skip_execution": false, + "target": { + "target_type": "meraki.endpoint", + "use_workflow_target": true + }, + "workflow_id": "definition_workflow_02OARCAWCPC9K1Xa28TIaQHu4JXVyBcjJP8", + "workflow_name": "Meraki - Update Network Appliance Firewall L3 Firewall Rules" + }, + "object_type": "definition_activity" + }, + { + "unique_name": "definition_activity_02Q3MBHT8OF4R7CMKUDSkjJ432YYzKXOYl8", + "name": "Set Variables", + "title": "Set Success Output Variables", + "type": "core.set_multiple_variables", + "base_type": "activity", + "properties": { + "continue_on_failure": false, + "description": "Set final output variables with comprehensive results including network info, operation details, and success status.", + "display_name": "Set Success Output Variables", + "skip_execution": false, + "variables_to_update": [ + { + "variable_to_update": "$workflow.definition_workflow_02Q2Q77WT8QTE4UkIuKUVeySa9Viy5opNWU.output.variable_workflow_02Q2Q77XBG9322NBbswlydiy7ZEAmNzlTX0$", + "variable_value_new": "Network: $workflow.definition_workflow_02Q2Q77WT8QTE4UkIuKUVeySa9Viy5opNWU.input.variable_workflow_02Q2QBASBNJPU4Jb0bAtz75rsEjBfxWl1zC$ - Firewall rule operation completed successfully. Operation: $activity.definition_activity_02Q2Q78VGCHXX73AyhxI9mrF6jQtpZEEOHQ.output.script_queries.operationResult$" + }, + { + "variable_to_update": "$workflow.definition_workflow_02Q2Q77WT8QTE4UkIuKUVeySa9Viy5opNWU.output.variable_workflow_02Q2Q77XBFC8H025dryrtHYnRJPx1un4EfB$", + "variable_value_new": "Firewall updated successfully." + }, + { + "variable_to_update": "$workflow.definition_workflow_02Q2Q77WT8QTE4UkIuKUVeySa9Viy5opNWU.output.variable_workflow_02Q2Q77XBFP4H7gfjwt0uyrKj8h9WQVEUQm$", + "variable_value_new": "200" + }, + { + "variable_to_update": "$workflow.definition_workflow_02Q2Q77WT8QTE4UkIuKUVeySa9Viy5opNWU.output.variable_workflow_02Q2Q77XBFIRE6QjQkXlVEVTnhSUvp49dFB$", + "variable_value_new": "" + }, + { + "variable_to_update": "$workflow.definition_workflow_02Q2Q77WT8QTE4UkIuKUVeySa9Viy5opNWU.output.variable_workflow_02Q2FWBACKUPOUT01$", + "variable_value_new": "$activity.definition_activity_02Q2Q78VGCHXX73AyhxI9mrF6jQtpZEEOHQ.output.script_queries.firewallRulesBackup$" + } + ] + }, + "object_type": "definition_activity" + } + ] + }, + { + "unique_name": "definition_activity_02Q2Q7CHNB89D5mzL3x0r8QFGkDhJ2VqP14", + "name": "Condition Branch", + "title": "No - Skip Update", + "type": "logic.condition_block", + "base_type": "activity", + "properties": { + "condition": { + "left_operand": "$activity.definition_activity_02Q2Q78VGCHXX73AyhxI9mrF6jQtpZEEOHQ.output.script_queries.skipUpdate$", + "operator": "eq", + "right_operand": true + }, + "continue_on_failure": false, + "description": "Skip Meraki API update when no changes are needed (duplicate detected or no matching rule to remove).", + "display_name": "No - Skip Update", + "skip_execution": false + }, + "object_type": "definition_activity", + "actions": [ + { + "unique_name": "definition_activity_02Q2Q78ZBEB4M7iXt72oUOH4ZAiakbfftbV", + "name": "Set Variables", + "title": "Set Error Variables", + "type": "core.set_multiple_variables", + "base_type": "activity", + "properties": { + "continue_on_failure": false, + "description": "Set final output variables with comprehensive results including network info, operation details, and success status.", + "display_name": "Set Error Variables", + "skip_execution": false, + "variables_to_update": [ + { + "variable_to_update": "$workflow.definition_workflow_02Q2Q77WT8QTE4UkIuKUVeySa9Viy5opNWU.output.variable_workflow_02Q2Q77XBG9322NBbswlydiy7ZEAmNzlTX0$", + "variable_value_new": "Network: $workflow.definition_workflow_02Q2Q77WT8QTE4UkIuKUVeySa9Viy5opNWU.input.variable_workflow_02Q2QBASBNJPU4Jb0bAtz75rsEjBfxWl1zC$ - Firewall rule skipped.$activity.definition_activity_02Q2Q78VGCHXX73AyhxI9mrF6jQtpZEEOHQ.output.script_queries.operationResult$" + }, + { + "variable_to_update": "$workflow.definition_workflow_02Q2Q77WT8QTE4UkIuKUVeySa9Viy5opNWU.output.variable_workflow_02Q2Q77XBFC8H025dryrtHYnRJPx1un4EfB$", + "variable_value_new": "Firewall rule skipped" + }, + { + "variable_to_update": "$workflow.definition_workflow_02Q2Q77WT8QTE4UkIuKUVeySa9Viy5opNWU.output.variable_workflow_02Q2Q77XBFP4H7gfjwt0uyrKj8h9WQVEUQm$", + "variable_value_new": 409 + }, + { + "variable_to_update": "$workflow.definition_workflow_02Q2Q77WT8QTE4UkIuKUVeySa9Viy5opNWU.output.variable_workflow_02Q2Q77XBFIRE6QjQkXlVEVTnhSUvp49dFB$", + "variable_value_new": "$activity.definition_activity_02Q2Q78VGCHXX73AyhxI9mrF6jQtpZEEOHQ.output.script_queries.operationResult$" + } + ] + }, + "object_type": "definition_activity" + } + ] + } + ] + } + ] + } + ], + "categories": [ + "category_1BMfMXSnJMyt5Ihqi7rWJr5N8cf" + ] + }, + "atomic_workflows": [ + "definition_workflow_02MN4XD4FINLC4h2i95QOdYECcfArPzx1eY", + "definition_workflow_02OARCAWCPC9K1Xa28TIaQHu4JXVyBcjJP8" + ], + "dependent_workflows": [ + "definition_workflow_02MN4XD4FINLC4h2i95QOdYECcfArPzx1eY", + "definition_workflow_02OARCAWCPC9K1Xa28TIaQHu4JXVyBcjJP8" + ] + } + ], + "atomic_workflows": [ + "definition_workflow_02LV5GIDUTY3X0WhuidM5ZWhkjL8wQCGAPq", + "definition_workflow_02L9TZH5W95870XebPF2g8kSyIYcawUvljX" + ], + "dependent_workflows": [ + "definition_workflow_02LV5GIDUTY3X0WhuidM5ZWhkjL8wQCGAPq", + "definition_workflow_02L9TZH5W95870XebPF2g8kSyIYcawUvljX", + "definition_workflow_02Q2Q77WT8QTE4UkIuKUVeySa9Viy5opNWU" + ] +} From f4111d077ce73a1715f52587732073bda1bb719d Mon Sep 17 00:00:00 2001 From: Christy Jones Date: Sun, 14 Jun 2026 15:26:28 -0400 Subject: [PATCH 4/6] remove obsolete bulk l3 firewall workflow definition Co-authored-by: Cursor --- ...w_02S5ARIDA6X2F2WjfxedD9dBIamPXcTcUFO.json | 1889 ----------------- 1 file changed, 1889 deletions(-) delete mode 100644 Meraki/Bulk L3 Firewall Rule Manager__definition_workflow_02S5ARIDA6X2F2WjfxedD9dBIamPXcTcUFO/definition_workflow_02S5ARIDA6X2F2WjfxedD9dBIamPXcTcUFO.json diff --git a/Meraki/Bulk L3 Firewall Rule Manager__definition_workflow_02S5ARIDA6X2F2WjfxedD9dBIamPXcTcUFO/definition_workflow_02S5ARIDA6X2F2WjfxedD9dBIamPXcTcUFO.json b/Meraki/Bulk L3 Firewall Rule Manager__definition_workflow_02S5ARIDA6X2F2WjfxedD9dBIamPXcTcUFO/definition_workflow_02S5ARIDA6X2F2WjfxedD9dBIamPXcTcUFO.json deleted file mode 100644 index 03b9a8f..0000000 --- a/Meraki/Bulk L3 Firewall Rule Manager__definition_workflow_02S5ARIDA6X2F2WjfxedD9dBIamPXcTcUFO/definition_workflow_02S5ARIDA6X2F2WjfxedD9dBIamPXcTcUFO.json +++ /dev/null @@ -1,1889 +0,0 @@ -{ - "workflow": { - "unique_name": "definition_workflow_02S5ARIDA6X2F2WjfxedD9dBIamPXcTcUFO", - "name": "Bulk L3 Firewall Rule Manager", - "title": "Bulk L3 Firewall Rule Manager", - "type": "generic.workflow", - "base_type": "workflow", - "variables": [ - { - "schema_id": "datatype.integer", - "properties": { - "value": 0, - "scope": "output", - "name": "Status Code", - "type": "datatype.integer", - "description": "The HTTP status code of the workflow execution.", - "is_required": false, - "display_on_wizard": false, - "is_invisible": false, - "variable_string_format": "" - }, - "unique_name": "variable_workflow_02S5ARIDRCJP865BBJDArddRLud9wIFN54Z", - "object_type": "variable_workflow" - }, - { - "schema_id": "datatype.string", - "properties": { - "value": "", - "scope": "local", - "name": "Temp Message", - "type": "datatype.string", - "description": "Temporary variable used to accumulate and format output messages from multiple network processing operations.", - "is_required": false, - "display_on_wizard": false, - "is_invisible": false, - "variable_string_format": "text" - }, - "unique_name": "variable_workflow_02S5ARIDREUYJ6FyD70PIRCUKxNM0mfNibU", - "object_type": "variable_workflow" - }, - { - "schema_id": "datatype.large_string", - "properties": { - "value": "", - "scope": "local", - "name": "Temp Firewall Rules Backup", - "type": "datatype.large_string", - "description": "Accumulates per-network firewall backup JSON objects until the final summary step serializes them into a JSON array.", - "is_required": false, - "display_on_wizard": false, - "is_invisible": false, - "variable_string_format": "" - }, - "unique_name": "variable_workflow_02S5FWBACKUPACC01", - "object_type": "variable_workflow" - }, - { - "schema_id": "datatype.integer", - "properties": { - "value": 0, - "scope": "input", - "name": "Position of the Rule", - "type": "datatype.integer", - "description": "0-based insertion position used only when adding a rule. Ignored for remove operations.", - "is_required": false, - "display_on_wizard": false, - "is_invisible": false, - "variable_string_format": "" - }, - "unique_name": "variable_workflow_02S5G3TQZRE3Q5yzsq9fvf6r7AYaQRWeq2A", - "object_type": "variable_workflow" - }, - { - "schema_id": "datatype.string", - "properties": { - "value": "", - "scope": "output", - "name": "Result", - "type": "datatype.string", - "description": "Summary of firewall rule management results across processed networks, including counts and per-organization outcomes.", - "is_required": false, - "display_on_wizard": false, - "is_invisible": false, - "variable_string_format": "text" - }, - "unique_name": "variable_workflow_02S5ARIDREDIL3gQ7VYT5lEBbCCpINTpxcZ", - "object_type": "variable_workflow" - }, - { - "schema_id": "datatype.large_string", - "properties": { - "value": "", - "scope": "output", - "name": "Firewall Rules Backup", - "type": "datatype.large_string", - "description": "JSON array of pre-change firewall rule backups for modified networks. Each entry preserves organization, network, timestamp, current rules, and requested rule payload.", - "is_required": false, - "display_on_wizard": false, - "is_invisible": false, - "variable_string_format": "" - }, - "unique_name": "variable_workflow_02S5FWBACKUPOUT01", - "object_type": "variable_workflow" - }, - { - "schema_id": "datatype.boolean", - "properties": { - "value": true, - "scope": "input", - "name": "Add Rule (true) or Remove Rule (false)", - "type": "datatype.boolean", - "description": "Operation to perform: Set to true to ADD the firewall rule to matching networks, or false to REMOVE matching firewall rules from matching networks.", - "is_required": false, - "display_on_wizard": false, - "is_invisible": false, - "variable_string_format": "" - }, - "unique_name": "variable_workflow_02S5AU0MI637M65d8mb4PWMvGVAhEcjeN4K", - "object_type": "variable_workflow" - }, - { - "schema_id": "variable_type_array_01JhSTW61I3ZU2IfL7dwQox83eFDzE1qUiA", - "properties": { - "value": [], - "scope": "input", - "name": "Network Tags to Search", - "type": "datatype.array", - "description": "Provide a list of network tags to search. Only networks in the user's organization with at least one matching tag from this list will be included. Note: The filtering is case-sensitive.\nExample:\n[\n \"example-tag\", \"another-tag\"\n]", - "is_required": true, - "display_on_wizard": false, - "is_invisible": false, - "variable_string_format": "" - }, - "unique_name": "variable_workflow_02S5ARIDRC9NE16NkQ6YrYWAtehxAZUuSbJ", - "object_type": "variable_workflow" - }, - { - "schema_id": "datatype.string", - "properties": { - "value": "", - "scope": "output", - "name": "Error Message", - "type": "datatype.string", - "description": "Detailed error message providing specific information about workflow failures including validation errors, API failures, network discovery issues, or firewall rule management problems for troubleshooting and resolution.", - "is_required": false, - "display_on_wizard": false, - "is_invisible": false, - "variable_string_format": "text" - }, - "unique_name": "variable_workflow_02S5ARIDRDHDR0xGcPb1HbVjBK9LjCSoM4N", - "object_type": "variable_workflow" - }, - { - "schema_id": "datatype.string", - "properties": { - "value": "", - "scope": "output", - "name": "Status Message", - "type": "datatype.string", - "description": "Overall execution status message indicating successful completion or high-level error information for the bulk firewall rule management operation.", - "is_required": false, - "display_on_wizard": false, - "is_invisible": false, - "variable_string_format": "text" - }, - "unique_name": "variable_workflow_02S5ARIDRDZJU5ULuZ73m7nrVQyiDaH61Ca", - "object_type": "variable_workflow" - }, - { - "schema_id": "datatype.string", - "properties": { - "value": "", - "scope": "input", - "name": "L3 Firewall Rule Definition", - "type": "datatype.string", - "description": "Complete L3 firewall rule definition in JSON format. Specify all rule properties including policy (allow/deny), protocol, source/destination CIDRs and ports, comment, and syslog settings. This rule is applied when adding a rule and used as the match criteria when removing.", - "is_required": true, - "display_on_wizard": false, - "is_invisible": false, - "variable_string_format": "json" - }, - "unique_name": "variable_workflow_02S5ARIDRD1EG0MlQerAT6HCVXWc6bL2H30", - "object_type": "variable_workflow" - } - ], - "properties": { - "atomic": { - "is_atomic": false - }, - "automation_rules": { - "type": [] - }, - "delete_workflow_instance": false, - "description": "This workflow manages (adds or removes) L3 firewall rules across multiple Meraki MX networks based on network tags. It automatically discovers networks with matching tags and applies the specified firewall rule configuration.\n\nThe workflow accepts a complete L3 firewall rule definition (including policy, protocol, source/destination CIDRs, ports, etc.) and can either add it to or remove it from all matching networks.\n\nPrerequisites:\n- User must have network admin privileges for target organizations\n- Target networks must exist, have specified tags, and support MX appliances\n- Valid L3 firewall rule definition in JSON format\n\nFeatures:\n- Add or remove firewall rules based on boolean flag\n- Position-based insertion for add operations\n- Automatic network discovery by tags\n- Bulk firewall rule management\n- Deterministic result summarization\n\nLimitations:\n- Maximum 500 matching networks per run\n- Case-sensitive tag matching\n- Rule definition must follow Meraki L3 firewall rule schema\n\nCaptures machine-readable JSON backups of pre-change firewall rules for each network that is actually modified.", - "display_name": "Bulk L3 Firewall Rule Manager", - "runtime_user": { - "override_target_runtime_user": false, - "specify_on_workflow_start": false, - "target_default": true - }, - "target": { - "target_type": "meraki.endpoint", - "specify_on_workflow_start": true - } - }, - "object_type": "definition_workflow", - "actions": [ - { - "unique_name": "definition_activity_02S5ARIXC38F81iknkzC4HkubqPwzyYxRpo", - "name": "Group", - "title": "Input Validation", - "type": "logic.group", - "base_type": "activity", - "properties": { - "continue_on_failure": false, - "description": "Validate input parameters to ensure they meet required format and constraints.", - "display_as_suggestion": false, - "display_name": "Input Validation", - "skip_execution": false - }, - "object_type": "definition_activity", - "actions": [ - { - "unique_name": "definition_activity_02S5ARJ135IBZ4r71D4TkjD00ScssYMiFaw", - "name": "Parallel Block", - "title": "Validate Required Inputs", - "type": "logic.parallel", - "base_type": "activity", - "properties": { - "continue_on_failure": false, - "description": "Validate all required input parameters in parallel to ensure efficient error detection.", - "display_name": "Validate Inputs", - "skip_execution": false - }, - "object_type": "definition_activity", - "blocks": [ - { - "unique_name": "definition_activity_02S5ARJ2W869J59dILQzBKZc65FExKuuMYT", - "name": "Parallel Branch", - "title": "Check Tags are provided", - "type": "logic.parallel_block", - "base_type": "activity", - "properties": { - "continue_on_failure": false, - "display_name": "Tags Provided?", - "skip_execution": false - }, - "object_type": "definition_activity", - "actions": [ - { - "unique_name": "definition_activity_02S5ARJ4V4SOZ4cPyw5eH2KCOMQf86hs09M", - "name": "Execute Python Script", - "title": "Validate Network Tags Array", - "type": "python3.script", - "base_type": "activity", - "properties": { - "action_timeout": 180, - "continue_on_failure": false, - "description": "Validates that the network tags array is not empty and contains valid tag values for network discovery.", - "display_name": "Parse Tags Array", - "script": "import sys\nimport json\n\n# Get the network tags input\ntags_input = sys.argv[1]\n\n# Check if array is empty\nif tags_input == \"[]\":\n isValid = False\n errorMessage = \"Network tags array is empty. At least one tag must be provided.\"\nelse:\n isValid = True\n errorMessage = \"\"", - "script_arguments": [ - "$workflow.definition_workflow_02S5ARIDA6X2F2WjfxedD9dBIamPXcTcUFO.input.variable_workflow_02S5ARIDRC9NE16NkQ6YrYWAtehxAZUuSbJ$" - ], - "script_queries": [ - { - "script_query": "isValid", - "script_query_name": "isValid", - "script_query_type": "boolean" - }, - { - "script_query": "errorMessage", - "script_query_name": "errorMessage", - "script_query_type": "string" - } - ], - "skip_execution": false - }, - "object_type": "definition_activity" - }, - { - "unique_name": "definition_activity_02S5ARJ6KSYXO3Oa5Px3Nqdf08KCJjqwMs8", - "name": "Condition Block", - "title": "Has Tags?", - "type": "logic.if_else", - "base_type": "activity", - "properties": { - "conditions": [], - "continue_on_failure": false, - "display_name": "Valid Format?", - "skip_execution": false - }, - "object_type": "definition_activity", - "blocks": [ - { - "unique_name": "definition_activity_02S5ARJ8EA6UN5xFEwI1fdw8vj44UHenHg3", - "name": "Condition Branch", - "title": "No Tags Provided", - "type": "logic.condition_block", - "base_type": "activity", - "properties": { - "condition": { - "left_operand": "$activity.definition_activity_02S5ARJ4V4SOZ4cPyw5eH2KCOMQf86hs09M.output.script_queries.isValid$", - "operator": "eq", - "right_operand": false - }, - "continue_on_failure": false, - "description": "Handle validation failure when no network tags are provided by the user.", - "display_name": "Empty/Invalid", - "skip_execution": false - }, - "object_type": "definition_activity", - "actions": [ - { - "unique_name": "definition_activity_02S5ARJAQTO8R3KvbLTiAjRju5Jy1Mw9j0D", - "name": "Set Variables", - "title": "Set Tag Validation Error", - "type": "core.set_multiple_variables", - "base_type": "activity", - "properties": { - "continue_on_failure": false, - "description": "Set error output variables when network tags validation fails.", - "display_name": "Set Output Variables", - "skip_execution": false, - "variables_to_update": [ - { - "variable_to_update": "$workflow.definition_workflow_02S5ARIDA6X2F2WjfxedD9dBIamPXcTcUFO.output.variable_workflow_02S5ARIDRDHDR0xGcPb1HbVjBK9LjCSoM4N$", - "variable_value_new": "$workflow.definition_workflow_02S5ARIDA6X2F2WjfxedD9dBIamPXcTcUFO.output.variable_workflow_02S5ARIDRDHDR0xGcPb1HbVjBK9LjCSoM4N$\n$activity.definition_activity_02S5ARJ4V4SOZ4cPyw5eH2KCOMQf86hs09M.output.script_queries.errorMessage$" - }, - { - "variable_to_update": "$workflow.definition_workflow_02S5ARIDA6X2F2WjfxedD9dBIamPXcTcUFO.output.variable_workflow_02S5ARIDRDZJU5ULuZ73m7nrVQyiDaH61Ca$", - "variable_value_new": "Failed" - }, - { - "variable_to_update": "$workflow.definition_workflow_02S5ARIDA6X2F2WjfxedD9dBIamPXcTcUFO.output.variable_workflow_02S5ARIDRCJP865BBJDArddRLud9wIFN54Z$", - "variable_value_new": "400" - } - ] - }, - "object_type": "definition_activity" - }, - { - "unique_name": "definition_activity_02S5ARJCKRHTF7KJdtOrdUMKjaMq2n4CuCK", - "name": "Completed", - "title": "End on Tag Validation Error", - "type": "logic.completed", - "base_type": "activity", - "properties": { - "completion_type": "failed-completed", - "continue_on_failure": false, - "description": "Terminate workflow execution when network tags validation fails.", - "display_name": "Exit Workflow", - "result_message": "Network tags validation failed", - "skip_execution": false - }, - "object_type": "definition_activity" - } - ] - } - ] - } - ] - }, - { - "unique_name": "definition_activity_02S5ARJ2W869J59dILQzBKZc65FExKuuMYU", - "name": "Parallel Branch", - "title": "Validate L3 Firewall Rule JSON", - "type": "logic.parallel_block", - "base_type": "activity", - "properties": { - "continue_on_failure": false, - "display_name": "Rule Valid JSON?", - "skip_execution": false - }, - "object_type": "definition_activity", - "actions": [ - { - "unique_name": "definition_activity_02S5ARJ4V4SOZ4cPyw5eH2KCOMQf86hs09N", - "name": "Execute Python Script", - "title": "Validate L3 Rule JSON Format", - "type": "python3.script", - "base_type": "activity", - "properties": { - "action_timeout": 180, - "continue_on_failure": false, - "description": "Validates that the L3 Firewall Rule Definition is valid JSON and contains required fields for Meraki L3 firewall rules. Automatically sets optional fields to 'any' if not provided.", - "display_name": "Validate Rule JSON", - "script": "import sys\nimport json\n\n# Get the rule definition input\nrule_input = sys.argv[1]\n\ntry:\n # Parse JSON\n rule = json.loads(rule_input)\n \n # Check required fields\n required_fields = ['policy', 'protocol', 'destCidr']\n missing_fields = [field for field in required_fields if field not in rule]\n \n if missing_fields:\n isValid = False\n errorMessage = f\"L3 Firewall Rule Definition is missing required fields: {', '.join(missing_fields)}\"\n normalizedRule = \"\"\n elif rule.get('policy') not in ['allow', 'deny']:\n isValid = False\n errorMessage = \"L3 Firewall Rule 'policy' must be either 'allow' or 'deny'\"\n normalizedRule = \"\"\n else:\n # Set optional fields to Meraki-compatible defaults if not provided\n rule.setdefault('srcCidr', 'any')\n rule.setdefault('srcPort', 'any')\n rule.setdefault('destPort', 'any')\n rule.setdefault('comment', '')\n rule.setdefault('syslogEnabled', False)\n \n isValid = True\n errorMessage = \"\"\n normalizedRule = json.dumps(rule)\nexcept json.JSONDecodeError as e:\n isValid = False\n errorMessage = f\"L3 Firewall Rule Definition is not valid JSON: {str(e)}\"\n normalizedRule = \"\"", - "script_arguments": [ - "$workflow.definition_workflow_02S5ARIDA6X2F2WjfxedD9dBIamPXcTcUFO.input.variable_workflow_02S5ARIDRD1EG0MlQerAT6HCVXWc6bL2H30$" - ], - "script_queries": [ - { - "script_query": "isValid", - "script_query_name": "isValid", - "script_query_type": "boolean" - }, - { - "script_query": "errorMessage", - "script_query_name": "errorMessage", - "script_query_type": "string" - }, - { - "script_query": "normalizedRule", - "script_query_name": "normalizedRule", - "script_query_type": "string" - } - ], - "skip_execution": false - }, - "object_type": "definition_activity" - }, - { - "unique_name": "definition_activity_02S5ARJ6KSYXO3Oa5Px3Nqdf08KCJjqwMs9", - "name": "Condition Block", - "title": "Valid Rule JSON?", - "type": "logic.if_else", - "base_type": "activity", - "properties": { - "conditions": [], - "continue_on_failure": false, - "display_name": "Valid JSON?", - "skip_execution": false - }, - "object_type": "definition_activity", - "blocks": [ - { - "unique_name": "definition_activity_02S5ARJ8EA6UN5xFEwI1fdw8vj44UHenHg4", - "name": "Condition Branch", - "title": "Invalid JSON", - "type": "logic.condition_block", - "base_type": "activity", - "properties": { - "condition": { - "left_operand": "$activity.definition_activity_02S5ARJ4V4SOZ4cPyw5eH2KCOMQf86hs09N.output.script_queries.isValid$", - "operator": "eq", - "right_operand": false - }, - "continue_on_failure": false, - "description": "Handle validation failure when L3 Firewall Rule JSON is invalid.", - "display_name": "Invalid", - "skip_execution": false - }, - "object_type": "definition_activity", - "actions": [ - { - "unique_name": "definition_activity_02S5ARJAQTO8R3KvbLTiAjRju5Jy1Mw9j0E", - "name": "Set Variables", - "title": "Set Rule Validation Error", - "type": "core.set_multiple_variables", - "base_type": "activity", - "properties": { - "continue_on_failure": false, - "description": "Set error output variables when L3 rule validation fails.", - "display_name": "Set Output Variables", - "skip_execution": false, - "variables_to_update": [ - { - "variable_to_update": "$workflow.definition_workflow_02S5ARIDA6X2F2WjfxedD9dBIamPXcTcUFO.output.variable_workflow_02S5ARIDRDHDR0xGcPb1HbVjBK9LjCSoM4N$", - "variable_value_new": "$workflow.definition_workflow_02S5ARIDA6X2F2WjfxedD9dBIamPXcTcUFO.output.variable_workflow_02S5ARIDRDHDR0xGcPb1HbVjBK9LjCSoM4N$\n$activity.definition_activity_02S5ARJ4V4SOZ4cPyw5eH2KCOMQf86hs09N.output.script_queries.errorMessage$" - }, - { - "variable_to_update": "$workflow.definition_workflow_02S5ARIDA6X2F2WjfxedD9dBIamPXcTcUFO.output.variable_workflow_02S5ARIDRDZJU5ULuZ73m7nrVQyiDaH61Ca$", - "variable_value_new": "Failed" - }, - { - "variable_to_update": "$workflow.definition_workflow_02S5ARIDA6X2F2WjfxedD9dBIamPXcTcUFO.output.variable_workflow_02S5ARIDRCJP865BBJDArddRLud9wIFN54Z$", - "variable_value_new": "400" - } - ] - }, - "object_type": "definition_activity" - }, - { - "unique_name": "definition_activity_02S5ARJCKRHTF7KJdtOrdUMKjaMq2n4CuCL", - "name": "Completed", - "title": "End on Rule Validation Error", - "type": "logic.completed", - "base_type": "activity", - "properties": { - "completion_type": "failed-completed", - "continue_on_failure": false, - "description": "Terminate workflow execution when L3 rule validation fails.", - "display_name": "Exit Workflow", - "result_message": "L3 Firewall Rule validation failed", - "skip_execution": false - }, - "object_type": "definition_activity" - } - ] - } - ] - } - ] - } - ] - } - ] - }, - { - "unique_name": "definition_activity_02S5ARJRK3ZX11h8MpSLFPrj5J8SNUhyogH", - "name": "Group", - "title": "Find Organizations", - "type": "logic.group", - "base_type": "activity", - "properties": { - "continue_on_failure": false, - "description": "Retrieve all organizations accessible to the authenticated API key and identify those containing networks with the specified tags for firewall rule management.", - "display_as_suggestion": false, - "display_name": "Find Organizations", - "skip_execution": false - }, - "object_type": "definition_activity", - "actions": [ - { - "unique_name": "definition_activity_02S5ARJVIG3V40czX2BiBnKSbDepGORUD4V", - "name": "Get Organizations", - "title": "Get Organizations", - "type": "workflow.atomic_workflow", - "base_type": "subworkflow", - "properties": { - "continue_on_failure": true, - "description": "Retrieve a list of organizations that the authenticated user has access to within their Meraki account. Error handling in the subsequent condition block validates both empty results and operation failures.", - "display_name": "Get Organizations", - "input": { - "variable_workflow_02LXK9KUOD0MH4qj4j062vm12gYmFb9y5N0": 100, - "variable_workflow_02LXKBG6HGMSD1YojnHQ3bM2cT6mP8A02A9": "", - "variable_workflow_02LXKCCX4AREX6odSHkvY5oECunJtqcLahK": "" - }, - "runtime_user": { - "target_default": true - }, - "skip_execution": false, - "target": { - "target_type": "meraki.endpoint", - "use_workflow_target": true - }, - "workflow_id": "definition_workflow_02LV5GIDUTY3X0WhuidM5ZWhkjL8wQCGAPq", - "workflow_name": "Meraki - Get Organizations" - }, - "object_type": "definition_activity" - }, - { - "unique_name": "definition_activity_02S5ARJXNKKXP4qXXqcA7hzO2HjXuCBDyNE", - "name": "Condition Block", - "title": "No Organizations Found?", - "type": "logic.if_else", - "base_type": "activity", - "properties": { - "conditions": [], - "continue_on_failure": false, - "description": "Validate that organizations are available before proceeding with network discovery and firewall rule management.", - "display_name": "Orgs Available?", - "skip_execution": false - }, - "object_type": "definition_activity", - "blocks": [ - { - "unique_name": "definition_activity_02S5ARJZQMMXO2vRZqaSnAnf6ywYtPvquYR", - "name": "Condition Branch", - "title": "No Organizations Available", - "type": "logic.condition_block", - "base_type": "activity", - "properties": { - "condition": { - "left_operand": { - "left_operand": "$activity.definition_activity_02S5ARJVIG3V40czX2BiBnKSbDepGORUD4V.output.variable_workflow_02LXKZA6725S35QBWMZCg1AlZnT11mbSFQi$", - "operator": "eq", - "right_operand": "[]" - }, - "operator": "or", - "right_operand": { - "left_operand": "$activity.definition_activity_02S5ARJVIG3V40czX2BiBnKSbDepGORUD4V.output.succeeded$", - "operator": "eq", - "right_operand": false - } - }, - "continue_on_failure": false, - "description": "Handle scenarios when organizations cannot be retrieved: either no organizations are accessible to the authenticated API key, or the Get Organizations API call failed due to authentication, network, or other API errors.", - "display_name": "None Available", - "skip_execution": false - }, - "object_type": "definition_activity", - "actions": [ - { - "unique_name": "definition_activity_02S5ARK2N8MXG5T9auuUmdm5aAC0osjleT2", - "name": "Set Variables", - "title": "Set No Organizations Error", - "type": "core.set_multiple_variables", - "base_type": "activity", - "properties": { - "continue_on_failure": false, - "description": "Set error output variables when no organizations are accessible.", - "display_name": "Set Output Variables", - "skip_execution": false, - "variables_to_update": [ - { - "variable_to_update": "$workflow.definition_workflow_02S5ARIDA6X2F2WjfxedD9dBIamPXcTcUFO.output.variable_workflow_02S5ARIDRDHDR0xGcPb1HbVjBK9LjCSoM4N$", - "variable_value_new": "No organizations found or accessible with the provided API key" - }, - { - "variable_to_update": "$workflow.definition_workflow_02S5ARIDA6X2F2WjfxedD9dBIamPXcTcUFO.output.variable_workflow_02S5ARIDRDZJU5ULuZ73m7nrVQyiDaH61Ca$", - "variable_value_new": "Failed" - }, - { - "variable_to_update": "$workflow.definition_workflow_02S5ARIDA6X2F2WjfxedD9dBIamPXcTcUFO.output.variable_workflow_02S5ARIDRCJP865BBJDArddRLud9wIFN54Z$", - "variable_value_new": "404" - } - ] - }, - "object_type": "definition_activity" - }, - { - "unique_name": "definition_activity_02S5ARK481DR81EoAvrJxq81YiOMEijgcCL", - "name": "Completed", - "title": "End on No Organizations", - "type": "logic.completed", - "base_type": "activity", - "properties": { - "completion_type": "failed-completed", - "continue_on_failure": false, - "description": "Terminate workflow execution when no organizations are accessible.", - "display_name": "Exit Workflow", - "result_message": "No organizations accessible", - "skip_execution": false - }, - "object_type": "definition_activity" - } - ] - } - ] - } - ] - }, - { - "unique_name": "definition_activity_02S5ARK5SHPLN0EZzo3MQpylUlIa96JIGad", - "name": "Group", - "title": "Deploy Rules", - "type": "logic.group", - "base_type": "activity", - "properties": { - "continue_on_failure": false, - "description": "Iterate through organizations to find networks with specified tags and manage (add or remove) the specified firewall rule across all matching networks.", - "display_as_suggestion": false, - "display_name": "Deploy Rules", - "skip_execution": false - }, - "object_type": "definition_activity", - "actions": [ - { - "unique_name": "definition_activity_02S5ARK8QSPWZ7Jh9gyjFhZyuC3wiWODqus", - "name": "Read Table from JSON", - "title": "Read Orgs from JSON", - "type": "corejava.read_table_from_json", - "base_type": "activity", - "properties": { - "action_timeout": 180, - "continue_on_failure": false, - "description": "Parse the JSON response containing organizations to extract organization IDs and names for network discovery.", - "display_name": "Parse Organizations", - "input_json": "$activity.definition_activity_02S5ARJVIG3V40czX2BiBnKSbDepGORUD4V.output.variable_workflow_02LXKZA6725S35QBWMZCg1AlZnT11mbSFQi$", - "jsonpath_query": "$.*", - "persist_output": true, - "populate_columns": false, - "skip_execution": false, - "table_columns": [ - { - "column_name": "id", - "column_type": "string" - }, - { - "column_name": "name", - "column_type": "string" - } - ] - }, - "object_type": "definition_activity" - }, - { - "unique_name": "definition_activity_02S5ARKJA9KBA7OPfzGcU4k88ADpw1nirdx", - "name": "For Each", - "title": "For Each Org", - "type": "logic.for_each", - "base_type": "activity", - "properties": { - "continue_on_failure": false, - "description": "Iterate through each organization to discover networks with matching tags for firewall rule management.", - "display_name": "Each Org", - "skip_execution": false, - "source_array": "$activity.definition_activity_02S5ARK8QSPWZ7Jh9gyjFhZyuC3wiWODqus.output.read_table_from_json$" - }, - "object_type": "definition_activity", - "actions": [ - { - "unique_name": "definition_activity_02S5ARKO577963d6OVPRDxzdiqGB9v4Hqta", - "name": "Meraki - Get Organization Networks", - "title": "Meraki - Get Organization Networks", - "type": "workflow.atomic_workflow", - "base_type": "subworkflow", - "properties": { - "continue_on_failure": true, - "description": "List networks in the current organization that match the specified tags. In bulk operations across multiple organizations, if one organization fails (due to API limits, permissions, or temporary issues), the workflow continues processing other organizations to maximize coverage.", - "display_name": "Meraki - Get Organization Networks", - "input": { - "variable_workflow_02L9TZH6C81473IDtFQ0NIzW3jmXrgchtN9": "$activity.definition_activity_02S5ARKJA9KBA7OPfzGcU4k88ADpw1nirdx.input.source_array[@].id$", - "variable_workflow_02LEUL5GOHN9T53T7jFQx3T2Yo04p5Xuz0r": false, - "variable_workflow_02LNK5WW0RD2A20mG7v1G7gsyE2N2RrKdQd": "", - "variable_workflow_02LNK5WW0RTCB1AXbOwJOBVWvUx1wdyKtkt": "withAnyTags", - "variable_workflow_02LNK5WW0S5E21Q9biFSXTiLbHr7cWP2K7y": "false", - "variable_workflow_02LNK5WW0SE2H1zmchhUHyfX7L0XGvKmB7W": 500, - "variable_workflow_02LNK5WW0SM7R5SjIvnnytN6vYzR7wfDHeE": "$workflow.definition_workflow_02S5ARIDA6X2F2WjfxedD9dBIamPXcTcUFO.input.variable_workflow_02S5ARIDRC9NE16NkQ6YrYWAtehxAZUuSbJ$", - "variable_workflow_02LNK5WW0STMX38BXiY2IvPQoHQliJHSCgE": [ - "appliance" - ], - "variable_workflow_02LNK5WW0T0NX26NyApdlTn355uYJwcQGqW": "", - "variable_workflow_02LNK5WW0T92U1HJd1sxzFrmVvwxy7NVaQn": "" - }, - "runtime_user": { - "target_default": true - }, - "skip_execution": false, - "target": { - "target_type": "meraki.endpoint", - "use_workflow_target": true - }, - "workflow_id": "definition_workflow_02L9TZH5W95870XebPF2g8kSyIYcawUvljX", - "workflow_name": "Meraki - Get Organization Networks" - }, - "object_type": "definition_activity" - }, - { - "unique_name": "definition_activity_02S5ARKQN81231fuSvPzIMP2nBbxGQhpoCI", - "name": "Condition Block", - "title": "Has Networks matching Tag?", - "type": "logic.if_else", - "base_type": "activity", - "properties": { - "conditions": [], - "continue_on_failure": false, - "description": "Validate that networks with matching tags exist in the current organization and that the Get Organization Networks API call succeeded before proceeding with firewall rule management.", - "display_name": "Networks Found?", - "skip_execution": false - }, - "object_type": "definition_activity", - "blocks": [ - { - "unique_name": "definition_activity_02S5ARKSTHBI53aIVuRLapgbTGkVnFa71Ko", - "name": "Condition Branch", - "title": "Yes", - "type": "logic.condition_block", - "base_type": "activity", - "properties": { - "condition": { - "left_operand": { - "left_operand": "$activity.definition_activity_02S5ARKO577963d6OVPRDxzdiqGB9v4Hqta.output.variable_workflow_02L9TZH6C8OZI7c9t9Z0h0JHAbqCc7LuTGT$", - "operator": "ne", - "right_operand": "[]" - }, - "operator": "and", - "right_operand": { - "left_operand": "$activity.definition_activity_02S5ARKO577963d6OVPRDxzdiqGB9v4Hqta.output.succeeded$", - "operator": "eq", - "right_operand": true - } - }, - "continue_on_failure": false, - "display_name": "Yes", - "skip_execution": false - }, - "object_type": "definition_activity", - "actions": [ - { - "unique_name": "definition_activity_02S5ARKVB9RHW2pnxv5ZFOgh83ouCvN7fHo", - "name": "Set Variables", - "title": "Set Success Variables", - "type": "core.set_multiple_variables", - "base_type": "activity", - "properties": { - "continue_on_failure": false, - "description": "Append organization processing results to the output message accumulator.", - "display_name": "Set Output Variables", - "skip_execution": false, - "variables_to_update": [ - { - "variable_to_update": "$workflow.definition_workflow_02S5ARIDA6X2F2WjfxedD9dBIamPXcTcUFO.local.variable_workflow_02S5ARIDREUYJ6FyD70PIRCUKxNM0mfNibU$", - "variable_value_new": "$workflow.definition_workflow_02S5ARIDA6X2F2WjfxedD9dBIamPXcTcUFO.local.variable_workflow_02S5ARIDREUYJ6FyD70PIRCUKxNM0mfNibU$\nResults for Organization: $activity.definition_activity_02S5ARKJA9KBA7OPfzGcU4k88ADpw1nirdx.input.source_array[@].name$" - } - ] - }, - "object_type": "definition_activity" - }, - { - "unique_name": "definition_activity_02S5ARKX8EDGY7iedfOFWnrvD9Gphc4RyLf", - "name": "Read Table from JSON", - "title": "Read Networks from JSON", - "type": "corejava.read_table_from_json", - "base_type": "activity", - "properties": { - "action_timeout": 180, - "continue_on_failure": false, - "description": "Parse the JSON response containing networks to extract network IDs and names for iterating through each network to apply firewall rule changes.", - "display_name": "Read Networks from JSON", - "input_json": "$activity.definition_activity_02S5ARKO577963d6OVPRDxzdiqGB9v4Hqta.output.variable_workflow_02L9TZH6C8OZI7c9t9Z0h0JHAbqCc7LuTGT$", - "jsonpath_query": "$.*", - "persist_output": true, - "populate_columns": false, - "skip_execution": false, - "table_columns": [ - { - "column_name": "id", - "column_type": "string" - }, - { - "column_name": "name", - "column_type": "string" - } - ] - }, - "object_type": "definition_activity" - }, - { - "unique_name": "definition_activity_02S5ARL6XX1WB6ZttXbUrzj37A2AgujqTbp", - "name": "For Each", - "title": "For Each Network", - "type": "logic.for_each", - "base_type": "activity", - "properties": { - "continue_on_failure": false, - "description": "Iterate through each discovered network to manage firewall rules for all matching networks in the current organization.", - "display_name": "For Each Network", - "skip_execution": false, - "source_array": "$activity.definition_activity_02S5ARKX8EDGY7iedfOFWnrvD9Gphc4RyLf.output.read_table_from_json$" - }, - "object_type": "definition_activity", - "actions": [ - { - "unique_name": "definition_activity_02S5ARLAZG1240oBSeGAtTPqPy45xfQckZe", - "name": "Meraki - Manage MX L3 Firewall Rules", - "title": "Meraki - Manage MX L3 Firewall Rules", - "type": "workflow.sub_workflow", - "base_type": "subworkflow", - "properties": { - "continue_on_failure": true, - "description": "Add or remove the specified L3 firewall rule on this network based on the Add/Remove flag. In bulk operations across multiple networks, individual network failures do not stop processing of remaining networks - this maximizes coverage even when some networks have issues (API limits, insufficient permissions, configuration conflicts, etc.).", - "display_name": "Meraki - Manage MX L3 Firewall Rules", - "input": { - "variable_workflow_02Q2QBASBNJPU4Jb0bAtz75rsEjBfxWl1zC": "$activity.definition_activity_02S5ARL6XX1WB6ZttXbUrzj37A2AgujqTbp.input.source_array[@].id$", - "variable_workflow_02Q2QE6HSDSEX1fzaLABhotUMJPPr6qdkNX": "$activity.definition_activity_02S5ARJ4V4SOZ4cPyw5eH2KCOMQf86hs09N.output.script_queries.normalizedRule$", - "variable_workflow_02Q2QEL1AO2C12qBxR6feTHxGJnyV0DnqKJ": "$workflow.definition_workflow_02S5ARIDA6X2F2WjfxedD9dBIamPXcTcUFO.input.variable_workflow_02S5AU0MI637M65d8mb4PWMvGVAhEcjeN4K$", - "variable_workflow_02Q2RX032F5FC0xIG13ihAQLIaoxWflVFkr": "$workflow.definition_workflow_02S5ARIDA6X2F2WjfxedD9dBIamPXcTcUFO.input.variable_workflow_02S5G3TQZRE3Q5yzsq9fvf6r7AYaQRWeq2A$", - "variable_workflow_02Q2FWBKORGID001": "$activity.definition_activity_02S5ARKJA9KBA7OPfzGcU4k88ADpw1nirdx.input.source_array[@].id$", - "variable_workflow_02Q2FWBKORGNAME01": "$activity.definition_activity_02S5ARKJA9KBA7OPfzGcU4k88ADpw1nirdx.input.source_array[@].name$", - "variable_workflow_02Q2FWBKNETNAME01": "$activity.definition_activity_02S5ARL6XX1WB6ZttXbUrzj37A2AgujqTbp.input.source_array[@].name$" - }, - "runtime_user": { - "target_default": true - }, - "skip_execution": false, - "target": { - "target_type": "meraki.endpoint", - "use_workflow_target": true - }, - "workflow_id": "definition_workflow_02Q2Q77WT8QTE4UkIuKUVeySa9Viy5opNWU", - "workflow_name": "Meraki - Manage MX L3 Firewall Rules" - }, - "object_type": "definition_activity" - }, - { - "unique_name": "definition_activity_02S5ARLE9IBX42GGlU2sppMN1uUxkvSLxA7", - "name": "Set Variables", - "title": "Append Network Results", - "type": "core.set_multiple_variables", - "base_type": "activity", - "properties": { - "continue_on_failure": false, - "description": "Append network firewall rule management results to the output message accumulator.", - "display_name": "Set Output Variables", - "skip_execution": false, - "variables_to_update": [ - { - "variable_to_update": "$workflow.definition_workflow_02S5ARIDA6X2F2WjfxedD9dBIamPXcTcUFO.local.variable_workflow_02S5ARIDREUYJ6FyD70PIRCUKxNM0mfNibU$", - "variable_value_new": "$workflow.definition_workflow_02S5ARIDA6X2F2WjfxedD9dBIamPXcTcUFO.local.variable_workflow_02S5ARIDREUYJ6FyD70PIRCUKxNM0mfNibU$\nNetwork: $activity.definition_activity_02S5ARL6XX1WB6ZttXbUrzj37A2AgujqTbp.input.source_array[@].name$ $activity.definition_activity_02S5ARLAZG1240oBSeGAtTPqPy45xfQckZe.output.variable_workflow_02Q2Q77XBFC8H025dryrtHYnRJPx1un4EfB$ $activity.definition_activity_02S5ARLAZG1240oBSeGAtTPqPy45xfQckZe.output.variable_workflow_02Q2Q77XBFIRE6QjQkXlVEVTnhSUvp49dFB$" - }, - { - "variable_to_update": "$workflow.definition_workflow_02S5ARIDA6X2F2WjfxedD9dBIamPXcTcUFO.local.variable_workflow_02S5FWBACKUPACC01$", - "variable_value_new": "$workflow.definition_workflow_02S5ARIDA6X2F2WjfxedD9dBIamPXcTcUFO.local.variable_workflow_02S5FWBACKUPACC01$\n$activity.definition_activity_02S5ARLAZG1240oBSeGAtTPqPy45xfQckZe.output.variable_workflow_02Q2FWBACKUPOUT01$" - } - ] - }, - "object_type": "definition_activity" - } - ] - } - ] - }, - { - "unique_name": "definition_activity_02S5ARLO3IUUX3qKNoJGDgtHVgovpHXhwE0", - "name": "Condition Branch", - "title": "No", - "type": "logic.condition_block", - "base_type": "activity", - "properties": { - "condition": { - "left_operand": { - "left_operand": "$activity.definition_activity_02S5ARKO577963d6OVPRDxzdiqGB9v4Hqta.output.variable_workflow_02L9TZH6C8OZI7c9t9Z0h0JHAbqCc7LuTGT$", - "operator": "eq", - "right_operand": "[]" - }, - "operator": "or", - "right_operand": { - "left_operand": "$activity.definition_activity_02S5ARKO577963d6OVPRDxzdiqGB9v4Hqta.output.succeeded$", - "operator": "ne", - "right_operand": true - } - }, - "continue_on_failure": false, - "display_name": "No", - "skip_execution": false - }, - "object_type": "definition_activity", - "actions": [ - { - "unique_name": "definition_activity_02S5ARLQZHV8V4dz8JyVogyc5GklYZimorl", - "name": "Set Variables", - "title": "Set Error Variables", - "type": "core.set_multiple_variables", - "base_type": "activity", - "properties": { - "continue_on_failure": false, - "description": "Append organization error details to the output message accumulator when no matching networks are found.", - "display_name": "Set Output Variables", - "skip_execution": false, - "variables_to_update": [ - { - "variable_to_update": "$workflow.definition_workflow_02S5ARIDA6X2F2WjfxedD9dBIamPXcTcUFO.local.variable_workflow_02S5ARIDREUYJ6FyD70PIRCUKxNM0mfNibU$", - "variable_value_new": "$workflow.definition_workflow_02S5ARIDA6X2F2WjfxedD9dBIamPXcTcUFO.local.variable_workflow_02S5ARIDREUYJ6FyD70PIRCUKxNM0mfNibU$\nOrganization: $activity.definition_activity_02S5ARKJA9KBA7OPfzGcU4k88ADpw1nirdx.input.source_array[@].name$ has no matching tags." - } - ] - }, - "object_type": "definition_activity" - } - ] - } - ] - } - ] - } - ] - }, - { - "unique_name": "definition_activity_02S5ARLSZ10EN5T98yvN0VcPSzrNSQ45oqi", - "name": "Group", - "title": "Summarize Outputs", - "type": "logic.group", - "base_type": "activity", - "properties": { - "continue_on_failure": false, - "description": "Generate a deterministic summary of firewall rule management results for administrator review.", - "display_as_suggestion": false, - "display_name": "Summarize Outputs", - "skip_execution": false - }, - "object_type": "definition_activity", - "actions": [ - { - "unique_name": "definition_activity_02S5ARLW1R1XY6IvpJdWTvcL0jz25NO2Pgf", - "name": "Execute Python Script", - "title": "Summarize Results", - "type": "python3.script", - "base_type": "activity", - "properties": { - "action_timeout": 180, - "continue_on_failure": false, - "description": "Generate a deterministic summary of firewall rule management results and derive final workflow status outputs.", - "display_name": "Summarize Results", - "script": "import json\nimport sys\n\ninput_text = sys.argv[1] if len(sys.argv) > 1 else \"\"\nraw_tags_input = sys.argv[2] if len(sys.argv) > 2 else \"[]\"\nraw_add_input = sys.argv[3] if len(sys.argv) > 3 else \"true\"\nraw_backup_input = sys.argv[4] if len(sys.argv) > 4 else \"\"\n\n\ndef normalize_text(text):\n return text.replace('\\r\\n', '\\n').replace('\\\\n', '\\n')\n\n\ndef format_tags(raw_tags):\n try:\n tags = json.loads(raw_tags)\n except (json.JSONDecodeError, TypeError):\n return \"Invalid tags format\"\n\n if isinstance(tags, list) and tags:\n return \", \".join(str(tag) for tag in tags)\n return \"No tags provided\"\n\n\ndef operation_label(raw_value):\n return \"Add\" if str(raw_value).lower() == \"true\" else \"Remove\"\n\n\ndef parse_network_line(line):\n body = line[len(\"Network:\"):].strip()\n markers = [\n \" Firewall updated successfully.\",\n \" Firewall rule skipped\",\n \" Error:\",\n \" Validation Error:\",\n ]\n matches = [(body.find(marker), marker) for marker in markers if body.find(marker) > 0]\n if not matches:\n return body, \"unknown\", body\n\n idx, marker = min(matches, key=lambda item: item[0])\n network_name = body[:idx].strip()\n detail = body[idx:].strip()\n\n if marker == \" Firewall updated successfully.\":\n category = \"success\"\n elif marker == \" Firewall rule skipped\":\n category = \"skipped\"\n else:\n category = \"failed\"\n\n return network_name, category, detail\n\n\ndef parse_results(text):\n parsed = {\n \"successful_networks\": [],\n \"skipped_networks\": [],\n \"failed_networks\": [],\n }\n current_org = None\n\n for raw_line in normalize_text(text).splitlines():\n line = raw_line.strip()\n if not line:\n continue\n if line.startswith(\"Results for Organization:\"):\n current_org = line.split(\"Results for Organization:\", 1)[1].strip()\n continue\n if line.startswith(\"Organization:\") and \"has no matching tags.\" in line:\n continue\n if line.startswith(\"Network:\") and current_org:\n network_name, category, detail = parse_network_line(line)\n payload = {\n \"organization\": current_org,\n \"network\": network_name,\n \"detail\": detail,\n }\n if category == \"success\":\n parsed[\"successful_networks\"].append(payload)\n elif category == \"skipped\":\n parsed[\"skipped_networks\"].append(payload)\n else:\n parsed[\"failed_networks\"].append(payload)\n\n return parsed\n\n\ndef parse_backup_entries(text):\n entries = []\n for raw_line in normalize_text(text).splitlines():\n line = raw_line.strip()\n if not line:\n continue\n try:\n payload = json.loads(line)\n except (json.JSONDecodeError, TypeError):\n continue\n if isinstance(payload, dict):\n entries.append(payload)\n return entries\n\n\ndef append_grouped(lines, heading, items):\n if not items:\n return\n lines.append(\"\")\n lines.append(heading)\n current_org = None\n for item in items:\n if current_org != item['organization']:\n current_org = item['organization']\n lines.append(f\"{current_org}:\")\n lines.append(f\" - {item['network']} - {item['detail']}\")\n\n\ndef build_summary(parsed, tags_string, op_label, backup_entries):\n organizations = {item['organization'] for key in parsed for item in parsed[key]}\n success_count = len(parsed['successful_networks'])\n skipped_count = len(parsed['skipped_networks'])\n failed_count = len(parsed['failed_networks'])\n\n lines = [\n \"Bulk L3 Firewall Rule Management Summary:\",\n f\"Operation: {op_label}\",\n f\"Target Tags: {tags_string}\",\n f\"Organizations with matching networks: {len(organizations)}\",\n f\"Successful operations: {success_count}\",\n f\"Skipped operations: {skipped_count}\",\n f\"Failed operations: {failed_count}\",\n f\"Backup entries captured: {len(backup_entries)}\",\n ]\n\n append_grouped(lines, \"Successful Operations by Organization:\", parsed['successful_networks'])\n append_grouped(lines, \"Skipped Operations by Organization:\", parsed['skipped_networks'])\n append_grouped(lines, \"Failed Operations by Organization:\", parsed['failed_networks'])\n return \"\\n\".join(lines)\n\n\nop_label = operation_label(raw_add_input)\ntags_string = format_tags(raw_tags_input)\nparsed_results = parse_results(input_text)\nbackup_entries = parse_backup_entries(raw_backup_input)\nsummary_text = build_summary(parsed_results, tags_string, op_label, backup_entries)\nbackup_json = json.dumps(backup_entries, separators=(\",\", \":\"))\n\nsuccess_count = len(parsed_results['successful_networks'])\nskipped_count = len(parsed_results['skipped_networks'])\nfailed_count = len(parsed_results['failed_networks'])\ntotal_count = success_count + skipped_count + failed_count\n\nif total_count == 0:\n final_status_code = 404\n final_status_message = \"Not Found\"\n final_error_message = \"No matching networks were found for the provided tags.\"\nelif failed_count > 0 and (success_count > 0 or skipped_count > 0):\n final_status_code = 207\n final_status_message = \"Partial Success\"\n final_error_message = f\"{failed_count} network operation(s) failed. Review Result for details.\"\nelif failed_count > 0:\n final_status_code = 500\n final_status_message = \"Failed\"\n final_error_message = f\"{failed_count} network operation(s) failed. Review Result for details.\"\nelif skipped_count > 0 and success_count == 0:\n final_status_code = 200\n final_status_message = \"No Changes\"\n final_error_message = \"\"\nelse:\n final_status_code = 200\n final_status_message = \"Success\"\n final_error_message = \"\"\n", - "script_arguments": [ - "$workflow.definition_workflow_02S5ARIDA6X2F2WjfxedD9dBIamPXcTcUFO.local.variable_workflow_02S5ARIDREUYJ6FyD70PIRCUKxNM0mfNibU$", - "$workflow.definition_workflow_02S5ARIDA6X2F2WjfxedD9dBIamPXcTcUFO.input.variable_workflow_02S5ARIDRC9NE16NkQ6YrYWAtehxAZUuSbJ$", - "$workflow.definition_workflow_02S5ARIDA6X2F2WjfxedD9dBIamPXcTcUFO.input.variable_workflow_02S5AU0MI637M65d8mb4PWMvGVAhEcjeN4K$", - "$workflow.definition_workflow_02S5ARIDA6X2F2WjfxedD9dBIamPXcTcUFO.local.variable_workflow_02S5FWBACKUPACC01$" - ], - "script_queries": [ - { - "script_query": "summary_text", - "script_query_name": "summary_text", - "script_query_type": "string" - }, - { - "script_query": "final_status_code", - "script_query_name": "final_status_code", - "script_query_type": "integer" - }, - { - "script_query": "final_status_message", - "script_query_name": "final_status_message", - "script_query_type": "string" - }, - { - "script_query": "final_error_message", - "script_query_name": "final_error_message", - "script_query_type": "string" - }, - { - "script_query": "backup_json", - "script_query_name": "firewall_rules_backup_json", - "script_query_type": "string" - } - ], - "skip_execution": false - }, - "object_type": "definition_activity" - }, - { - "unique_name": "definition_activity_02S5ARLW1R1XY6IvpJdWTvcL0jz25NO2Pgh", - "name": "Set Variables", - "title": "Set Final Outputs", - "type": "core.set_multiple_variables", - "base_type": "activity", - "properties": { - "continue_on_failure": false, - "description": "Set the final result summary, status message, status code, error message, and firewall backup JSON array.", - "display_name": "Set Final Outputs", - "skip_execution": false, - "variables_to_update": [ - { - "variable_to_update": "$workflow.definition_workflow_02S5ARIDA6X2F2WjfxedD9dBIamPXcTcUFO.output.variable_workflow_02S5ARIDREDIL3gQ7VYT5lEBbCCpINTpxcZ$", - "variable_value_new": "$activity.definition_activity_02S5ARLW1R1XY6IvpJdWTvcL0jz25NO2Pgf.output.script_queries.summary_text$" - }, - { - "variable_to_update": "$workflow.definition_workflow_02S5ARIDA6X2F2WjfxedD9dBIamPXcTcUFO.output.variable_workflow_02S5ARIDRDZJU5ULuZ73m7nrVQyiDaH61Ca$", - "variable_value_new": "$activity.definition_activity_02S5ARLW1R1XY6IvpJdWTvcL0jz25NO2Pgf.output.script_queries.final_status_message$" - }, - { - "variable_to_update": "$workflow.definition_workflow_02S5ARIDA6X2F2WjfxedD9dBIamPXcTcUFO.output.variable_workflow_02S5ARIDRCJP865BBJDArddRLud9wIFN54Z$", - "variable_value_new": "$activity.definition_activity_02S5ARLW1R1XY6IvpJdWTvcL0jz25NO2Pgf.output.script_queries.final_status_code$" - }, - { - "variable_to_update": "$workflow.definition_workflow_02S5ARIDA6X2F2WjfxedD9dBIamPXcTcUFO.output.variable_workflow_02S5ARIDRDHDR0xGcPb1HbVjBK9LjCSoM4N$", - "variable_value_new": "$activity.definition_activity_02S5ARLW1R1XY6IvpJdWTvcL0jz25NO2Pgf.output.script_queries.final_error_message$" - }, - { - "variable_to_update": "$workflow.definition_workflow_02S5ARIDA6X2F2WjfxedD9dBIamPXcTcUFO.output.variable_workflow_02S5FWBACKUPOUT01$", - "variable_value_new": "$activity.definition_activity_02S5ARLW1R1XY6IvpJdWTvcL0jz25NO2Pgf.output.script_queries.firewall_rules_backup_json$" - } - ] - }, - "object_type": "definition_activity" - } - ] - } - ], - "categories": [ - "category_1BMfMXSnJMyt5Ihqi7rWJr5N8cf" - ] - }, - "subworkflows": [ - { - "workflow": { - "unique_name": "definition_workflow_02Q2Q77WT8QTE4UkIuKUVeySa9Viy5opNWU", - "name": "Meraki - Manage MX L3 Firewall Rules", - "title": "Meraki - Manage MX L3 Firewall Rules", - "type": "generic.workflow", - "base_type": "workflow", - "variables": [ - { - "schema_id": "datatype.string", - "properties": { - "value": "", - "scope": "input", - "name": "Network ID", - "type": "datatype.string", - "description": "Meraki network identifier where firewall rules will be managed. Format: N_XXXXXXXXXX. Must be a valid MX network with Layer 3 firewall capabilities. Found in Meraki Dashboard under Network-wide > General.", - "is_required": true, - "display_on_wizard": false, - "is_invisible": false, - "variable_string_format": "text" - }, - "unique_name": "variable_workflow_02Q2QBASBNJPU4Jb0bAtz75rsEjBfxWl1zC", - "object_type": "variable_workflow" - }, - { - "schema_id": "datatype.string", - "properties": { - "value": "", - "scope": "input", - "name": "Organization ID", - "type": "datatype.string", - "description": "Organization identifier associated with the network being modified.", - "is_required": true, - "display_on_wizard": false, - "is_invisible": false, - "variable_string_format": "text" - }, - "unique_name": "variable_workflow_02Q2FWBKORGID001", - "object_type": "variable_workflow" - }, - { - "schema_id": "datatype.string", - "properties": { - "value": "", - "scope": "input", - "name": "Organization Name", - "type": "datatype.string", - "description": "Organization display name associated with the network being modified.", - "is_required": true, - "display_on_wizard": false, - "is_invisible": false, - "variable_string_format": "text" - }, - "unique_name": "variable_workflow_02Q2FWBKORGNAME01", - "object_type": "variable_workflow" - }, - { - "schema_id": "datatype.string", - "properties": { - "value": "", - "scope": "input", - "name": "Network Name", - "type": "datatype.string", - "description": "Network display name associated with the network being modified.", - "is_required": true, - "display_on_wizard": false, - "is_invisible": false, - "variable_string_format": "text" - }, - "unique_name": "variable_workflow_02Q2FWBKNETNAME01", - "object_type": "variable_workflow" - }, - { - "schema_id": "datatype.string", - "properties": { - "value": "", - "scope": "output", - "name": "Status Message", - "type": "datatype.string", - "description": "Brief status message indicating workflow outcome. Examples: 'Success: Firewall rule operation completed', 'Validation Error: Invalid policy value', 'Error: GET operation failed'. Useful for quick status checks.", - "is_required": false, - "display_on_wizard": false, - "is_invisible": false, - "variable_string_format": "text" - }, - "unique_name": "variable_workflow_02Q2Q77XBFC8H025dryrtHYnRJPx1un4EfB", - "object_type": "variable_workflow" - }, - { - "schema_id": "datatype.string", - "properties": { - "value": "", - "scope": "output", - "name": "Error Message", - "type": "datatype.string", - "description": "Detailed error message when workflow fails. Includes specific validation errors, API failures, or processing issues to help troubleshoot problems.", - "is_required": false, - "display_on_wizard": false, - "is_invisible": false, - "variable_string_format": "text" - }, - "unique_name": "variable_workflow_02Q2Q77XBFIRE6QjQkXlVEVTnhSUvp49dFB", - "object_type": "variable_workflow" - }, - { - "schema_id": "datatype.string", - "properties": { - "value": "", - "scope": "output", - "name": "Result", - "type": "datatype.string", - "description": "Comprehensive summary of firewall rule operation including network ID, operation performed, rule details, and success/failure status. Useful for logging and monitoring workflow executions.", - "is_required": false, - "display_on_wizard": false, - "is_invisible": false, - "variable_string_format": "text" - }, - "unique_name": "variable_workflow_02Q2Q77XBG9322NBbswlydiy7ZEAmNzlTX0", - "object_type": "variable_workflow" - }, - { - "schema_id": "datatype.large_string", - "properties": { - "value": "", - "scope": "output", - "name": "Firewall Rules Backup", - "type": "datatype.large_string", - "description": "JSON snapshot of the pre-change firewall rules for a network that is actually updated. Empty when no change is applied.", - "is_required": false, - "display_on_wizard": false, - "is_invisible": false, - "variable_string_format": "" - }, - "unique_name": "variable_workflow_02Q2FWBACKUPOUT01", - "object_type": "variable_workflow" - }, - { - "schema_id": "datatype.string", - "properties": { - "value": "", - "scope": "input", - "name": "Rule", - "type": "datatype.string", - "description": "JSON object defining the firewall rule to add. When removing, the same rule fields are used as the match criteria for rules to remove.", - "is_required": true, - "display_on_wizard": false, - "is_invisible": false, - "variable_string_format": "json" - }, - "unique_name": "variable_workflow_02Q2QE6HSDSEX1fzaLABhotUMJPPr6qdkNX", - "object_type": "variable_workflow" - }, - { - "schema_id": "datatype.integer", - "properties": { - "value": 0, - "scope": "output", - "name": "Status Code", - "type": "datatype.integer", - "description": "HTTP status code returned by the Meraki API operations. Common values: 200 (success), 400 (validation error), 401 (unauthorized), 404 (network not found), 429 (rate limited).", - "is_required": false, - "display_on_wizard": false, - "is_invisible": false, - "variable_string_format": "" - }, - "unique_name": "variable_workflow_02Q2Q77XBFP4H7gfjwt0uyrKj8h9WQVEUQm", - "object_type": "variable_workflow" - }, - { - "schema_id": "datatype.integer", - "properties": { - "value": 0, - "scope": "input", - "name": "Position", - "type": "datatype.integer", - "description": "Rule insertion position in firewall list (0-based index). Used only when adding a rule. Ignored during remove operations.", - "is_required": false, - "display_on_wizard": false, - "is_invisible": false, - "variable_string_format": "" - }, - "unique_name": "variable_workflow_02Q2RX032F5FC0xIG13ihAQLIaoxWflVFkr", - "object_type": "variable_workflow" - }, - { - "schema_id": "datatype.boolean", - "properties": { - "value": true, - "scope": "input", - "name": "Add or Remove Rule", - "type": "datatype.boolean", - "description": "Operation mode: true = Add new firewall rule at the specified position, false = Remove matching firewall rules. Position is ignored when removing.", - "is_required": false, - "display_on_wizard": false, - "is_invisible": false, - "variable_string_format": "" - }, - "unique_name": "variable_workflow_02Q2QEL1AO2C12qBxR6feTHxGJnyV0DnqKJ", - "object_type": "variable_workflow" - } - ], - "properties": { - "atomic": { - "is_atomic": false - }, - "automation_rules": { - "type": [] - }, - "delete_workflow_instance": false, - "description": "Manages Meraki MX Layer 3 outbound firewall rules with add/remove operations and position-based insertion. Includes validation, duplicate detection, and error handling.\n\nFeatures:\n- Add new firewall rules at the specified position (0=first/highest priority, 1=second, etc.)\n- Remove matching firewall rules using the rule's traffic key fields: policy, protocol, srcPort, srcCidr, destPort, and destCidr\n- Preserve the provided comment on add operations\n- Validate required rule fields: policy, protocol, and destCidr\n- Apply Meraki-compatible defaults for optional fields when omitted: srcCidr=any, srcPort=any, destPort=any, comment=\"\", syslogEnabled=false\n- Error handling for GET/UPDATE operations\n- Comprehensive output variables\n- Position validation with automatic boundary adjustment for add operations only\n\nEmits a JSON snapshot of the pre-change firewall rule state for every network update that is actually applied.", - "display_name": "Meraki - Manage MX L3 Firewall Rules", - "runtime_user": { - "target_default": true - }, - "target": { - "target_type": "meraki.endpoint", - "specify_on_workflow_start": true - } - }, - "object_type": "definition_workflow", - "actions": [ - { - "unique_name": "definition_activity_02Q2RQABK55WX3su2ycT7dg2xZjh86uCPDE", - "name": "Group", - "title": "Input Validation & Requirements Check", - "type": "logic.group", - "base_type": "activity", - "properties": { - "continue_on_failure": false, - "description": "Comprehensive validation of all input parameters before processing firewall rules. Validates JSON structure, required fields, policy values, and data types to ensure safe and reliable firewall rule operations.", - "display_name": "Input Validation & Requirements Check", - "skip_execution": false - }, - "object_type": "definition_activity", - "actions": [ - { - "unique_name": "definition_activity_02Q2Q77ZB7LYU259f0SCVlJBZfjGrUaXvEG", - "name": "JSONPath Query", - "title": "Validate Rule JSON Structure", - "type": "corejava.jsonpathquery", - "base_type": "activity", - "properties": { - "action_timeout": 180, - "continue_on_failure": true, - "description": "Validate that the Rule input contains all required firewall rule fields and can be parsed as valid JSON", - "display_name": "Validate Rule JSON Structure", - "input_json": "$workflow.definition_workflow_02Q2Q77WT8QTE4UkIuKUVeySa9Viy5opNWU.input.variable_workflow_02Q2QE6HSDSEX1fzaLABhotUMJPPr6qdkNX$", - "jsonpath_queries": [ - { - "jsonpath_query": "$.policy", - "jsonpath_query_name": "policy", - "jsonpath_query_type": "string" - }, - { - "jsonpath_query": "$.protocol", - "jsonpath_query_name": "protocol", - "jsonpath_query_type": "string" - }, - { - "jsonpath_query": "$.destPort", - "jsonpath_query_name": "destPort", - "jsonpath_query_type": "string" - }, - { - "jsonpath_query": "$.destCidr", - "jsonpath_query_name": "destCidr", - "jsonpath_query_type": "string" - }, - { - "jsonpath_query": "$.srcPort", - "jsonpath_query_name": "srcPort", - "jsonpath_query_type": "string" - }, - { - "jsonpath_query": "$.srcCidr", - "jsonpath_query_name": "srcCidr", - "jsonpath_query_type": "string" - } - ], - "skip_execution": false - }, - "object_type": "definition_activity" - }, - { - "unique_name": "definition_activity_02Q2ROPCRNOG915LCfZBLVjbez3kON0u37H", - "name": "Condition Block", - "title": "Check JSON Parsing Success", - "type": "logic.if_else", - "base_type": "activity", - "properties": { - "continue_on_failure": false, - "description": "Check if the JSONPath query succeeded, indicating valid JSON structure. If failed, terminate with validation error.", - "display_name": "Check JSON Parsing Success", - "skip_execution": false - }, - "object_type": "definition_activity", - "blocks": [ - { - "unique_name": "definition_activity_02Q2ROPDE0QA6053P9xEh0yZXXO32bput3C", - "name": "Condition Branch", - "title": "JSON Parsing Failed", - "type": "logic.condition_block", - "base_type": "activity", - "properties": { - "condition": { - "left_operand": "$activity.definition_activity_02Q2Q77ZB7LYU259f0SCVlJBZfjGrUaXvEG.output.succeeded$", - "operator": "eq", - "right_operand": false - }, - "continue_on_failure": false, - "description": "Triggered when Rule input contains invalid JSON or missing required fields that prevent JSONPath parsing.", - "display_name": "JSON Parsing Failed", - "skip_execution": false - }, - "object_type": "definition_activity", - "actions": [ - { - "unique_name": "definition_activity_02Q2ROPEDARFA7PBhcwETk5Q5OwqM19ULKI", - "name": "Set Variables", - "title": "Set JSON Validation Error Details", - "type": "core.set_multiple_variables", - "base_type": "activity", - "properties": { - "continue_on_failure": false, - "description": "Populate output variables with JSON validation error details including missing fields or malformed JSON structure.", - "display_name": "Set JSON Validation Error Details", - "skip_execution": false, - "variables_to_update": [ - { - "variable_to_update": "$workflow.definition_workflow_02Q2Q77WT8QTE4UkIuKUVeySa9Viy5opNWU.output.variable_workflow_02Q2Q77XBFIRE6QjQkXlVEVTnhSUvp49dFB$", - "variable_value_new": "Invalid Rule JSON format or missing required fields. Required fields: policy, protocol, destPort, destCidr, srcPort, srcCidr. Error: $activity.definition_activity_02Q2Q77ZB7LYU259f0SCVlJBZfjGrUaXvEG.output.error.message$" - }, - { - "variable_to_update": "$workflow.definition_workflow_02Q2Q77WT8QTE4UkIuKUVeySa9Viy5opNWU.output.variable_workflow_02Q2Q77XBFP4H7gfjwt0uyrKj8h9WQVEUQm$", - "variable_value_new": 400 - }, - { - "variable_to_update": "$workflow.definition_workflow_02Q2Q77WT8QTE4UkIuKUVeySa9Viy5opNWU.output.variable_workflow_02Q2Q77XBFC8H025dryrtHYnRJPx1un4EfB$", - "variable_value_new": "Validation Error: Invalid JSON format" - }, - { - "variable_to_update": "$workflow.definition_workflow_02Q2Q77WT8QTE4UkIuKUVeySa9Viy5opNWU.output.variable_workflow_02Q2Q77XBG9322NBbswlydiy7ZEAmNzlTX0$", - "variable_value_new": "Validation failed: Invalid Rule JSON format or missing required fields." - } - ] - }, - "object_type": "definition_activity" - }, - { - "unique_name": "definition_activity_02Q2ROPF58RDD0sjSO9bGp4DzjUweeagNBs", - "name": "Completed", - "title": "Terminate - Invalid JSON Format", - "type": "logic.completed", - "base_type": "activity", - "properties": { - "completion_type": "failed-completed", - "continue_on_failure": false, - "description": "Terminate workflow execution due to invalid JSON format or missing required fields in Rule input.", - "display_name": "Terminate - Invalid JSON Format", - "result_message": "Rule JSON validation failed: Invalid format or missing required fields", - "skip_execution": false - }, - "object_type": "definition_activity" - } - ] - } - ] - }, - { - "unique_name": "definition_activity_02Q2ROPFQD0MP5NtfWODy25UtjDGNrYW0hq", - "name": "Condition Block", - "title": "Validate Policy Values (Allow/Deny)", - "type": "logic.if_else", - "base_type": "activity", - "properties": { - "continue_on_failure": false, - "description": "Validate policy field contains only valid firewall values: 'allow' or 'deny'. Invalid values cause workflow failure with error details.", - "display_name": "Validate Policy Values (Allow/Deny)", - "skip_execution": false - }, - "object_type": "definition_activity", - "blocks": [ - { - "unique_name": "definition_activity_02Q2ROPGDCJ174fSZlvWUnNzi7auvuxBMET", - "name": "Condition Branch", - "title": "Policy Value Not Allow/Deny", - "type": "logic.condition_block", - "base_type": "activity", - "properties": { - "condition": { - "left_operand": "$activity.definition_activity_02Q2Q77ZB7LYU259f0SCVlJBZfjGrUaXvEG.output.jsonpath_queries.policy$", - "operator": "mregex", - "right_operand": "^(?!allow$|deny$).*" - }, - "continue_on_failure": false, - "description": "Triggered when policy field contains invalid values. Only 'allow' and 'deny' are permitted for Meraki firewall rules.", - "display_name": "Policy Value Not Allow/Deny", - "skip_execution": false - }, - "object_type": "definition_activity", - "actions": [ - { - "unique_name": "definition_activity_02Q2ROPHD68UM2MuwS8gNyZOmq30fRAqwoi", - "name": "Set Variables", - "title": "Set Policy Validation Error Details", - "type": "core.set_multiple_variables", - "base_type": "activity", - "properties": { - "continue_on_failure": false, - "description": "Populate output variables with detailed policy validation error info including invalid value and expected values.", - "display_name": "Set Policy Validation Error Details", - "skip_execution": false, - "variables_to_update": [ - { - "variable_to_update": "$workflow.definition_workflow_02Q2Q77WT8QTE4UkIuKUVeySa9Viy5opNWU.output.variable_workflow_02Q2Q77XBFIRE6QjQkXlVEVTnhSUvp49dFB$", - "variable_value_new": "Invalid policy value: '$activity.definition_activity_02Q2Q77ZB7LYU259f0SCVlJBZfjGrUaXvEG.output.jsonpath_queries.policy$'. Must be 'allow' or 'deny'." - }, - { - "variable_to_update": "$workflow.definition_workflow_02Q2Q77WT8QTE4UkIuKUVeySa9Viy5opNWU.output.variable_workflow_02Q2Q77XBFP4H7gfjwt0uyrKj8h9WQVEUQm$", - "variable_value_new": 400 - }, - { - "variable_to_update": "$workflow.definition_workflow_02Q2Q77WT8QTE4UkIuKUVeySa9Viy5opNWU.output.variable_workflow_02Q2Q77XBFC8H025dryrtHYnRJPx1un4EfB$", - "variable_value_new": "Validation Error: Invalid policy value" - }, - { - "variable_to_update": "$workflow.definition_workflow_02Q2Q77WT8QTE4UkIuKUVeySa9Viy5opNWU.output.variable_workflow_02Q2Q77XBG9322NBbswlydiy7ZEAmNzlTX0$", - "variable_value_new": "Validation failed: Invalid policy value in Rule input. Expected 'allow' or 'deny'." - } - ] - }, - "object_type": "definition_activity" - }, - { - "unique_name": "definition_activity_02Q2ROPI5T6467Yhe0ppqwtt53CJcawngSs", - "name": "Completed", - "title": "Terminate - Invalid Policy Value", - "type": "logic.completed", - "base_type": "activity", - "properties": { - "completion_type": "failed-completed", - "continue_on_failure": false, - "description": "Terminate workflow execution due to invalid policy value in Rule input. Completes with failure status and detailed error info.", - "display_name": "Terminate - Invalid Policy Value", - "result_message": "Policy validation failed: '$activity.definition_activity_02Q2Q77ZB7LYU259f0SCVlJBZfjGrUaXvEG.output.jsonpath_queries.policy$' is not a valid policy value", - "skip_execution": false - }, - "object_type": "definition_activity" - } - ] - } - ] - } - ] - }, - { - "unique_name": "definition_activity_02Q2Q8UZC4U2B23Y8N5JEgL2bSMSXmATZA0", - "name": "Group", - "title": "Firewall Rules Processing", - "type": "logic.group", - "base_type": "activity", - "properties": { - "continue_on_failure": false, - "description": "Main processing group that retrieves current firewall rules, modifies them based on Add/Remove input, and updates the network configuration.", - "display_name": "Firewall Rules Processing", - "skip_execution": false - }, - "object_type": "definition_activity", - "actions": [ - { - "unique_name": "definition_activity_02Q2Q78SQ9K1F74GEslWWM4grvtwqaSkPgS", - "name": "Meraki - Get Network Appliance Firewall L3 Firewall Rules", - "title": "Meraki - Get Network Appliance Firewall L3 Firewall Rules", - "type": "workflow.atomic_workflow", - "base_type": "subworkflow", - "properties": { - "continue_on_failure": true, - "description": "API endpoint is used to retrieve the Layer 3 (L3) firewall rules configured on a network appliance. Layer 3 firewall rules control traffic based on IP addresses, protocols, and ports, allowing administrators to manage traffic flows and secure the network.", - "display_name": "Meraki - Get Network Appliance Firewall L3 Firewall Rules", - "input": { - "variable_workflow_02MN4XD4FIP4W3FWhabwFykP3DiQaZHmfTR": "$workflow.definition_workflow_02Q2Q77WT8QTE4UkIuKUVeySa9Viy5opNWU.input.variable_workflow_02Q2QBASBNJPU4Jb0bAtz75rsEjBfxWl1zC$" - }, - "runtime_user": { - "target_default": true - }, - "skip_execution": false, - "target": { - "target_type": "meraki.endpoint", - "use_workflow_target": true - }, - "workflow_id": "definition_workflow_02MN4XD4FINLC4h2i95QOdYECcfArPzx1eY", - "workflow_name": "Meraki - Get Network Appliance Firewall L3 Firewall Rules" - }, - "object_type": "definition_activity" - }, - { - "unique_name": "definition_activity_02Q2ROPL8N11N31v9pX6ZYgIkG2awCfZHl3", - "name": "Condition Block", - "title": "Handle GET Firewall Rules Failure", - "type": "logic.if_else", - "base_type": "activity", - "properties": { - "continue_on_failure": false, - "description": "Check if GET firewall rules API failed and handle errors gracefully by setting output variables and terminating execution.", - "display_name": "Handle GET Firewall Rules Failure", - "skip_execution": false - }, - "object_type": "definition_activity", - "blocks": [ - { - "unique_name": "definition_activity_02Q2ROPLUOOLF1nU43WStZiBIunZTq5BEz7", - "name": "Condition Branch", - "title": "GET API Operation Failed", - "type": "logic.condition_block", - "base_type": "activity", - "properties": { - "condition": { - "left_operand": "$activity.definition_activity_02Q2Q78SQ9K1F74GEslWWM4grvtwqaSkPgS.output.succeeded$", - "operator": "eq", - "right_operand": false - }, - "continue_on_failure": false, - "description": "Triggered when GET firewall rules API fails due to network issues, authentication problems, invalid Network ID, or API errors.", - "display_name": "GET API Operation Failed", - "skip_execution": false - }, - "object_type": "definition_activity", - "actions": [ - { - "unique_name": "definition_activity_02Q2ROPN2DMDO1r53CLaUG1gxNRIUpUEghK", - "name": "Set Variables", - "title": "Set GET Failure Error Details", - "type": "core.set_multiple_variables", - "base_type": "activity", - "properties": { - "continue_on_failure": false, - "description": "Populate output variables with comprehensive error info from failed GET operation including Network ID, error message, and HTTP status.", - "display_name": "Set GET Failure Error Details", - "skip_execution": false, - "variables_to_update": [ - { - "variable_to_update": "$workflow.definition_workflow_02Q2Q77WT8QTE4UkIuKUVeySa9Viy5opNWU.output.variable_workflow_02Q2Q77XBFIRE6QjQkXlVEVTnhSUvp49dFB$", - "variable_value_new": "Failed to retrieve firewall rules for Network ID: $workflow.definition_workflow_02Q2Q77WT8QTE4UkIuKUVeySa9Viy5opNWU.input.variable_workflow_02Q2QBASBNJPU4Jb0bAtz75rsEjBfxWl1zC$. Error: $activity.definition_activity_02Q2Q78SQ9K1F74GEslWWM4grvtwqaSkPgS.output.error.message$" - }, - { - "variable_to_update": "$workflow.definition_workflow_02Q2Q77WT8QTE4UkIuKUVeySa9Viy5opNWU.output.variable_workflow_02Q2Q77XBFP4H7gfjwt0uyrKj8h9WQVEUQm$", - "variable_value_new": "$activity.definition_activity_02Q2Q78SQ9K1F74GEslWWM4grvtwqaSkPgS.output.variable_workflow_02MN4XD4FIQOG0udjI82Gn6l19oPOym5CTt$" - }, - { - "variable_to_update": "$workflow.definition_workflow_02Q2Q77WT8QTE4UkIuKUVeySa9Viy5opNWU.output.variable_workflow_02Q2Q77XBFC8H025dryrtHYnRJPx1un4EfB$", - "variable_value_new": "Error: GET operation failed" - }, - { - "variable_to_update": "$workflow.definition_workflow_02Q2Q77WT8QTE4UkIuKUVeySa9Viy5opNWU.output.variable_workflow_02Q2Q77XBG9322NBbswlydiy7ZEAmNzlTX0$", - "variable_value_new": "Failed to retrieve or modify firewall rules due to GET operation failure." - } - ] - }, - "object_type": "definition_activity" - }, - { - "unique_name": "definition_activity_02Q2ROPO3VBO62oFYc1RUo0hxo4eSOh6pED", - "name": "Completed", - "title": "Terminate - GET Operation Failed", - "type": "logic.completed", - "base_type": "activity", - "properties": { - "completion_type": "succeeded", - "continue_on_failure": false, - "description": "Gracefully terminate workflow when GET firewall rules fails. Completes with success status but comprehensive error details in outputs.", - "display_name": "Terminate - GET Operation Failed", - "result_message": "Workflow completed with GET error: $activity.definition_activity_02Q2Q78SQ9K1F74GEslWWM4grvtwqaSkPgS.output.error.message$", - "skip_execution": false - }, - "object_type": "definition_activity" - } - ] - } - ] - }, - { - "unique_name": "definition_activity_02Q2Q78U5XHWB0bAe6yWQZWyl3qO6LgFkPw", - "name": "JSONPath Query", - "title": "Extract Rules Array from Response", - "type": "corejava.jsonpathquery", - "base_type": "activity", - "properties": { - "action_timeout": 180, - "continue_on_failure": false, - "description": "Extract the 'rules' array from the GET firewall rules API response to prepare for rule modification.", - "display_name": "Extract Rules Array from Response", - "input_json": "$activity.definition_activity_02Q2Q78SQ9K1F74GEslWWM4grvtwqaSkPgS.output.variable_workflow_02MN4XD4FIS801gUrtzo8CEOhwoP6kzmgvY$", - "jsonpath_queries": [ - { - "jsonpath_query": "$.rules", - "jsonpath_query_name": "rules", - "jsonpath_query_type": "string" - } - ], - "skip_execution": false - }, - "object_type": "definition_activity" - }, - { - "unique_name": "definition_activity_02Q2Q78VGCHXX73AyhxI9mrF6jQtpZEEOHQ", - "name": "Execute Python Script", - "title": "Add or Remove Firewall Rule", - "type": "python3.script", - "base_type": "activity", - "properties": { - "action_timeout": 180, - "continue_on_failure": false, - "description": "Process firewall rules based on Add/Remove boolean and Position. Add: check for an existing rule with matching traffic key fields, then insert at the requested position if no duplicate exists. Remove: delete rules that match those same traffic key fields; position is ignored.", - "display_name": "Add or Remove Firewall Rule", - "script": "from datetime import datetime, timezone\nimport sys, json\n\nexistingRules = sys.argv[1] # This is already the rules array, not full response\nruleInput = sys.argv[2]\naddOrRemove = sys.argv[3].lower() == 'true' # true = add, false = remove\nposition = int(sys.argv[4]) # position to insert rule\norganizationId = sys.argv[5]\norganizationName = sys.argv[6]\nnetworkId = sys.argv[7]\nnetworkName = sys.argv[8]\n\n# Parse inputs - existingRules is already just the rules array\nrulesArray = json.loads(existingRules)\nnewRule = json.loads(ruleInput)\n\n# Filter out the default rule from existing rules (API update should not include it)\n# Default rule format: comment=\"Default rule\", policy=\"allow\", protocol=\"Any/any\", all ports/CIDRs=\"Any/any\"\nuser_rules = []\nfor rule in rulesArray:\n is_default_rule = (\n rule.get('comment', '') == 'Default rule' and\n rule.get('policy', '').lower() == 'allow' and\n rule.get('protocol', '').lower() == 'any' and\n rule.get('destCidr', '').lower() == 'any' and\n rule.get('srcCidr', '').lower() == 'any' and\n rule.get('destPort', '').lower() == 'any' and\n rule.get('srcPort', '').lower() == 'any'\n )\n if not is_default_rule:\n user_rules.append(rule)\n\n\ndef build_backup_snapshot():\n backup_payload = {\n 'organizationId': organizationId,\n 'organizationName': organizationName,\n 'networkId': networkId,\n 'networkName': networkName,\n 'operation': 'add' if addOrRemove else 'remove',\n 'timestamp': datetime.now(timezone.utc).isoformat().replace('+00:00', 'Z'),\n 'currentFirewallRules': rulesArray,\n 'requestedRulePayload': newRule,\n }\n return json.dumps(backup_payload, separators=(',', ':'))\n\nbackupJson = ''\n\nif addOrRemove:\n # Add rule - first check for duplicates against ALL existing rules (including default), then insert at specified position\n # Use comment as provided in input (or empty string if not provided)\n if 'comment' not in newRule:\n newRule['comment'] = \"\"\n\n # Define the key fields to check for duplicates (exclude comment and syslogEnabled)\n key_fields = ['policy', 'protocol', 'destPort', 'destCidr', 'srcPort', 'srcCidr']\n\n # Check if a rule with the same key fields already exists in ALL rules (including default rule)\n # Use case-insensitive comparison for field values\n duplicate_found = False\n for existing_rule in rulesArray: # Check against ALL rules, not just user_rules\n if all(existing_rule.get(field, '').lower() == newRule.get(field, '').lower() for field in key_fields):\n duplicate_found = True\n break\n\n if duplicate_found:\n # Don't add duplicate rule\n result = f\"Duplicate rule detected.\"\n newRules = json.dumps(user_rules) # Return unchanged user rules (no default rule)\n skipUpdate = True\n else:\n backupJson = build_backup_snapshot()\n # No duplicate found, proceed with adding the rule\n # Ensure position is within valid range for user rules\n max_position = len(user_rules)\n if position > max_position:\n position = max_position # Insert at end if position too high\n elif position < 0:\n position = 0 # Insert at beginning if position negative\n\n user_rules.insert(position, newRule)\n result = f\"Rule added successfully at position {position} (no duplicate found)\"\n newRules = json.dumps(user_rules) # Return user rules without default rule\n skipUpdate = False\nelse:\n # Remove rule - find and remove matching rules by comparing key fields among user rules\n original_count = len(user_rules)\n\n # Define the key fields to match for rule identification\n key_fields = ['policy', 'protocol', 'destPort', 'destCidr', 'srcPort', 'srcCidr']\n\n # Filter out rules that match ALL key fields of the input rule (case-insensitive)\n filtered_rules = [rule for rule in user_rules\n if not all(rule.get(field, '').lower() == newRule.get(field, '').lower() for field in key_fields)]\n\n user_rules = filtered_rules\n removed_count = original_count - len(user_rules)\n\n if removed_count == 0:\n result = f\"No matching rule found to remove\"\n skipUpdate = True # No changes to make\n elif removed_count == 1:\n backupJson = build_backup_snapshot()\n result = f\"Successfully removed 1 matching rule\"\n skipUpdate = False # Changes made, need to update\n else:\n # This shouldn't happen in normal scenarios but handle it defensively\n backupJson = build_backup_snapshot()\n result = f\"Warning: Removed {removed_count} identical rules (unexpected)\"\n skipUpdate = False # Changes made, need to update\n\n # Output user rules array without default rule\n newRules = json.dumps(user_rules)\n\nprint(newRules)\n", - "script_arguments": [ - "$activity.definition_activity_02Q2Q78U5XHWB0bAe6yWQZWyl3qO6LgFkPw.output.jsonpath_queries.rules$", - "$workflow.definition_workflow_02Q2Q77WT8QTE4UkIuKUVeySa9Viy5opNWU.input.variable_workflow_02Q2QE6HSDSEX1fzaLABhotUMJPPr6qdkNX$", - "$workflow.definition_workflow_02Q2Q77WT8QTE4UkIuKUVeySa9Viy5opNWU.input.variable_workflow_02Q2QEL1AO2C12qBxR6feTHxGJnyV0DnqKJ$", - "$workflow.definition_workflow_02Q2Q77WT8QTE4UkIuKUVeySa9Viy5opNWU.input.variable_workflow_02Q2RX032F5FC0xIG13ihAQLIaoxWflVFkr$", - "$workflow.definition_workflow_02Q2Q77WT8QTE4UkIuKUVeySa9Viy5opNWU.input.variable_workflow_02Q2FWBKORGID001$", - "$workflow.definition_workflow_02Q2Q77WT8QTE4UkIuKUVeySa9Viy5opNWU.input.variable_workflow_02Q2FWBKORGNAME01$", - "$workflow.definition_workflow_02Q2Q77WT8QTE4UkIuKUVeySa9Viy5opNWU.input.variable_workflow_02Q2QBASBNJPU4Jb0bAtz75rsEjBfxWl1zC$", - "$workflow.definition_workflow_02Q2Q77WT8QTE4UkIuKUVeySa9Viy5opNWU.input.variable_workflow_02Q2FWBKNETNAME01$" - ], - "script_queries": [ - { - "script_query": "newRules", - "script_query_name": "newRules", - "script_query_type": "string" - }, - { - "script_query": "result", - "script_query_name": "operationResult", - "script_query_type": "string" - }, - { - "script_query": "skipUpdate", - "script_query_name": "skipUpdate", - "script_query_type": "boolean" - }, - { - "script_query": "backupJson", - "script_query_name": "firewallRulesBackup", - "script_query_type": "string" - } - ], - "skip_execution": false - }, - "object_type": "definition_activity" - }, - { - "unique_name": "definition_activity_02Q2Q7CHB8K1E3dxs6m7n9QCDFhHj2LaP89", - "name": "Condition Block", - "title": "Should Update Firewall Rules?", - "type": "logic.if_else", - "base_type": "activity", - "properties": { - "continue_on_failure": false, - "description": "Check if firewall rules should be updated based on duplicate detection results. Skip API call if no changes are needed.", - "display_name": "Should Update Firewall Rules?", - "skip_execution": false - }, - "object_type": "definition_activity", - "blocks": [ - { - "unique_name": "definition_activity_02Q2Q7CHI5T4F2wn8k1x0N9bCzRgHm3VqPe", - "name": "Condition Branch", - "title": "Yes - Update Required", - "type": "logic.condition_block", - "base_type": "activity", - "properties": { - "condition": { - "left_operand": "$activity.definition_activity_02Q2Q78VGCHXX73AyhxI9mrF6jQtpZEEOHQ.output.script_queries.skipUpdate$", - "operator": "eq", - "right_operand": false - }, - "continue_on_failure": false, - "description": "Proceed with Meraki API update when changes were made to the rules array.", - "display_name": "Yes - Update Required", - "skip_execution": false - }, - "object_type": "definition_activity", - "actions": [ - { - "unique_name": "definition_activity_02Q2Q78XAX8TO45HOyCGANT6Xz0IjI86LZo", - "name": "Meraki - Update Network Appliance Firewall L3 Firewall Rules", - "title": "Meraki - Update Network Appliance Firewall L3 Firewall Rules", - "type": "workflow.atomic_workflow", - "base_type": "subworkflow", - "properties": { - "continue_on_failure": false, - "description": "Update the L3 firewall rules of an MX network", - "display_name": "Meraki - Update Network Appliance Firewall L3 Firewall Rules", - "input": { - "variable_workflow_02OARCAWCPEKW6FIUW4231asrsW5zdZSuRt": "$workflow.definition_workflow_02Q2Q77WT8QTE4UkIuKUVeySa9Viy5opNWU.input.variable_workflow_02Q2QBASBNJPU4Jb0bAtz75rsEjBfxWl1zC$", - "variable_workflow_02OARCAWCPFCO3SCyjYumx4q9hDvyXUUjZd": "$activity.definition_activity_02Q2Q78VGCHXX73AyhxI9mrF6jQtpZEEOHQ.output.script_queries.newRules$", - "variable_workflow_02OARCAWCPG4G4L7GFWJjCfqcwghsizomSK": "" - }, - "runtime_user": { - "target_default": true - }, - "skip_execution": false, - "target": { - "target_type": "meraki.endpoint", - "use_workflow_target": true - }, - "workflow_id": "definition_workflow_02OARCAWCPC9K1Xa28TIaQHu4JXVyBcjJP8", - "workflow_name": "Meraki - Update Network Appliance Firewall L3 Firewall Rules" - }, - "object_type": "definition_activity" - }, - { - "unique_name": "definition_activity_02Q3MBHT8OF4R7CMKUDSkjJ432YYzKXOYl8", - "name": "Set Variables", - "title": "Set Success Output Variables", - "type": "core.set_multiple_variables", - "base_type": "activity", - "properties": { - "continue_on_failure": false, - "description": "Set final output variables with comprehensive results including network info, operation details, and success status.", - "display_name": "Set Success Output Variables", - "skip_execution": false, - "variables_to_update": [ - { - "variable_to_update": "$workflow.definition_workflow_02Q2Q77WT8QTE4UkIuKUVeySa9Viy5opNWU.output.variable_workflow_02Q2Q77XBG9322NBbswlydiy7ZEAmNzlTX0$", - "variable_value_new": "Network: $workflow.definition_workflow_02Q2Q77WT8QTE4UkIuKUVeySa9Viy5opNWU.input.variable_workflow_02Q2QBASBNJPU4Jb0bAtz75rsEjBfxWl1zC$ - Firewall rule operation completed successfully. Operation: $activity.definition_activity_02Q2Q78VGCHXX73AyhxI9mrF6jQtpZEEOHQ.output.script_queries.operationResult$" - }, - { - "variable_to_update": "$workflow.definition_workflow_02Q2Q77WT8QTE4UkIuKUVeySa9Viy5opNWU.output.variable_workflow_02Q2Q77XBFC8H025dryrtHYnRJPx1un4EfB$", - "variable_value_new": "Firewall updated successfully." - }, - { - "variable_to_update": "$workflow.definition_workflow_02Q2Q77WT8QTE4UkIuKUVeySa9Viy5opNWU.output.variable_workflow_02Q2Q77XBFP4H7gfjwt0uyrKj8h9WQVEUQm$", - "variable_value_new": "200" - }, - { - "variable_to_update": "$workflow.definition_workflow_02Q2Q77WT8QTE4UkIuKUVeySa9Viy5opNWU.output.variable_workflow_02Q2Q77XBFIRE6QjQkXlVEVTnhSUvp49dFB$", - "variable_value_new": "" - }, - { - "variable_to_update": "$workflow.definition_workflow_02Q2Q77WT8QTE4UkIuKUVeySa9Viy5opNWU.output.variable_workflow_02Q2FWBACKUPOUT01$", - "variable_value_new": "$activity.definition_activity_02Q2Q78VGCHXX73AyhxI9mrF6jQtpZEEOHQ.output.script_queries.firewallRulesBackup$" - } - ] - }, - "object_type": "definition_activity" - } - ] - }, - { - "unique_name": "definition_activity_02Q2Q7CHNB89D5mzL3x0r8QFGkDhJ2VqP14", - "name": "Condition Branch", - "title": "No - Skip Update", - "type": "logic.condition_block", - "base_type": "activity", - "properties": { - "condition": { - "left_operand": "$activity.definition_activity_02Q2Q78VGCHXX73AyhxI9mrF6jQtpZEEOHQ.output.script_queries.skipUpdate$", - "operator": "eq", - "right_operand": true - }, - "continue_on_failure": false, - "description": "Skip Meraki API update when no changes are needed (duplicate detected or no matching rule to remove).", - "display_name": "No - Skip Update", - "skip_execution": false - }, - "object_type": "definition_activity", - "actions": [ - { - "unique_name": "definition_activity_02Q2Q78ZBEB4M7iXt72oUOH4ZAiakbfftbV", - "name": "Set Variables", - "title": "Set Error Variables", - "type": "core.set_multiple_variables", - "base_type": "activity", - "properties": { - "continue_on_failure": false, - "description": "Set final output variables with comprehensive results including network info, operation details, and success status.", - "display_name": "Set Error Variables", - "skip_execution": false, - "variables_to_update": [ - { - "variable_to_update": "$workflow.definition_workflow_02Q2Q77WT8QTE4UkIuKUVeySa9Viy5opNWU.output.variable_workflow_02Q2Q77XBG9322NBbswlydiy7ZEAmNzlTX0$", - "variable_value_new": "Network: $workflow.definition_workflow_02Q2Q77WT8QTE4UkIuKUVeySa9Viy5opNWU.input.variable_workflow_02Q2QBASBNJPU4Jb0bAtz75rsEjBfxWl1zC$ - Firewall rule skipped.$activity.definition_activity_02Q2Q78VGCHXX73AyhxI9mrF6jQtpZEEOHQ.output.script_queries.operationResult$" - }, - { - "variable_to_update": "$workflow.definition_workflow_02Q2Q77WT8QTE4UkIuKUVeySa9Viy5opNWU.output.variable_workflow_02Q2Q77XBFC8H025dryrtHYnRJPx1un4EfB$", - "variable_value_new": "Firewall rule skipped" - }, - { - "variable_to_update": "$workflow.definition_workflow_02Q2Q77WT8QTE4UkIuKUVeySa9Viy5opNWU.output.variable_workflow_02Q2Q77XBFP4H7gfjwt0uyrKj8h9WQVEUQm$", - "variable_value_new": 409 - }, - { - "variable_to_update": "$workflow.definition_workflow_02Q2Q77WT8QTE4UkIuKUVeySa9Viy5opNWU.output.variable_workflow_02Q2Q77XBFIRE6QjQkXlVEVTnhSUvp49dFB$", - "variable_value_new": "$activity.definition_activity_02Q2Q78VGCHXX73AyhxI9mrF6jQtpZEEOHQ.output.script_queries.operationResult$" - } - ] - }, - "object_type": "definition_activity" - } - ] - } - ] - } - ] - } - ], - "categories": [ - "category_1BMfMXSnJMyt5Ihqi7rWJr5N8cf" - ] - }, - "atomic_workflows": [ - "definition_workflow_02MN4XD4FINLC4h2i95QOdYECcfArPzx1eY", - "definition_workflow_02OARCAWCPC9K1Xa28TIaQHu4JXVyBcjJP8" - ], - "dependent_workflows": [ - "definition_workflow_02MN4XD4FINLC4h2i95QOdYECcfArPzx1eY", - "definition_workflow_02OARCAWCPC9K1Xa28TIaQHu4JXVyBcjJP8" - ] - } - ], - "atomic_workflows": [ - "definition_workflow_02LV5GIDUTY3X0WhuidM5ZWhkjL8wQCGAPq", - "definition_workflow_02L9TZH5W95870XebPF2g8kSyIYcawUvljX" - ], - "dependent_workflows": [ - "definition_workflow_02LV5GIDUTY3X0WhuidM5ZWhkjL8wQCGAPq", - "definition_workflow_02L9TZH5W95870XebPF2g8kSyIYcawUvljX", - "definition_workflow_02Q2Q77WT8QTE4UkIuKUVeySa9Viy5opNWU" - ] -} From 5e3d2a3729c74bbb6b0c61f9521c035dbcd4fa18 Mon Sep 17 00:00:00 2001 From: Christy Jones Date: Sun, 14 Jun 2026 16:02:58 -0400 Subject: [PATCH 5/6] rename toolkit docs to exchange naming Co-authored-by: Cursor --- README.md | 2 +- Toolkit/README.md | 10 +-- Toolkit/cli/README.md | 8 +- Toolkit/cursor/README.md | 6 +- .../SKILL.md | 4 +- .../reference.md | 3 +- .../SKILL.md | 13 +-- .../reference.md | 4 +- Toolkit/examples/README.md | 2 +- Toolkit/examples/review-output.md | 27 +++--- .../README.md | 2 +- .../reference.md | 4 +- Toolkit/{review => exchange-review}/README.md | 16 ++-- .../{review => exchange-review}/reference.md | 8 +- Toolkit/mcp/README.md | 2 +- WorkflowsSubmissionProcess.md | 90 +++---------------- 16 files changed, 72 insertions(+), 129 deletions(-) rename Toolkit/cursor/{workflow-review-remediation => exchange-remediation}/SKILL.md (96%) rename Toolkit/cursor/{workflow-review-remediation => exchange-remediation}/reference.md (75%) rename Toolkit/cursor/{exchange-workflow-review => exchange-review}/SKILL.md (69%) rename Toolkit/cursor/{exchange-workflow-review => exchange-review}/reference.md (73%) rename Toolkit/{remediation => exchange-remediation}/README.md (97%) rename Toolkit/{remediation => exchange-remediation}/reference.md (94%) rename Toolkit/{review => exchange-review}/README.md (71%) rename Toolkit/{review => exchange-review}/reference.md (75%) diff --git a/README.md b/README.md index f451d6e..3df0e87 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ The `Toolkit/` area provides reusable helpers for the workflow review checklist - solution engineers or contributors using another LLM - external contributors who want a documented CLI and MCP-based path -See `Toolkit/README.md` for the audience guide and `WorkflowReviewChecklist.md` for the canonical review contract. +See `Toolkit/README.md` for the audience guide and `WorkflowReviewChecklist.md` for the canonical review contract. The toolkit’s review and remediation guides now live under `Toolkit/exchange-review/` and `Toolkit/exchange-remediation/` so the folder naming matches the installable Cursor skills. ## Contributing diff --git a/Toolkit/README.md b/Toolkit/README.md index 4195de2..f0b1de4 100644 --- a/Toolkit/README.md +++ b/Toolkit/README.md @@ -2,7 +2,7 @@ This toolkit turns the internal workflow review standard into reusable review and remediation paths for the team. -The packaged internal review standard is the default source for review and remediation helpers. Everything in `Toolkit/` is designed to help contributors apply that standard more consistently, while still allowing repo-based development overrides when needed. +`WorkflowReviewChecklist.md` at the repository root is the canonical review contract. The CLI ships a packaged fallback copy for installed environments, but everything in `Toolkit/` should reference the root checklist first so the source of truth stays in one place. ## Who It Is For @@ -12,14 +12,14 @@ Use `Toolkit/cursor/` to install thin Cursor skill wrappers that call the shared ### Other LLM users -Use `Toolkit/review/` and `Toolkit/remediation/` as copyable playbooks, then run the CLI in `Toolkit/cli/` for deterministic enumeration and checklist resolution. +Use `Toolkit/exchange-review/` and `Toolkit/exchange-remediation/` as copyable playbooks, then run the CLI in `Toolkit/cli/` for deterministic enumeration and checklist resolution. ### External contributors Start with: 1. `WorkflowReviewChecklist.md` -2. `Toolkit/review/README.md` +2. `Toolkit/exchange-review/README.md` 3. `Toolkit/cli/README.md` That path does not require Cursor. @@ -33,8 +33,8 @@ Toolkit/ ├── cursor/ # Layer 2: thin Cursor skill wrappers ├── examples/ # Example commands and sample output ├── mcp/ # Layer 3: stdio MCP guidance -├── remediation/ # Public remediation guidance and mode reference -└── review/ # Public review guidance and checklist companion +├── exchange-remediation/ # Public remediation guidance and mode reference +└── exchange-review/ # Public review guidance and checklist companion ``` ## Layering diff --git a/Toolkit/cli/README.md b/Toolkit/cli/README.md index 42270de..30a05a0 100644 --- a/Toolkit/cli/README.md +++ b/Toolkit/cli/README.md @@ -48,7 +48,7 @@ PYTHONPATH=src python3 -m workflow_review checklist PYTHONPATH=src python3 -m workflow_review checklist --show ``` -When installed, the CLI resolves the packaged internal review standard automatically if neither `--checklist` nor `WORKFLOW_REVIEW_CHECKLIST` is set. +When run from this repository, the CLI resolves `WorkflowReviewChecklist.md` at the repo root first. If the repo-root file is not available, it falls back to the packaged checklist that ships with the installed package. ### Prepare a review run @@ -71,10 +71,10 @@ The CLI resolves the checklist in this order: 1. `--checklist /path/to/file.md` 2. `WORKFLOW_REVIEW_CHECKLIST=/path/to/file.md` -3. the packaged internal review standard -4. the repo-root `WorkflowReviewChecklist.md` +3. the repo-root `WorkflowReviewChecklist.md` +4. the packaged internal review standard -That keeps the packaged internal standard available by default while still allowing the team to point at another checked-in copy during development. +That keeps the repository copy canonical while still allowing the team to point at another checklist during development or use the packaged fallback when installed elsewhere. ## MCP diff --git a/Toolkit/cursor/README.md b/Toolkit/cursor/README.md index b68b2bd..be52b03 100644 --- a/Toolkit/cursor/README.md +++ b/Toolkit/cursor/README.md @@ -12,12 +12,12 @@ From the repository root: bash Toolkit/cursor/install_cursor_skills.sh ``` -This installs the public workflow-review skills into `~/.cursor/skills`. +This installs the public `exchange-review` and `exchange-remediation` skills into `~/.cursor/skills`. ## Included wrappers -- `exchange-workflow-review` -- `workflow-review-remediation` +- `exchange-review` +- `exchange-remediation` ## Design rule diff --git a/Toolkit/cursor/workflow-review-remediation/SKILL.md b/Toolkit/cursor/exchange-remediation/SKILL.md similarity index 96% rename from Toolkit/cursor/workflow-review-remediation/SKILL.md rename to Toolkit/cursor/exchange-remediation/SKILL.md index 100cfb7..77903cf 100644 --- a/Toolkit/cursor/workflow-review-remediation/SKILL.md +++ b/Toolkit/cursor/exchange-remediation/SKILL.md @@ -1,10 +1,10 @@ --- -name: workflow-review-remediation +name: exchange-remediation description: Apply approved workflow review fixes safely by building a remediation plan, honoring remediation and safety modes, and preserving workflow identity unless the user explicitly approves structural changes. disable-model-invocation: true --- -# Workflow Review Remediation +# Exchange Remediation ## When to use diff --git a/Toolkit/cursor/workflow-review-remediation/reference.md b/Toolkit/cursor/exchange-remediation/reference.md similarity index 75% rename from Toolkit/cursor/workflow-review-remediation/reference.md rename to Toolkit/cursor/exchange-remediation/reference.md index a20cf7c..233f3c0 100644 --- a/Toolkit/cursor/workflow-review-remediation/reference.md +++ b/Toolkit/cursor/exchange-remediation/reference.md @@ -1,4 +1,4 @@ -# Cursor Remediation Wrapper Reference +# Exchange Remediation Wrapper Reference Use this wrapper with the shared CLI in `Toolkit/cli/`. @@ -15,3 +15,4 @@ PYTHONPATH="/Toolkit/cli/src" python3 -m workflow_review plan-remedia - Stay inside the chosen remediation scope. - Treat branch, output, target, and category changes as potentially major. - Re-review after edits. +- Keep `WorkflowReviewChecklist.md` at the repository root as the canonical source for the checklist, with the packaged copy as the installed fallback. diff --git a/Toolkit/cursor/exchange-workflow-review/SKILL.md b/Toolkit/cursor/exchange-review/SKILL.md similarity index 69% rename from Toolkit/cursor/exchange-workflow-review/SKILL.md rename to Toolkit/cursor/exchange-review/SKILL.md index 994ce2d..86a706d 100644 --- a/Toolkit/cursor/exchange-workflow-review/SKILL.md +++ b/Toolkit/cursor/exchange-review/SKILL.md @@ -1,15 +1,15 @@ --- -name: exchange-workflow-review -description: Review exported Cisco workflow JSON against the internal workflow review standard, enumerate the export first, and produce severity-ranked findings plus remediation suggestions and an overall readiness assessment. +name: exchange-review +description: Review exported Cisco workflow JSON against the canonical workflow review standard, enumerate the export first, and produce severity-ranked findings plus remediation suggestions and an overall readiness assessment. --- -# Exchange Workflow Review +# Exchange Review ## When to use - The user provides a workflow export JSON file. - The user wants Exchange review, standards validation, or approval-readiness feedback. -- The user wants findings aligned to the internal workflow review standard. +- The user wants findings aligned to the canonical workflow review standard. ## Start here prompts @@ -23,7 +23,7 @@ description: Review exported Cisco workflow JSON against the internal workflow r ## Required flow -1. Use the packaged internal review standard when available. Fall back to the repo-root `WorkflowReviewChecklist.md` only for team development overrides. +1. Use `WorkflowReviewChecklist.md` from the repository root when this repo is checked out. Fall back to the packaged checklist only when the toolkit is installed outside the repo. 2. Resolve the real skill directory and the repo root from this skill's file location. Do not assume the current working directory is the repo. 3. Run the shared CLI first to review the export and establish scope: - The review flow should enumerate first and return remediation suggestions in the first pass. @@ -39,7 +39,7 @@ description: Review exported Cisco workflow JSON against the internal workflow r - Do not skip embedded subworkflows. - Prefer user-facing names over raw internal IDs. - Use `reference.md` for the local review sequence and severity guidance. -- Keep the internal review standard as the source of truth if there is any conflict. +- Keep the canonical review standard as the source of truth if there is any conflict. ## Output sections @@ -50,3 +50,4 @@ description: Review exported Cisco workflow JSON against the internal workflow r - Low priority issues - Workflow-by-workflow notes - Overall assessment +- Remediation suggestions diff --git a/Toolkit/cursor/exchange-workflow-review/reference.md b/Toolkit/cursor/exchange-review/reference.md similarity index 73% rename from Toolkit/cursor/exchange-workflow-review/reference.md rename to Toolkit/cursor/exchange-review/reference.md index d46980b..60735cd 100644 --- a/Toolkit/cursor/exchange-workflow-review/reference.md +++ b/Toolkit/cursor/exchange-review/reference.md @@ -1,4 +1,4 @@ -# Cursor Review Wrapper Reference +# Exchange Review Wrapper Reference Use this wrapper with the shared CLI in `Toolkit/cli/`. @@ -16,4 +16,4 @@ PYTHONPATH="/Toolkit/cli/src" python3 -m workflow_review prepare-revi - Cover all embedded workflows. - Lead with findings. - Include remediation suggestions in the first pass output. -- Keep the packaged internal review standard canonical, with repo-root overrides only for team development. +- Keep `WorkflowReviewChecklist.md` at the repository root canonical, with the packaged checklist as the fallback for installed environments. diff --git a/Toolkit/examples/README.md b/Toolkit/examples/README.md index e244327..2f83d29 100644 --- a/Toolkit/examples/README.md +++ b/Toolkit/examples/README.md @@ -22,4 +22,4 @@ PYTHONPATH=src python3 -m workflow_review plan-remediation "../../Meraki/CheckAv ## Sample output -See `review-output.md` for the intended structure of a reviewer-facing summary. +See `review-output.md` for the intended structure of a reviewer-facing summary. The actual review contract lives in `WorkflowReviewChecklist.md` at the repository root. diff --git a/Toolkit/examples/review-output.md b/Toolkit/examples/review-output.md index 8fa3ae0..b629a5d 100644 --- a/Toolkit/examples/review-output.md +++ b/Toolkit/examples/review-output.md @@ -1,30 +1,37 @@ # Sample Review Output ```markdown -## Workflow Review Results +## Exchange Review Results ### Enumeration - Workflow 1: Parent Workflow - Workflow 2: Embedded Workflow -### High Priority Issues +### Critical issues +- None. + +### High priority issues - Activity "Submit API Request" does not surface failure details in the workflow outputs. -### Low Priority Issues +### Medium priority issues +- None. + +### Low priority issues - The main workflow description is too thin for Exchange reviewers. -### Workflow-by-Workflow Notes +### Workflow-by-workflow notes #### Parent Workflow -- Error Handling: Add explicit success and failure output updates. +- Logic & Flow: Add explicit success and failure output updates. #### Embedded Workflow - Essential Hygiene & Security: Replace a real org ID default with a placeholder. -### Overall Assessment +### Overall assessment - Score: 7/10 - Approval readiness: approve with suggestions -- Top improvements: - - improve output handling - - remove production-like defaults - - strengthen reviewer-facing descriptions + +### Remediation suggestions +- Improve output handling. +- Remove production-like defaults. +- Strengthen reviewer-facing descriptions. ``` diff --git a/Toolkit/remediation/README.md b/Toolkit/exchange-remediation/README.md similarity index 97% rename from Toolkit/remediation/README.md rename to Toolkit/exchange-remediation/README.md index 27b0eb9..35fc4e7 100644 --- a/Toolkit/remediation/README.md +++ b/Toolkit/exchange-remediation/README.md @@ -1,4 +1,4 @@ -# Workflow Remediation Guide +# Exchange Remediation Guide Use this guide after a review has identified accepted findings to fix. diff --git a/Toolkit/remediation/reference.md b/Toolkit/exchange-remediation/reference.md similarity index 94% rename from Toolkit/remediation/reference.md rename to Toolkit/exchange-remediation/reference.md index 0a327f5..a8ed654 100644 --- a/Toolkit/remediation/reference.md +++ b/Toolkit/exchange-remediation/reference.md @@ -1,8 +1,8 @@ -# Workflow Remediation Reference +# Exchange Remediation Reference ## Prerequisite -Use workflow review first unless you already have an approved findings list. +Use exchange review first unless you already have an approved findings list. ## Remediation modes diff --git a/Toolkit/review/README.md b/Toolkit/exchange-review/README.md similarity index 71% rename from Toolkit/review/README.md rename to Toolkit/exchange-review/README.md index 82b4d7d..e934e3b 100644 --- a/Toolkit/review/README.md +++ b/Toolkit/exchange-review/README.md @@ -1,6 +1,10 @@ -# Workflow Review Guide +# Exchange Review Guide -Use this guide when you want to review a workflow export against the internal workflow review standard. +Use this guide when you want to review a workflow export against the canonical workflow review standard. + +## Source of truth + +`WorkflowReviewChecklist.md` at the repository root is the canonical review contract. The CLI also ships a packaged copy for installed environments, but this repo should reference the root checklist first. ## Start here prompts @@ -30,14 +34,14 @@ The `prepare-review` output is intentionally structured so it can be handed to a If you are using another LLM directly: -1. Use the internal review standard used by the team -2. Attach the workflow JSON export -3. Use the reference in `reference.md` to keep the review sequence and output format consistent +1. Use `WorkflowReviewChecklist.md` from the repository root. +2. Attach the workflow JSON export. +3. Use the reference in `reference.md` to keep the review sequence and output format consistent. ## Expectations - Do not skip embedded subworkflows. -- Keep the internal review standard as the source of truth. +- Keep the canonical review standard as the source of truth. - Lead with findings, sorted by severity. - Include remediation suggestions in the first review output. - Use user-facing names instead of raw internal IDs whenever possible. diff --git a/Toolkit/review/reference.md b/Toolkit/exchange-review/reference.md similarity index 75% rename from Toolkit/review/reference.md rename to Toolkit/exchange-review/reference.md index a6d53ed..3b25ba8 100644 --- a/Toolkit/review/reference.md +++ b/Toolkit/exchange-review/reference.md @@ -1,10 +1,8 @@ -# Workflow Review Reference +# Exchange Review Reference ## Source of truth -The canonical review standard is the packaged internal review standard used by the workflow review CLI and MCP server. - -Use the packaged standard whenever it is available. This reference is the lightweight companion for tools and wrappers in `Toolkit/`, with repo-root overrides only for team development. +The canonical review standard is `WorkflowReviewChecklist.md` at the repository root. The CLI ships a packaged copy for installed environments, but checked-out work in this repo should reference the root file first. ## Mandatory sequence @@ -62,4 +60,4 @@ PYTHONPATH=src python3 -m workflow_review inspect-workflow-export "/path/to/work PYTHONPATH=src python3 -m workflow_review prepare-review "/path/to/workflow.json" --json ``` -When installed, the CLI resolves the packaged internal review standard automatically unless `--checklist` or `WORKFLOW_REVIEW_CHECKLIST` is provided. +When installed, the CLI resolves `WorkflowReviewChecklist.md` from the repository root first. If you run the toolkit outside a checkout, the packaged checklist is used as the fallback. diff --git a/Toolkit/mcp/README.md b/Toolkit/mcp/README.md index 47f9c42..bb98dec 100644 --- a/Toolkit/mcp/README.md +++ b/Toolkit/mcp/README.md @@ -55,7 +55,7 @@ If your MCP client supports stdio servers, use the installed command directly an { "mcpServers": { "cisco-workflow-review": { - "command": "/Users/christyaajones/Library/Python/3.9/bin/workflow-review-mcp", + "command": "/path/to/installed/workflow-review-mcp", "args": [], "env": {} } diff --git a/WorkflowsSubmissionProcess.md b/WorkflowsSubmissionProcess.md index bad61fe..904d4bb 100644 --- a/WorkflowsSubmissionProcess.md +++ b/WorkflowsSubmissionProcess.md @@ -1,85 +1,17 @@ - # Submission Process for Exchange Candidates -## Using "WorkflowReviewChecklist.md" with AI to review you Workflow - -A Markdown file has been created and checked into the Public Repo which contains criterial for Workflow review prior to submission. This file can be provided to your LLM of choice as well as the JSON payload representing your workflow. -https://github.com/CiscoDevNet/CiscoWorkflowsAutomation/blob/main/WorkflowReviewChecklist.md - -The public `Toolkit/` directory in this repository now provides three ways to apply that checklist: - -- `Toolkit/cli/` for command-line review helpers -- `Toolkit/cursor/` for installable Cursor wrappers -- `Toolkit/mcp/` for a thin stdio MCP path - -The LLM will apply the critera against your workflow highlighting: -- Workflow Inputs & Parameters -- Targets & Target Groups -- Atomics & API Usage -- Groups & Categories -- Logic & Flow -- Error Handling -- Essential Hygiene & Security - -Please use this Checklist with your LLM of choice. We have tested with Cisco's Circuit LLM (recommendation of Claude 4.5) however you are not -retricted in your LLM selection. The LLM will be used against the exported JSON Workflow from your Workfspace. - -Here is an example Prompt: -- "Please perform an analysis of the Cisco Workflow provided in the JSON file using the criteria checklist found in the included markdown file." - -## Understanding Analysis Results - -The analysis will break down issues into High, Medium, Low Priority (Some LLMs may describe as Critical, Medium, Low). All critical issues should be addressed before submitting for review. Designers should also do their best to address Medium and Low comments before submissions where appropriate. - -Note: Reviewers will rerun this Analysis as part of the review process. -
-
-# Uploading Workflow for Submission -Workflows are submitted to the Exchange from the Workflow Designer under "More Actions"/"Share"/"Submit to Exchange". -This will bring the submitter to the submission screens where the information below must be provided. - -Use the information below to fill out the form requirements on submission. Submitters can monitor the status from the Automation/Exchange menu where the current status is provided. - -If a submission is rejected, which is common, the comments provided will explain the reasons. At this point the develloper of the workflow can return to their Workspace, unlock their workflow, and make the necessary edits. When complete, they can submit the workflow again. - -## Integration -- Indicate the relevant domains the Workflow operates across +This legacy guide has been superseded by the public toolkit. -## Display Name -- Name of your Workflow +## Use these canonical references instead -## Author -- Your Email Address - -## Contact & Support Information -- Your Email Address or possibly a Group Email Alias if more relevant. - -## Short Description +- `WorkflowReviewChecklist.md` for the review contract +- `Toolkit/README.md` for the toolkit overview and source-of-truth chain +- `Toolkit/exchange-review/README.md` for review guidance +- `Toolkit/exchange-remediation/README.md` for remediation guidance +- `Toolkit/cli/README.md` for command-line usage +- `Toolkit/cursor/README.md` for installable Cursor wrappers +- `Toolkit/mcp/README.md` for stdio MCP usage -## Installation Instructions -The following is an example format of what can be provided in the Installations Instructions -This may vary depending on your submission. -Note the section Additional Reference Material can reference links added in the next section for "External Links" +## Why this file stays short -### Workflow Name -- The Name of your Workflow -> -### Key Features -- (Examples) -- Query JSON Placeholder Web Service for list of Users -- Locates specific User based on inputted Name -- Scan the returned data to find a matching email address for that User -- Return the Email if found, 400 if no User is located -> -### Installation Steps -1. (Examples) -2. Download the workflow from the Exchange -3. The Workflow will install an HTTP Target, "JSON Placeholder Web Service", if this Target does not already exist. -4. There is no specific configuration required for this HTTP endpoint. -> -### Prerequisites -- Note and requirements such as access rights, etc.. required to run this workflow. -> -### Additional Reference Material -- Please see the reference under External Links to the "Learning Series" YouTube content. - +The old submission walkthrough overlapped with the toolkit and used outdated wording. Keeping this file as a pointer avoids stale instructions and reduces the chance of contributors following the wrong path. From bc60ed2c83345819635a7904ab89c87cd0afb282 Mon Sep 17 00:00:00 2001 From: Christy Jones Date: Sun, 14 Jun 2026 16:24:34 -0400 Subject: [PATCH 6/6] docs: prioritize MCP installation path Co-authored-by: Cursor --- README.md | 6 +++--- Toolkit/README.md | 26 +++++++++++++++----------- Toolkit/cli/README.md | 2 +- Toolkit/cursor/README.md | 8 ++++++++ Toolkit/mcp/README.md | 11 ++++++++--- WorkflowsSubmissionProcess.md | 4 ++-- 6 files changed, 37 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 3df0e87..79f4ad4 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ CiscoWorkflowsAutomation/ ├── LICENSE # Repository license terms ├── Meraki/ # Cisco Meraki automation workflows and integrations ├── Restful_WebService/ # RESTful API integration workflows and examples -├── Toolkit/ # Public review, remediation, CLI, MCP, and Cursor helpers +├── Toolkit/ # Public review, remediation, CLI, primary MCP integration, and optional Cursor helpers └── SECURITY.md # Security policies and reporting procedures ``` @@ -25,11 +25,11 @@ CiscoWorkflowsAutomation/ The `Toolkit/` area provides reusable helpers for the workflow review checklist in this repository. It is intended for: -- Cursor and VS Code users who want installable skill wrappers +- VS Code and Cursor users who want a user-level MCP integration that works across workspaces - solution engineers or contributors using another LLM - external contributors who want a documented CLI and MCP-based path -See `Toolkit/README.md` for the audience guide and `WorkflowReviewChecklist.md` for the canonical review contract. The toolkit’s review and remediation guides now live under `Toolkit/exchange-review/` and `Toolkit/exchange-remediation/` so the folder naming matches the installable Cursor skills. +See `Toolkit/README.md` for the audience guide and `WorkflowReviewChecklist.md` for the canonical review contract. The recommended editor integration is the MCP path in `Toolkit/mcp/`, while the review and remediation guides live under `Toolkit/exchange-review/` and `Toolkit/exchange-remediation/`. ## Contributing diff --git a/Toolkit/README.md b/Toolkit/README.md index f0b1de4..15bb427 100644 --- a/Toolkit/README.md +++ b/Toolkit/README.md @@ -6,9 +6,11 @@ This toolkit turns the internal workflow review standard into reusable review an ## Who It Is For -### Cursor and VS Code users +### VS Code and Cursor users -Use `Toolkit/cursor/` to install thin Cursor skill wrappers that call the shared CLI. +Start with `Toolkit/mcp/` for the primary cross-workspace install path. The MCP server is the recommended integration for VS Code and Cursor because one user-level configuration can make it available in every workspace. + +Use `Toolkit/cursor/` only if you also want optional thin skill wrappers that call the shared CLI. ### Other LLM users @@ -30,9 +32,9 @@ That path does not require Cursor. Toolkit/ ├── README.md ├── cli/ # Layer 1: shared Python CLI and core helpers -├── cursor/ # Layer 2: thin Cursor skill wrappers +├── mcp/ # Layer 2: primary stdio MCP integration for editors +├── cursor/ # Layer 3: optional thin Cursor skill wrappers ├── examples/ # Example commands and sample output -├── mcp/ # Layer 3: stdio MCP guidance ├── exchange-remediation/ # Public remediation guidance and mode reference └── exchange-review/ # Public review guidance and checklist companion ``` @@ -40,8 +42,8 @@ Toolkit/ ## Layering - Layer 1: the CLI is the shared core for enumeration, checklist resolution, review preparation with remediation suggestions, and remediation planning. -- Layer 2: Cursor skills stay thin and delegate deterministic work to the CLI. -- Layer 3: the MCP server wraps the same core instead of re-implementing logic. +- Layer 2: the MCP server wraps the same core instead of re-implementing logic and is the recommended editor integration. +- Layer 3: Cursor skills stay thin, optional, and delegate deterministic work to the CLI. ## Quick Start @@ -53,16 +55,18 @@ python3 -m workflow_review inspect-workflow-export "/path/to/workflow.json" python3 -m workflow_review prepare-review "/path/to/workflow.json" --json ``` -### Install the public Cursor wrappers +### Install the workflow review MCP ```bash -bash Toolkit/cursor/install_cursor_skills.sh +bash Toolkit/mcp/install_mcp.sh ``` -### Install the workflow review MCP +This is the recommended setup for VS Code and Cursor because it is configured at the user level rather than tied to a single workspace. + +### Install the optional Cursor wrappers ```bash -bash Toolkit/mcp/install_mcp.sh +bash Toolkit/cursor/install_cursor_skills.sh ``` ### Run the stdio MCP scaffold @@ -72,7 +76,7 @@ cd Toolkit/cli python3 -m workflow_review.mcp_server ``` -For installed-client usage, see `Toolkit/mcp/README.md` for Cursor, VS Code, and generic stdio MCP examples that point at `workflow-review-mcp` directly and start with `review`; `inspect_export` is available as an advanced helper. +For installed-client usage, see `Toolkit/mcp/README.md` for Cursor, VS Code, and generic stdio MCP examples that point at `workflow-review-mcp` directly and start with `review`; `inspect_export` is available as an advanced helper. Treat `Toolkit/cursor/` as an optional convenience layer rather than the primary install surface. ## Scope diff --git a/Toolkit/cli/README.md b/Toolkit/cli/README.md index 30a05a0..de30fe1 100644 --- a/Toolkit/cli/README.md +++ b/Toolkit/cli/README.md @@ -86,4 +86,4 @@ workflow-review-mcp For the easiest MCP setup, run `bash Toolkit/mcp/install_mcp.sh` from the repo root. It installs the package and prints the exact command path to use. -See `../mcp/README.md` for client configuration examples. +See `../mcp/README.md` for client configuration examples. For VS Code and Cursor, MCP is the recommended editor-facing install path; the Cursor skill wrappers are optional thin helpers on top of the same core. diff --git a/Toolkit/cursor/README.md b/Toolkit/cursor/README.md index be52b03..b215089 100644 --- a/Toolkit/cursor/README.md +++ b/Toolkit/cursor/README.md @@ -4,6 +4,10 @@ This directory contains thin Cursor wrappers for the public toolkit. The wrappers intentionally delegate deterministic work to the shared CLI in `../cli/` and keep only the agent-facing orchestration in `SKILL.md`. +## Position in the toolkit + +These wrappers are a secondary convenience layer. They are not the canonical install path and they are not the source of truth for the toolkit. For VS Code and Cursor, prefer the MCP setup in `../mcp/` first when you want the toolkit available in all workspaces. + ## Install From the repository root: @@ -19,6 +23,10 @@ This installs the public `exchange-review` and `exchange-remediation` skills int - `exchange-review` - `exchange-remediation` +## When to use this + +Use these wrappers when you want skill-style prompting in Cursor after the MCP path is already available or when you specifically want a lightweight prompt wrapper around the shared CLI. + ## Design rule If a change can live in the CLI, it should live in the CLI. The Cursor wrappers should stay focused on: diff --git a/Toolkit/mcp/README.md b/Toolkit/mcp/README.md index bb98dec..6d774b1 100644 --- a/Toolkit/mcp/README.md +++ b/Toolkit/mcp/README.md @@ -1,9 +1,13 @@ # Workflow Review MCP -This directory documents the Layer 3 stdio MCP path for the public toolkit. +This directory documents the primary stdio MCP path for the public toolkit. The MCP server is intentionally thin. It wraps the same core package used by the CLI instead of re-implementing workflow review logic. +## Recommended path + +For VS Code and Cursor, this is the primary integration surface. MCP is the recommended default because it can be configured once at the user level and then used across all workspaces. + ## Current tools - `review` @@ -40,7 +44,7 @@ Use one of these simple prompts to kick off the review: ### Cursor -Use `review` as the starting action. Cursor will enumerate the export first, then return findings and remediation suggestions. +Use `review` as the starting action. Cursor will enumerate the export first, then return findings and remediation suggestions. Prefer this MCP path over workspace-local skill wrappers when you want the toolkit available everywhere. See `cursor.example.json`. It includes both: @@ -55,7 +59,7 @@ If your MCP client supports stdio servers, use the installed command directly an { "mcpServers": { "cisco-workflow-review": { - "command": "/path/to/installed/workflow-review-mcp", + "command": "/path/to/installed/workflow-review-mcp", "args": [], "env": {} } @@ -85,6 +89,7 @@ If the command is not on `PATH`, replace it with the full path to the installed ## Design intent +- User-level editor integration first - Local stdio transport first - Shared core with the CLI - Narrow tools with explicit scope diff --git a/WorkflowsSubmissionProcess.md b/WorkflowsSubmissionProcess.md index 904d4bb..ddaf780 100644 --- a/WorkflowsSubmissionProcess.md +++ b/WorkflowsSubmissionProcess.md @@ -6,11 +6,11 @@ This legacy guide has been superseded by the public toolkit. - `WorkflowReviewChecklist.md` for the review contract - `Toolkit/README.md` for the toolkit overview and source-of-truth chain +- `Toolkit/mcp/README.md` for the recommended VS Code and Cursor install path - `Toolkit/exchange-review/README.md` for review guidance - `Toolkit/exchange-remediation/README.md` for remediation guidance - `Toolkit/cli/README.md` for command-line usage -- `Toolkit/cursor/README.md` for installable Cursor wrappers -- `Toolkit/mcp/README.md` for stdio MCP usage +- `Toolkit/cursor/README.md` for optional Cursor skill wrappers ## Why this file stays short