From 3523804efd35030a5831a2c8e571e2e336ae439e Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Wed, 20 May 2026 22:00:30 +0200 Subject: [PATCH] docs: add security & quality assessments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add comprehensive security and software-quality artifacts (architecture patterns, ISO‑25010 profile, technology profile, software quality scan, and findings) and publish an updated roadmap. Also integrate Elsa.Diagnostics.ConsoleLogs into the solution and ModularServer app, and apply small config/whitespace fixes to support the addition. These documents are intended for review and human validation; no runtime behavior changes are introduced beyond the project wiring and minor appsettings/formatting fixes. --- .../elsa-release-announcements/SKILL.md | 182 +++ .../agents/openai.yaml | 4 + .../announcement_pack.cpython-314.pyc | Bin 0 -> 14040 bytes .../__pycache__/post_discord.cpython-314.pyc | Bin 0 -> 11957 bytes .../scripts/announcement_pack.py | 283 +++++ .../scripts/post_discord.py | 194 +++ Elsa.sln | 15 + ROADMAP.md | 126 +- .../elsa-core-architecture-patterns.md | 280 +++++ doc/security-assessment/elsa-core-iso25010.md | 1109 +++++++++++++++++ doc/security-assessment/elsa-core-profile.md | 375 ++++++ .../elsa-core-software-quality.md | 150 +++ ...workflows-elsa-core-findings-2026-05-20.md | 672 ++++++++++ ...software-quality-iso25010-assessment.skill | Bin 0 -> 70500 bytes .../Elsa.ModularServer.Web.csproj | 1 + .../Elsa.ModularServer.Web/appsettings.json | 1 + .../ScheduledSpecificInstantTask.cs | 2 +- 17 files changed, 3380 insertions(+), 14 deletions(-) create mode 100644 .agents/skills/elsa-release-announcements/SKILL.md create mode 100644 .agents/skills/elsa-release-announcements/agents/openai.yaml create mode 100644 .agents/skills/elsa-release-announcements/scripts/__pycache__/announcement_pack.cpython-314.pyc create mode 100644 .agents/skills/elsa-release-announcements/scripts/__pycache__/post_discord.cpython-314.pyc create mode 100755 .agents/skills/elsa-release-announcements/scripts/announcement_pack.py create mode 100755 .agents/skills/elsa-release-announcements/scripts/post_discord.py create mode 100644 doc/security-assessment/elsa-core-architecture-patterns.md create mode 100644 doc/security-assessment/elsa-core-iso25010.md create mode 100644 doc/security-assessment/elsa-core-profile.md create mode 100644 doc/security-assessment/elsa-core-software-quality.md create mode 100644 doc/security-assessment/elsa-workflows-elsa-core-findings-2026-05-20.md create mode 100644 doc/security-assessment/software-quality-iso25010-assessment.skill diff --git a/.agents/skills/elsa-release-announcements/SKILL.md b/.agents/skills/elsa-release-announcements/SKILL.md new file mode 100644 index 000000000..e8d46c226 --- /dev/null +++ b/.agents/skills/elsa-release-announcements/SKILL.md @@ -0,0 +1,182 @@ +--- +name: elsa-release-announcements +description: Draft, approve, and publish Elsa release announcements for Discord, LinkedIn, and X after an Elsa Core, Elsa Studio, Elsa Extensions, or similarly configured Elsa release has completed. Use when Codex needs to turn release notes, GitHub release URLs, package/feed availability, and build results into channel-specific community and social posts; supports direct Discord webhook posting and optional Buffer, Typefully, Zapier, Make, or manual publishing workflows for LinkedIn and X. +--- + +# Elsa Release Announcements + +## Overview + +Use this skill after a release has been published and packages are available. The output is an announcement pack with Discord, LinkedIn, and X copy tailored to each channel, plus optional publishing steps. + +Keep this skill separate from `elsa-release`: release execution verifies tags, GitHub releases, and packages; announcements communicate the finished release. + +## Recommended Publishing Setup + +Use a draft-and-approval workflow by default. + +- Discord: post directly with a Discord incoming webhook when `DISCORD_RELEASE_WEBHOOK_URL` is configured. If the target is an Announcement Channel, publish/crosspost the webhook message with `--crosspost` and a bot token in `DISCORD_BOT_TOKEN`. +- LinkedIn + X: prefer Buffer or Typefully for queueing/scheduling when accounts are connected. +- Single orchestration flow: use Zapier or Make when the team wants one approval-triggered workflow that can post Discord plus social channels. +- Manual fallback: produce copy-ready Markdown/plain-text drafts when no publishing service is configured. + +Current service fit: + +- Buffer supports publishing to LinkedIn and X/Twitter and has API support for creating posts across supported channels. +- Typefully supports multi-platform publishing for X/Twitter and LinkedIn through its API. +- Discord webhooks are the simplest reliable path for posting into a Discord channel. +- No service choice should be hard-coded into the release process; credentials, approval, and account ownership vary by team. + +## Inputs + +Collect or infer: + +- Product/repository: Elsa Core, Elsa Studio, Elsa Extensions, or another Elsa project. +- Version and release kind: stable, preview, or RC. +- GitHub release URL. +- Release notes file or GitHub release body. +- Package availability: NuGet, feedz.io, Docker, npm, or other relevant feeds. +- Important callouts: breaking changes, upgrade notes, known issues, migration docs, docs links. +- Desired publish mode: `draft-only`, `discord`, `buffer`, `typefully`, `zapier`, `make`, or `manual`. + +Do not publish anything until the user explicitly approves the final text and target channels. + +## Workflow + +1. Verify release readiness. + - Confirm the release exists and is public. + - Confirm package/feed publishing completed for the release kind. + - Confirm links work: GitHub release, NuGet/feed package search, docs/changelog. + +2. Generate an announcement pack. + - Use `scripts/announcement_pack.py` with release notes as input when available. + - Treat script output as a scaffold; rewrite it into polished copy. + - Keep factual claims tied to the release notes and package availability. + +3. Adapt by channel. + - Discord: rich community post with a strong headline, Discord emoji shortcodes, release links near the top, grouped highlights, practical upgrade notes, and a clear testing/feedback ask for previews or RCs. + - LinkedIn: polished product/developer narrative, stable release value, major improvements, and one clear link. + - X: one compact post or a short thread. Put the release link in the first post and avoid overloading a single post. + +4. Ask for approval. + - Show the exact message for each channel. + - State whether posting is direct, queued/scheduled, or manual. + - Do not include secrets or webhook URLs in chat output. + +5. Publish or prepare drafts. + - Discord direct: use `scripts/post_discord.py` with `DISCORD_RELEASE_WEBHOOK_URL`. + - Discord Announcement Channels: add `--crosspost` only when `DISCORD_BOT_TOKEN` is configured for a bot that can publish messages in that channel. + - Buffer/Typefully: use their API only when credentials and account/channel IDs are already configured. + - Zapier/Make: POST the approved payload to the configured webhook only when the user has provided the endpoint. + - Manual: save or present the final channel-specific drafts. + +6. Verify. + - For direct posts, confirm the API call succeeded. + - For queued posts, confirm the returned queue/schedule status or draft URL when available. + - Record what was posted, where, and when. + +## Announcement Shape + +Use this structure for the announcement pack: + +```markdown +# Elsa Announcement Pack + +## Facts + +## Discord + +## LinkedIn + +## X single-post option + +## X thread option + +## Links +``` + +Channel guidance: + +- Discord can be more direct, celebratory, and useful: mention the release, top changes, package availability, and links. Prefer `:rocket:`, `:point_right:`, `:sparkles:`, `:tools:`, `:test_tube:`, and similar Discord emoji shortcodes over raw Unicode emoji. +- Discord stable releases should say the stable version is available and ask for feedback on upgrades or regressions. +- Discord preview/RC releases should explicitly say they are intended for testing and validation before stable release. +- LinkedIn should explain the release in terms of developer value and project momentum, with fewer implementation details. +- X should be concise. Use a thread when there are more than two high-signal points. +- Stable releases may say packages are available on NuGet only after verifying that publish succeeded. +- Preview/RC announcements must clearly say preview/RC and avoid implying production stability. + +## Discord Style + +Use this shape for Discord drafts and adapt the details to the actual release: + +```markdown +:rocket: **Elsa Workflows 3.7.0 is here!** + +We've published the stable **Elsa 3.7.0** release across **Elsa Core** and **Elsa Studio**. + +:point_right: Core: https://github.com/elsa-workflows/elsa-core/releases/tag/3.7.0 +:point_right: Studio: https://github.com/elsa-workflows/elsa-studio/releases/tag/3.7.0 + +This release brings a solid set of improvements around **authentication**, **workflow diagnostics**, **Studio extensibility**, and the **modular server runtime**. + +### :sparkles: Highlights + +:closed_lock_with_key: **Modern authentication support in Elsa Studio** +Summarize the high-value change in one or two practical sentences. + +:compass: **Improved workflow instance diagnostics** +Summarize the most user-visible diagnostics improvements. + +:jigsaw: **Modular server runtime improvements** +Summarize the Core/runtime changes. + +### :tools: Upgrade notes +Call out compatibility or dependency changes users should validate. + +### :raised_hands: Feedback welcome +Ask users to report upgrade issues, regressions, and bugs. +``` + +## Helper Usage + +Generate drafts: + +```bash +python3 .agents/skills/elsa-release-announcements/scripts/announcement_pack.py \ + --product "Elsa Core" \ + --version 3.7.0 \ + --release-kind stable \ + --release-url https://github.com/elsa-workflows/elsa-core/releases/tag/3.7.0 \ + --notes-file doc/changelogs/3.7.0.md \ + --package-url 'https://www.nuget.org/packages?q=Elsa' +``` + +Post to Discord after approval: + +```bash +DISCORD_RELEASE_WEBHOOK_URL="..." \ +python3 .agents/skills/elsa-release-announcements/scripts/post_discord.py \ + --message-file announcements/discord-3.7.0.md +``` + +Post and publish to a Discord Announcement Channel after approval: + +```bash +DISCORD_RELEASE_WEBHOOK_URL="..." \ +DISCORD_BOT_TOKEN="..." \ +python3 .agents/skills/elsa-release-announcements/scripts/post_discord.py \ + --message-file announcements/discord-3.7.0.md \ + --crosspost +``` + +For `--crosspost`, the webhook post is sent with `wait=true` so Discord returns the created message ID. The script then calls Discord's crosspost endpoint for that message. The bot must have access to the Announcement Channel; if the bot did not create the message, grant the channel permissions needed to publish another sender's message. + +## Guardrails + +- Never publish without explicit user approval of the exact channel text. +- Never expose webhook URLs, API tokens, access tokens, or account IDs in final responses. +- Do not claim package availability until verified. +- Do not claim a feature is new unless release notes or commits support it. +- Do not use the same wording blindly across all channels. +- Keep LinkedIn/X posts free of internal build details unless they matter to users. +- If a platform API fails or credentials are missing, fall back to copy-ready drafts. diff --git a/.agents/skills/elsa-release-announcements/agents/openai.yaml b/.agents/skills/elsa-release-announcements/agents/openai.yaml new file mode 100644 index 000000000..e9ce88cb1 --- /dev/null +++ b/.agents/skills/elsa-release-announcements/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Elsa Release Announcements" + short_description: "Draft and publish Elsa release announcements" + default_prompt: "Use $elsa-release-announcements to prepare Discord, LinkedIn, and X posts for the latest Elsa release." diff --git a/.agents/skills/elsa-release-announcements/scripts/__pycache__/announcement_pack.cpython-314.pyc b/.agents/skills/elsa-release-announcements/scripts/__pycache__/announcement_pack.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..96a4bb14c28d133b2bf141db1177f89e93000dfd GIT binary patch literal 14040 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 r0uP!k@KBk%tJYE9@E literal 0 HcmV?d00001 diff --git a/.agents/skills/elsa-release-announcements/scripts/__pycache__/post_discord.cpython-314.pyc b/.agents/skills/elsa-release-announcements/scripts/__pycache__/post_discord.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7d08f98e8461da5e1b72db02317b9b7a86eec3d0 GIT binary patch literal 11957 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 int: + args = parse_args() + notes = read_notes(args.notes_file) + highlights = extract_highlights(notes) + links = [args.release_url, *args.package_url, *args.docs_url] + + pack = render_pack( + product=args.product, + version=args.version, + release_kind=args.release_kind, + release_url=args.release_url, + package_urls=args.package_url, + docs_urls=args.docs_url, + highlights=highlights, + ) + + if args.output: + output = Path(args.output).expanduser() + output.parent.mkdir(parents=True, exist_ok=True) + output.write_text(pack, encoding="utf-8") + print(output) + else: + print(pack) + + return 0 + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--product", required=True, help="Product name, e.g. Elsa Core.") + parser.add_argument("--version", required=True, help="Release version.") + parser.add_argument("--release-kind", required=True, choices=("stable", "preview", "rc"), help="Release kind.") + parser.add_argument("--release-url", required=True, help="GitHub release URL.") + parser.add_argument("--notes-file", help="Curated release notes Markdown file.") + parser.add_argument("--package-url", action="append", default=[], help="Package/feed URL. Can be repeated.") + parser.add_argument("--docs-url", action="append", default=[], help="Docs or migration URL. Can be repeated.") + parser.add_argument("--output", help="Optional output Markdown path.") + return parser.parse_args() + + +def read_notes(path: str | None) -> str: + if not path: + return "" + return Path(path).expanduser().read_text(encoding="utf-8") + + +def extract_highlights(notes: str) -> list[str]: + if not notes: + return [] + + section_match = re.search(r"^##\s+.*Highlights\s*$([\s\S]*?)(?:^---$|^##\s+)", notes, flags=re.MULTILINE) + source = section_match.group(1) if section_match else notes + highlights: list[str] = [] + for line in source.splitlines(): + stripped = line.strip() + if stripped.startswith("- "): + highlights.append(clean_markdown(stripped[2:])) + if len(highlights) == 5: + break + return highlights + + +def clean_markdown(value: str) -> str: + value = re.sub(r"\[[^\]]+\]\(([^)]+)\)", r"\1", value) + value = value.replace("`", "") + return re.sub(r"\s+", " ", value).strip() + + +def render_pack( + *, + product: str, + version: str, + release_kind: str, + release_url: str, + package_urls: list[str], + docs_urls: list[str], + highlights: list[str], +) -> str: + availability = availability_text(release_kind, package_urls) + highlight_text = "\n".join(f"- {highlight}" for highlight in highlights) if highlights else "- Add the top release highlights here after reviewing the release notes." + link_text = "\n".join(f"- {url}" for url in [release_url, *package_urls, *docs_urls]) + stable_label = "stable " if release_kind == "stable" else f"{release_kind} " + + discord = render_discord( + product=product, + version=version, + release_kind=release_kind, + release_url=release_url, + availability=availability, + highlights=highlights, + ) + + linkedin = f"""\ +{product} {version} is now available. + +This {stable_label}release includes important improvements for developers building with Elsa. Highlights include: + +{highlight_text} + +Read the full release notes: {release_url} +""" + + x_single = f"""\ +{product} {version} is now available. + +{availability} + +Highlights: +{compact_highlights(highlights)} + +Release notes: {release_url} +""" + + x_thread = f"""\ +1/{product} {version} is now available. + +{availability} + +Release notes: {release_url} + +2/Highlights: +{compact_highlights(highlights)} + +3/Upgrade notes, package links, and the full changelog are in the release notes. +""" + + return "\n".join( + [ + f"# {product} {version} Announcement Pack", + "", + "## Facts", + "", + f"- Product: {product}", + f"- Version: {version}", + f"- Release kind: {release_kind}", + f"- Release URL: {release_url}", + "", + "## Discord", + "", + discord.strip(), + "", + "## LinkedIn", + "", + linkedin.strip(), + "", + "## X single-post option", + "", + x_single.strip(), + "", + "## X thread option", + "", + x_thread.strip(), + "", + "## Links", + "", + link_text, + "", + "", + "", + ] + ) + + +def availability_text(release_kind: str, package_urls: list[str]) -> str: + if release_kind == "stable" and package_urls: + return "Packages are available on the configured feeds." + if release_kind == "stable": + return "Packages are available once the release pipeline has completed." + if release_kind == "preview": + return "This is a preview release intended for early validation." + return "This is a release candidate intended for final validation before stable release." + + +def render_discord( + *, + product: str, + version: str, + release_kind: str, + release_url: str, + availability: str, + highlights: list[str], +) -> str: + heading = discord_heading(product, version, release_kind) + intro = discord_intro(product, version, release_kind) + highlight_text = render_discord_highlights(highlights) + upgrade_notes = render_upgrade_notes(release_kind) + validation = render_validation_note(release_kind) + + return f"""\ +:rocket: **{heading}** + +{intro} + +:point_right: Release notes: {release_url} + +{availability} + +### :sparkles: Highlights + +{highlight_text} + +{upgrade_notes} + +{validation} +""" + + +def discord_heading(product: str, version: str, release_kind: str) -> str: + if release_kind == "stable": + return f"{product} {version} is here!" + if release_kind == "preview": + return f"{product} {version} preview is here!" + return f"{product} {version} RC is here!" + + +def discord_intro(product: str, version: str, release_kind: str) -> str: + if release_kind == "stable": + return f"We've published the stable **{product} {version}** release." + if release_kind == "preview": + return f"We've published a preview release for **{product} {version}** for early testing and feedback." + return f"We've published a release candidate for **{product} {version}**." + + +def render_discord_highlights(highlights: list[str]) -> str: + if not highlights: + return """:compass: **Release improvements** +Add the top release highlights here after reviewing the release notes.""" + + icons = [ + ":closed_lock_with_key:", + ":compass:", + ":art:", + ":jigsaw:", + ":zap:", + ":bug:", + ] + lines: list[str] = [] + for index, highlight in enumerate(highlights[:6]): + icon = icons[index % len(icons)] + title, _, details = highlight.partition(":") + if details: + lines.append(f"{icon} **{title.strip()}**\n{details.strip()}") + else: + lines.append(f"{icon} **{highlight}**") + return "\n\n".join(lines) + + +def render_upgrade_notes(release_kind: str) -> str: + heading = "### :tools: Upgrade notes" + if release_kind == "stable": + body = """Review the full release notes before upgrading, especially if you host Elsa Studio yourself, customize Studio components, use custom authentication, or integrate with secured Elsa APIs.""" + else: + body = """There may be compatibility changes to validate before production use. Pay special attention if you host Elsa Studio yourself, customize Studio components, use custom authentication, or integrate with secured Elsa APIs.""" + return f"{heading}\n{body}" + + +def render_validation_note(release_kind: str) -> str: + if release_kind == "stable": + return """### :raised_hands: Feedback welcome +Please report issues, regressions, or upgrade notes you run into so we can keep improving the 3.7 line.""" + + return """### :test_tube: Please test it +This release is intended for testing and validation before the final stable release. Feedback, bug reports, and PRs are very welcome :raised_hands:""" + + +def compact_highlights(highlights: list[str]) -> str: + if not highlights: + return "- See the release notes for details." + return "\n".join(f"- {highlight}" for highlight in highlights[:3]) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/.agents/skills/elsa-release-announcements/scripts/post_discord.py b/.agents/skills/elsa-release-announcements/scripts/post_discord.py new file mode 100755 index 000000000..8a12bfbda --- /dev/null +++ b/.agents/skills/elsa-release-announcements/scripts/post_discord.py @@ -0,0 +1,194 @@ +#!/usr/bin/env python3 +"""Post an approved release announcement to Discord via webhook.""" + +from __future__ import annotations + +import argparse +import json +import os +import shutil +import subprocess +import sys +import tempfile +import urllib.error +import urllib.parse +import urllib.request +from pathlib import Path +from typing import Any + + +def main() -> int: + args = parse_args() + webhook_url = args.webhook_url or os.getenv("DISCORD_RELEASE_WEBHOOK_URL") + if not webhook_url: + print("error: provide --webhook-url or DISCORD_RELEASE_WEBHOOK_URL", file=sys.stderr) + return 1 + + message = Path(args.message_file).expanduser().read_text(encoding="utf-8").strip() + if not message: + print("error: message file is empty", file=sys.stderr) + return 1 + + bot_token = args.bot_token or os.getenv("DISCORD_BOT_TOKEN") + if args.crosspost and not bot_token: + print("error: provide --bot-token or DISCORD_BOT_TOKEN when using --crosspost", file=sys.stderr) + return 1 + + payload = json.dumps({"content": message[:2000]}).encode("utf-8") + status, body = post_payload(webhook_url, payload, wait=args.crosspost) + if status not in (200, 204): + print(f"error: Discord returned HTTP {status}", file=sys.stderr) + return 1 + + if args.crosspost: + message_data = parse_json_body(body) + channel_id = message_data.get("channel_id") + message_id = message_data.get("id") + if not channel_id or not message_id: + print("error: Discord did not return a message id for crossposting", file=sys.stderr) + return 1 + + crosspost_status, crosspost_body = crosspost_message(channel_id, message_id, bot_token or "") + if crosspost_status not in (200, 204): + print(f"error: Discord crosspost returned HTTP {crosspost_status}", file=sys.stderr) + if crosspost_body: + print(crosspost_body, file=sys.stderr) + return 1 + + if args.crosspost: + print("Posted and published Discord announcement.") + else: + print("Posted Discord announcement.") + return 0 + + +def post_payload(webhook_url: str, payload: bytes, *, wait: bool) -> tuple[int, str]: + url = append_query(webhook_url, {"wait": "true"}) if wait else webhook_url + if shutil.which("curl"): + return post_with_curl(url, payload) + + request = urllib.request.Request( + url, + data=payload, + headers={ + "Content-Type": "application/json", + "User-Agent": "ElsaReleaseAnnouncements/1.0", + }, + method="POST", + ) + try: + with urllib.request.urlopen(request, timeout=30) as response: + return response.status, response.read().decode("utf-8", errors="replace") + except urllib.error.HTTPError as e: + return e.code, e.read().decode("utf-8", errors="replace") + + +def post_with_curl(webhook_url: str, payload: bytes) -> tuple[int, str]: + with tempfile.NamedTemporaryFile() as payload_file, tempfile.NamedTemporaryFile() as body_file: + payload_file.write(payload) + payload_file.flush() + + # Feed curl its config via stdin so the webhook URL does not appear in + # command output or shell history. + curl_config = "\n".join( + [ + f'url = "{webhook_url}"', + 'request = "POST"', + 'header = "Content-Type: application/json"', + f'data-binary = "@{payload_file.name}"', + 'write-out = "%{http_code}"', + "silent", + "show-error", + f'output = "{body_file.name}"', + ] + ) + result = subprocess.run( + ["curl", "--config", "-"], + input=curl_config, + text=True, + capture_output=True, + check=False, + ) + body_file.seek(0) + body = body_file.read().decode("utf-8", errors="replace") + + if result.returncode != 0: + print(result.stderr.strip() or "error: curl failed", file=sys.stderr) + return 0, "" + + return int(result.stdout.strip()), body + + +def crosspost_message(channel_id: str, message_id: str, bot_token: str) -> tuple[int, str]: + url = f"https://discord.com/api/v10/channels/{channel_id}/messages/{message_id}/crosspost" + if shutil.which("curl"): + with tempfile.NamedTemporaryFile() as body_file: + curl_config = "\n".join( + [ + f'url = "{url}"', + 'request = "POST"', + f'header = "Authorization: Bot {bot_token}"', + 'write-out = "%{http_code}"', + "silent", + "show-error", + f'output = "{body_file.name}"', + ] + ) + result = subprocess.run( + ["curl", "--config", "-"], + input=curl_config, + text=True, + capture_output=True, + check=False, + ) + body_file.seek(0) + body = body_file.read().decode("utf-8", errors="replace") + + if result.returncode != 0: + print(result.stderr.strip() or "error: curl failed", file=sys.stderr) + return 0, "" + return int(result.stdout.strip()), body + + request = urllib.request.Request( + url, + headers={ + "Authorization": f"Bot {bot_token}", + "User-Agent": "ElsaReleaseAnnouncements/1.0", + }, + method="POST", + ) + try: + with urllib.request.urlopen(request, timeout=30) as response: + return response.status, response.read().decode("utf-8", errors="replace") + except urllib.error.HTTPError as e: + return e.code, e.read().decode("utf-8", errors="replace") + + +def append_query(url: str, query: dict[str, str]) -> str: + parts = urllib.parse.urlsplit(url) + existing = dict(urllib.parse.parse_qsl(parts.query)) + existing.update(query) + return urllib.parse.urlunsplit( + (parts.scheme, parts.netloc, parts.path, urllib.parse.urlencode(existing), parts.fragment) + ) + + +def parse_json_body(body: str) -> dict[str, Any]: + try: + parsed = json.loads(body) + except json.JSONDecodeError: + return {} + return parsed if isinstance(parsed, dict) else {} + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--message-file", required=True, help="Approved Discord message file.") + parser.add_argument("--webhook-url", help="Discord webhook URL. Prefer DISCORD_RELEASE_WEBHOOK_URL.") + parser.add_argument("--crosspost", action="store_true", help="Publish the created message from an Announcement Channel.") + parser.add_argument("--bot-token", help="Discord bot token for crossposting. Prefer DISCORD_BOT_TOKEN.") + return parser.parse_args() + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/Elsa.sln b/Elsa.sln index f3eb8785a..eb11e4bb4 100644 --- a/Elsa.sln +++ b/Elsa.sln @@ -339,6 +339,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Diagnostics.ConsoleLog EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Diagnostics.ConsoleLogs.IntegrationTests", "test\integration\Elsa.Diagnostics.ConsoleLogs.IntegrationTests\Elsa.Diagnostics.ConsoleLogs.IntegrationTests.csproj", "{93E9213A-694D-4AB4-870E-05E44F793133}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Diagnostics.ConsoleLogs", "src\modules\Elsa.Diagnostics.ConsoleLogs\Elsa.Diagnostics.ConsoleLogs.csproj", "{B4C5BDFC-90CC-4CF1-BD03-2F0081C53F0F}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -1331,6 +1333,18 @@ Global {93E9213A-694D-4AB4-870E-05E44F793133}.Release|x64.Build.0 = Release|Any CPU {93E9213A-694D-4AB4-870E-05E44F793133}.Release|x86.ActiveCfg = Release|Any CPU {93E9213A-694D-4AB4-870E-05E44F793133}.Release|x86.Build.0 = Release|Any CPU + {B4C5BDFC-90CC-4CF1-BD03-2F0081C53F0F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B4C5BDFC-90CC-4CF1-BD03-2F0081C53F0F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B4C5BDFC-90CC-4CF1-BD03-2F0081C53F0F}.Debug|x64.ActiveCfg = Debug|Any CPU + {B4C5BDFC-90CC-4CF1-BD03-2F0081C53F0F}.Debug|x64.Build.0 = Debug|Any CPU + {B4C5BDFC-90CC-4CF1-BD03-2F0081C53F0F}.Debug|x86.ActiveCfg = Debug|Any CPU + {B4C5BDFC-90CC-4CF1-BD03-2F0081C53F0F}.Debug|x86.Build.0 = Debug|Any CPU + {B4C5BDFC-90CC-4CF1-BD03-2F0081C53F0F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B4C5BDFC-90CC-4CF1-BD03-2F0081C53F0F}.Release|Any CPU.Build.0 = Release|Any CPU + {B4C5BDFC-90CC-4CF1-BD03-2F0081C53F0F}.Release|x64.ActiveCfg = Release|Any CPU + {B4C5BDFC-90CC-4CF1-BD03-2F0081C53F0F}.Release|x64.Build.0 = Release|Any CPU + {B4C5BDFC-90CC-4CF1-BD03-2F0081C53F0F}.Release|x86.ActiveCfg = Release|Any CPU + {B4C5BDFC-90CC-4CF1-BD03-2F0081C53F0F}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -1450,6 +1464,7 @@ Global {195FD304-EC3F-4350-93C8-AFE80C4E6896} = {5BA4A8FA-F7F4-45B3-AEC8-8886D35AAC79} {D8739449-22DC-42D4-85A4-4BA547B0B458} = {18453B51-25EB-4317-A4B3-B10518252E92} {93E9213A-694D-4AB4-870E-05E44F793133} = {1B8D5897-902E-4632-8698-E89CAF3DDF54} + {B4C5BDFC-90CC-4CF1-BD03-2F0081C53F0F} = {78FD90A4-90A5-445F-97F2-74BA835AFA5D} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {D4B5CEAA-7D70-4FCB-A68E-B03FBE5E0E5E} diff --git a/ROADMAP.md b/ROADMAP.md index 93a651d59..c27195307 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -4,6 +4,8 @@ Last refreshed: 2026-05-19 This roadmap is a product direction document, not a fixed release calendar. Elsa is developed through a mix of core maintainer work, customer-funded work, and community contributions, so sequencing can change when real-world demand changes. The intent is stable: make Elsa the most productive, dependable, and extensible workflow platform for the .NET ecosystem. +This version is grounded in the current `elsa-core`, `elsa-studio`, and `elsa-extensions` repositories, including source code, open issues, open pull requests, releases, and discussions where available. + ## North Star Elsa should feel like the natural workflow engine for .NET teams: @@ -13,6 +15,79 @@ Elsa should feel like the natural workflow engine for .NET teams: - open enough to embed, customize, extend, and automate without fighting the framework - powerful enough to connect systems, run long-lived processes, expose human tasks, and observe what is happening +## Capability Checklist + +Legend: `[x]` shipped foundation, `[~]` partially shipped or needs productization, `[ ]` roadmap candidate. + +### Engine + +- [x] Non-blocking activity execution +- [x] Fork, join, and explicit flowchart merge modes +- [x] State machine core activity +- [~] State machine Studio authoring, docs, and examples +- [~] Graceful shutdown and interrupted recovery +- [ ] Full workflow execution recovery UX +- [ ] Compensation / saga support +- [ ] Variable history tracking +- [ ] Native workflow-aware background execution +- [ ] Actor-runtime abstraction + +### Runtime And Operations + +- [x] Runtime admin pause, resume, status, and force-drain endpoints +- [x] Distributed runtime package +- [x] Structured logs +- [x] Console logs +- [x] Studio structured-log and console-log foundations +- [~] Durable structured log persistence +- [~] Scheduler and message-bus foundations through Quartz, Hangfire, MassTransit, Kafka, and Azure Service Bus +- [ ] OpenTelemetry traces and default workflow metrics +- [ ] Scheduler/message reliability hardening for clustered production workloads +- [ ] Production security guide +- [ ] Enterprise deployment checklist + +### Authoring And Studio + +- [x] Visual designer foundation +- [x] Modular Studio shell, feature system, menus, widgets, themes, localization, and remote feature gating +- [x] Studio custom elements for embedding definition editors, instance viewers, and workflow lists +- [x] Activity unit testing helpers +- [x] ElsaScript experimental DSL +- [~] React Flow, sequence, and state-machine designer foundations +- [~] Workflow organization with labels/categories/folders +- [~] Workflow progress/timeline surface +- [~] Studio OIDC and identity modules +- [~] Studio diagnostics pages for structured and console logs +- [~] Studio alterations module +- [ ] Designer reliability/regression hardening +- [ ] Input validation and activity version visibility +- [ ] Async dispatch/run UX +- [ ] User preferences, table state, and layout persistence +- [ ] First-class workflow debugging +- [ ] Designer extensibility, embedding, and white-label recipes +- [ ] Tenant/role-based activity visibility +- [ ] AI-assisted workflow generation + +### Integrations + +- [x] HTTP, scheduling, scripting, and persistence provider foundations +- [x] Connections and Secrets foundations +- [x] Extension packages for SQL, CSV, Email, Slack, Telnyx, GitHub DevOps, Azure Storage, Azure Service Bus, Kafka, MassTransit/RabbitMQ, Quartz, Hangfire, Dapper, MongoDB, Elasticsearch, OpenTelemetry, Logging, Agents, OpenAPI, Webhooks, OrchardCore, IO, compression, and ProtoActor-backed runtime/caching +- [~] Modular package loading and manifest metadata +- [~] OpenAPI activity/provider foundations +- [ ] Connector SDK +- [ ] Marketplace/plugin installation +- [ ] Agent provider matrix and MCP/tool lifecycle +- [ ] Dynamic activity generation from OpenAPI, Azure Functions, registered methods, and schemas +- [ ] Azure DevOps, Teams, OneDrive, SharePoint, Google Docs, and Google Sheets integration strategy +- [ ] SQL authoring quality: Studio drag/drop reliability and IntelliSense +- [ ] Dapper package split and MongoDB secrets parity +- [ ] MassTransit v9 strategy +- [ ] Azure Functions / worker-service hosting guidance +- [ ] WatchFileSystem and command-line automation activities +- [ ] Data pipeline / ETL primitives +- [ ] BPMN interoperability + ## Current Foundations These are already present in the codebase and should be treated as foundations for the next roadmap slices: @@ -28,6 +103,11 @@ These are already present in the codebase and should be treated as foundations f - ElsaScript DSL and blob storage integration in [`Elsa.Dsl.ElsaScript`](src/modules/Elsa.Dsl.ElsaScript) and [`Elsa.WorkflowProviders.BlobStorage.ElsaScript`](src/modules/Elsa.WorkflowProviders.BlobStorage.ElsaScript). - Activity unit testing helpers and guidance in [`src/common/Elsa.Testing.Shared`](src/common/Elsa.Testing.Shared) and [`doc/qa/test-guidelines.md`](doc/qa/test-guidelines.md). - Label infrastructure in [`Elsa.Labels`](src/modules/Elsa.Labels), which is the likely backend foundation for workflow categories, tags, and folders. +- Elsa Studio is already a modular Blazor product shell with workflow authoring, instance browsing, designer modules, diagnostics, authentication, localization, branding, custom elements, and early React wrapper work in [elsa-workflows/elsa-studio](https://github.com/elsa-workflows/elsa-studio). +- Studio `main` includes structured-log and console-log modules, newer React Flow/sequence/state-machine designer work, OIDC/identity infrastructure, custom elements, and an alterations module. +- Elsa Extensions is an active modular integration repository with 70+ module projects in [elsa-workflows/elsa-extensions](https://github.com/elsa-workflows/elsa-extensions), targeting `net8.0`, `net9.0`, and `net10.0`. +- Extensions already provide broad integration foundations: Connections, Secrets, Agents, OpenAPI, SQL/CSV/data tooling, messaging, schedulers, cloud storage, logging, webhooks, persistence providers, and external system activities. +- Recent extension work adds package manifest metadata, Kafka multitenancy/schema triggers, Quartz lifecycle/job cleanup fixes, Dapper bookmark cleanup, and other operational hardening. The public roadmap issue remains useful history: [elsa-workflows/elsa-core#3232](https://github.com/elsa-workflows/elsa-core/issues/3232). Several items in that issue are now done in code but still open in the issue body, so this file should be considered the current working roadmap. @@ -40,6 +120,7 @@ High-value items: - Complete the graceful-shutdown operational slice: back-pressure-aware bookmark queueing, health checks, pause persistence across reactivation, and contract tests. The remaining task list is visible in [`specs/002-graceful-shutdown/tasks.md`](specs/002-graceful-shutdown/tasks.md). - Close the workflow recovery story around interrupted, crashed, and stuck-running instances. This directly addresses [#4833](https://github.com/elsa-workflows/elsa-core/issues/4833) and should include Studio-facing recovery states, operator actions, and clear audit records. - Harden distributed execution semantics: child workflow completion, bookmark races, duplicate dispatch, timer/delay behavior, and clustered refresh/reload. Community signal shows this repeatedly in [discussion #5857](https://github.com/elsa-workflows/elsa-core/discussions/5857), [#7397](https://github.com/elsa-workflows/elsa-core/issues/7397), [#7405](https://github.com/elsa-workflows/elsa-core/issues/7405), and related FlowJoin/bookmark issues. +- Treat scheduler and messaging correctness as release-blocking infrastructure. Extensions issues around Quartz clustering and recovery ([elsa-extensions#109](https://github.com/elsa-workflows/elsa-extensions/issues/109), [elsa-extensions#101](https://github.com/elsa-workflows/elsa-extensions/issues/101)), Hangfire duplicate jobs ([elsa-extensions#121](https://github.com/elsa-workflows/elsa-extensions/issues/121)), MassTransit stimulus routing ([elsa-extensions#72](https://github.com/elsa-workflows/elsa-extensions/issues/72)), and Kafka extensibility ([elsa-extensions#134](https://github.com/elsa-workflows/elsa-extensions/issues/134)) all point to the same production theme: clustered workload behavior must be boring, observable, and customizable. - Turn the draft native background execution architecture into an implementation plan. [#7356](https://github.com/elsa-workflows/elsa-core/issues/7356) and [#7313](https://github.com/elsa-workflows/elsa-core/issues/7313) point toward an engine-owned, workflow-aware runtime that can evolve toward an actor-model abstraction without coupling Elsa to Orleans, Proto.Actor, or any single backend. - Treat persistence and migration reliability as a product feature: provider-specific migration validation, large-tenant performance tests, safer defaults, and upgrade notes that cover SQL Server, PostgreSQL, MySQL, SQLite, Oracle, and MongoDB scenarios. - Maintain a security hardening track: document scripting trust boundaries, production-safe Docker posture, OIDC setup, default auth patterns, and secrets redaction. [#7096](https://github.com/elsa-workflows/elsa-core/issues/7096) is a reminder that optional code execution must be explained and guarded clearly. @@ -58,17 +139,22 @@ Recommended success measures: High-value items: - Ship workflow organization as a coherent feature: labels/categories, folder-like views, search/filter by metadata, and Studio support. This consolidates [#5872](https://github.com/elsa-workflows/elsa-core/issues/5872), [#6307](https://github.com/elsa-workflows/elsa-core/issues/6307), the existing `Elsa.Labels` module, and workflow definition `CustomProperties`. +- Make designer reliability a visible workstream. Recent Studio issues show expression/input rendering regressions after 3.6 ([elsa-studio#791](https://github.com/elsa-workflows/elsa-studio/issues/791), [elsa-studio#781](https://github.com/elsa-workflows/elsa-studio/issues/781), [elsa-studio#795](https://github.com/elsa-workflows/elsa-studio/issues/795)); these should drive a regression harness for designer rendering, property editors, expression descriptors, drag/drop, and WASM/Server parity. - Make workflow progress visible to application users: a current-state/step API, timeline model, and embeddable progress component. This responds to [discussion #6012](https://github.com/elsa-workflows/elsa-core/discussions/6012) and should reuse execution logs, activity records, call-stack tracking, and real-time workflow updates. - Finish the state machine product surface. The core activity exists, but [#5085](https://github.com/elsa-workflows/elsa-core/issues/5085) should be closed only when JSON serialization, Studio authoring, docs, and examples make state machines approachable. -- Build first-class workflow testing and debugging: test runners for full workflows, breakpoint-like inspection, replay from execution logs where feasible, better failed-activity retry flows, and Studio affordances for fault investigation. This expands the activity test helper work that addressed [#3978](https://github.com/elsa-workflows/elsa-core/issues/3978). -- Improve designer extensibility and embedding: custom activity property editors, custom list actions, embeddable designer/viewer recipes, and clear Blazor/WASM/Server guidance. Community demand appears in [#4743](https://github.com/elsa-workflows/elsa-core/issues/4743), [#6685](https://github.com/elsa-workflows/elsa-core/issues/6685), [discussion #7246](https://github.com/elsa-workflows/elsa-core/discussions/7246), and older designer issues. +- Build first-class workflow testing and debugging: test runners for full workflows, breakpoint-like inspection, replay from execution logs where feasible, better failed-activity retry flows, child/descendant workflow instance navigation, and Studio affordances for fault investigation. This expands the activity test helper work that addressed [#3978](https://github.com/elsa-workflows/elsa-core/issues/3978) and matches Studio requests for child workflow visibility ([elsa-studio#152](https://github.com/elsa-workflows/elsa-studio/issues/152)) and breakpoint debugging ([elsa-studio discussion #662](https://github.com/elsa-workflows/elsa-studio/discussions/662)). +- Improve Studio authoring fundamentals: input validation ([elsa-studio#15](https://github.com/elsa-workflows/elsa-studio/issues/15)), activity version indicators ([elsa-studio#284](https://github.com/elsa-workflows/elsa-studio/issues/284)), async `/dispatch` instead of blocking `/execute` where appropriate ([elsa-studio#811](https://github.com/elsa-workflows/elsa-studio/issues/811)), designer image export ([elsa-studio#585](https://github.com/elsa-workflows/elsa-studio/issues/585)), and expression evaluation controls ([elsa-studio#643](https://github.com/elsa-workflows/elsa-studio/issues/643)). +- Improve designer extensibility, embedding, and white-labeling: custom activity property editors, custom list actions, embeddable designer/viewer recipes, custom elements, React wrapper direction, auth modes, base-path hosting, branding/theme, and clear Blazor/WASM/Server guidance. Community demand appears in [#4743](https://github.com/elsa-workflows/elsa-core/issues/4743), [#6685](https://github.com/elsa-workflows/elsa-core/issues/6685), [discussion #7246](https://github.com/elsa-workflows/elsa-core/discussions/7246), [elsa-studio#137](https://github.com/elsa-workflows/elsa-studio/issues/137), and [elsa-studio discussion #665](https://github.com/elsa-workflows/elsa-studio/discussions/665). +- Ship user preference and UI state persistence as a Studio platform feature. [elsa-studio#703](https://github.com/elsa-workflows/elsa-studio/issues/703) already scopes theme, layout, table state, local/session storage, reset behavior, and future server profile storage. - Promote ElsaScript from experiment to useful authoring path: stable syntax, import/export round-tripping, editor diagnostics, examples, and clear boundaries with JSON and visual authoring. See [#7055](https://github.com/elsa-workflows/elsa-core/issues/7055) and the current [`Elsa.Dsl.ElsaScript`](src/modules/Elsa.Dsl.ElsaScript) module. +- Decide the Studio UI framework direction before broad UX work. [elsa-studio#714](https://github.com/elsa-workflows/elsa-studio/issues/714) proposes a MudBlazor-to-Radzen migration, while current work still touches both ecosystems; roadmap work should avoid expensive churn. Recommended success measures: - teams with 100+ workflow definitions can find and govern them without naming hacks - a developer can unit test an activity, integration test a workflow, and debug a failed instance from documented recipes - common Studio customization no longer requires replacing entire pages or reverse engineering internals +- designer regressions are caught before release across Blazor Server, WASM, and embedded component scenarios ## 3. Integrations And Ecosystem @@ -76,9 +162,14 @@ Recommended success measures: High-value items: -- Create an OpenAPI activity provider that turns an OpenAPI document into typed designer activities. This is a recurring ask in [#2961](https://github.com/elsa-workflows/elsa-core/issues/2961) and [#6360](https://github.com/elsa-workflows/elsa-core/issues/6360), and it is the best foundation for a broad connector story. -- Define a connector SDK before adding many one-off integrations. The SDK should cover authentication, secrets, generated activities, testing, metadata, versioning, and packaging. -- Continue marketplace and plugin infrastructure. The Nuplane modular server, sample package, package manifest metadata, and [#7310](https://github.com/elsa-workflows/elsa-core/issues/7310) point to a compelling future where Elsa distributions can install safe custom modules without users maintaining a separate host app. +- Create an Extension Platform track. Package manifests, shell-feature discovery, Connections/Secrets adoption, generated activity providers, test harness patterns, documentation, and contribution templates should make extensions feel like product-quality packages rather than repo-adjacent samples. +- Create an OpenAPI activity provider that turns an OpenAPI document into typed designer activities. This is a recurring ask in [#2961](https://github.com/elsa-workflows/elsa-core/issues/2961) and [#6360](https://github.com/elsa-workflows/elsa-core/issues/6360), and it is the best foundation for a broad connector story. The existing Extensions OpenAPI work should be reconciled with this product goal. +- Define a connector SDK before adding many one-off integrations. The SDK should cover authentication, secrets, generated activities, testing, metadata, versioning, packaging, Studio property editors, and manifest-driven installation. +- Continue marketplace and plugin infrastructure. The Nuplane modular server, sample package, extension package manifest metadata, and [#7310](https://github.com/elsa-workflows/elsa-core/issues/7310) point to a compelling future where Elsa distributions can install safe custom modules without users maintaining a separate host app. +- Promote Agents to a strategic integration lane: provider matrix, OpenAI and Claude PR resolution, local/OpenRouter/custom endpoints, MCP/tool lifecycle, tool selection in Studio, and auditability. [elsa-extensions#58](https://github.com/elsa-workflows/elsa-extensions/issues/58), [elsa-extensions#98](https://github.com/elsa-workflows/elsa-extensions/pull/98), and [elsa-extensions#63](https://github.com/elsa-workflows/elsa-extensions/pull/63) show both demand and active work. +- Prioritize enterprise productivity integrations by leverage: Azure DevOps has a concrete implementation path ([elsa-extensions#124](https://github.com/elsa-workflows/elsa-extensions/issues/124), [elsa-extensions#125](https://github.com/elsa-workflows/elsa-extensions/pull/125)); Teams, OneDrive, SharePoint, Google Docs, and Google Sheets should follow a shared connector model rather than separate bespoke designs. +- Treat dynamic activity generation as a platform primitive. Azure Functions ([elsa-extensions#39](https://github.com/elsa-workflows/elsa-extensions/issues/39)) and registered-method activities ([elsa-extensions#48](https://github.com/elsa-workflows/elsa-extensions/issues/48)) both point toward schema/method-driven activity generation that can also serve OpenAPI, SDK-generated connectors, and internal enterprise APIs. +- Improve data and automation authoring quality: SQL drag/drop reliability ([elsa-extensions#79](https://github.com/elsa-workflows/elsa-extensions/issues/79)), SQL IntelliSense ([elsa-extensions#88](https://github.com/elsa-workflows/elsa-extensions/issues/88)), Dapper package splitting ([elsa-extensions#131](https://github.com/elsa-workflows/elsa-extensions/issues/131), [elsa-extensions#132](https://github.com/elsa-workflows/elsa-extensions/pull/132)), MongoDB secrets parity ([elsa-extensions#126](https://github.com/elsa-workflows/elsa-extensions/issues/126)), WatchFileSystem ([elsa-extensions#90](https://github.com/elsa-workflows/elsa-extensions/issues/90)), and command-line activities ([elsa-extensions#36](https://github.com/elsa-workflows/elsa-extensions/issues/36)). - Resolve the MassTransit strategy after the v9 licensing change. [discussion #6583](https://github.com/elsa-workflows/elsa-core/discussions/6583) raises a practical ecosystem risk; Elsa should either provide a clean split or reduce dependency weight through a smaller messaging abstraction. - Clarify Azure Functions and worker-service hosting patterns. [discussion #4707](https://github.com/elsa-workflows/elsa-core/discussions/4707) and [discussion #7420](https://github.com/elsa-workflows/elsa-core/discussions/7420) show demand for non-traditional hosts, Windows services, and serverless-adjacent deployments. - Add data movement and streaming workflow primitives. [#4809](https://github.com/elsa-workflows/elsa-core/issues/4809) frames this as datasets, linked services, transforms, and stream-oriented processing inspired by Azure Data Factory and stream analytics. @@ -89,6 +180,7 @@ Recommended success measures: - a team can connect to a REST API from its OpenAPI spec without hand-building HTTP activities - marketplace packages can declare features, dependencies, settings, infrastructure needs, and security posture - integrations are tested with the same rigor as core modules, not shipped as opaque examples +- extension maturity is visible to users as released, main-only, PR open, proposal, or needs-maintainer ## 4. Observability And Operations @@ -98,8 +190,9 @@ High-value items: - Finish the diagnostics trilogy: structured logs, console logs, and an explicit OpenTelemetry boundary. Structured and console logs now exist; OpenTelemetry traces/metrics need a current module story because [PR #5810](https://github.com/elsa-workflows/elsa-core/pull/5810) previously introduced a module, [#5988](https://github.com/elsa-workflows/elsa-core/issues/5988) asks for default metrics, and the current repo no longer contains an `Elsa.OpenTelemetry` module. - Add default workflow semantic metrics: started, resumed, suspended, faulted, completed, active, activity executed/faulted, queue depth, recovery count, drain count, and dispatch latency. Align with OpenTelemetry semantic convention work where possible. -- Build Studio diagnostics pages that are useful under pressure: live console, structured logs, trace/metric links, workflow incident timelines, source health, dropped-event counters, and export/copy affordances. +- Build Studio diagnostics pages that are useful under pressure: live console, structured logs, trace/metric links, workflow incident timelines, source health, dropped-event counters, source selection, filters, URL state, export/copy affordances, and direct deep links to workflow instances. - Make execution history easier to reason about: distinguish faulted, interrupted, cancelled, crash-recovered, retried, and operator-modified workflows consistently across API, Studio, logs, and metrics. +- Connect diagnostics to workflow navigation: trace/span IDs, log source IDs, child workflow chains, alterations, runtime admin actions, and recovery actions should be correlated instead of presented as isolated tables. Recommended success measures: @@ -116,6 +209,8 @@ High-value items: - Publish canonical OIDC recipes for Blazor Server, WASM, separate server/studio, and all-in-one hosts. [#7181](https://github.com/elsa-workflows/elsa-core/issues/7181) shows both implementation and documentation demand. - Provide a production security guide: API keys, JWT/OIDC, default admin bootstrap, scripting trust levels, C# expression risks, Docker demo boundaries, secret masking, tenant isolation, and permission design. - Expand authorization coverage tests around workflow instances, runtime admin, diagnostics, labels, tenants, and HTTP endpoint activities. +- Add Studio governance controls: tenant/role-based activity visibility, permission-aware menus/routes, feature-gated modules, and clear behavior for hidden activities in existing workflow definitions. [elsa-studio#584](https://github.com/elsa-workflows/elsa-studio/issues/584) captures the authoring side of this enterprise need. +- Complete localization and white-label readiness: translation contribution docs, coverage status, missing key checks, branding hooks, and supportable customization patterns. Studio issues and discussions show setup/coverage friction in [elsa-studio#771](https://github.com/elsa-workflows/elsa-studio/issues/771), [elsa-studio discussion #695](https://github.com/elsa-workflows/elsa-studio/discussions/695), and [elsa-studio discussion #678](https://github.com/elsa-workflows/elsa-studio/discussions/678). - Improve multi-tenant ergonomics: tenant-agnostic workflows, high tenant counts, tenant validation modes, cache isolation, and clear migration guidance after the 3.6 tenant ID convention changes. - Create an enterprise deployment checklist for Kubernetes, reverse proxies/base paths, TLS/custom CAs, database migrations, health checks, backups, and disaster recovery. @@ -133,6 +228,8 @@ High-value items: - Build AI-assisted workflow generation that produces multiple visible activities from intent rather than hiding logic in one script activity. This direction is proposed in [discussion #7367](https://github.com/elsa-workflows/elsa-core/discussions/7367). - Provide an Elsa MCP/tooling surface for reading, validating, editing, and explaining workflow JSON/ElsaScript. This would make Elsa a strong fit for AI-enabled .NET development environments. +- Align AI authoring with the Extensions Agents work: provider abstractions, MCP tools, OpenAI/Claude/local model support, tool approval, secrets handling, and Studio UX should share contracts instead of creating parallel AI stacks. +- Build a Studio copilot only after the authoring contracts are stable: validation, generated activity metadata, designer APIs, diagnostics links, and test scaffolding should be available before AI generation becomes prominent. [elsa-studio#553](https://github.com/elsa-workflows/elsa-studio/issues/553) has clear community signal and maintainer interest, but it should be an accelerator on top of reliable authoring. - Add "explain this workflow", "find risky activities", "suggest tests", and "generate migration notes" capabilities backed by workflow graph metadata. - Pair AI generation with validation: generated workflows should include test scaffolds, required input/output definitions, secrets handling, and clear review diffs. @@ -147,15 +244,17 @@ Recommended success measures: Near term: 1. Finish runtime confidence work: graceful shutdown remaining tasks, recovery clarity, distributed runtime regressions, security documentation, and OIDC recipes. -2. Make workflow authoring less painful: organization, search, progress/timeline APIs, testing docs, and state machine Studio/docs completion. -3. Publish the updated roadmap and keep release discussions/milestones linked from it so users can plan adoption. +2. Stabilize Studio authoring: designer regression harness, input/property-editor fixes, async dispatch/run UX, state machine Studio/docs completion, and a clear UI framework direction. +3. Make workflow authoring easier to manage at scale: organization, search, progress/timeline APIs, testing docs, and user preference/table-state persistence. +4. Reconcile shipped extension foundations with roadmap status: package manifests, Connections/Secrets, OpenAPI, Agents, schedulers, messaging, and integration maturity labels. Mid term: 1. OpenAPI activity provider plus connector SDK. -2. Marketplace/plugin installation path built on Nuplane, shell features, and package manifests. -3. OpenTelemetry module boundary and default workflow metrics. -4. Workflow debugging and replay-oriented incident analysis. +2. Extension Platform: generated activities, contribution harnesses, Studio extension recipes, package manifest maturity, and marketplace/plugin installation path built on Nuplane and shell features. +3. OpenTelemetry module boundary, default workflow metrics, and Studio trace/log/incident correlation. +4. Workflow debugging, replay-oriented incident analysis, child workflow navigation, and operator recovery UX. +5. Agents provider matrix and MCP/tool lifecycle, with Studio UX that keeps generated workflows inspectable. Longer term: @@ -167,7 +266,8 @@ Longer term: ## Maintainership Recommendations - Keep this roadmap in source control and mirror major changes to [#3232](https://github.com/elsa-workflows/elsa-core/issues/3232). Users are explicitly asking for roadmap visibility in [discussion #7202](https://github.com/elsa-workflows/elsa-core/discussions/7202) and the latest roadmap issue comment. -- Use labels or milestones to connect issues to these roadmap themes. Many high-value issues are currently unlabeled, which makes demand hard to see. +- Use labels, milestones, or GitHub Projects to connect Core, Studio, and Extensions issues to these roadmap themes. Many high-value issues are currently unlabeled, which makes demand hard to see. - Close or update stale roadmap items that are already implemented, especially state machine, activity testing, ElsaScript, diagnostics, and graceful shutdown foundations. -- Prefer platform primitives over one-off features: connector SDK before many connectors, plugin system before bespoke managed extensibility, runtime abstraction before framework-specific actor work. +- Prefer platform primitives over one-off features: connector SDK before many connectors, Extension Platform before bespoke package work, plugin system before bespoke managed extensibility, runtime abstraction before framework-specific actor work. - Do not hide reliability work under "maintenance". Recovery, distributed correctness, migrations, security posture, and observability are core product features for a workflow engine. +- Enable GitHub Discussions for `elsa-extensions` or explicitly route roadmap discussion into labeled issues/projects. Right now extension demand is visible, but fragmented across issue comments and open pull requests. diff --git a/doc/security-assessment/elsa-core-architecture-patterns.md b/doc/security-assessment/elsa-core-architecture-patterns.md new file mode 100644 index 000000000..84c63b2ed --- /dev/null +++ b/doc/security-assessment/elsa-core-architecture-patterns.md @@ -0,0 +1,280 @@ +# Elsa Workflows Core — Architecture pattern assessment + +| Field | Value | +|---|---| +| **Repository / project** | elsa-workflows/elsa-core | +| **Git ref** | release/3.7.0 | +| **Version** | 3.7.0 | +| **Assessment date (UTC)** | 2026-05-20T00:00:00Z | +| **Assessment method** | Static code analysis, Documentation review, Architecture pattern mapping | +| **Model and tools** | Claude Sonnet 4.6 · Static code analysis · Architecture Pattern Catalog v0.3 | +| **Assessment scope** | All 50 source projects in src/ — core engine + all modules | +| **Related documents** | elsa-core-profile.md, elsa-core-software-quality.md, elsa-core-iso25010.md | + +> **AI-assisted assessment — human review required** +> This document was produced by static analysis of the release/3.7.0 branch. No tests were executed and no runtime behaviour was observed. Ratings reflect what the framework provides out of the box; patterns marked Extensible require host-application code. Confirm every critical finding against the live codebase before using this assessment for architectural decisions. + +--- + +## Coverage level legend + +| Symbol | Level | Meaning | +|---|---|---| +| ✅ | Fully covered | First-class, production-ready support out of the box | +| 🟡 | Partially covered | Addresses the problem but with limitations or missing capabilities | +| 🔧 | Extensible / bring your own | Provides the hook; implementation is up to you | +| ❌ | Not covered | Outside scope; a separate tool is needed | + +--- + +## 1. Integration patterns + +| Pattern | Coverage | Assessment | +|---|---|---| +| API gateway | ❌ | Elsa exposes REST endpoints (via `Elsa.Workflows.Api`) and provides auth middleware but is not an API gateway product. Routing, rate limiting, and cross-cutting enforcement belong to the host (YARP, NGINX, AWS API Gateway). | +| Request-reply (REST/RPC/gRPC) | ✅ | The `Elsa.Workflows.Api` module delivers a full REST API (FastEndpoints). `SendHttpRequest` and `SendHttpRequestBase` activities enable synchronous HTTP calls from within workflows. OpenAPI/Swagger docs are generated in development mode. | +| Asynchronous request-reply | ✅ | The bookmark/stimulus subsystem is a first-class async request-reply mechanism: a workflow suspends on a `Bookmark` and resumes when a matching stimulus arrives (`IStimulusSender`, `IWorkflowInbox`). `BackgroundWorkflowDispatcher` decouples dispatch from execution. | +| Message channel / message bus | 🟡 | The internal `Elsa.Mediator` module provides in-process channels (`ICommandsChannel`, `INotificationsChannel`, `IJobsChannel`) with background, sequential, and parallel publishing strategies. There is no native integration with external brokers (RabbitMQ, Azure Service Bus, etc.); the docker-compose file ships a RabbitMQ service only as a development convenience; no code wire-up exists in the library. | +| Publish-subscribe | 🟡 | Internal pub-sub is first class: `INotificationSender` broadcasts `INotification` events to all registered `INotificationHandler` instances. `IEventPublisher` / `PublishEvent` activity allows workflow-level event broadcasting that can trigger or resume other workflows. External broker pub-sub is not covered. | +| Webhook / callback | 🟡 | The `Elsa.Http` module provides `HttpEndpoint` activities that act as incoming webhook receivers. `SendHttpRequest` can call external URLs. However, there is no outbound webhook dispatch service (no `IWebhookDispatcher`, no HMAC signing, no retry queue for outbound callbacks); host applications must build this. | +| Point-to-point channel | 🟡 | The bookmark queue (`IBookmarkQueue`, `BookmarkQueueWorker`) implements exactly-one-receiver semantics for work items. The distributed variant (`DistributedBookmarkQueueWorker`) adds distributed locking to prevent duplicate processing. No external broker-backed point-to-point channel. | +| Data pipeline / ETL | 🟡 | `BulkDispatchWorkflows` and `ForEach`/`ParallelForEach` activities can orchestrate batch processing pipelines over data collections. No native connector framework for external data sources or built-in ETL transforms. | +| File-based exchange | 🟡 | `Elsa.WorkflowProviders.BlobStorage` (FluentStorage) allows workflow definitions to be loaded from blob/file storage. `DownloadHttpFile` / `WriteFileHttpResponse` activities handle file transfer in HTTP workflows. No generic file-event trigger or file watcher built in. | +| Anti-corruption layer (ACL) | 🔧 | The `IActivityStateFilter` / `IActivityStateFilterManager` pipeline allows host code to intercept and transform activity I/O. The reference implementation (`HttpRequestAuthenticationHeaderFilter`) masks sensitive headers. A full ACL translating a legacy data model must be written by the host application. | +| Strangler fig | 🔧 | No explicit strangler-fig tooling. The modular feature system (`IShellFeature`, `FeatureBase`) and the ability to register custom `IActivityProvider` implementations allow incremental replacement of legacy process logic, but the routing and traffic-splitting infrastructure is external. | +| Messaging bridge | ❌ | No broker bridge/adapter; connecting incompatible messaging systems requires an external component. | +| Gateway aggregation | 🔧 | Workflow activities can call multiple HTTP services and aggregate responses, but there is no dedicated API gateway aggregation layer. Custom composite activities can serve this role. | +| Gateway offloading | ❌ | Cross-cutting concerns (TLS termination, rate limiting, request correlation) must be handled by the host's API gateway, not Elsa. | +| Ambassador | ❌ | No sidecar/ambassador proxy pattern; network concerns are handled by ASP.NET Core middleware or external infrastructure. | +| Sidecar | ❌ | Not applicable to a workflow engine library. | +| Service mesh | ❌ | Not applicable; requires external infrastructure (Istio, Linkerd). | + +--- + +## 2. Processing and workflow patterns + +| Pattern | Coverage | Assessment | +|---|---|---| +| Saga / process orchestration | ✅ | This is the central purpose of Elsa. Long-running, multi-step, stateful workflow orchestration is first-class. Durable state is checkpointed between activities, compensation is partially supported via `IIncidentStrategy`, and the `Alterations` subsystem allows runtime correction of running workflows. | +| Choreography | 🟡 | The internal `INotificationSender`/`IEventPublisher` + `Event` activity allow decentralised event-driven coordination between concurrently running workflows. However, there is no visual choreography view, and cross-workflow event schemas are not enforced. | +| Competing consumers | ✅ | `BookmarkQueueWorker` processes items from the `IBookmarkQueue`; multiple application instances compete via `DistributedBookmarkQueueWorker` (file-system distributed lock, Medallion.Threading). Named dispatcher channels (`DispatcherChannel`) allow workload segmentation. | +| Priority queue | ❌ | No priority queue support in the bookmark queue or dispatcher. All queued items are processed FIFO. | +| Sequential convoy | 🟡 | `SingletonStrategy` and `CorrelatedSingletonStrategy` activation validators prevent concurrent instances on the same correlation ID, effectively serialising processing per correlation key. No general partitioned sequential convoy for arbitrary message streams. | +| Pipes and filters | ✅ | First-class dual pipeline: `IWorkflowExecutionPipeline` and `IActivityExecutionPipeline` each have a composable middleware builder. Workflow and activity execution pass through registered middleware stages. | +| Batch processing | 🟡 | `BulkDispatchWorkflows` and `ForEach`/`ParallelForEach` support batch iteration. `RestartInterruptedWorkflowsTask` processes stale instances in configurable batch sizes. No dedicated batch job scheduler (Quartz, Hangfire) is included; the built-in scheduler is time-trigger based. | +| Stream processing | ❌ | No continuous stream processing. Workflows are discrete instances; there is no Kafka/EventHub consumer or reactive stream pipeline. | +| Rule engine | ❌ | No embedded rule engine. Conditional logic lives in `If`/`Switch` activities driven by C#/JavaScript/Liquid expressions. For an external rule engine, a custom activity is needed. | +| Calculation engine | 🔧 | Multi-language expression evaluation (C#, JavaScript, Python, Liquid, ElsaScript) can serve as a calculation engine within activities, but there is no dedicated domain-specific calculation runtime. | +| Document / output generation | ❌ | No PDF, Word, or structured document generation. Custom activities calling external rendering services are needed. | +| Scheduler agent supervisor | ✅ | `RestartInterruptedWorkflowsTask` (recurring task, distributed lock guarded) detects workflows that have exceeded the `InactivityThreshold` and restarts them. `HeartbeatGenerator` monitors per-instance liveness. This is a first-class supervisor pattern. | +| Claim check | 🔧 | The `IStorageDriver` abstraction (with `MemoryStorageDriver` and blob-backed drivers) allows large variable data to be stored externally and referenced by ID within the workflow state. The check-in/check-out mechanics must be coded in custom activities. | +| Compensating transaction | 🟡 | `IIncidentStrategy` hooks (e.g., `ContinueWithIncidentsStrategy`) handle faults at the activity level, but there is no out-of-the-box backward compensation chain (no "undo stack"). Compensation logic must be manually modelled as workflow branches or alteration handlers. | + +--- + +## 3. Data management patterns + +| Pattern | Coverage | Assessment | +|---|---|---| +| CQRS | 🟡 | Internal mediator (`ICommandSender` / `INotificationSender`) separates commands from notifications. Workflow management and runtime stores are separate EF Core `DbContext` instances, enabling independent read/write tuning. However, there are no dedicated read-model projections; queries hit the same store as writes. | +| Event sourcing | ❌ | Workflow execution is snapshot-based (full state serialised to `WorkflowState`), not event-sourced. The `WorkflowExecutionLogStore` provides an append-only execution log for auditing but is not a replayable event stream from which state can be reconstructed. | +| Cache / cache-aside | ✅ | `Elsa.Caching` module with `IMemoryCache` is used throughout. `CachingTriggerStore` and `CachingWorkflowRuntimeFeature` implement cache-aside for triggers and workflow definitions. Cache invalidation is notification-driven (`InvalidateTriggersCache`, `InvalidateWorkflowsCache`). | +| Read replica | 🔧 | EF Core supports read replicas via connection string configuration; Elsa's `WorkflowManagementPersistenceFeature` and `WorkflowRuntimePersistenceFeature` can be pointed at read replicas. No first-class API; host must configure EF Core accordingly. | +| Data warehouse / data mart | ❌ | No analytics or data warehouse integration. | +| Materialized view | ❌ | No materialized view pattern. Aggregations are computed at query time. | +| Index table | 🟡 | EF Core migrations create indices on frequently queried columns (workflow instance status, correlation ID, definition ID). These are hardcoded in migration files and not dynamically configurable via an index-table abstraction. | +| Sharding | ❌ | No sharding support. Multi-tenancy isolation uses separate databases per tenant via `Tenant.GetConnectionString()`, which approximates horizontal partitioning, but there is no query router or rebalancing tooling. | +| External configuration store | 🟡 | `Elsa.KeyValues` provides a key-value store (`IKeyValueStore`) that can persist arbitrary named values. `ConfigurationTenantsProvider` and `ConfigurationBasedUserProvider` load data from `IConfiguration` (which can be backed by Azure App Config, AWS SSM, etc.). No dedicated dynamic configuration store abstraction. | +| Binary / blob storage | ✅ | `Elsa.WorkflowProviders.BlobStorage` (FluentStorage) provides a pluggable `IBlobStorageProvider` for loading workflow definitions from blob storage. `IStorageDriver` allows workflow variable data to be externally stored. | +| Search index | ❌ | No full-text search index. Workflow instance and definition queries use EF Core `IQueryable` filtering only. | +| Database per service | 🟡 | Management and Runtime each have their own EF Core `DbContext` (`ManagementElsaDbContext`, `RuntimeElsaDbContext`) and can be pointed at separate databases. Tenant-specific connection strings are supported. However, all modules in a shell share the same database by default; isolation is opt-in. | +| Shared database | ✅ | Default deployment uses a single SQLite/PostgreSQL/SQL Server/MySQL/Oracle database shared across all modules. This is the expected configuration for single-server deployments. | + +--- + +## 4. Reliability and resilience patterns + +| Pattern | Coverage | Assessment | +|---|---|---| +| Circuit breaker | 🔧 | `IResilienceStrategy` (Polly) is the extension point. `HttpResilienceStrategy` registers a retry policy; a circuit-breaker `IResilienceStrategy` can be added by calling `AddResilienceStrategyType()` in `ResilienceFeature`. No circuit-breaker strategy is shipped out of the box. | +| Retry with exponential backoff | ✅ | `HttpResilienceStrategy` provides configurable retry with exponential backoff, jitter, and max attempts via Polly `RetryStrategyOptions`. The `ResilientActivityInvoker` applies the strategy and records `RetryAttemptRecord`s for diagnostics. Any `IResilientActivity` can use this. | +| Bulkhead | 🔧 | Named dispatcher channels (`DispatcherChannel`) logically partition workflow execution queues, providing bulkhead-like isolation between workload classes. No Polly `BulkheadOptions` strategy is shipped; one can be registered via `IResilienceStrategy`. | +| Queue-based load levelling | 🟡 | `BookmarkQueueWorker` decouples workflow execution from stimulus arrival, smoothing spikes. The queue is stored in a database table (not a durable message broker), so it lacks broker-level durability and dead-letter support. | +| Rate limiting / throttling | ❌ | No rate-limiting middleware or activity throttling. ASP.NET Core rate-limiting middleware can be added by the host application. | +| Idempotency | 🟡 | `SingletonStrategy` and `CorrelatedSingletonStrategy` prevent duplicate workflow instances per correlation key. Bookmark hashing (`IStimulusHasher`) deduplicates stimulus delivery. However, there is no general-purpose idempotency key store for idempotent API calls or external operations. | +| Health endpoint monitoring | 🟡 | `services.AddHealthChecks()` is wired in the reference server (`Elsa.Server.Web`) and `app.MapHealthChecks("/")` is configured. However, no Elsa-specific health checks (e.g., workflow runtime healthy, scheduler running, database connected) are registered in the framework; the host gets a bare ASP.NET Core health endpoint. | +| Leader election | 🟡 | `InstanceHeartbeatMonitorService` uses `IDistributedLockProvider` (Medallion.Threading / file-system lock) to elect a single monitor instance across a cluster. `SingleNodeTaskAttribute` marks tasks that should run on only one node; `TaskExecutor` honours this attribute. True Raft/etcd-based leader election is not included. | +| Deployment stamps | ❌ | No first-class multi-region stamp deployment tooling. | +| Geode | ❌ | No geo-distribution support. | +| Quarantine | ❌ | No quarantine/validation gate for inbound messages or data. | +| Timeout | ✅ | `Delay`, `StartAt`, `Timer`, and `Cron` activities implement time-based waits. `CancellationToken` propagation allows workflow-level and activity-level timeout cancellation. `JintOptions` exposes a JavaScript execution timeout. | + +--- + +## 5. Security patterns + +| Pattern | Coverage | Assessment | +|---|---|---| +| Authentication (AuthN) | ✅ | `Elsa.Identity` ships JWT Bearer and API key authentication out of the box (`DefaultAuthenticationFeature`). JwtBearer and `AspNetCore.Authentication.ApiKey` are configured via a multi-scheme policy scheme. User/application/role providers are pluggable (configuration-based, store-based, or custom). | +| Authorisation (AuthZ) RBAC/ABAC | 🟡 | Role-based authorisation is implemented: `Role` entities carry a `Permissions` collection (string claims), and `IAuthorizationPolicy` is applied per endpoint. There is no ABAC (attribute-based) engine; fine-grained resource-level policies beyond role+permission strings are not supported. | +| Token service / OAuth 2.0 / OIDC | 🟡 | `IAccessTokenIssuer` issues JWT access and refresh tokens (RS256/HS256). The identity module does not act as a full OAuth 2.0 authorization server (no authorization code flow, no PKCE, no discovery endpoint). For production SSO, an external IdP (Keycloak, Azure AD) should be used; Elsa's token service is intended for machine-to-machine and Studio authentication. | +| Federated identity | 🔧 | `DefaultAuthenticationFeature` registers a JWT Bearer handler that accepts tokens from any configured issuer. Plugging in an external OIDC IdP requires host-level configuration of `JwtBearerOptions` (audience, authority). No built-in OIDC callback flow. | +| Valet key | ✅ | `Elsa.SasTokens` provides `ITokenService` (ASP.NET Core Data Protection backed) for creating time-limited, signed access tokens. The `HttpEndpoint` activity uses SAS tokens to generate caller-specific signed URLs. | +| Encryption at rest and in transit | 🔧 | HTTPS is handled by the ASP.NET Core/Kestrel host. Data-at-rest encryption requires database-level or column-level encryption configured outside Elsa. ASP.NET Core Data Protection backs `SasTokens`. No field-level encryption of workflow variables is provided. | +| Payload signing | 🟡 | SAS token service signs payloads using Data Protection. No general HMAC webhook signature for outbound HTTP calls; the `SendHttpRequest` activity supports a configurable `Authorization` header but not automatic payload signing. | +| Secrets management | ❌ | No secrets manager integration (HashiCorp Vault, Azure Key Vault, AWS Secrets Manager). Sensitive configuration is read from `IConfiguration` which must be backed by a secrets provider at the host level. | +| Defence in depth | 🟡 | Multiple independent layers are present: JWT + API key auth, ASP.NET Core authorization policies, `IActivityStateFilter` data masking, sandboxed expression evaluation (Jint JS engine). However, some layers require explicit host configuration; they are not automatically composed. | +| Zero trust / least privilege | 🔧 | Permission strings on roles allow least-privilege API access. Multi-tenancy isolation is available. `LocalHostPermissionRequirement` restricts the security-root policy to localhost by default. A full zero-trust implementation (per-resource, per-request verification, mutual TLS) is not built in. | +| Audit log | ✅ | `IWorkflowExecutionLogStore` persists an immutable, per-activity execution log record for every workflow run, including activity ID, instance ID, timestamps, and custom event data. The log is queryable via the REST API. This constitutes a functional workflow audit trail. | +| Data masking / tokenisation | 🟡 | `IActivityStateFilter` / `IActivityStateFilterManager` provides a framework for masking activity I/O before persisting state. The reference filter (`HttpRequestAuthenticationHeaderFilter`) masks HTTP `Authorization` headers. General PII tokenisation must be implemented as custom filters. | + +--- + +## 6. Communication and messaging patterns + +| Pattern | Coverage | Assessment | +|---|---|---| +| Notification / push | 🟡 | SignalR hub (`WorkflowInstanceHub`, `RealTimeWorkflowUpdatesFeature`) pushes workflow progress events to connected Studio clients. The feature is available but disabled by default in the reference server (awaiting authenticated Studio requests). Server-sent events and WebSockets for other consumers are not built in. | +| Mail service (transactional email) | ❌ | No email activity or SMTP integration in the open-source library. The docker-compose file includes smtp4dev for local testing but there is no `SendEmail` activity or `IEmailSender` abstraction. | +| Scheduled job / cron | ✅ | `Cron`, `Timer`, `Delay`, and `StartAt` activities are first-class. `LocalScheduler` (in-memory, in-process) and `DefaultWorkflowScheduler` manage trigger schedules. `RecurringTask` / `SingleNodeTaskAttribute` pattern handles periodic maintenance tasks. | +| Dead letter channel | ❌ | No dead-letter queue. Unprocessable bookmark queue items are purged after TTL expiry (`BookmarkQueuePurgeOptions`); they are not routed to a dead-letter store for inspection or replay. | +| Message expiry / TTL | ✅ | `WorkflowInboxMessage` has an `ExpiresAt` field. `BookmarkQueuePurgeOptions.Ttl` controls item expiry. Stale items are purged by `PurgeBookmarkQueueRecurringTask`. | +| Correlation identifier | ✅ | Correlation ID is a first-class concept throughout: workflow instances carry `CorrelationId`, stimuli carry `StimulusMetadata.CorrelationId`, and `CorrelationStrategy` / `CorrelatedSingletonStrategy` use it for activation control and deduplication. | + +--- + +## 7. Scalability and deployment patterns + +| Pattern | Coverage | Assessment | +|---|---|---| +| Horizontal scaling | 🟡 | `Elsa.Workflows.Runtime.Distributed` enables multi-node operation: `DistributedWorkflowRuntime`, `DistributedWorkflowClient` (with distributed locking via Medallion.Threading), and `DistributedBookmarkQueueWorker` coordinate across instances. The default lock provider is file-system based (suitable for single-machine multi-instance, not cross-host); production clustering requires a Redis/SQL distributed lock provider to be substituted. | +| Vertical scaling | ❌ | Not a pattern Elsa addresses; handled by infrastructure. | +| Auto-scaling | ❌ | No auto-scaling hooks. Kubernetes HPA or cloud provider auto-scaling can be applied externally. | +| Blue-green deployment | 🔧 | Workflow versioning (major/minor version on definitions) supports running old and new versions concurrently. Blue-green traffic routing is an infrastructure concern external to Elsa. | +| Canary deployment | 🔧 | Same as blue-green: version-based routing is possible but requires external traffic management. | +| Feature flags | ❌ | No feature flag mechanism; `IShellFeature` / `FeatureBase` are compile-time/startup-time module toggles, not runtime feature flags (no LaunchDarkly integration, no `IFeatureManager` from Microsoft.FeatureManagement). | +| Compute resource consolidation | ✅ | The library is designed to be embedded in a single .NET process alongside the host application. `Elsa.ModularServer.Web` and `Elsa.Server.Web` demonstrate collocating engine, API, scheduling, and management in one process. | +| Static content hosting | ❌ | Not applicable to the workflow engine library (static assets belong to Elsa Studio). | +| Serverless / FaaS | ❌ | No FaaS adapter. Elsa requires a long-running ASP.NET Core host. | + +--- + +## 8. Observability and operations patterns + +| Pattern | Coverage | Assessment | +|---|---|---| +| Metrics, logs, traces (three pillars) | 🟡 | Structured `ILogger` logging is used throughout. Polly telemetry listeners (`RetryTelemetryListener`) emit retry events. `WorkflowExecutionLogRecord` is a domain-level execution log. However, there is no first-party OpenTelemetry integration, no metrics export (Prometheus, OTLP), and no distributed trace propagation (W3C Trace Context headers are not automatically forwarded across workflow activity calls). | +| Distributed tracing | ❌ | No `ActivitySource` / `OpenTelemetry.Api` instrumentation in the library. Trace context must be propagated manually by host-application code. | +| Log aggregation | 🔧 | Standard `ILogger` output can be directed to any sink (Serilog, NLog, Application Insights) by the host. Elsa does not bundle a log aggregation stack. | +| Health check / readiness / liveness | 🟡 | ASP.NET Core `AddHealthChecks()` is configured in the reference server. No Elsa-specific health check providers (workflow runtime healthy, EF Core migration complete, scheduler running) are shipped in the library. | +| Chaos engineering | ❌ | No chaos engineering tooling. | +| Synthetic monitoring | ❌ | No synthetic monitoring built in. | +| Alert routing and on-call | ❌ | No alert routing; external observability platforms (PagerDuty, Datadog) must be used. | + +--- + +## 9. Frontend and API design patterns + +| Pattern | Coverage | Assessment | +|---|---|---| +| Backend for frontend (BFF) | ❌ | No BFF pattern. The REST API (`Elsa.Workflows.Api`) is a general-purpose API shared by all consumers (Studio, integrators, scripts). | +| Multi-tenancy | ✅ | First-class multi-tenancy: `Elsa.Tenants` provides a pluggable tenant resolution pipeline (`ITenantResolverPipelineBuilder`), `ITenantStore`, per-tenant connection strings, and `TenantResolutionMiddleware`. Tenant isolation for workflow definitions and instances is supported via `TenantId` on all entities. | +| Server-side rendering (SSR) | ❌ | elsa-core is a server-side workflow library/engine; UI/UX patterns apply to Elsa Studio (separate repository), not to this library. | +| Progressive web app (PWA) | ❌ | elsa-core is a server-side workflow library/engine; UI/UX patterns apply to Elsa Studio (separate repository), not to this library. | +| Micro-frontends | ❌ | elsa-core is a server-side workflow library/engine; UI/UX patterns apply to Elsa Studio (separate repository), not to this library. | +| GraphQL API | ❌ | No GraphQL endpoint. The REST API uses FastEndpoints. | +| Hypermedia / HATEOAS | ❌ | No HATEOAS; REST responses return flat resource representations. | + +--- + +## 10. Distributed systems coordination patterns + +| Pattern | Coverage | Assessment | +|---|---|---| +| Distributed locking | ✅ | `Medallion.Threading` (`IDistributedLockProvider`) is used in `DistributedWorkflowClient`, `DistributedBookmarkQueueWorker`, and `InstanceHeartbeatMonitorService`. The default provider is `FileSystemDistributedSynchronizationProvider` (single-machine). Redis, SQL Server, or PostgreSQL providers can be substituted via DI. | +| Two-phase commit (2PC) | ❌ | No distributed 2PC. Consistency is achieved through pessimistic distributed locking and idempotent retry, not atomic distributed transactions. | +| Consensus (Raft/Paxos) | ❌ | No consensus protocol. Leader/coordinator selection relies on optimistic distributed locking. | +| Eventual consistency / BASE | ✅ | The system is designed around eventual consistency: workflow dispatching is asynchronous, bookmark delivery may be delayed, state commits are periodic (configurable via `IWorkflowCommitStateHandler`). Callers must tolerate stale reads of workflow instance status. | +| Outbox pattern | ❌ | No transactional outbox. Notification publishing and workflow dispatching are not atomically bundled with database writes. In high-reliability scenarios, an external outbox (MassTransit Outbox, Wolverine) must be layered on. | +| Change data capture (CDC) | ❌ | No CDC integration. | +| Inbox pattern | 🟡 | `IWorkflowInbox` / `WorkflowInboxMessage` (now superseded by the Stimulus API) provides an inbox table that stores messages until they can be delivered to a matching workflow. The deduplication is hash-based (`IStimulusHasher`). The implementation is partial: it is marked `[Obsolete]` and has no dead-letter or replay mechanism. | + +--- + +## 11. AI and intelligent system patterns + +| Pattern | Coverage | Assessment | +|---|---|---| +| Model serving / inference endpoint | ❌ | No ML model serving; a custom HTTP activity calling an inference endpoint is required. | +| RAG | ❌ | No retrieval-augmented generation support. | +| AI agent orchestration | 🔧 | Elsa's long-running workflow orchestration primitives (human-in-loop via `HttpEndpoint` callback, event-driven resumption, Fork/Join, distributed execution) can serve as the control plane for multi-agent pipelines. This requires building agent activities; nothing is provided out of the box. | +| Human-in-the-loop | 🟡 | The bookmark/stimulus pattern natively supports suspending a workflow until a human action is received (e.g., `HttpEndpoint` callback, custom signal). The `Alterations` subsystem allows operators to manually modify running workflow state. There is no built-in task-assignment UI or human task service; custom activity + external task management is needed. | +| Shadow mode / champion-challenger | ❌ | No shadow execution mode. | + +--- + +## 12–17. UX, accessibility, CX, information architecture, design system, frontend performance patterns + +All patterns in categories 12 through 17 are rated **❌ Not covered**. + +elsa-core is a server-side workflow library/engine. UI/UX patterns (UX design, accessibility, customer experience, information architecture, design systems, and frontend performance) apply to Elsa Studio, which is a separate repository (`elsa-workflows/elsa-studio`). These categories are not assessable for this library. + +--- + +## Summary coverage matrix + +| Category | ✅ Fully | 🟡 Partially | 🔧 Extensible | ❌ Not covered | +|---|---|---|---|---| +| 1. Integration patterns | 2 | 5 | 3 | 7 | +| 2. Processing and workflow patterns | 5 | 4 | 2 | 3 | +| 3. Data management patterns | 3 | 4 | 2 | 4 | +| 4. Reliability and resilience patterns | 2 | 4 | 3 | 3 | +| 5. Security patterns | 3 | 4 | 3 | 2 | +| 6. Communication and messaging patterns | 3 | 1 | 0 | 2 | +| 7. Scalability and deployment patterns | 1 | 1 | 3 | 4 | +| 8. Observability and operations patterns | 0 | 2 | 1 | 4 | +| 9. Frontend and API design patterns | 1 | 0 | 0 | 6 | +| 10. Distributed systems coordination | 2 | 1 | 0 | 4 | +| 11. AI and intelligent system patterns | 0 | 1 | 1 | 3 | +| 12–17. UX / frontend patterns (all) | 0 | 0 | 0 | all | +| **Totals (cat. 1–11)** | **22** | **27** | **18** | **42** | + +--- + +## Architecture fit summary + +### Right tool when: + +- You need durable, long-running orchestration of multi-step business processes in a .NET application, including suspend/resume across process restarts. +- Your domain requires complex conditional branching, parallel execution (`Fork`, `Parallel`, `ParallelForEach`), and sequential coordination inside a single business process. +- You need human-in-the-loop workflows that pause on an HTTP callback or an external signal and resume after a person acts. +- You run on ASP.NET Core and want a REST API, a visual designer, and embedded scheduling in a single deployable unit. +- You need multi-tenant SaaS workflow automation with per-tenant connection strings and tenant-scoped workflow isolation. +- You want to invoke external HTTP services from within workflows and need retry-with-backoff (via Polly) built into those activity calls. +- Your workload is event-driven — triggering workflows from HTTP endpoints, cron schedules, or domain events — and you want durable correlation of long-lived process instances. +- You need runtime workflow correction (adding/modifying/cancelling running instances) via the Alterations API without redeployment. + +### Wrong tool when: + +- You need real-time stream processing of continuous data (Kafka, EventHub consumer pipelines) — Elsa is discrete-instance based. +- You need a full external message broker integration (RabbitMQ, Azure Service Bus publish/subscribe at scale) without writing a custom adapter. +- You need OpenTelemetry distributed tracing instrumentation out of the box — no `ActivitySource` instrumentation exists in this library. +- You need a feature-complete OAuth 2.0 authorization server (PKCE, authorization code flow, discovery endpoint) — the identity module provides tokens for Studio/API access only. +- You are targeting FaaS/serverless execution environments — Elsa requires a continuously running ASP.NET Core host. +- You need event sourcing with full replay capability — workflow state is snapshot-based, not an event-sourced log. +- You need a transactional outbox guarantee for publishing events atomically with database writes — this is not implemented. + +### Elsa Workflows Core in context: named gaps with recommendations + +| Gap | Severity | Concrete recommendation | +|---|---|---| +| No transactional outbox | High | In distributed deployments, dispatching a workflow command and committing the trigger state in separate steps can lead to duplicate or lost executions. Integrate MassTransit Outbox or Wolverine as the `IWorkflowDispatcher` backend to guarantee at-least-once delivery atomically with the database transaction. | +| Distributed lock provider defaults to file system | High | `FileSystemDistributedSynchronizationProvider` does not work across machines with separate file systems. Replace with `DistributedLock.Redis` or `DistributedLock.SqlServer` via a single DI registration before deploying multiple nodes. | +| No OpenTelemetry instrumentation | Medium | There are no `ActivitySource` spans, no metric counters, and no trace context propagation. Add an `IWorkflowExecutionMiddleware` and `IActivityExecutionMiddleware` that start and stop OpenTelemetry `Activity` spans; wire W3C `traceparent` headers through `SendHttpRequest` calls. | +| No circuit breaker shipped | Medium | `HttpResilienceStrategy` provides retry but no circuit breaker. A transient outage of a downstream HTTP service will exhaust retries on every workflow resumption. Implement a `CircuitBreakerResilienceStrategy : IResilienceStrategy` using `Polly.Extensions.Http` and register it in `ResilienceFeature`. | +| No dead-letter channel | Medium | Failed or undeliverable bookmark queue items are silently purged after TTL expiry with no visibility. Add a `DeadLetterBookmarkStore` that captures expired items with their last exception, plus a REST endpoint to inspect and replay them. | +| Health checks are host-owned, not framework-owned | Medium | The reference server wires `AddHealthChecks()` but registers no Elsa-specific checks. Ship `IHealthCheck` implementations for: workflow runtime responsive, EF Core migrations current, scheduler running, distributed lock provider reachable. This enables Kubernetes readiness/liveness probes to detect partial failures. | +| No secrets management integration | Medium | Sensitive data (API keys used in workflow activities, SMTP credentials) flows through `IConfiguration` and is persisted in workflow state unless filtered. Integrate ASP.NET Core's `ISecretManager` abstraction or add a `SecretsStorageDriver : IStorageDriver` backed by Azure Key Vault / HashiCorp Vault for variables tagged as sensitive. | +| Partial compensation / no undo stack | Medium | There is no built-in backward compensation chain. When a multi-step workflow fails mid-way, compensating already-completed steps must be hand-coded as workflow branches or `Alteration` handlers. Consider implementing a `CompensationScope` composite activity that records completed compensable steps and executes them in reverse order on fault. | +| Multi-tenancy isolation is shared-table by default | Low–Medium | By default, all tenants share EF Core tables with a `TenantId` column filter. A misconfigured query can expose cross-tenant data. For regulated environments, use per-tenant connection strings (already supported via `Tenant.GetConnectionString()`) and test isolation explicitly. | +| No feature flags at runtime | Low | `IShellFeature` toggles are startup-time only. If a rollout strategy (canary, A/B) for a new workflow version is required, integrate Microsoft.FeatureManagement or a similar SDK; Elsa's workflow versioning system alone is not sufficient for traffic-level rollout control. | \ No newline at end of file diff --git a/doc/security-assessment/elsa-core-iso25010.md b/doc/security-assessment/elsa-core-iso25010.md new file mode 100644 index 000000000..741df9bab --- /dev/null +++ b/doc/security-assessment/elsa-core-iso25010.md @@ -0,0 +1,1109 @@ +# Elsa Workflows Core — ISO 25010 quality assessment + +| Field | Value | +|---|---| +| **Repository / project** | elsa-workflows/elsa-core | +| **Git ref** | release/3.7.0 | +| **Version** | 3.7.0 | +| **Assessment date (UTC)** | 2026-05-20T00:00:00Z | +| **Assessment method** | Static code analysis, Documentation review, ISO 25010:2023 framework | +| **Model and tools** | Claude Sonnet 4.6 · Static code analysis · ISO 25010:2023 | +| **Assessment scope** | All 50 source projects in src/ — primary focus on core engine, security, resilience modules | +| **Related documents** | elsa-core-profile.md, elsa-core-architecture-patterns.md, elsa-core-software-quality.md | + +> ⚠️ **AI-assisted assessment — human review required** +> +> This document was produced by an AI model (Claude Sonnet 4.6) using static code analysis +> and the ISO 25010:2023 framework. AI assessments are based on pattern recognition +> over source files and documentation. They may contain incorrect assumptions, missed +> context, or findings that do not apply to your specific operational environment. +> +> **This document is a working draft — not a final approved report.** +> Human engineers and architects must complete all "Human completion required" checklists +> and all verdict fields before this document can be treated as approved. +> The final approved report is this document with all verdict fields completed, +> signed off by a named reviewer. + +--- + +> **How to use this document** +> +> 1. Review each "What Claude assesses" block — correct any wrong assumptions in the Notes field +> 2. Complete each "Human completion required" checklist — gather evidence from CONFIG, RUNTIME, and OPS sources +> 3. Fill in each Verdict field based on the combined evidence (Claude's + yours) +> 4. Where Claude confidence is Low or Very low, weight your own evidence more heavily +> 5. Sign off the document and record the reviewer name and date on each verdict + +--- + +## 1. Functional suitability + +### 1.1 Functional completeness + +**Evidence sources:** `CODE` `DOCS` +**Claude confidence:** High — README feature list cross-checked against interface contracts and activity inventory + +**What Claude assesses (code-level):** +- The activity library in `src/modules/Elsa.Workflows.Core/Activities/` contains a broad primitive set: `Sequence`, `Flowchart`, `Fork`, `Parallel`, `ParallelForEach`, `ForEach`, `For`, `While`, `If`, `Switch`, `Break`, `Complete`, `End`, `Fault`, `Finish`, `SetVariable`, `Correlate`, `DynamicActivity`, `Inline`, and `Workflow` itself. The primitives cover sequential, branching, looping, parallel, and fault-handling patterns. +- The README explicitly lists built-in activities for HTTP calls, email, scheduling, messaging, and PDF generation — modules for HTTP (`Elsa.Http`), scheduling (`Elsa.Scheduling`), and scripting (C#, JavaScript, Python, Liquid) are present as separate module projects. +- Workflow versioning is present through `IWorkflowDefinitionPublisher.PublishAsync / RetractAsync / RevertVersionAsync` and `GetDraftAsync/SaveDraftAsync` — covering the full definition lifecycle. +- Persistence is covered by EF Core providers for SQLite, SQL Server, PostgreSQL, MySQL, and Oracle. The `IWorkflowDefinitionStore` / `IWorkflowInstanceStore` abstraction permits additional providers. +- Workflow alteration (`Elsa.Alterations`, `Elsa.Alterations.Core`) enables in-flight workflow modification. +- `IWorkflowRuntime`, `IWorkflowClient`, `IWorkflowDispatcher` cover runtime start, resume, and dispatch surface. +- In-memory stores exist for all major entities — meaning fully functional operation without external persistence for testing/embedded scenarios. +- The README states Elsa supports .NET 6 and beyond; `Directory.Build.props` targets net8.0;net9.0;net10.0, so .NET 6/7 are no longer in scope for this branch — assumption: the README text predates this release line. + +**Human completion required:** +- [ ] Verify that all features listed in the README are actually enabled/shipped in the v3.7.0 NuGet packages — some may be optional add-ons not in this repo (DOCS) +- [ ] Confirm that claimed MongoDB/Dapper persistence providers are shipped or externally maintained — no Dapper or Mongo .csproj found in this repository at this ref (DESIGN) +- [ ] Validate that workflow versioning (publish/retract/revert) round-trips correctly through all supported persistence providers (RUNTIME) + +**Verdict (human to complete):** +[ ] ✅ Meets requirements [ ] 🟡 Partially meets [ ] ❌ Does not meet [ ] ⬜ Cannot assess +Notes: ___ +Reviewed by: ___ · Date: ___ + +--- + +### 1.2 Functional correctness + +**Evidence sources:** `CODE` `RUNTIME` +**Claude confidence:** Medium — pipeline structure and tests suggest correctness intent; behavioural correctness requires runtime execution + +**What Claude assesses (code-level):** +- The activity execution pipeline (`ActivityExecutionPipeline`, `WorkflowExecutionPipeline`) is well-structured and testable via `IActivityExecutionMiddleware`. Incorrect execution order would be visible in the pipeline builder tests. +- `ValidatingWorkflowDispatcher` decorates the base dispatcher and validates channel configuration before dispatch — a correctness guard at the dispatch boundary. +- `WorkflowHost.RunWorkflowAsync` explicitly guards against resuming workflows not in the `Running` state and logs a warning — preventing silent incorrect state transitions. +- 532 test methods across 12 unit and 9 integration projects provide regression coverage. Integration tests exercise actual workflow execution paths. +- `IIncidentStrategy` provides pluggable incident handling, meaning incorrect fault propagation would be customisable; however, the correctness of built-in strategies requires runtime validation. +- Typed `Input` / `Output` generic constraints (87 source files use them) reduce the risk of incorrect data type assumptions between activities. +- Assumption: test coverage percentage is unknown — 532 tests across ~2,416 source files is a ratio that could indicate under-coverage. + +**Human completion required:** +- [ ] Run the full test suite and confirm all tests pass on the release/3.7.0 branch (RUNTIME) +- [ ] Review integration test coverage for the core workflow execution paths (fork/join, long-running suspend/resume, fault handling) (CODE) +- [ ] Identify whether mutation testing or property-based testing is used anywhere — not observed in the static scan (CODE) + +**Verdict (human to complete):** +[ ] ✅ Meets requirements [ ] 🟡 Partially meets [ ] ❌ Does not meet [ ] ⬜ Cannot assess +Notes: ___ +Reviewed by: ___ · Date: ___ + +--- + +### 1.3 Functional appropriateness + +**Evidence sources:** `DESIGN` `OPS` +**Claude confidence:** Low — appropriateness is a stakeholder judgement; code indicates design intent only + +**What Claude assesses (code-level):** +- The design separates _definition_ (what a workflow does) from _instance_ (a running execution), which aligns with standard workflow engine patterns. The separation is consistent with established domain models (Windows Workflow Foundation, BPMN conceptually). +- The `IWorkflowActivationStrategy` abstraction allows per-workflow control over singleton, per-correlationId, or custom instantiation — indicating the design anticipated multi-tenancy and concurrency appropriateness concerns. +- The feature system (`IShellFeature` / `DependsOn`) allows adopters to select only the capabilities they need, which is appropriate for a library meant to be embedded in diverse host applications. +- Assumption: whether the current feature surface matches the documented roadmap (issue #3232 referenced in README) cannot be determined from static analysis. + +**Human completion required:** +- [ ] Validate with product owners that the current API surface matches stakeholder requirements for v3.7.0 (DESIGN) +- [ ] Review GitHub issue #3232 (roadmap) and confirm in-scope items are delivered (DESIGN) +- [ ] Collect feedback from adopters on whether built-in activities cover their primary use cases without excessive custom activity development (OPS) + +**Verdict (human to complete):** +[ ] ✅ Meets requirements [ ] 🟡 Partially meets [ ] ❌ Does not meet [ ] ⬜ Cannot assess +Notes: ___ +Reviewed by: ___ · Date: ___ + +--- + +## 2. Performance efficiency + +### 2.1 Time behaviour + +**Evidence sources:** `CODE` `RUNTIME` +**Claude confidence:** Low — structural patterns suggest async-first design; actual latencies require measurement + +**What Claude assesses (code-level):** +- All public async APIs use `Task` or `ValueTask` return types and accept `CancellationToken`. `ConfigureAwait.Fody` is enabled as a build weaver across all source projects, injecting `ConfigureAwait(false)` at compile time — eliminating a common ASP.NET throughput bottleneck without manual annotation. +- `IBookmarkQueueWorker`, `BackgroundStimulusDispatcher`, `BackgroundWorkflowDispatcher`, and `BookmarkQueueWorker` use queue-based background processing, decoupling dispatch latency from execution latency. +- The caching layer (`Elsa.Caching`) uses `IMemoryCache` with change-token invalidation, reducing repeated definition lookups. `CachingWorkflowRuntimeFeature` exists for the runtime layer. +- `BenchmarkDotNet` is used in `Elsa.Workflows.PerformanceTests/ConsoleActivitiesBenchmark` — a `WriteLine` workflow benchmark exists, showing performance measurement tooling is in place. +- No use of `Span`, `Memory`, or `ArrayPool` was found in `Elsa.Workflows.Core` — high-volume serialization paths may allocate more than necessary. This is an assumption based on absence of these patterns; they may exist in lower-level helpers not scanned. + +**Human completion required:** +- [ ] Run BenchmarkDotNet suite and record baseline throughput (workflow executions/sec) for common patterns (RUNTIME) +- [ ] Profile activity execution pipeline under load for GC pressure (RUNTIME) +- [ ] Confirm whether the single `ConsoleActivitiesBenchmark` is the only benchmark or whether more exist in the project (CODE) + +**Verdict (human to complete):** +[ ] ✅ Meets requirements [ ] 🟡 Partially meets [ ] ❌ Does not meet [ ] ⬜ Cannot assess +Notes: ___ +Reviewed by: ___ · Date: ___ + +--- + +### 2.2 Resource utilisation + +**Evidence sources:** `CODE` `RUNTIME` +**Claude confidence:** Low — async patterns are sound; memory allocation profile requires runtime profiling + +**What Claude assesses (code-level):** +- `ResilienceContextPool.Shared.Get()` and `.Return()` in `ResilientActivityInvoker` shows explicit context pooling for Polly resilience contexts — avoiding per-invocation allocation. +- `IAsyncDisposable` / `IDisposable` patterns appear in 28 files (e.g., `InstanceHeartbeatMonitorService`, `WorkflowHost`), reducing timer and scope leakage. +- The `BookmarkQueueWorker` uses an `RateLimitedFuncExtensions` pattern to prevent runaway processing — indicating awareness of CPU consumption boundaries. +- No explicit memory limits, object pool for `ActivityExecutionContext`, or `RecyclableMemoryStream` usage was observed in the core execution path — large workflow state serialization could produce high allocations per execution. +- Assumption: resource utilisation in production depends on concurrency settings and payload sizes, which are configurable and not assessable statically. + +**Human completion required:** +- [ ] Profile memory allocations per workflow execution under representative load (RUNTIME) +- [ ] Measure CPU utilisation per 100 concurrent workflow executions under load test (RUNTIME) +- [ ] Review whether `ActivityExecutionContext` and `WorkflowExecutionContext` objects are pooled or newly allocated per run (CODE) + +**Verdict (human to complete):** +[ ] ✅ Meets requirements [ ] 🟡 Partially meets [ ] ❌ Does not meet [ ] ⬜ Cannot assess +Notes: ___ +Reviewed by: ___ · Date: ___ + +--- + +### 2.3 Capacity + +**Evidence sources:** `RUNTIME` `OPS` +**Claude confidence:** Very low — capacity is entirely a runtime/operational concern + +**What Claude assesses (code-level):** +- The distributed runtime (`Elsa.Workflows.Runtime.Distributed`) with Medallion.Threading distributed locking indicates the design supports horizontal scaling — the lock provider abstracts node-local vs. distributed locking. +- `InstanceHeartbeatService` / `InstanceHeartbeatMonitorService` form a node-registry pattern that tracks active instances — a prerequisite for capacity-aware load distribution. +- No static configuration caps (max concurrent workflows, queue depth limits, database connection pool sizes) were observed in the source code defaults — these are expected to be configured per deployment. + +**Human completion required:** +- [ ] Define and document capacity targets (concurrent workflow instances, peak throughput) (OPS) +- [ ] Run load tests to determine capacity ceiling per node configuration (RUNTIME) +- [ ] Verify that the bookmark queue depth and processing rate are observable and configurable for the target workload (CONFIG + RUNTIME) + +**Verdict (human to complete):** +[ ] ✅ Meets requirements [ ] 🟡 Partially meets [ ] ❌ Does not meet [ ] ⬜ Cannot assess +Notes: ___ +Reviewed by: ___ · Date: ___ + +--- + +## 3. Compatibility + +### 3.1 Co-existence + +**Evidence sources:** `CONFIG` `RUNTIME` +**Claude confidence:** Low — co-existence depends on deployment topology, which cannot be fully assessed statically + +**What Claude assesses (code-level):** +- The application (`Elsa.Server.Web`) configures `AllowedHosts: "*"` in appsettings — no host restriction in the default config, which is typical for library reference hosts. +- HTTP routes are configurable via `HttpActivityOptions` (`BasePath`, `ApiRoutePrefix: "elsa/api"`) — reducing the chance of path collision with other co-hosted applications. +- The EF Core schema defaults to `"Elsa"` (`ElsaDbContextBase.ElsaSchema`) with a configurable `SchemaName` option — allowing co-existence in shared databases without table name collisions. +- The feature module system (`IShellFeature`) means Elsa services are scoped into a `CShells` shell container — reducing DI registration conflicts with other application services. Assumption: the actual isolation boundary of `CShells` is not examined in detail here. +- Multi-tenant prefix routing (`TenantPrefixHttpEndpointRoutesProvider`) adds URL prefixes per tenant, which aids HTTP path co-existence in shared deployments. + +**Human completion required:** +- [ ] Test Elsa co-hosted with common ASP.NET middleware stacks (authentication middleware ordering, SignalR, rate-limiting middleware) (RUNTIME) +- [ ] Verify that the `elsa/api` route prefix does not conflict with any existing API routes in representative host applications (CONFIG + RUNTIME) +- [ ] Confirm EF Core schema isolation works correctly when Elsa shares a database with the host application (RUNTIME) + +**Verdict (human to complete):** +[ ] ✅ Meets requirements [ ] 🟡 Partially meets [ ] ❌ Does not meet [ ] ⬜ Cannot assess +Notes: ___ +Reviewed by: ___ · Date: ___ + +--- + +### 3.2 Interoperability + +**Evidence sources:** `CODE` `CONFIG` +**Claude confidence:** Medium — integration points are visible in code; correctness of data exchange requires runtime validation + +**What Claude assesses (code-level):** +- `Elsa.Http` provides bidirectional HTTP interoperability: inbound via `HttpWorkflowsMiddleware` (workflow-as-HTTP-endpoint) and outbound via `SendHttpRequest`-style activities. +- `Elsa.Workflows.Api` exposes a REST API surface with FastEndpoints across endpoints for definitions, instances, activity descriptors, bookmarks, tasks, events, and scripting — covering the full management plane. +- Swagger/OpenAPI integration is present via `Elsa.Api.Common` (`SwaggerExtensions.cs`) and FastEndpoints-based OpenAPI generation, enabling machine-readable API contracts. +- SAS token interoperability uses ASP.NET Core `IDataProtection` for time-limited, HMAC-protected tokens — a standard .NET interoperability mechanism. +- Scripting interoperability covers C#, JavaScript (Jint), Python (`Elsa.Expressions.Python`), and Liquid — enabling diverse host application scripting ecosystems. +- The `Elsa.Api.Client` project provides a typed .NET client library for REST API consumers, reducing integration friction. +- Assumption: message queue integration (RabbitMQ, Azure Service Bus etc.) is not present in this repository at this ref — the README mentions it but no corresponding module was found. This may be in a separate repository. + +**Human completion required:** +- [ ] Confirm whether message bus integration modules (RabbitMQ, Azure Service Bus, MassTransit) are shipped as separate NuGet packages not in this repository (DESIGN) +- [ ] Validate OpenAPI spec accuracy against actual endpoint behaviour using automated contract tests (RUNTIME) +- [ ] Test `Elsa.Api.Client` against the server API with version-matched packages (RUNTIME) + +**Verdict (human to complete):** +[ ] ✅ Meets requirements [ ] 🟡 Partially meets [ ] ❌ Does not meet [ ] ⬜ Cannot assess +Notes: ___ +Reviewed by: ___ · Date: ___ + +--- + +## 4. Interaction capability + +> Note: Elsa Core is a workflow engine library, not a user-facing UI product. All sub-characteristics below are assessed against the **developer/API consumer** experience. "Users" means developers embedding or consuming the library. + +### 4.1 Appropriateness recognisability + +**Evidence sources:** `CODE` `DOCS` +**Claude confidence:** Medium — entry points and feature names are inspected; discoverability experience is partly subjective + +**What Claude assesses (code-level):** +- The primary entry point is `services.AddElsa(elsa => { ... })` — a single fluent registration method consistent with ASP.NET Core conventions. This matches what developers expect from a .NET library. +- The `UseXxx` pattern (`UseElsaScriptBlobStorage`, `UseWorkflowRuntime`, etc.) follows the established ASP.NET Core `IApplicationBuilder.Use*` idiom, aiding recognisability for .NET developers. +- 175 `*Feature*.cs` files expose discoverable capability toggles — each with `[ShellFeature(DisplayName = "...", Description = "...", DependsOn = [...])]` attributes that self-describe purpose and dependencies. +- The README is well-structured with a Docker quick-start, feature list, code examples (HTTP + email workflow in ~15 lines), and visual designer screenshots — reducing the time to first impression. +- NuGet documentation XML (`GenerateDocumentationFile=true` globally) is enabled — IntelliSense summaries are generated for all public APIs. + +**Human completion required:** +- [ ] Survey new adopters on how long it takes to run the first workflow — time-to-hello-world is a key recognisability metric (OPS) +- [ ] Check whether the NuGet package descriptions and tags match the library's actual capabilities (CONFIG) + +**Verdict (human to complete):** +[ ] ✅ Meets requirements [ ] 🟡 Partially meets [ ] ❌ Does not meet [ ] ⬜ Cannot assess +Notes: ___ +Reviewed by: ___ · Date: ___ + +--- + +### 4.2 Learnability + +**Evidence sources:** `DOCS` +**Claude confidence:** Medium — README and inline documentation quality are assessed; external docs site content is not accessible from code + +**What Claude assesses (code-level):** +- The README provides a quick-start Docker command, a C# workflow code example, and a visual designer screenshot — multiple learning paths are represented. +- The `doc/` directory exists at the repository root — its contents were not fully explored, but its presence suggests additional documentation artefacts. +- The `Elsa.Identity` module includes a `README.md` — suggesting module-level documentation practice exists, though coverage across all 50 modules is unverified. +- `src/common/Elsa.Testing.Shared/` and `Elsa.Testing.Shared.Integration/` provide helper classes (`WorkflowTestFixture`, `ActivityTestFixture`, `RunWorkflowResultAssertions`) that reduce the learning curve for adopters writing tests. +- Links to a dedicated documentation site (`v3.elsaworkflows.io` referenced indirectly) and Discord, Stack Overflow, and Gurubase AI are present in the README badges — multiple learning support channels exist. +- Assumption: depth and accuracy of the external docs site are not assessable from code alone. + +**Human completion required:** +- [ ] Review the `doc/` directory contents and confirm documentation accuracy against the v3.7.0 API (DOCS) +- [ ] Verify that module-level README files exist for the most commonly used modules beyond `Elsa.Identity` (DOCS) +- [ ] Assess external documentation site (v3.elsaworkflows.io) completeness and currency against this release (DOCS) + +**Verdict (human to complete):** +[ ] ✅ Meets requirements [ ] 🟡 Partially meets [ ] ❌ Does not meet [ ] ⬜ Cannot assess +Notes: ___ +Reviewed by: ___ · Date: ___ + +--- + +### 4.3 Operability + +**Evidence sources:** `CODE` `CONFIG` +**Claude confidence:** Medium — management API surface and operational endpoints are inspectable + +**What Claude assesses (code-level):** +- `/health/ready` and `/health/live` endpoints are registered (`services.AddHealthChecks()`, `app.MapHealthChecks("/")`), enabling readiness and liveness probing — a baseline operational requirement. +- The REST management API (`Elsa.Workflows.Api`) provides endpoints for workflow definitions (CRUD, publish/retract/revert), instances, activity executions, bookmarks, tasks, events, and features — covering the full operational lifecycle from outside the process. +- `InstanceHeartbeatService` / `InstanceHeartbeatMonitorService` provide node-level liveness tracking with configurable intervals — enabling distributed operations. +- The appsettings structure uses `reloadConfigOnChange: true` — configuration can be updated without restart for supported settings. +- Log levels are configurable per namespace in `appsettings.json` — operators can tune verbosity without code changes. +- Assumption: the health check at `/` (root path) is a simplification for the reference app; production deployments may need dedicated `/health/ready` and `/health/live` path mappings. + +**Human completion required:** +- [ ] Confirm that health checks are wired to meaningful infrastructure probes (DB connectivity, message bus) rather than always returning healthy (CODE + RUNTIME) +- [ ] Verify that the management REST API supports pagination and filtering for large workflow instance datasets (CODE) +- [ ] Test the node heartbeat mechanism under simulated node failure to confirm detection latency meets operational SLAs (RUNTIME) + +**Verdict (human to complete):** +[ ] ✅ Meets requirements [ ] 🟡 Partially meets [ ] ❌ Does not meet [ ] ⬜ Cannot assess +Notes: ___ +Reviewed by: ___ · Date: ___ + +--- + +### 4.4 User error protection + +**Evidence sources:** `CODE` +**Claude confidence:** High — validation patterns are directly inspectable in code + +**What Claude assesses (code-level):** +- `ValidatingWorkflowDispatcher` validates channel existence before dispatch and returns a typed `DispatchWorkflowResponse.UnknownChannel()` error — preventing silent misdispatch. +- `DefaultAccessTokenIssuer` throws descriptive exceptions for missing signing key, issuer, or audience configuration (`"No signing key configured"`, `"No issuer configured"`, `"No audience configured"`) — failing fast at configuration time rather than silently issuing malformed tokens. +- Nullable reference types are enabled globally — the compiler enforces null-safety across all 50 projects, reducing null-dereference errors at runtime. +- `IWorkflowDefinitionPublisher` uses typed result objects (`PublishWorkflowDefinitionResult`) rather than raw booleans, giving callers structured error information. +- `IIncidentStrategy` provides a pluggable mechanism to handle activity faults — preventing unhandled exceptions from silently terminating workflows without record. +- `IWorkflowActivationStrategy` allows per-workflow control over duplicate-instance protection (e.g., singleton activation), preventing accidental duplicate workflow starts. +- Assumption: input validation on API endpoint request models is delegated to FastEndpoints' built-in validation — this was not directly examined. + +**Human completion required:** +- [ ] Verify that API endpoints validate request models (required fields, format constraints) and return structured 400 Bad Request responses (CODE + RUNTIME) +- [ ] Confirm that `IIncidentStrategy` defaults (when no custom strategy is configured) surface faults visibly rather than swallowing them (CODE) + +**Verdict (human to complete):** +[ ] ✅ Meets requirements [ ] 🟡 Partially meets [ ] ❌ Does not meet [ ] ⬜ Cannot assess +Notes: ___ +Reviewed by: ___ · Date: ___ + +--- + +### 4.5 User engagement + +**Evidence sources:** `DESIGN` `RUNTIME` +**Claude confidence:** Very low — as a developer library, "engagement" maps to developer experience satisfaction, which cannot be assessed from code + +**What Claude assesses (code-level):** +- The fluent builder API (`IWorkflowBuilder`, `WorkflowBase`), `AddElsa(elsa => ...)` registration, and `UseXxx` extension methods suggest deliberate attention to developer ergonomics. +- The visual designer integration (`Elsa Studio`) is referenced in README but lives in a separate repository — code-level evidence of the embedded designer experience is not present in this repository. +- The Docker quick-start with a single `docker run` command lowers the activation barrier, which correlates positively with developer engagement. +- Community channels (Discord, Stack Overflow) and AI assistant (Gurubase) are listed — indicating investment in the developer community experience. + +**Human completion required:** +- [ ] Collect developer satisfaction metrics (GitHub stars trajectory, Discord activity, NuGet downloads) to assess engagement trends (OPS) +- [ ] Gather adopter feedback on the API ergonomics, particularly around workflow definition authoring in code vs. designer (OPS) +- [ ] Assess time-to-first-working-integration for a new developer unfamiliar with the library (OPS) + +**Verdict (human to complete):** +[ ] ✅ Meets requirements [ ] 🟡 Partially meets [ ] ❌ Does not meet [ ] ⬜ Cannot assess +Notes: ___ +Reviewed by: ___ · Date: ___ + +--- + +### 4.6 Inclusivity + +**Evidence sources:** `CODE` +**Claude confidence:** Medium — i18n and cultural sensitivity in a developer library is a narrow concern; assessed at log message and error message level + +**What Claude assesses (code-level):** +- No `IStringLocalizer`, `.resx` resource files, or `ResourceManager` usage was found in any module — error messages and log strings are hardcoded in English. For a developer-facing library this is common and typically acceptable. +- `StringComparison.OrdinalIgnoreCase` and `InvariantCulture` comparisons appear in 43 files — indicating culture-safe string handling in identifiers and route comparison, which prevents locale-dependent bugs. +- No accessibility-specific code was found, which is expected for an API library without a UI component. +- The Docker entrypoint and setup scripts are in English. The README is English-only. Assumption: documentation in other languages is not provided. + +**Human completion required:** +- [ ] Determine whether internationalisation of error messages is a requirement for target customer segments — if multi-language deployments are needed, localisation infrastructure is absent (DESIGN) +- [ ] Confirm that all route and identifier comparisons use `OrdinalIgnoreCase` consistently rather than relying on system locale (CODE) + +**Verdict (human to complete):** +[ ] ✅ Meets requirements [ ] 🟡 Partially meets [ ] ❌ Does not meet [ ] ⬜ Cannot assess +Notes: ___ +Reviewed by: ___ · Date: ___ + +--- + +### 4.7 User assistance + +**Evidence sources:** `DOCS` +**Claude confidence:** Medium — inline documentation and README quality are assessable; external docs site is not + +**What Claude assesses (code-level):** +- `GenerateDocumentationFile=true` is set globally in `Directory.Build.props` — all public APIs emit XML documentation consumed by IDEs as IntelliSense tooltips. +- 59 out of 76 contracts in `Elsa.Workflows.Core/Contracts/` have `` tags — coverage is high but not complete. The suppressed warning `CS1591` (missing XML doc) means undocumented public members do not break the build. +- The `[ShellFeature(DisplayName = "...", Description = "...")]` attribute pattern embeds user-visible descriptions for every feature toggle — these descriptions surface in the feature discovery API (`/elsa/api/features`). +- Discord, Stack Overflow, and a linked docs site provide human-assisted support channels. +- The `IActivityDescriber` and `IActivityDescriptorModifier` interfaces suggest runtime activity self-description — activity metadata (name, description, ports) is available to tooling. +- Warning `CS1591` is suppressed globally — this reduces XML doc coverage enforcement. + +**Human completion required:** +- [ ] Audit XML doc coverage across the 50 modules beyond the `Workflows.Core` contracts — the suppressed CS1591 warning means gaps may be widespread (CODE) +- [ ] Verify that the external documentation site (v3.elsaworkflows.io) references accurate API for v3.7.0 (DOCS) +- [ ] Assess whether activity descriptions in the designer and API surface are sufficient for non-author adopters (OPS) + +**Verdict (human to complete):** +[ ] ✅ Meets requirements [ ] 🟡 Partially meets [ ] ❌ Does not meet [ ] ⬜ Cannot assess +Notes: ___ +Reviewed by: ___ · Date: ___ + +--- + +### 4.8 Self-descriptiveness + +**Evidence sources:** `CODE` +**Claude confidence:** Medium — runtime self-description APIs are inspectable; richness of metadata is partly a runtime concern + +**What Claude assesses (code-level):** +- `IActivityRegistry` and `IActivityDescriber` provide runtime enumeration and description of all registered activities — enabling tooling (designer, API clients) to discover capabilities without documentation. +- The `/elsa/api/features` endpoint (inferred from `Elsa.Workflows.Api/Endpoints/Features/`) exposes enabled feature state at runtime. +- `[ShellFeature(DisplayName, Description, DependsOn)]` attribute metadata is machine-readable — the module system can report its own configuration. +- `IActivity.Version` (integer) on each activity type provides version self-identification — important for workflow definition compatibility checks. +- The workflow JSON format carries type names and versions inline (`IActivity.Type`, `IActivity.Version`), making serialised workflow definitions self-describing. +- `IActivityDescriptorModifier` allows enrichment of activity descriptors post-registration — supporting dynamic self-description extensions. + +**Human completion required:** +- [ ] Verify that the `/elsa/api/features` endpoint returns accurate feature state reflecting the actual DI registrations (RUNTIME) +- [ ] Confirm that the OpenAPI spec at the `swagger` endpoint accurately reflects all registered FastEndpoints routes (RUNTIME) + +**Verdict (human to complete):** +[ ] ✅ Meets requirements [ ] 🟡 Partially meets [ ] ❌ Does not meet [ ] ⬜ Cannot assess +Notes: ___ +Reviewed by: ___ · Date: ___ + +--- + +## 5. Reliability + +### 5.1 Faultlessness + +**Evidence sources:** `CODE` `RUNTIME` +**Claude confidence:** Low — static analysis shows defensive patterns; actual defect rate requires production data + +**What Claude assesses (code-level):** +- Exception handling appears in 267 source files — try/catch blocks are widespread. The `ResilientActivityInvoker` uses a `finally` block to ensure Polly context is always returned to the pool regardless of outcome. +- `DataProtectorTokenService.TryDecryptToken` catches all exceptions silently (bare `catch { }`) and returns `false` — this is intentional for a try-parse pattern but suppresses diagnostic information. +- Nullable reference types enabled globally reduce NullReferenceException risks. +- `ObsoleteAttribute` is used with `error: false` (soft deprecation) rather than hard errors — reducing the risk of compile-time breakage from API evolution while still guiding consumers. +- 12 unit + 9 integration test projects with 532 test methods provide regression protection, but coverage percentage is unknown. +- `TreatWarningsAsErrors=false` globally means code quality warnings do not block builds — a potential source of accumulated defects. + +**Human completion required:** +- [ ] Obtain production defect rate and mean time between failures from an operational deployment (OPS) +- [ ] Enable and review compiler warning output to assess the volume of suppressed warnings (CODE) +- [ ] Run static analysis tools (Roslyn analyzers, SonarQube) and triage findings (CODE) + +**Verdict (human to complete):** +[ ] ✅ Meets requirements [ ] 🟡 Partially meets [ ] ❌ Does not meet [ ] ⬜ Cannot assess +Notes: ___ +Reviewed by: ___ · Date: ___ + +--- + +### 5.2 Availability + +**Evidence sources:** `CODE` `RUNTIME` `OPS` +**Claude confidence:** Low — availability is an operational measurement; code shows structural enablers only + +**What Claude assesses (code-level):** +- Liveness and readiness health endpoints are registered (`app.MapHealthChecks("/")`) — infrastructure probes can detect unhealthy instances. +- `InstanceHeartbeatService` writes periodic heartbeats to a key-value store; `InstanceHeartbeatMonitorService` reads and evaluates them — providing cluster-level node availability awareness. +- Distributed locking via Medallion.Threading ensures workflow execution is coordinated across nodes without requiring a single-instance deployment. +- The `BookmarkQueueWorker` pattern decouples workflow execution from trigger delivery — message queue durability (if an external broker is used) can extend effective availability. +- Health checks at `app.MapHealthChecks("/")` are on the root path in the reference app, which may conflict with normal application responses. Production deployments should map these to dedicated paths. + +**Human completion required:** +- [ ] Measure uptime SLA in a representative deployment over at least 30 days (OPS) +- [ ] Confirm health check probes are connected to actual dependency health (DB, message bus) and not trivially returning 200 (RUNTIME) +- [ ] Test rolling deployment (node drain and restart) to verify zero-downtime upgrade behaviour (RUNTIME) + +**Verdict (human to complete):** +[ ] ✅ Meets requirements [ ] 🟡 Partially meets [ ] ❌ Does not meet [ ] ⬜ Cannot assess +Notes: ___ +Reviewed by: ___ · Date: ___ + +--- + +### 5.3 Fault tolerance + +**Evidence sources:** `CODE` +**Claude confidence:** Medium — Polly integration, incident strategies, and distributed lock usage are directly inspectable + +**What Claude assesses (code-level):** +- `Elsa.Resilience.Core` integrates Polly v8 (`Polly`, `Polly.Extensions` package references) via `IResilienceStrategy` and `ResilientActivityInvoker`. Activities implementing `IResilientActivity` can declare a resilience strategy (retry, circuit-breaker, hedging) per-activity through configurable strategy catalogs. +- `IIncidentStrategy` / `IIncidentStrategyResolver` provide pluggable fault handling at the workflow level — when an activity faults, the strategy determines whether to suspend, fault the workflow, or retry. +- `ITransientExceptionDetector` / `DefaultTransientExceptionStrategy` distinguish transient from permanent failures — Polly retries are applied only to transient exceptions. +- `RetryTelemetryListener` emits retry attempt telemetry through Polly's telemetry pipeline — attempts are recorded and surfaced in the designer via `context.SetRetriesAttemptedFlag()`. +- `IResilienceStrategyCatalog` allows per-deployment strategy registration — operators can define environment-specific retry policies without code changes. +- Distributed locking (`Medallion.Threading`) prevents concurrent execution of the same workflow instance across nodes — a fault isolation boundary. + +**Human completion required:** +- [ ] Test fault tolerance under simulated transient failure (DB unavailability, network timeouts) to verify retry policies engage correctly (RUNTIME) +- [ ] Verify that the circuit-breaker strategy (if provided as a built-in strategy) opens under sustained failure and closes on recovery (RUNTIME) +- [ ] Confirm that unhandled activity exceptions do not silently terminate the workflow process without state persistence (RUNTIME) + +**Verdict (human to complete):** +[ ] ✅ Meets requirements [ ] 🟡 Partially meets [ ] ❌ Does not meet [ ] ⬜ Cannot assess +Notes: ___ +Reviewed by: ___ · Date: ___ + +--- + +### 5.4 Recoverability + +**Evidence sources:** `CODE` `OPS` +**Claude confidence:** Low — state persistence architecture is clear from code; actual recovery time requires failure injection testing + +**What Claude assesses (code-level):** +- `IWorkflowStateSerializer` (with `JsonWorkflowStateSerializer` implementation) serialises complete workflow execution state — this state is persisted to `IWorkflowInstanceStore`, enabling resume from any persisted checkpoint. +- The bookmark system (`IBookmarkStore`, `IBookmarkPersister`) persists suspension points, allowing long-running workflows to resume after process restart. +- `IWorkflowDefinitionStore` / `IWorkflowInstanceStore` abstractions support EF Core providers for all major RDBMS — state persistence durability depends on the provider and its transaction guarantees. +- EF Core migrations are applied via `RunMigrationsHostedService` on startup — schema recovery is automated. +- The distributed runtime's `DistributedBookmarkQueueWorker` handles bookmark processing with distributed lock protection — preventing double-processing after node recovery. +- Assumption: whether workflow instances interrupted mid-execution (process crash between activity completions) can be correctly resumed depends on the persistence provider's transaction boundary — this requires runtime validation. + +**Human completion required:** +- [ ] Test crash recovery: kill the process mid-workflow-execution and verify the workflow resumes correctly after restart (RUNTIME) +- [ ] Measure recovery time objective (RTO) under a process restart scenario with the default EF Core Sqlite provider (RUNTIME) +- [ ] Verify that partially-executed activity state (activity started but not completed) is handled correctly on resume (RUNTIME) + +**Verdict (human to complete):** +[ ] ✅ Meets requirements [ ] 🟡 Partially meets [ ] ❌ Does not meet [ ] ⬜ Cannot assess +Notes: ___ +Reviewed by: ___ · Date: ___ + +--- + +## 6. Security + +### 6.1 Confidentiality + +**Evidence sources:** `CODE` `CONFIG` +**Claude confidence:** High — security configurations and data protection patterns are directly inspectable + +**What Claude assesses (code-level):** +- SAS tokens use `Microsoft.AspNetCore.DataProtection.IDataProtector` (AES-256 GCM by default in ASP.NET Core) for time-limited token encryption — `DataProtectorTokenService` wraps payload serialization and protection/unprotection. +- JWT tokens carry per-user `TenantId` claims — tenant context is embedded in the access token and cannot be spoofed without the signing key. +- API keys are hashed (SHA-256 + random salt via `DefaultSecretHasher`) before storage — plaintext secrets are not persisted. +- The `appsettings.json` reference configuration includes a sample `SigningKey: "sufficiently-large-secret-signing-key"` — this is a placeholder; production must override. No hardcoded production secrets were found in source, but the sample config uses a weak key name that could be copy-paste deployed. +- Multi-tenant EF Core query filters (`SetTenantIdFilter`) apply `HasQueryFilter` globally via `IEntityModelCreatingHandler` — tenant-scoped queries enforce data confidentiality at the ORM layer. +- No TLS configuration is enforced in code (left to ASP.NET Core hosting configuration) — this is appropriate for a library but must be documented for operators. + +**Human completion required:** +- [ ] Verify that production appsettings do not contain `"sufficiently-large-secret-signing-key"` or other placeholder values (CONFIG) +- [ ] Confirm that the `DataProtection` key ring is configured with a durable key store (Azure Key Vault, filesystem, etc.) and not the ephemeral in-process default in production (CONFIG) +- [ ] Audit that tenant ID query filters are correctly applied to all entity types in all EF Core configurations (not only `Elsa.Persistence.EFCore.Common`) (CODE) + +**Verdict (human to complete):** +[ ] ✅ Meets requirements [ ] 🟡 Partially meets [ ] ❌ Does not meet [ ] ⬜ Cannot assess +Notes: ___ +Reviewed by: ___ · Date: ___ + +--- + +### 6.2 Integrity + +**Evidence sources:** `CODE` +**Claude confidence:** High — input validation, SAS token integrity, and workflow state integrity patterns are inspectable + +**What Claude assesses (code-level):** +- SAS tokens use `IDataProtector.ToTimeLimitedDataProtector().Protect()` — any tampered or expired token fails decryption and is rejected at the `TryDecryptToken` level before any workflow action is taken. +- JWT signature validation is enforced via `ConfigureJwtBearerOptions` and `ValidateIdentityTokenOptions` — forged tokens are rejected. +- Workflow definitions carry `int Version` on each activity — version mismatch detection supports integrity of workflow execution against stale definitions. +- EF Core `SaveChangesAsync` runs pre-save handlers (`IEntitySavingHandler`) — providing a hook for integrity enforcement (e.g., `ApplyTenantId` sets tenant ownership on creation). +- `IWorkflowStateSerializer` uses `System.Text.Json` — JSON serialisation with type discriminators, not binary/custom formats — reducing integrity risks from format confusion. +- `ValidatingWorkflowDispatcher` validates channel integrity before dispatch — unconfigured channels are rejected. + +**Human completion required:** +- [ ] Verify that workflow definition imports (JSON import via API) validate schema and activity type compatibility before persisting (CODE + RUNTIME) +- [ ] Confirm that the `IEntitySavingHandler` pipeline cannot be bypassed by direct EF Core operations in custom code (DESIGN) +- [ ] Test that tampered SAS tokens are rejected with appropriate HTTP error responses (RUNTIME) + +**Verdict (human to complete):** +[ ] ✅ Meets requirements [ ] 🟡 Partially meets [ ] ❌ Does not meet [ ] ⬜ Cannot assess +Notes: ___ +Reviewed by: ___ · Date: ___ + +--- + +### 6.3 Non-repudiation + +**Evidence sources:** `CODE` +**Claude confidence:** Medium — execution log storage exists; completeness of audit trail requires deeper review + +**What Claude assesses (code-level):** +- `IActivityExecutionStore` (in `Elsa.Workflows.Runtime/Contracts/`) and `StoreActivityExecutionLogSink` / `StoreWorkflowExecutionLogSink` persist per-activity execution records — these records provide an execution-level audit trail. +- `RetryAttemptRecord` persists retry attempt history including `ActivityInstanceId`, `WorkflowInstanceId`, `AttemptNumber`, and `RetryDelay` — retried operations are traceable. +- JWT claims include the user `Name` and `TenantId` — authentication events can be correlated with workflow actions if the JWT is propagated through execution contexts. +- `ILogRecordStore` / `ILogRecordSink` abstract log record persistence — these are distinct from the application log (ILogger) and represent workflow-level event records. +- No explicit audit event for authentication or API access was observed at the code level (e.g., no "user logged in", "workflow definition modified by X" event emitter). This is a potential gap for compliance use cases. + +**Human completion required:** +- [ ] Determine whether regulatory or contractual requirements mandate full audit trails for workflow operations — if so, assess gap between current execution log and a full audit log (DESIGN) +- [ ] Verify that `IActivityExecutionStore` records include sufficient actor identity (user/app ID) to support non-repudiation claims (CODE) +- [ ] Confirm that log records are immutable once written (no update/delete API exposed) (CODE + RUNTIME) + +**Verdict (human to complete):** +[ ] ✅ Meets requirements [ ] 🟡 Partially meets [ ] ❌ Does not meet [ ] ⬜ Cannot assess +Notes: ___ +Reviewed by: ___ · Date: ___ + +--- + +### 6.4 Accountability + +**Evidence sources:** `CODE` +**Claude confidence:** High — identity model and token structure are directly inspectable + +**What Claude assesses (code-level):** +- JWT tokens embed `Name`, `TenantId`, roles, and permissions claims — authenticated API requests carry caller identity in the token. +- The `DefaultAccessTokenIssuer` encodes `user.Name` as a `JwtRegisteredClaimNames.Name` claim — the acting user is cryptographically bound to each token. +- API key authentication (`DefaultApiKeyProvider`, `AdminApiKeyProvider`) associates each key with an `Application` entity, which has an `Id`, `Name`, and `TenantId` — application-level actions can be attributed to a named application. +- `ClaimsTenantResolver` and `CurrentUserTenantResolver` extract tenant context from claims — multi-tenant operations are tenant-attributed. +- `IUserManager` / `IRoleManager` manage user-role assignments — role-based access enables post-hoc attribution of permissions to actions. +- No code-level evidence of per-request access logging (HTTP access log emitting user identity per API call) was found — this may be delegated to ASP.NET Core middleware or infrastructure. + +**Human completion required:** +- [ ] Verify that ASP.NET Core request logging or application-level middleware captures authenticated user identity per API request (RUNTIME + CONFIG) +- [ ] Confirm that `IActivityExecutionStore` records correlate to the authenticated user/app that triggered the workflow (CODE) + +**Verdict (human to complete):** +[ ] ✅ Meets requirements [ ] 🟡 Partially meets [ ] ❌ Does not meet [ ] ⬜ Cannot assess +Notes: ___ +Reviewed by: ___ · Date: ___ + +--- + +### 6.5 Authenticity + +**Evidence sources:** `CODE` `CONFIG` +**Claude confidence:** High — authentication schemes and configuration are directly readable + +**What Claude assesses (code-level):** +- `DefaultAuthenticationFeature` configures a multi-scheme policy (`Jwt-or-ApiKey`) that routes authentication based on the `Authorization` header format — JWT Bearer for token-based clients, API key for application clients. +- JWT Bearer validation is configured via `ConfigureJwtBearerOptions` and `ValidateIdentityTokenOptions` — token signature, issuer, audience, and expiry are all validated. +- `DefaultSecretHasher` uses SHA-256 with cryptographically random 32-byte salt (`RandomNumberGenerator.GetBytes(32)`) for password and secret hashing — password storage follows a salted-hash pattern. However, SHA-256 is a fast hash; for password hashing, a slow adaptive function (BCrypt, Argon2, PBKDF2) is the current security best practice. This is a notable finding. +- `IRandomStringGenerator` / `DefaultClientIdGenerator` / `DefaultSecretGenerator` use cryptographically random generation for client IDs and secrets. +- `LocalHostRequirementHandler` / `LocalHostPermissionRequirementHandler` allow localhost bypass — this must be disabled in production; its presence in the feature set is appropriate for development convenience. + +**Human completion required:** +- [ ] Evaluate whether SHA-256 + salt is sufficient for the threat model, or whether a slow adaptive password hash (BCrypt/Argon2/PBKDF2) should replace it — this is a security finding that should be assessed by a security engineer (CODE) +- [ ] Confirm that `LocalHostRequirementHandler` bypass is disabled or restricted in production deployments (CONFIG) +- [ ] Test that expired and invalid-issuer JWT tokens are correctly rejected with 401 responses (RUNTIME) + +**Verdict (human to complete):** +[ ] ✅ Meets requirements [ ] 🟡 Partially meets [ ] ❌ Does not meet [ ] ⬜ Cannot assess +Notes: ___ +Reviewed by: ___ · Date: ___ + +--- + +### 6.6 Resistance + +**Evidence sources:** `CODE` `RUNTIME` +**Claude confidence:** Medium — structural resistance patterns are visible; exploit testing requires runtime assessment + +**What Claude assesses (code-level):** +- `NuGetAudit=enable` / `NuGetAuditMode=all` in `Directory.Build.props` — all dependencies (including transitive) are audited for known CVEs at build time. This is a proactive supply-chain resistance measure. +- `EnableTrimAnalyzer=true` is set — IL trimming analysis is enabled, which can expose unsafe reflection patterns that could be exploited in trimmed deployments. +- No evidence of input sanitization or output encoding beyond JSON serialization was found — for HTTP trigger workflows that accept untrusted HTTP body content, this may be relevant. +- Rate limiting is present only in `RateLimitedFuncExtensions` (bookmark queue processing), not at the API ingress layer — no ASP.NET Core rate limiting middleware was observed in `Program.cs`. +- No CORS policy configuration was found in the reference `Program.cs` scan — CORS posture is unclear. +- The `DataProtectorTokenService` silently catches all decryption failures — this prevents error oracle attacks on SAS tokens. + +**Human completion required:** +- [ ] Add API-level rate limiting (ASP.NET Core `RateLimiter` middleware) and confirm it is applied to sensitive endpoints (workflow trigger, authentication) (CODE + CONFIG) +- [ ] Perform penetration testing on the REST API surface, particularly HTTP workflow endpoints that accept untrusted external input (RUNTIME) +- [ ] Configure and validate CORS policy to restrict cross-origin access to the management API (CONFIG) +- [ ] Verify that NuGet audit results are clean (no known high/critical CVEs in dependencies) at this release (CODE) + +**Verdict (human to complete):** +[ ] ✅ Meets requirements [ ] 🟡 Partially meets [ ] ❌ Does not meet [ ] ⬜ Cannot assess +Notes: ___ +Reviewed by: ___ · Date: ___ + +--- + +## 7. Maintainability + +### 7.1 Modularity + +**Evidence sources:** `CODE` +**Claude confidence:** Medium — module boundaries and dependency declarations are directly inspectable + +**What Claude assesses (code-level):** +- The repository contains 50 distinct source projects in `src/`, each as a separate NuGet package with explicit project references — no circular references are enforced by the build system's project graph. +- 141 `IShellFeature`/`IFeature`-implementing files and 175 `*Feature*.cs` files declare `DependsOn` chains — feature-to-feature dependencies are explicit and machine-readable. +- The module system (`Elsa.Common/Elsa.Features`) provides a `Module` abstraction that aggregates features into cohesive units (`AddElsa(...)`) while keeping individual features independently installable. +- Core abstractions (`IActivity`, `IWorkflowRunner`, `IWorkflowRuntime`, store interfaces) live in `Elsa.Workflows.Core` and `Elsa.Workflows.Management` — implementations live in separate provider modules (`Elsa.Persistence.EFCore.*`). This pattern prevents implementation leakage into the core. +- The `Elsa.Resilience.Core` / `Elsa.Resilience` split follows the same core/implementation pattern. +- Assumption: circular dependencies between non-project namespaces within a single project were not checked — namespace-level coupling is not assessed here. + +**Human completion required:** +- [ ] Run dependency analysis (e.g., `dotnet-depends`, NDepend) to visualise cross-project dependency graph and identify any unexpected coupling (CODE) +- [ ] Verify that adding a new persistence provider requires only implementing the store interfaces and registering a new feature — no changes to core modules (CODE) + +**Verdict (human to complete):** +[ ] ✅ Meets requirements [ ] 🟡 Partially meets [ ] ❌ Does not meet [ ] ⬜ Cannot assess +Notes: ___ +Reviewed by: ___ · Date: ___ + +--- + +### 7.2 Reusability + +**Evidence sources:** `CODE` +**Claude confidence:** Medium — abstraction layers and generic patterns are directly inspectable + +**What Claude assesses (code-level):** +- 76 interfaces in `Elsa.Workflows.Core/Contracts/` alone define a broad, stable abstraction surface. Each interface is a seam for substitution and reuse. +- `Elsa.Testing.Shared` and `Elsa.Testing.Shared.Integration` provide reusable test infrastructure (`WorkflowTestFixture`, `ActivityTestFixture`, `RunWorkflowResultAssertions`) — consumers can reuse these without duplication. +- The `IResilienceStrategy` / `IResilienceStrategyCatalog` pattern allows custom resilience strategies to be registered and reused across activities. +- 87 files use typed `Input` / `Output` generics — the generic activity port pattern is reusable across all activity implementations. +- The scheduling (`IScheduler`, `IWorkflowScheduler`) and expressions (`IExpressionHandler`) abstractions are designed for substitution, with multiple implementations per interface. +- `ConfigureAwait.Fody` weaving is reusable infrastructure — new projects that add `Fody` and `ConfigureAwait.Fody` to their project file inherit the pattern without code changes. + +**Human completion required:** +- [ ] Assess whether external consumers (NuGet users) can implement and register custom persistence providers, custom activities, and custom resilience strategies without forking the repository (CODE + DESIGN) +- [ ] Review whether testing shared infrastructure is published as a separate NuGet package for test project consumers (CONFIG) + +**Verdict (human to complete):** +[ ] ✅ Meets requirements [ ] 🟡 Partially meets [ ] ❌ Does not meet [ ] ⬜ Cannot assess +Notes: ___ +Reviewed by: ___ · Date: ___ + +--- + +### 7.3 Analysability + +**Evidence sources:** `CODE` `OPS` +**Claude confidence:** Medium — logging and diagnostic patterns are inspectable; production observability requires operational tooling review + +**What Claude assesses (code-level):** +- `ILogger` is used across 106 source files — structured logging is pervasive. Log levels are configurable per namespace in appsettings. +- `IActivityExecutionStore`, `ILogRecordStore`, and `StoreWorkflowExecutionLogSink` provide workflow-level execution records separate from the application log — enabling post-mortem analysis. +- `RetryTelemetryListener` emits retry attempt data through Polly telemetry — retry behaviour is observable. +- `ILoggerStateGenerator` (contract in `Workflows.Core`) enables custom log state enrichment per activity — diagnostic context can be injected. +- No OpenTelemetry (`ActivitySource`, `System.Diagnostics.Activity`) instrumentation was found in the core execution path — distributed tracing is not natively emitted. Assumption: OpenTelemetry may be configured externally via ASP.NET Core's built-in instrumentation for HTTP, but workflow-level spans are not emitted. +- The Datadog Docker compose file (`docker-compose-datadog+otel-collector.yml`) in `src/apps/Elsa.Server.Web/` suggests intent for observability integration, but no matching `ActivitySource` code was found. + +**Human completion required:** +- [ ] Determine whether OpenTelemetry distributed tracing spans are emitted for workflow and activity execution — if not, consider adding `ActivitySource` instrumentation to the execution pipeline (CODE) +- [ ] Verify that the Datadog/OTel collector compose file wires up correctly with the application's logging/metrics output (CONFIG + RUNTIME) +- [ ] Confirm that workflow execution log records are queryable with sufficient filters to support incident investigation (OPS) + +**Verdict (human to complete):** +[ ] ✅ Meets requirements [ ] 🟡 Partially meets [ ] ❌ Does not meet [ ] ⬜ Cannot assess +Notes: ___ +Reviewed by: ___ · Date: ___ + +--- + +### 7.4 Modifiability + +**Evidence sources:** `CODE` +**Claude confidence:** Medium — extension points and deprecation patterns are inspectable + +**What Claude assesses (code-level):** +- `IActivityExecutionPipelineBuilder` and `IWorkflowExecutionPipelineBuilder` use a middleware chain pattern — new cross-cutting behaviours can be inserted without modifying existing components. +- `IEntityModelCreatingHandler` and `IEntitySavingHandler` allow EF Core behaviour extension without subclassing `ElsaDbContextBase` — open/closed principle is applied. +- `IWorkflowActivationStrategy`, `IIncidentStrategy`, and `IResilienceStrategy` are all extension points — common customisation needs are covered by stable seams. +- `[Obsolete("...", error: false)]` is used (e.g., `IWorkflowDefinitionPublisher.New`) rather than hard removals — backward compatibility is maintained during migration periods. The comment in `Directory.Build.props` explicitly states: "Obsolete API warnings - Suppressed for backward compatibility during migration period". +- `CS0618` (obsolete API usage) is suppressed globally — this means internal obsolete API calls do not produce build warnings and may indicate areas that need cleanup. +- `TreatWarningsAsErrors=false` reduces code quality enforcement during modification. + +**Human completion required:** +- [ ] Review the list of `[Obsolete]` members to determine if any have been pending removal for multiple release cycles — plan removal schedule to avoid obsolete API accumulation (CODE) +- [ ] Assess whether the middleware pipeline pattern is documented sufficiently for contributors to add new middleware components (DOCS) +- [ ] Consider enabling `TreatWarningsAsErrors` for a specific warning subset (e.g., CS8600–CS8629 nullable warnings) to enforce modifiability discipline (CODE) + +**Verdict (human to complete):** +[ ] ✅ Meets requirements [ ] 🟡 Partially meets [ ] ❌ Does not meet [ ] ⬜ Cannot assess +Notes: ___ +Reviewed by: ___ · Date: ___ + +--- + +### 7.5 Testability + +**Evidence sources:** `CODE` +**Claude confidence:** High — test infrastructure and interface-based design are directly inspectable + +**What Claude assesses (code-level):** +- `IActivityTestRunner` and `IWorkflowRunner` interfaces allow test harnesses to run workflows in isolation via DI substitution. +- `Elsa.Testing.Shared` provides `FakeActivityExecutionContextSchedulerStrategy`, `FakeWorkflowExecutionContextSchedulerStrategy`, `XunitLogger`, and `CapturingTextWriter` — a dedicated fake/stub library exists for test support. +- `WorkflowTestFixture` and `ActivityTestFixture` provide base classes for integration and unit tests respectively — reducing per-test setup boilerplate. +- 12 unit test projects and 9 integration test projects exist — the testing effort is structurally separated by scope. +- 1 BenchmarkDotNet performance test project exists — performance regression testing is in place, albeit with a single benchmark. +- All major services implement interfaces — every service can be replaced with a test double at the DI layer. +- `IActivityTestRunner.RunAsync(WorkflowGraph, IActivity)` supports running individual activities in isolation — unit-level activity testing is supported by design. +- `RunWorkflowResultAssertions` provides assertion helpers — fluent assertion patterns reduce test verbosity. + +**Human completion required:** +- [ ] Measure and report code coverage percentage for the unit and integration test suites (RUNTIME) +- [ ] Identify whether there are any testability gaps in the distributed runtime path (`DistributedWorkflowRuntime`) — distributed scenarios may require additional fake infrastructure (CODE) +- [ ] Expand the BenchmarkDotNet suite to cover multi-activity workflow patterns, not only `WriteLine` (CODE) + +**Verdict (human to complete):** +[ ] ✅ Meets requirements [ ] 🟡 Partially meets [ ] ❌ Does not meet [ ] ⬜ Cannot assess +Notes: ___ +Reviewed by: ___ · Date: ___ + +--- + +## 8. Flexibility + +### 8.1 Adaptability + +**Evidence sources:** `CODE` `CONFIG` +**Claude confidence:** Medium — configuration extension points and provider swap patterns are inspectable + +**What Claude assesses (code-level):** +- All persistence, scheduling, identity, caching, and resilience concerns are abstracted behind interfaces and registered via the feature system — swapping any component (e.g., switching from EF Core SQLite to PostgreSQL) requires only a feature configuration change. +- `appsettings.json` binding via `IOptions` with `ShellConfiguration` sections allows all configurable values to be overridden without code changes — runtime configuration adaptation is supported. +- Multi-tenant configuration (`Multitenancy.Tenants[]`) allows per-tenant connection strings, HTTP prefixes, and other settings to be independently configured. +- `IStorageDriver` and `IBlobWorkflowFormatHandler` abstractions allow storage backends to be adapted without core changes. +- Expression language is pluggable — C#, JavaScript, Python, and Liquid are independently replaceable or addable by implementing `IExpressionHandler`. +- `HostBuilder.reloadConfigOnChange: true` enables live configuration reload without restart. +- EF Core schema name and migrations history table are statically configurable properties (`ElsaDbContextBase.ElsaSchema`, `MigrationsHistoryTable`) — schema adaptation for shared database deployments is supported. + +**Human completion required:** +- [ ] Verify that swapping a persistence provider (e.g., EF Core SQLite to PostgreSQL) in a running deployment requires only configuration changes and migration application (RUNTIME) +- [ ] Confirm that custom expression language providers can be registered externally without modifying this repository (CODE) +- [ ] Test multi-tenant configuration reload to confirm tenant settings changes are picked up without restart (RUNTIME) + +**Verdict (human to complete):** +[ ] ✅ Meets requirements [ ] 🟡 Partially meets [ ] ❌ Does not meet [ ] ⬜ Cannot assess +Notes: ___ +Reviewed by: ___ · Date: ___ + +--- + +### 8.2 Scalability + +**Evidence sources:** `CODE` `CONFIG` `RUNTIME` +**Claude confidence:** Low — distributed architecture patterns are visible; actual scaling behaviour requires load testing + +**What Claude assesses (code-level):** +- `Elsa.Workflows.Runtime.Distributed` implements `IWorkflowRuntime` with distributed locking, distributed definition refresh/reload, and distributed bookmark queue workers — horizontal scaling of the workflow runtime is architecturally supported. +- Medallion.Threading is used for distributed locking — the lock provider is abstracted, supporting Redis, SQL Server, PostgreSQL, and file-system backends for lock coordination. +- `DistributedBookmarkQueueWorker` and `DistributedWorkflowDefinitionsReloader` coordinate across nodes using distributed locks and signals — preventing work duplication under multi-node deployments. +- `InstanceHeartbeatService` / `InstanceHeartbeatMonitorService` provide node-level registration and monitoring — a prerequisite for cluster-aware routing. +- The background dispatch pattern (`BackgroundWorkflowDispatcher`, `BackgroundStimulusDispatcher`) decouples trigger receipt from execution — enabling queue-based horizontal scaling with an external message broker. +- Docker Compose files (`docker-compose-kafka.yml`) suggest Kafka integration has been tested — indicating message-bus-backed scalability patterns have been explored. + +**Human completion required:** +- [ ] Conduct horizontal scale-out test: run 3+ nodes with a shared PostgreSQL database and distributed Redis lock provider; verify no duplicate executions or lost workflows (RUNTIME) +- [ ] Define and document maximum supported workflow instance concurrency per node configuration (OPS) +- [ ] Verify that `Medallion.Threading` lock provider selection (Redis/SQL/PostgreSQL) is documented for production deployments (CONFIG) + +**Verdict (human to complete):** +[ ] ✅ Meets requirements [ ] 🟡 Partially meets [ ] ❌ Does not meet [ ] ⬜ Cannot assess +Notes: ___ +Reviewed by: ___ · Date: ___ + +--- + +### 8.3 Installability + +**Evidence sources:** `CODE` `CONFIG` +**Claude confidence:** High — Docker, NuGet, and CI/CD artefacts are directly inspectable + +**What Claude assesses (code-level):** +- Four Dockerfiles are present (`ElsaServer.Dockerfile`, `ElsaServer-Datadog.Dockerfile`, `ElsaServerAndStudio.Dockerfile`, `ElsaStudio.Dockerfile`) — multiple Docker image variants are maintained for different deployment configurations. +- `docker-compose.yml`, `docker-compose-kafka.yml`, and `docker-compose-datadog+otel-collector.yml` provide ready-to-use orchestration configurations for standard and advanced scenarios. +- `entrypoint.sh` and `init-db-postgres.sh` scripts handle startup initialisation and PostgreSQL database setup — reducing manual configuration steps. +- EF Core migrations are applied automatically at startup via `RunMigrationsHostedService` — zero-configuration database installation. +- The `packages.yml` GitHub Actions workflow publishes NuGet packages — the NuGet distribution channel is CI-managed. +- The `pr.yml` CI pipeline runs `Compile + Test` on Ubuntu with .NET 10 on every PR — installability regressions would be caught early. +- A Docker quick-start (`docker pull` + `docker run`) is documented in the README with a single command — installation friction is minimal for evaluation. +- `setup/` directory in `Elsa.Server.Web` exists — contents were not inspected but suggest additional setup documentation. + +**Human completion required:** +- [ ] Verify that the Docker images are published to Docker Hub and tagged correctly for v3.7.0 (CONFIG) +- [ ] Test fresh installation from NuGet on a new ASP.NET Core project to confirm the getting-started steps in the README are accurate for this release (RUNTIME) +- [ ] Confirm that `RunMigrationsHostedService` handles migration conflicts gracefully in multi-node deployments (RUNTIME) + +**Verdict (human to complete):** +[ ] ✅ Meets requirements [ ] 🟡 Partially meets [ ] ❌ Does not meet [ ] ⬜ Cannot assess +Notes: ___ +Reviewed by: ___ · Date: ___ + +--- + +### 8.4 Replaceability + +**Evidence sources:** `CODE` `DESIGN` +**Claude confidence:** Low — replaceability requires knowledge of actual adopter migration patterns, which are operational + +**What Claude assesses (code-level):** +- The persistence abstraction (`IWorkflowDefinitionStore`, `IWorkflowInstanceStore`, `IBookmarkStore`, etc.) decouples Elsa from any specific persistence technology — a host application can replace the entire persistence layer by registering alternative implementations. +- The `IWorkflowRuntime` interface means the runtime implementation (`LocalWorkflowClient`, `DistributedWorkflowRuntime`) is replaceable — an alternative runtime (e.g., Azure Durable Functions bridge) could be substituted. +- Workflow state is serialized to JSON (`IWorkflowStateSerializer`) — the persisted state is a documented, version-stamped format, making it portable if migrating to a different runtime. +- `[Obsolete]` markers (with `error: false`) document migration paths for adopters replacing deprecated APIs — a replaceability aid for version-to-version migration. +- Assumption: whether Elsa can be gradually replaced by a different workflow engine in an existing deployment (partial migration) depends on host application integration patterns and is not assessable from code. + +**Human completion required:** +- [ ] Document a migration guide for moving from Elsa 2.x to 3.x and from earlier 3.x versions to 3.7.0 (DOCS) +- [ ] Assess whether workflow state stored in earlier schema versions can be migrated to v3.7.0 without data loss (RUNTIME) +- [ ] Gather feedback from adopters who have swapped persistence providers or upgraded from earlier versions (OPS) + +**Verdict (human to complete):** +[ ] ✅ Meets requirements [ ] 🟡 Partially meets [ ] ❌ Does not meet [ ] ⬜ Cannot assess +Notes: ___ +Reviewed by: ___ · Date: ___ + +--- + +## 9. Safety + +> Note: Elsa is a workflow engine library. Physical safety does not apply. Safety sub-characteristics below are interpreted in the context of: preventing data loss, preventing financial harm from incorrect workflow execution, preventing system corruption, and preventing uncontrolled external action from misconfigured workflows. + +### 9.1 Operational constraint + +**Evidence sources:** `CODE` `DESIGN` +**Claude confidence:** Low — operational constraints depend on deployment policies outside the library's scope + +**What Claude assesses (code-level):** +- `IWorkflowActivationStrategy` allows deployers to constrain workflow instantiation (singleton, correlation-scoped) — preventing uncontrolled parallel execution of dangerous workflows. +- `ValidatingWorkflowDispatcher` enforces channel constraints before dispatch — unconfigured channels are rejected rather than silently ignored. +- The `FaultBehaviour` mechanism (via `IIncidentStrategy`) allows workflows to be halted on specific error conditions — providing an operational constraint boundary. +- `CancellationToken` propagation is pervasive (730 source files) — external cancellation can stop workflow execution mid-flight. +- No built-in rate limiting on workflow trigger endpoints was found — an unconstrained public HTTP trigger endpoint could be abused to generate unbounded workflow instances. + +**Human completion required:** +- [ ] Define and implement rate limits on public-facing HTTP workflow trigger endpoints (`/workflows/...`) (CODE + CONFIG) +- [ ] Document recommended `IWorkflowActivationStrategy` settings for workflows that invoke external financial or transactional systems (DESIGN) +- [ ] Confirm that workflow cancellation propagates correctly through all async paths, including external HTTP calls in activities (RUNTIME) + +**Verdict (human to complete):** +[ ] ✅ Meets requirements [ ] 🟡 Partially meets [ ] ❌ Does not meet [ ] ⬜ Cannot assess +Notes: ___ +Reviewed by: ___ · Date: ___ + +--- + +### 9.2 Risk identification + +**Evidence sources:** `DESIGN` `OPS` +**Claude confidence:** Very low — risk identification is a design and operational concern; not assessable from code alone + +**What Claude assesses (code-level):** +- No built-in risk identification or workflow safety analysis tooling was found in the codebase — risk identification is entirely the responsibility of the adopter and their workflow design. +- The `Elsa.Alterations` module allows in-flight workflow modification — this introduces a risk of unintended state transitions if alteration plans are incorrectly specified. No guardrails or simulation mode for alteration plans were observed. +- The `IIncidentStrategy` system surfaces workflow faults — but does not proactively predict or warn about risky workflow patterns at definition time. +- Assumption: the library does not claim to provide workflow safety analysis; this is a domain-level concern for adopters. + +**Human completion required:** +- [ ] Conduct a domain-level risk assessment for any workflows that invoke financial transactions, external APIs with side effects, or irreversible operations (DESIGN) +- [ ] Assess whether the `Elsa.Alterations` alteration plan mechanism requires additional validation or simulation capability before applying in production (DESIGN) +- [ ] Document known risk scenarios and recommended mitigations in the operational documentation (DOCS) + +**Verdict (human to complete):** +[ ] ✅ Meets requirements [ ] 🟡 Partially meets [ ] ❌ Does not meet [ ] ⬜ Cannot assess +Notes: ___ +Reviewed by: ___ · Date: ___ + +--- + +### 9.3 Fail safe + +**Evidence sources:** `CODE` `RUNTIME` +**Claude confidence:** Low — fail-safe patterns in the code are observable; effectiveness under failure requires injection testing + +**What Claude assesses (code-level):** +- When an activity faults, `IIncidentStrategy` determines the response — the pluggable strategy means fail-safe behaviour is configurable per deployment. The built-in strategies can halt the workflow (preventing further action) or retry. +- Polly pipeline integration (`ResilientActivityInvoker`) ensures that transient activity failures do not immediately corrupt workflow state — retries and circuit-breakers provide buffering. +- Distributed locking ensures that a workflow instance is executed on at most one node at a time — preventing split-brain execution that could cause double-actions. +- `WorkflowState` is persisted before suspension points (bookmarks) — a crash during execution loses at most the current activity's progress, not prior completed steps. +- The `DataProtectorTokenService.TryDecryptToken` silently fails on tampered tokens rather than throwing — token-based features fail closed (deny access) rather than open. +- `WorkflowHost.RunWorkflowAsync` logs a warning and returns without executing when the workflow is not in the `Running` state — preventing unintended action on terminated workflows. + +**Human completion required:** +- [ ] Inject database failure mid-execution and verify workflow state is recoverable from the last persisted bookmark (RUNTIME) +- [ ] Test what happens when the distributed lock provider is unavailable — confirm the system fails safely rather than proceeding without locking (RUNTIME) +- [ ] Document the default `IIncidentStrategy` configuration and its fail-safe behaviour for adopters (DOCS) + +**Verdict (human to complete):** +[ ] ✅ Meets requirements [ ] 🟡 Partially meets [ ] ❌ Does not meet [ ] ⬜ Cannot assess +Notes: ___ +Reviewed by: ___ · Date: ___ + +--- + +### 9.4 Hazard warning + +**Evidence sources:** `CODE` +**Claude confidence:** Medium — warning patterns in code are directly inspectable + +**What Claude assesses (code-level):** +- `WorkflowHost.RunWorkflowAsync` emits `LogWarning` when a resume is attempted on a non-Running workflow — a code-level hazard warning to operators. +- `DefaultAccessTokenIssuer` throws descriptive exceptions for missing security configuration (`"No signing key configured"`) — a startup-time hazard warning. +- The `RetryTelemetryListener` records retry attempt metadata — operators can observe retry storms as a leading indicator of service degradation. +- `InstanceHeartbeatMonitorService` raises a notification (`INotificationSender`) when node heartbeats are missed — a cluster-level hazard warning mechanism. +- `ValidatingWorkflowDispatcher` returns a typed `DispatchWorkflowResponse.UnknownChannel()` — callers receive an explicit signal rather than a silent no-op. +- The reference `appsettings.json` includes a comment-like placeholder signing key (`"sufficiently-large-secret-signing-key"`) — this is a hazard if deployed to production without replacement. No runtime warning is emitted for weak signing keys. + +**Human completion required:** +- [ ] Add a startup warning or validation check that emits a log warning (at minimum) when the signing key matches the default placeholder value (CODE) +- [ ] Verify that heartbeat miss notifications are observable through a configured alerting channel in production (CONFIG + OPS) +- [ ] Confirm that retry storm patterns (high retry counts within a short window) are surfaced through monitoring dashboards (OPS) + +**Verdict (human to complete):** +[ ] ✅ Meets requirements [ ] 🟡 Partially meets [ ] ❌ Does not meet [ ] ⬜ Cannot assess +Notes: ___ +Reviewed by: ___ · Date: ___ + +--- + +### 9.5 Safe integration + +**Evidence sources:** `CODE` `CONFIG` +**Claude confidence:** Low — integration safety depends on host application configuration and external system behaviour, both outside this codebase + +**What Claude assesses (code-level):** +- `CancellationToken` is accepted on all external-facing activity execution methods — integration calls can be cancelled without orphaning workflow state. +- SAS tokens for public event/bookmark triggers are time-limited (`ToTimeLimitedDataProtector()`) — integration endpoints have built-in expiry to reduce exposure window. +- `DataProtectorTokenService` wraps SAS token operations with ASP.NET Core Data Protection — integration secrets are encrypted in transit. +- HTTP workflow triggers authenticate the caller before executing workflows (through the JWT/API key middleware) — external HTTP integrations require credentials. +- The `Elsa.Http` module's `HttpWorkflowsMiddleware` routes inbound HTTP to workflow bookmarks/triggers — the mapping is based on a SHA-256 bookmark hash, which provides a non-enumerable trigger namespace. +- No input schema validation was observed for inbound HTTP workflow trigger payloads — malformed or oversized payloads may reach activity code without prior validation. + +**Human completion required:** +- [ ] Implement request size limits and basic schema validation on HTTP workflow trigger endpoints to prevent resource exhaustion (CODE + CONFIG) +- [ ] Audit all activities that make outbound HTTP calls (e.g., `SendHttpRequest`) for timeout configuration and redirected-request safety (CODE) +- [ ] Test that cancelling a workflow execution mid-HTTP-call correctly cleans up the outbound HTTP request (RUNTIME) + +**Verdict (human to complete):** +[ ] ✅ Meets requirements [ ] 🟡 Partially meets [ ] ❌ Does not meet [ ] ⬜ Cannot assess +Notes: ___ +Reviewed by: ___ · Date: ___ + +--- + +## Summary and sign-off + +### Verdict summary +*Complete this table after all individual verdicts are filled in.* + +| Characteristic | Sub-characteristics | ✅ Meets | 🟡 Partial | ❌ Does not meet | ⬜ Cannot assess | +|---|---|---|---|---|---| +| Functional suitability | 3 | | | | | +| Performance efficiency | 3 | | | | | +| Compatibility | 2 | | | | | +| Interaction capability | 8 | | | | | +| Reliability | 4 | | | | | +| Security | 6 | | | | | +| Maintainability | 5 | | | | | +| Flexibility | 4 | | | | | +| Safety | 5 | | | | | +| **Total** | **40** | | | | | + +--- + +### Top findings from Claude's code analysis +*Three strongest positive signals, three most significant gaps.* + +**Strongest evidenced qualities:** + +1. **Deep security layering with good separation of concerns.** Multi-scheme authentication (JWT Bearer + API key), HMAC-signed SAS tokens via ASP.NET Core Data Protection, per-entity tenant isolation via EF Core global query filters (`SetTenantIdFilter`), and salted password hashing are all present and structurally sound. The `NuGetAudit=all` build-time supply-chain check adds a proactive defence layer. + +2. **Exceptionally high testability by design.** Every significant service is behind an interface; a dedicated `Elsa.Testing.Shared` library provides fakes, fixtures, and assertion helpers; and 12 unit + 9 integration test projects with 532 test methods demonstrate sustained investment in testability. The `IActivityTestRunner` enables isolated single-activity testing, which is rare in workflow engines. + +3. **Mature modularity and extensibility.** 50 independently deployable NuGet packages, 175 `*Feature*.cs` files with explicit `DependsOn` declarations, dual-layer core/implementation splits for persistence and resilience, and ConfigureAwait.Fody weaving as a compile-time cross-cutting concern all show architectural discipline applied consistently across the codebase. + +**Most significant gaps (code-level):** + +1. **Password hashing uses SHA-256 + salt rather than an adaptive slow hash.** `DefaultSecretHasher` uses `SHA256.Create()` with a 32-byte random salt. SHA-256 is a fast cryptographic hash — at current GPU speeds, it is insufficiently slow for password storage. BCrypt, Argon2, or PBKDF2 with a high iteration count is the current security best practice. This affects user passwords and application secrets. This finding should be assessed and resolved by a security engineer. + +2. **No OpenTelemetry distributed tracing instrumentation in the workflow execution pipeline.** Despite a Datadog/OTel Docker Compose file suggesting intent, no `ActivitySource` or `System.Diagnostics.Activity` spans are emitted within the workflow or activity execution pipeline. This means distributed traces will not show workflow-level spans, limiting observability in complex microservice deployments and reducing analysability under production load. + +3. **Rate limiting absent at the API ingress layer.** No ASP.NET Core `RateLimiter` middleware was observed in `Program.cs` or any module feature, and HTTP workflow trigger endpoints (potentially public-facing) are not rate-limited in the default configuration. This is a safety and security exposure: a public HTTP endpoint could be used to trigger unbounded workflow instantiation without throttling. + +--- + +### Attributes not assessable from code and documentation alone +The following were assessed with Very low or Low confidence and require operational evidence: + +- **Performance efficiency / Capacity** — requires load test results and per-node concurrency benchmarks +- **Reliability / Availability** — requires uptime metrics, SLA measurement, and production deployment review +- **Reliability / Faultlessness** — requires production error rate data and test coverage measurement +- **Reliability / Recoverability** — requires failure injection testing (process crash mid-execution) +- **Flexibility / Scalability** — requires horizontal scale-out load test with distributed lock provider +- **Safety (all sub-characteristics)** — requires domain-specific risk assessment by workflow designers and operators +- **Interaction capability / User engagement** — requires developer community research and adoption metrics +- **Compatibility / Co-existence** — requires deployment testing alongside representative co-hosted middleware stacks + +--- + +### Final approval + +| Field | Value | +|---|---| +| **Review completed by** | ___ | +| **Review date (UTC)** | ___ | +| **Verdict fields complete** | Yes / No — [n of 40 completed] | +| **Human completion checklists worked through** | Yes / Partial / No | +| **Approved as final report** | Yes / No | +| **Conditions or caveats** | ___ | \ No newline at end of file diff --git a/doc/security-assessment/elsa-core-profile.md b/doc/security-assessment/elsa-core-profile.md new file mode 100644 index 000000000..526412da4 --- /dev/null +++ b/doc/security-assessment/elsa-core-profile.md @@ -0,0 +1,375 @@ +# Elsa Workflows — Technology Profile + +| Field | Value | +|---|---| +| **Repository / project** | elsa-workflows/elsa-core | +| **Git ref** | release/3.7.0 | +| **Version** | 3.7.0 | +| **Assessment date (UTC)** | 2026-05-20T00:00:00Z | +| **Assessment method** | Static code analysis, Documentation review | +| **Model and tools** | Claude Sonnet 4.6 | +| **Assessment scope** | All 50 source projects in src/ — primary focus: Elsa.Workflows.Core, Elsa.Workflows.Runtime, Elsa.Workflows.Management, Elsa.Http, Elsa.Identity, Elsa.Tenants, Elsa.Resilience, Elsa.Scheduling, Elsa.Alterations | +| **Related documents** | elsa-core-architecture-patterns.md, elsa-core-software-quality.md, elsa-core-iso25010.md | + +> ⚠️ **AI-assisted assessment — human review required** +> +> This document was produced by an AI model (Claude Sonnet 4.6) using static code +> analysis and structured pattern catalogs. AI-assisted assessments are based on pattern +> recognition and reasoning over source files and documentation. They may contain incorrect +> assumptions, missed context, or findings that do not apply to your specific operational +> environment. +> +> **This document is a draft input to a human review process — not a final approved report.** +> All findings should be verified by a qualified engineer or architect familiar with the +> codebase before being acted upon, communicated externally, or used as the basis for +> architectural or compliance decisions. + +--- + +## 1. What Elsa Workflows Core Is + +Elsa Workflows (elsa-core) is an open-source .NET library and engine for embedding workflow execution inside any .NET application. It provides the data model, execution runtime, persistence abstractions, expression evaluation, security, and optional HTTP/messaging integrations needed to author and run both short-running and long-running business processes entirely within a .NET host process. + +Workflows can be authored in three ways: as typed C# classes derived from `WorkflowBase`, as JSON definitions loaded at runtime from a store or file system, or as ElsaScript `.elsa` text files parsed and compiled at startup. All three representations compile to the same in-memory activity graph that the execution engine evaluates. + +**What Elsa Workflows Core is NOT:** + +- It is not the Elsa Studio visual designer front-end. The Studio is a separate Blazor WebAssembly application that lives in a distinct repository (`elsa-studio`). This assessment covers only the engine and server library (`elsa-core`). +- It is not a cloud-hosted or managed workflow service. There is no SaaS offering — all components run in-process inside the consuming application. +- It is not a BPMN 2.0 engine and does not parse BPMN XML. The activity model is proprietary. +- It is not a low-code/no-code platform by itself. Designer capabilities require the separate Elsa Studio front-end. +- It is not a replacement for a message broker or event bus. While it can consume events via its bookmark/stimulus model, it relies on external infrastructure (RabbitMQ, Azure Service Bus via MassTransit) for durable message transport in distributed deployments. +- It is not a reporting or monitoring tool. Observability is delegated to OpenTelemetry exporters and external platforms. + +**Scope statement:** This profile covers the `release/3.7.0` branch of `elsa-workflows/elsa-core`, specifically the 50 projects under `/src`. The primary focus modules are listed in the header. + +--- + +## 2. Core Architecture + +### 2.1 Target Frameworks and Deployment + +Elsa targets `net8.0`, `net9.0`, and `net10.0`. The `Directory.Packages.props` file manages separate package version sets per framework, enabling multi-targeting from a single NuGet package publish. Binaries ship as Symbol Packages (`.snupkg`) with Source Link enabled. The server application (`Elsa.Server.Web`) is a standard ASP.NET Core Web project; no custom runtime host is required. + +### 2.2 Module and Feature System + +Every feature is a class that derives from `FeatureBase`. Features declare dependencies via `[DependsOn]` attributes, which the `IModule` implementation resolves into a topologically ordered apply sequence. Consumer code opts in through fluent extension methods on `IServiceCollection`: + +```csharp +services.AddElsa(elsa => +{ + elsa + .UseWorkflowManagement(management => management.UseEntityFrameworkCore(ef => ef.UseSqlite())) + .UseWorkflowRuntime(runtime => runtime.UseEntityFrameworkCore(...).UseDistributedRuntime()) + .UseIdentity(identity => identity.UseConfigurationBasedUserProvider(...)) + .UseCSharp() + .UseJavaScript() + .UseLiquid() + .UseHttp() + .UseScheduling(); +}); +``` + +The top-level `AddElsa` method creates an `IModule` backed by `AppFeature` → `ElsaFeature`, which itself depends on `WorkflowsFeature`, `FlowchartFeature`, `DefaultWorkflowRuntimeFeature`, and `WorkflowManagementFeature`. All optional modules (identity, tenancy, resilience, expressions) are additive. + +### 2.3 Execution Model + +Elsa uses an async, scheduler-driven execution model. The entry point is `IWorkflowRunner`, which constructs a `WorkflowExecutionContext` and drives it through an `IWorkflowExecutionPipeline`. The pipeline is a middleware chain built with `IWorkflowExecutionPipelineBuilder`; the default middleware stack is: + +| Middleware | Role | +|---|---| +| `EngineExceptionHandlingMiddleware` | Catches unhandled engine-level exceptions | +| `DefaultActivitySchedulerMiddleware` | Runs the scheduler loop, dequeuing and invoking `ActivityWorkItem` entries until the queue is empty | + +The inner scheduler loop calls `IActivityInvoker`, which drives each activity through its own `IActivityExecutionPipeline`. The default activity pipeline includes: + +| Middleware | Role | +|---|---| +| `ExceptionHandlingMiddleware` | Activity-level exception trapping and incident recording | +| `ExecutionLogMiddleware` | Writes `WorkflowExecutionLogEntry` records | +| `LoggingMiddleware` | Structured logger state injection | +| `NotificationPublishingMiddleware` | Mediator notifications before/after activity execution | +| `DefaultActivityInvokerMiddleware` | Resolves `IActivity.ExecuteAsync` and completes the context | + +`WorkflowExecutionContext` holds the full mutable state of a running workflow instance: the activity scheduler, bookmark set, completion callbacks, output register, variable storage, correlation ID, tenant ID, and a reference to the `WorkflowGraph`. `ActivityExecutionContext` is the per-activity scope; it exposes `ExpressionExecutionContext` for expression evaluation and delegates state changes back to its parent `WorkflowExecutionContext`. + +### 2.4 Activity Model + +All activities implement `IActivity`. Composite activities that schedule children implement `IComposite` or derive from `Container`. Activities that can start a workflow from an external event implement `ITrigger`. Ports (named outcomes such as `"True"`, `"False"`, or `"Done"`) are declared via `[Port]` attributes or `[FlowNode]` on flowchart activities. + +The built-in activity library covers: + +| Category | Activities | +|---|---| +| Sequence/control flow | `Sequence`, `Flowchart`, `Fork`, `If`, `Switch`, `While`, `For`, `ForEach`, `Break`, `Parallel`, `ParallelForEach` | +| Flowchart-specific | `FlowDecision`, `FlowFork`, `FlowJoin`, `FlowSwitch` | +| I/O | `WriteLine`, `ReadLine`, `SetVariable`, `SetName`, `Correlate` | +| Lifecycle | `Start`, `End`, `Finish`, `Fault`, `Complete`, `Inline` | +| HTTP | `HttpEndpoint`, `SendHttpRequest`, `FlowSendHttpRequest`, `WriteHttpResponse`, `DownloadHttpFile`, `WriteFileHttpResponse` | +| Scheduling | `Timer`, `Cron`, `StartAt`, `Delay` | +| C# | `RunCSharp` | + +### 2.5 Bookmark and Stimulus Model + +Long-running workflows suspend by creating one or more `Bookmark` objects. A bookmark records the activity node, activity instance, a hashed payload key (the _stimulus hash_), optional callback method name, and `AutoBurn`/`AutoComplete` flags. The engine transitions the workflow to `WorkflowStatus.Running / WorkflowSubStatus.Suspended` when any bookmarks remain after the scheduler queue empties. + +External events resume suspended workflows via the `IStimulusSender` service. `SendAsync` hashes the inbound stimulus, looks up matching bookmarks via `IBookmarkBoundWorkflowService`, and either resumes existing instances or triggers new ones if triggers match. This decouples the event source (HTTP endpoint, message consumer, scheduled timer, custom trigger) from the workflow host. + +```csharp +// Activity creates a bookmark to suspend and wait for an external event +await context.CreateBookmarkAsync(new CreateBookmarkArgs +{ + Payload = new HttpEndpointBookmarkPayload(path, method), + CallbackMethodName = nameof(HandleRequest) +}); +``` + +### 2.6 Workflow Runtime Variants + +| Runtime | Class | Use Case | +|---|---|---| +| **Local** | `LocalWorkflowRuntime` | Single-node; no distributed locking. Development and single-instance deployments only. | +| **Distributed** | `DistributedWorkflowRuntime` | Multi-node; wraps local execution in a `DistributedWorkflowClient` that acquires a `DistributedLock.Core` lock before running a workflow instance. Prevents concurrent execution of the same instance across pods. | + +Both runtimes expose `IWorkflowRuntime`, which produces `IWorkflowClient` instances. The `DistributedBookmarkQueueWorker` additionally acquires a cluster-wide lock before processing the bookmark queue, preventing duplicate processing on multi-pod deployments. The `Medallion.Threading` library provides the underlying distributed lock abstraction. + +### 2.7 Workflow State Serialization and Persistence + +`WorkflowState` is the serializable snapshot of a running instance. It contains status, correlation, bookmarks, activity execution contexts (as `ActivityExecutionContextState`), variable values, incidents, and completion callbacks. The `IWorkflowStateExtractor` converts a live `WorkflowExecutionContext` to `WorkflowState` and back. + +Persistence is organized into two store groups: + +**Management stores** (workflow definitions and instances): +- `IWorkflowDefinitionStore` — CRUD for `WorkflowDefinition` entities (versioned, with `DefinitionId` + integer version) +- `IWorkflowInstanceStore` — CRUD for `WorkflowInstance` entities + +**Runtime stores** (execution data): +- `IBookmarkStore`, `IBookmarkQueueStore` +- `IActivityExecutionStore` +- `IWorkflowExecutionLogStore`, `IActivityExecutionLogStore` +- `ITriggerStore` +- `IKeyValueStore` + +All stores are interface-backed. Provided implementations: + +| Backend | Module(s) | +|---|---| +| In-memory | Default in `Elsa.Workflows.Runtime` (lost on restart — not for production) | +| EF Core — SQLite | `Elsa.Persistence.EFCore.Sqlite` | +| EF Core — SQL Server | `Elsa.Persistence.EFCore.SqlServer` | +| EF Core — PostgreSQL | `Elsa.Persistence.EFCore.PostgreSql` | +| EF Core — MySQL | `Elsa.Persistence.EFCore.MySql` | +| EF Core — Oracle | `Elsa.Persistence.EFCore.Oracle` | +| Blob storage | `Elsa.WorkflowProviders.BlobStorage` (workflow definitions only, via FluentStorage) | + +MongoDB and Dapper adapters referenced in the README and older documentation are not present in the `release/3.7.0` source tree. They may exist as third-party or commercial packages outside this repository. + +### 2.8 Expression Engine + +Elsa provides a pluggable expression evaluation system via `IExpressionHandler` implementations registered per language name. Each input on an activity accepts an `Expression` object with a `Type` discriminator (language name) and a `Value` payload. + +| Language | Provider module | Engine | +|---|---|---| +| C# | `Elsa.Expressions.CSharp` | `Microsoft.CodeAnalysis.CSharp.Scripting` (Roslyn) | +| JavaScript | `Elsa.Expressions.JavaScript` | Jint 4.x | +| Python | `Elsa.Expressions.Python` | `pythonnet` 3.x (requires Python runtime on host) | +| Liquid | `Elsa.Expressions.Liquid` | Fluid.Core 2.x | +| ElsaScript DSL | `Elsa.Dsl.ElsaScript` | Custom regex-based parser + compiler | +| Literal / Delegate | `Elsa.Expressions` | Native .NET | + +The JavaScript engine (Jint) supports optional CLR access (`AllowClrAccess`) and optional `getConfig` access for reading `IConfiguration` values; both are disabled by default for security. Python requires a Python runtime path configured via `PYTHONNET_PYDLL` or application settings. + +--- + +## 3. Key Capabilities + +### 3.1 Workflow Authoring + +| Capability | Detail | +|---|---| +| Code-first C# | Derive from `WorkflowBase` or `WorkflowBase`, implement `Build(IWorkflowBuilder)` | +| JSON definitions | Store `WorkflowDefinition` entities; loaded via `IWorkflowDefinitionStore` at runtime | +| ElsaScript text DSL | `.elsa` files; JavaScript-inspired syntax compiled to Elsa activity graphs | +| Activity host registration | Mark a CLR class as an activity host; public async methods are auto-discovered as activities via `HostMethodActivity` | +| Visual designer | Requires the separate Elsa Studio Blazor app | + +### 3.2 Workflow Execution + +| Capability | Detail | +|---|---| +| Synchronous in-process execution | `IWorkflowRunner.RunAsync` executes synchronously within the calling scope | +| Background dispatch | `IWorkflowDispatcher` queues execution to a background worker via `IBackgroundTaskDispatcher` | +| Parallel branches | `Fork`, `FlowFork`, `Parallel`, `ParallelForEach` schedule sibling activities concurrently within a single execution context | +| Long-running / suspended | Workflow suspends when bookmark queue drains; resumes on stimulus | +| Workflow versioning | `WorkflowDefinition` carries `DefinitionId` + integer `Version`; running instances track `DefinitionVersion` | +| Workflow import/export | `WorkflowDefinitionExporter` / `WorkflowDefinitionImporter`; bulk ZIP export with optional transitive consumer inclusion | +| Child workflows | `DispatchWorkflow` / `BulkDispatchWorkflows` activities; parent can wait for child completion | +| Instance activation strategies | `SingletonStrategy`, `CorrelationStrategy`, `CorrelatedSingletonStrategy` — prevent duplicate instances | +| Execution log | Per-activity execution records written via `IActivityExecutionStore`; configurable log persistence mode | +| Commit strategies | Configurable checkpointing: per-activity, per-workflow, or global defaults | + +### 3.3 State Management + +| Capability | Detail | +|---|---| +| Variable storage drivers | `WorkflowInstanceStorageDriver` (in-memory state); `WorkflowStorageDriver` (external store); extensible via `IStorageDriver` | +| Incident strategies | `FaultStrategy` (halt on first fault), `ContinueWithIncidentsStrategy` (record and continue) | +| Correlation | Workflows carry an optional `CorrelationId`; bookmark lookup uses correlation for routing | +| Heartbeat | `WorkflowHeartbeatGenerator` updates an `UpdatedAt` timestamp; `RestartInterruptedWorkflowsTask` uses inactivity thresholds to detect and restart stuck instances | + +### 3.4 HTTP Integration + +| Capability | Detail | +|---|---| +| `HttpEndpoint` trigger | Registers an ASP.NET Core route; creates a bookmark; resumes workflow on inbound request | +| `SendHttpRequest` | Outbound HTTP call; response properties accessible as activity output | +| `WriteHttpResponse` | Writes arbitrary response body/status to the current HTTP context | +| `DownloadHttpFile` | Downloads a remote file and returns a stream | +| SAS token support | `Elsa.SasTokens` module provides time-limited signed URLs for webhook callbacks | +| Resilience per request | HTTP activities participate in the `IResilientActivityInvoker` pipeline | + +### 3.5 Scheduling + +| Capability | Detail | +|---|---| +| `Timer` | Fires at a fixed interval; creates a bookmark on each tick | +| `Cron` | Fires on a Cronos cron expression schedule | +| `StartAt` | Fires once at a specific `DateTimeOffset` | +| `Delay` | Suspends the workflow for a fixed duration | +| Recurring background tasks | Configurable schedules per task type via `RecurringTaskOptions` | + +### 3.6 Identity and Authentication + +| Capability | Detail | +|---|---| +| JWT authentication | `IdentityTokenOptions` configures signing key, issuer, audience, access/refresh token lifetimes | +| API key authentication | `AspNetCore.Authentication.ApiKey` integration | +| Users | `IUserStore`, `IUserManager`, `IUserProvider`; configuration-based or store-backed providers | +| Applications | `IApplicationStore` for OAuth client credentials | +| Roles and permissions | `IRoleStore`, `IRoleManager`; default admin role uses wildcard permission (`"*"`) | +| Default admin bootstrap | `DefaultAdminUserFeature` creates a seeded admin user from `DefaultAdminUserOptions` if none exists | + +### 3.7 Multi-Tenancy + +| Capability | Detail | +|---|---| +| Tenant store | `ITenantStore` with `ConfigurationTenantsProvider` (appsettings) and `StoreTenantsProvider` (EF Core) | +| Tenant resolution pipeline | Pluggable pipeline via `ITenantResolutionPipeline`; resolvers execute in order until one resolves | +| Built-in HTTP resolvers | `HeaderTenantResolver`, `HostTenantResolver`, `RoutePrefixTenantResolver` | +| Identity resolvers | `ClaimsTenantResolver` (JWT claim), `CurrentUserTenantResolver` (user record lookup) | +| Tenant isolation | EF Core query filters include `TenantId`; empty string = default tenant; `null` = tenant-agnostic | +| ASP.NET Core middleware | `Elsa.Tenants.AspNetCore` provides `TenantResolutionMiddleware` | + +### 3.8 Resilience + +| Capability | Detail | +|---|---| +| Activity-level retry | Activities implement `IResilientActivity`; `IResilientActivityInvoker` wraps execution in a Polly `ResiliencePipeline` | +| Strategy catalog | `IResilienceStrategyCatalog` with `IResilienceStrategySource` implementations | +| Configuration-backed strategies | `ConfigurationResilienceStrategySource` reads from `appsettings.json` under `Resilience:Strategies` | +| Transient exception detection | `ITransientExceptionDetector` / `DefaultTransientExceptionStrategy` | +| Retry attempt recording | `IRetryAttemptRecorder` stores attempt history into `ActivityExecutionContext` | +| Distributed lock resilience | `Elsa.Common` wraps distributed lock acquisition in a Polly retry pipeline | + +### 3.9 Alterations API + +| Alteration type | Effect | +|---|---| +| `Cancel` | Cancels all workflow instances in the alteration plan | +| `CancelActivity` | Cancels a specific running activity by activity ID or activity instance ID | +| `Migrate` | Migrates a workflow instance to a specified newer definition version | +| `ModifyVariable` | Overwrites a named variable's value in the running instance | +| `ScheduleActivity` | Forces a specific activity to be scheduled for execution | + +### 3.10 Workflow Definition Management + +| Capability | Detail | +|---|---| +| Definition versioning | `DefinitionId` (logical) + `Version` (integer); `IsPublished` and `IsLatest` flags | +| Import/export | ZIP archive with deterministic file names; optional recursive consumer inclusion | +| Consumer graph | `IWorkflowReferenceGraphBuilder` resolves all workflows that embed a given definition as a sub-workflow | +| Read-only mode | `UseReadOnlyMode(true)` disables mutation endpoints | +| Workflow providers | Extensible via `IWorkflowProvider`; blob storage provider available | + +--- + +## 4. Integration Context + +### 4.1 Inbound Triggers + +| Source | Mechanism | +|---|---| +| HTTP request | `HttpEndpoint` activity; ASP.NET Core route registered at startup | +| Scheduled timer | `Timer`, `Cron`, `StartAt`, `Delay` — self-managed via recurring tasks | +| External event / message | Custom `ITrigger` or `IStimulusSender.SendAsync` call from message consumer | +| Manual start via API | `POST /workflow-definitions/{id}/execute` via `Elsa.Workflows.Api` | +| Activity bookmark resume | `IStimulusSender` / `IWorkflowResumer` matching on stimulus hash | + +### 4.2 Persistence Infrastructure + +| Concern | Options | +|---|---| +| Relational databases | SQLite, SQL Server, PostgreSQL, MySQL, Oracle (all via EF Core) | +| Workflow definition files | Blob storage via FluentStorage (Azure Blob, local file, others) | +| Distributed locking | `DistributedLock.Core` abstraction; file-system provider (default), database providers available | + +### 4.3 Messaging + +MassTransit (`MassTransit`, `MassTransit.RabbitMQ`, `MassTransit.Azure.ServiceBus.Core`) is listed in `Directory.Packages.props`, indicating first-class support for RabbitMQ and Azure Service Bus. However, in the `release/3.7.0` source tree no MassTransit integration module is present under `src/modules`. MassTransit integration likely ships as a separate extension package or commercial add-on. + +### 4.4 Observability + +| Signal type | Integration | +|---|---| +| Distributed traces | OpenTelemetry (`OpenTelemetry.Extensions.Hosting`, OTLP exporter, ASP.NET Core / HTTP / SqlClient instrumentation) | +| Logs | `ILogger` throughout; Serilog integration available | +| APM | Datadog APM (`Datadog.Trace.Bundle` 3.32.0) in `Directory.Packages.props`; separate Dockerfile and Docker Compose for Datadog | + +### 4.5 Reference Server and Docker + +`Elsa.Server.Web` is the reference ASP.NET Core server. Docker Compose files (`docker/docker-compose.yml`) provide pre-configured containers for PostgreSQL, SQL Server, MySQL, Oracle, MongoDB, RabbitMQ, Redis, and SMTP4Dev. A load balancer reference app (`Elsa.Server.LoadBalancer`) demonstrates YARP-based multi-instance deployment. + +--- + +## 5. Known Limitations + +**Designer / Studio gaps:** +- The Elsa Studio designer supports only Flowchart activities. `Sequence` and `StateMachine` composite types cannot be designed visually; they are code-only or JSON-only. +- Starting a workflow from the designer is restricted to trigger-free, input-free workflows. +- UI input validation in the designer is not implemented. +- The workflow instance viewer does not yet render input/output values. + +**Runtime:** +- `LocalWorkflowRuntime` has no cluster safety. Must not be used on multi-pod deployments without switching to `DistributedWorkflowRuntime`. +- The distributed runtime uses distributed locks to serialize execution per workflow instance. High-throughput scenarios with many concurrent short-running instances may experience lock contention. +- Heartbeat-based interrupted workflow detection relies on polling (`RestartInterruptedWorkflowsTask`). There is no push-based dead-instance detection. +- Near-real-time resumption under load depends on correct tuning of `TriggerBookmarkQueueRecurringTask` polling schedules. + +**Persistence:** +- MongoDB and Dapper adapters are referenced in documentation but are absent from the `release/3.7.0` source tree. +- In-memory stores (the default when no EF Core module is registered) do not survive process restarts and are unsuitable for production use. +- EF Core migrations must be applied before version upgrades that include schema changes. + +**Expressions:** +- Python expressions require a native Python runtime installed on the host. Container deployments must include Python in the base image. +- Enabling `AllowClrAccess` for JavaScript expressions provides unrestricted .NET type access from within Jint scripts — a significant security risk for user-defined workflows. +- The ElsaScript DSL uses a regex-based parser, limiting its reliability for complex or nested syntax. + +**Multi-tenancy:** +- Background jobs and scheduled tasks use a separate `TenantTaskManager` that must be explicitly wired; missing configuration causes tenant context loss during background execution. +- The tenant-ID convention changed in 3.6.0 (empty string for default, `null` for tenant-agnostic). Existing databases require migration before upgrading past 3.5.x. + +**Security:** +- The identity module's `DefaultSecretHasher` uses SHA-256 for password hashing — not a key-derivation function. Production deployments using the built-in identity provider are vulnerable to offline dictionary attacks. +- The `DefaultAdminUserFeature` creates a seeded admin user with wildcard permissions. Leaving default credentials unchanged is a documented risk. +- The sample `appsettings.json` contains a default JWT signing key committed to source control; production deployments must override via secrets management. + +**Observability:** +- Execution log records can grow very large for deeply nested workflows. No automatic archival or TTL is applied to execution log records. +- No first-party metrics dashboard or SLA tracking; operators must instrument via OpenTelemetry and external platforms. + +**Roadmap gaps (as of 3.7.0):** +- No StateMachine visual designer support. +- No native MassTransit integration module in the `src/` tree. +- No MongoDB or Dapper persistence modules in the `src/` tree. +- OpenTelemetry Redis instrumentation is listed in dependencies but there is no built-in Elsa Redis workflow state store. diff --git a/doc/security-assessment/elsa-core-software-quality.md b/doc/security-assessment/elsa-core-software-quality.md new file mode 100644 index 000000000..8d538cc33 --- /dev/null +++ b/doc/security-assessment/elsa-core-software-quality.md @@ -0,0 +1,150 @@ +# Elsa Workflows Core — Software quality scan + +| Field | Value | +|---|---| +| **Repository / project** | elsa-workflows/elsa-core | +| **Git ref** | release/3.7.0 | +| **Version** | 3.7.0 | +| **Assessment date (UTC)** | 2026-05-20T00:00:00Z | +| **Assessment method** | Static code analysis, Software quality scan | +| **Model and tools** | Claude Sonnet 4.6 · Static code analysis · Software Quality Catalog v0.1 | +| **Assessment scope** | All source projects in src/ (50 projects) and test/ (23 projects) | +| **Related documents** | elsa-core-profile.md, elsa-core-architecture-patterns.md, elsa-core-iso25010.md | + +> ⚠️ **AI-assisted assessment — human review required** +> +> This document was produced by an AI model (Claude Sonnet 4.6) using static code analysis and structured quality catalogs. Findings are derived from static pattern detection across source files. They identify signals that warrant human judgment; they are not definitive defect reports. Severity ratings reflect the risk profile of each pattern, not confirmed impact. +> +> **This document is a draft input to a human review process — not a final approved report.** +> All findings should be verified by a qualified engineer familiar with the codebase before being acted upon. + +--- + +## 9. Software quality scan + +**Scan date:** 2026-05-20 +**Scope:** All source projects in src/ (50 projects) and test/ (23 projects) +**Coverage report:** Not present — Coverlet/OpenCover XML not found in repository + +> Assessment status key: +> ✅ Fully assessed — systematic, deterministic finding +> 🟡 Partially assessed — pattern-detectable, needs context to interpret +> 🔴 Judgment required — heuristic; human review needed + +--- + +### 9.1 OOP and component design + +| Practice | Status | Finding | Recommendation | +|---|---|---|---| +| Single Responsibility | 🔴 | `ActivityExecutionContext` (818 lines, ~94 member declarations) and `WorkflowExecutionContext` (716 lines, ~53 method-level members) both combine scheduling, memory, property bag, bookmark management, service location, and state transition responsibilities in a single class. Each exhibits 5–6 distinct concern clusters detectable by field grouping. `HttpEndpoint` (520 lines) merges trigger registration, request validation, file upload handling, and MIME-type enforcement into one activity class. | Decompose `ActivityExecutionContext` and `WorkflowExecutionContext` into focused collaborators (e.g. separate bookmark manager, memory accessor, service locator façade). Extract file and MIME validation out of `HttpEndpoint` into a dedicated request-validation pipeline. | +| Open/Closed | 🟡 | No systemic `switch`/`if-else` on type strings found in the workflow core. The codebase relies extensively on interfaces, middleware pipelines, and strategy patterns to extend behaviour. `DefaultIncidentStrategyResolver` and `DefaultExpressionDescriptorProvider` use type-based dispatch but with polymorphic lookup rather than raw string comparisons — acceptable OCP practice. | No action required for the core. Monitor `ActivityJsonConverter`, which deserialises by type name string and contains one custom fallback branch; ensure new activity types are registered rather than hard-coded. | +| Liskov Substitution | 🟡 | 17 `throw new NotImplementedException()` calls found project-wide. In production code: `JsonIgnoreCompositeRootConverter.Read` (intentional — write-only converter), `RootActivityNodeConverter.Read`, `DownloadableContentHandlerBase.HandleAsync`, and `HttpStatusCodeCaseForWorkflowInstanceConverter.Write` — all are deliberate narrow contracts. No LSP violation in primary workflow execution path detected. | Mark narrow contracts with `[Obsolete]` or XML doc warnings rather than silent `NotImplementedException`; consider sealed/abstract partial implementations to enforce the intent. | +| Interface Segregation | 🟡 | `IWorkflowBuilder` has 28+ methods covering both fluent mutation and workflow-construction concerns — a moderately fat interface. `IActivity` is lean (2 methods + 6 properties) — well-segregated. `IActivityRegistry` exposes both registration (write) and lookup (read) operations in a single contract. | Split `IWorkflowBuilder` into `IWorkflowBuilderConfiguration` (mutating builder setters) and `IWorkflowBuilderFactory` (build-time methods). Consider splitting `IActivityRegistry` into `IActivityRegistryWriter` and `IActivityRegistryReader`. | +| Dependency Inversion | ✅ | Constructor injection is consistent throughout the codebase. All sampled services (`ActivityRegistry`, `WorkflowHost`, `ActivityDescriber`, `WorkflowGraphBuilder`) receive only interfaces or abstractions in their constructors. `new ConcreteType()` in class bodies is confined to value objects, DTOs, and intentional in-method command construction (e.g. `BackgroundWorkflowDispatcher` creates command records, not services). | No action required. DI discipline is strong. | +| God classes | 🟡 | `ActivityExecutionContext` (818 lines, 94 member declarations) and `WorkflowExecutionContext` (716 lines, 53 method members) exceed the god-class threshold on both line count and method count. `Store` (635 lines) is a broad generic repository aggregating Add, AddMany, Save, Update, Delete, Count, Query, and Find operations. `HttpEndpoint` (520 lines) is a heavyweight activity. | See SRP recommendation above. For `Store`, consider splitting bulk-operation helpers into extension methods or a dedicated `BulkStore` specialisation. | +| Primitive obsession | 🔴 | `WorkflowExecutionContext` constructor accepts `string? correlationId`, `string? parentWorkflowInstanceId`, `string? triggerActivityId` — three adjacent `string?` parameters with similar shapes and semantics. `ActivityIncident` constructor takes five `string` parameters. `Bookmark` default constructor passes six string literals. These are latent swap-argument bugs and hinder readability. | Introduce typed value objects: `CorrelationId`, `WorkflowInstanceId`, `TriggerActivityId`. A source generator or record wrapper is sufficient. | + +--- + +### 9.2 Coupling and cohesion + +| Practice | Status | Finding | Recommendation | +|---|---|---|---| +| Afferent / efferent coupling | ✅ | `ActivityExecutionContext` is the highest-fan-in class in the codebase — referenced by virtually every module through its public API surface (activities, middleware, extensions, services). `WorkflowExecutionContext` is the second. Both are intentional hub types. `Elsa.Workflows.Core` has very high efferent coupling (30+ using directives in key files) as the foundational module — all other modules depend on it; it depends on no higher-level modules. | The coupling topology is architecturally justified for a workflow engine. However, the extension-method files (`ActivityExecutionContextExtensions.cs` at 520 lines, `ExpressionExecutionContextExtensions.cs` at 570 lines) should be reviewed for methods that could be inlined or moved to feature modules to reduce artificial coupling. | +| Cyclic dependencies | ✅ | No assembly-level cycles detected. `Elsa.Workflows.Core` → `Elsa.Common` → no Elsa.Workflows reference. `Elsa.Workflows.Runtime` → `Elsa.Workflows.Core` is the expected layering. `Elsa.Workflows.Api` depends on `Elsa.Workflows.Management` and `Elsa.Workflows.Runtime` but neither depends back on the API layer. | No action required. | +| LCOM (Lack of Cohesion of Methods) | 🔴 | `ActivityExecutionContext` contains groups of methods that share no common fields: service-location methods (`GetRequiredService`, `GetOrCreateService`) reference only `WorkflowExecutionContext`; bookmark methods reference `_newBookmarks`; property bag methods reference `Properties`; memory-block methods reference `ExpressionExecutionContext`. These clusters indicate low cohesion — a god-class symptom. Similar pattern in `WorkflowExecutionContext`. | Decompose along cohesion boundaries. A `IActivityServiceLocator`, `IActivityBookmarkManager`, and `IActivityMemoryAccessor` wrapping the context would restore cohesion. | +| Law of Demeter | 🟡 | Several extension methods chain 3+ hops: `context.WorkflowExecutionContext.Workflow.Options.CommitStrategyName` (`DefaultActivityInvokerMiddleware`), `context.WorkflowExecutionContext.Workflow.Options.IncidentStrategyType` (`DefaultIncidentStrategyResolver`). These cross three object boundaries. Fewer than 10 locations detected. | Introduce façade properties on `WorkflowExecutionContext` (`CommitStrategyName`, `IncidentStrategyType`) that delegate internally, hiding the traversal. | +| Tell, don't ask | 🟡 | `WorkflowHost.RunWorkflowAsync` checks `WorkflowState.Status != WorkflowStatus.Running` before delegating — query-then-act on the same object. The pattern is isolated to the deprecated `WorkflowHost` class. Core workflow execution pipelines use tell-style scheduling (`ScheduleActivityAsync`). | Low priority given the `[Obsolete]` marker on `WorkflowHost`. No action required in new code. | +| Feature envy | 🟡 | `ActivityExecutionContextExtensions.InputEvaluation.cs` and `ActivityExecutionContextExtensions.cs` (520 lines combined) contain methods whose logic primarily manipulates the state of `ActivityExecutionContext` — suggesting the behaviour belongs on the class itself. This is a known .NET pattern for keeping context classes from growing, but the extensions are so numerous they become de facto class members. | Audit which extensions are called only from within a single activity and move them to protected helpers on `Activity` base classes. | + +--- + +### 9.3 Code complexity and size + +| Practice | Status | Threshold | Violations | Worst offender | +|---|---|---|---|---| +| Cyclomatic complexity | 🟡 | >10 warn, >20 severe | ~3–5 methods at warn level, 1–2 at severe | `HttpEndpoint.HandleRequestAsync` (multiple nested if/return paths, estimated CC ~14); `ActivityDescriber.DescribeActivityAsync` (multiple LINQ + conditional branches, estimated CC ~12) | +| Method length | ✅ | >50 lines warn, >100 severe | ~4 methods exceed 50 lines in non-generated production code | `HttpEndpoint.HandleRequestAsync` (~88 lines); `WorkflowExecutionContext.CreateAsync` overloads (~50 lines each); `ActivityDescriber.DescribeActivityAsync` (~70 lines) | +| Class length | ✅ | >300 lines warn, >700 severe | 4 at warn; 2 at severe | **Severe:** `ActivityExecutionContext` (818 lines), `WorkflowExecutionContext` (716 lines). **Warn:** `Store` (635 lines), `ExpressionExecutionContextExtensions` (570 lines), `ActivityExecutionContextExtensions` (520 lines), `HttpEndpoint` (520 lines). EF Core migration-generated files excluded. | +| Nesting depth | 🟡 | >4 levels | ~2–3 locations at depth 4–5 | `HttpEndpoint.HandleRequestAsync` nesting depth ~5 (method → try → if → if → if); `TenantTaskManager` catch blocks reach ~4 levels inside async lambdas | +| Parameter count | ✅ | >5 params | 3 constructors | `WorkflowExecutionContext` private constructor (13 parameters); `Workflow` constructor (12 parameters); `ActivityIncident` constructor (6 parameters). All use named parameters at call sites, reducing swap risk, but the 13-parameter constructor is a complexity indicator. | + +--- + +### 9.4 Code hygiene + +| Practice | Status | Finding | +|---|---|---| +| Code duplication (DRY) | 🟡 | File-validation methods in `HttpEndpoint` (`ValidateFileSizes`, `ValidateFileExtensionWhitelist`, `ValidateFileExtensionBlacklist`, `ValidateFileMimeTypes`) share a repeated structure: check limit input → return true if not configured → evaluate collection → set response status code 413 or 415. This structural repetition (~15 lines each) could be extracted into a generic validation pipeline. EF Core migration files (15+ generated files, 500+ lines each) excluded. | +| Dead code | 🟡 | `PropertyOptionsResolver.cs` contains ~8 lines of commented-out constructor injection code. `PersistentVariableState.cs` has 5 lines of commented-out class body. `ActivityExecutionContextExtensions.InputEvaluation.cs` has 4 lines of commented-out log sanitisation code with a TODO. `ActivitySchedulerFactory.cs` contains `//public IActivityScheduler CreateScheduler() => new StackBasedActivityScheduler();`. These are relics of active migration and not dead production paths, but they accumulate noise. | +| Magic literals | 🟡 | `BackgroundActivityExecutionContextExtensions` uses string keys `"BackgroundCompletion"`, `"BackgroundScheduledActivities"` as property bag keys directly in multiple methods. `Switch.cs` uses `"ScheduledActivityIds"` as a property key. These magic strings are repeated across get/set pairs and risk typo-based bugs. `WorkflowStorageDriver` defines a `const string` for its key — the correct pattern. | +| TODO / FIXME comments | ✅ | 12 TODO comments found in production source (0 FIXME/HACK). Notable items: `DefaultAlterationRunner.cs` — architectural concern about double-save on DB; `WorkflowStateExtractor.cs` — temporary solution acknowledged; `HttpEndpoint/WriteFileHttpResponse.cs` — cached file not deleted; `Elsa.Http/DownloadableContentHandlers` — file caching not implemented; `NotificationLoggingMiddleware.cs` — logging stub. None are in critical execution paths but several reflect unimplemented features. | +| Commented-out code | 🟡 | 6 distinct blocks of commented-out code found: `PropertyOptionsResolver.cs` (8 lines), `PersistentVariableState.cs` (5 lines), `ActivityExecutionContextExtensions.InputEvaluation.cs` (4 lines with TODO), `ActivitySchedulerFactory.cs` (1 line), `IActivityPropertyOptionsProvider.cs` (1 commented method signature). Benign but should be removed or converted to tracked issues. | +| Naming conventions | 🟡 | Integration test methods named `Test1`, `Test2`, `Test3` found in 8+ test files (`ToJsonTests.cs`, `JsonConverterTest.cs`, `MigrationTests.cs`, `SetGetVariables/Tests.cs`, `JavaScriptListsAndArrays/Tests.cs`, `JavaScriptNativeVariables/Tests.cs`, `JsonObjectSerialization/Tests.cs`, `WorkflowDefinitionStorePopulation/Tests.cs`). Production code naming is consistent and idiomatic. No single-character variables in production paths. | + +--- + +### 9.5 Testability and test quality + +| Practice | Status | Finding | +|---|---|---| +| Dependency injection usage | ✅ | Consistent constructor injection throughout all sampled service and activity classes. `new ConcreteType()` in service bodies is limited to command/request record creation (e.g. `new DispatchWorkflowDefinitionCommand(...)` in `BackgroundWorkflowDispatcher`) — data-only DTOs, not services. No `new ConcreteService()` anti-pattern found in class bodies. | +| Test coverage | ✅ | Coverage report not present in repository — cannot assess line/branch coverage numerically. | +| Test assertion quality | 🟡 | Unit tests in `Elsa.Activities.UnitTests` (e.g. `IfTests.cs`) are exemplary: each `[Fact]` has 2–4 `Assert.*` calls with descriptive failure messages, follows Arrange-Act-Assert, uses `[Theory]/[InlineData]` for parameterised cases, and tests edge cases. Integration tests show mixed quality: tests named `Test1`/`Test2` in `SetGetVariables/Tests.cs` have `[Fact(DisplayName = "...")]` which partially mitigates the opaque name, but `ToJsonTests.cs` and `JsonConverterTest.cs` use bare `Test1`/`Test2` names without display names. | +| Test pyramid shape | ✅ | 12 unit test projects, 8 integration test projects, 1 component test project, 1 performance test project. Unit tests significantly outnumber integration tests — healthy pyramid shape. The ~530-test total (by `[Fact]/[Theory]` count) is concentrated in unit tests (~78 files) vs integration tests (~8 files with assertions). | +| Test isolation | 🟡 | `static readonly DefinitionId = Guid.NewGuid().ToString()` found in 4+ integration test workflow classes. These are set at class-load time and are effectively constant per test run — not mutated during tests, so they do not introduce state contamination. `TestSettings.IncidentStrategyType` is a `static` mutable property in `test/integration/.../Incidents/Statics/TestSettings.cs`, which could cause ordering-dependent failures if tests set it concurrently. No `Thread.Sleep` found in test files. | + +--- + +### 9.6 API and contract design + +| Practice | Status | Finding | +|---|---|---| +| HTTP status code semantics | 🟡 | `HttpEndpoint` correctly uses `StatusCodes.Status413PayloadTooLarge` for size violations, `StatusCodes.Status415UnsupportedMediaType` for invalid MIME types/extensions, and `StatusCodes.Status400BadRequest` for invalid JSON payloads. Status codes are semantically accurate. No 200-for-error or 403-for-validation misuse detected in sampled endpoints. | +| Response shape consistency | 🟡 | Error responses in `HttpEndpoint` write anonymous `{ Message = "..." }` JSON objects directly — not a shared typed error envelope. `Elsa.Workflows.Api` endpoints use FastEndpoints-style `Request`/`Response` pairs. No global error schema was found to be enforced. Inconsistency risk exists between workflow endpoint responses and HTTP activity error responses. | +| Configuration externalisation | 🟡 | `appsettings.json` in `Elsa.Server.Web` contains a default JWT signing key `"sufficiently-large-secret-signing-key"`, three hashed user passwords, and an application client secret. This is a development/demo configuration file. All values are externalisable via `IConfiguration` (the `Program.cs` correctly binds identity sections from configuration). The `DefaultConnectionString` constant in `Elsa.Persistence.EFCore.Common/Constants.cs` (`"Data Source=elsa.sqlite.db;Cache=Shared;"`) is a fallback default — acceptable for development, must be overridden in production. | +| Null handling discipline | ✅ | `enable` is set globally in `Directory.Build.props`. All sampled interfaces use `string?` for optional members and non-nullable for required members. `WorkflowExecutionContext` constructor uses `IDictionary?` with explicit null-coalescing. No `#nullable disable` overrides found in sampled files. Nullable discipline is strong. | + +--- + +### 9.7 Operational quality + +| Practice | Status | Finding | +|---|---|---| +| Structured logging | 🟡 | The vast majority of log calls correctly use message templates with named placeholders. Two violations found: `Elsa.Mediator/Services/JobQueue.cs` line 26: `logger.LogWarning($"Job {jobId} was not found")` — uses string interpolation instead of a structured template. `Elsa.Server.Web/ActivityHosts/Penguin.cs` line 35: `logger.LogInformation($"The penguin is eating {food}!")` — sample app shipped in repo. Both defeat semantic log querying and cause string allocation before log-level check under high load. | +| Exception handling | 🟡 | `TenantTaskManager` uses `catch (Exception e) when (!e.IsFatal())` with logging and non-rethrow — appropriate for background recurring tasks. Two **empty** `catch (Exception)` blocks found in `DefaultExpressionDescriptorProvider.cs`: expression parsing failures silently return a fallback expression with no log call. `ObjectConverter.cs` has two broad `catch (Exception e)` blocks for type-conversion fallback — reasonable context, but no log entry is produced for conversion failures. | +| Async/await correctness | ✅ | `ConfigureAwait.Fody` is in use (confirmed by `FodyWeavers.xml` in multiple modules) — `ConfigureAwait(false)` is injected automatically at build time; manual calls not required. No `.Result` blocking calls on live `Task`/`ValueTask` instances found (the `t.Result` in `BulkCancel/Endpoint.cs` accesses a completed `Task` after `await Task.WhenAll(tasks)` — safe). Three `async void` methods found: `ScheduledTimer.Callback`, `HeartbeatGenerator.GenerateHeartbeatAsync`, `ConfigurationTenantsProvider.OnOptionsChanged` — all are timer or `IOptionsMonitor` callbacks where `async void` is the required signature, each wrapping a try/catch. | +| Disposable resource handling | 🟡 | `IDisposable` implementations are consistently provided wherever `IDisposable` is implemented (`TenantScope`, `DefaultTenantService`, `ScheduledTimer`, `WorkflowHost`). `CancellationTokenSource` objects are disposed after use in `WorkflowHost.RunWorkflowAsync`. No `new HttpClient()` per-request instantiation detected. `static readonly SemaphoreSlim Semaphore = new(1, 1)` in `Store` is a static disposable that is never disposed — acceptable for application-lifetime singletons, but worth noting. | +| Security hygiene | 🟡 | **SHA-256 for password hashing:** `DefaultSecretHasher` uses `SHA256.Create()` with a concatenated password+salt. SHA-256 is not a password-hashing algorithm (it is fast by design), making offline dictionary attacks and rainbow-table attacks feasible. Replace with PBKDF2 (`Rfc2898DeriveBytes`, ≥100,000 iterations), bcrypt, or Argon2id. **Committed default signing key:** `appsettings.json` contains `"SigningKey": "sufficiently-large-secret-signing-key"` in the server sample app. **Raw SQL construction:** `BulkUpsertExtensions.cs` constructs SQL strings using `$"INSERT INTO \"{tableName}\""` with table/column names sourced from EF Core metadata (not user input) — low injection risk currently but warrants ongoing review. No API keys or secrets found embedded in `.cs` files. | + +--- + +### 9.8 Quality scan summary + +| Category | Checks run | ✅ Clean | 🟡 Needs review | 🔴 Action required | +|---|---|---|---|---| +| 9.1 OOP and component design | 7 | 1 | 3 | 3 | +| 9.2 Coupling and cohesion | 6 | 2 | 3 | 1 | +| 9.3 Code complexity and size | 5 | 2 | 2 | 1 | +| 9.4 Code hygiene | 6 | 1 | 4 | 1 | +| 9.5 Testability and test quality | 5 | 2 | 2 | 0 | +| 9.6 API and contract design | 4 | 2 | 2 | 0 | +| 9.7 Operational quality | 5 | 1 | 4 | 0 | +| **Total** | **38** | **11** | **20** | **7** | + +**Top 3 priority findings:** + +1. **SHA-256 used for password hashing in `DefaultSecretHasher`** (`src/modules/Elsa.Identity/Services/DefaultSecretHasher.cs`). SHA-256 is a fast general-purpose hash, not a key-derivation function. It makes offline dictionary attacks and rainbow-table attacks feasible. Replace with `Rfc2898DeriveBytes` (PBKDF2-SHA256, ≥100,000 iterations), `BCrypt.Net`, or `Konscious.Security.Cryptography` (Argon2id). Affects all deployments using the built-in identity provider. + +2. **God-class `ActivityExecutionContext` and `WorkflowExecutionContext`** (`src/modules/Elsa.Workflows.Core/Contexts/`). At 818 and 716 lines respectively, with 94 and 53 member declarations, both classes accumulate scheduling, memory, bookmarks, service location, state transitions, and metadata management. Low cohesion and very high afferent coupling make them the highest-risk classes for regression on any change. Decomposition into focused collaborators (bookmark manager, memory accessor, service-location façade) would reduce change-blast-radius and improve testability. + +3. **Development signing key and credentials committed in `appsettings.json`** (`src/apps/Elsa.Server.Web/appsettings.json`). The JWT signing key `"sufficiently-large-secret-signing-key"` and three user credential entries are committed to source control. If accidentally deployed to production without environment-specific overrides, this presents an authentication bypass risk. Add an explicit `.gitignore` rule for `appsettings.Production.json`, document the requirement to override `Identity:Tokens:SigningKey` via secrets management (Azure Key Vault, AWS Secrets Manager, Docker secrets), and remove all user entries from the committed config in favour of runtime-only seeding. + +--- + +**Not assessed in this scan:** +- Modularity and module-level cohesion — requires architectural review of assembly dependency graph tooling +- Whether SOLID violations are design intent or mistakes — requires domain knowledge of the workflow engine's evolution +- Test correctness (whether tests verify the right behaviour) — requires domain review +- Cyclomatic complexity exact counts — requires running a static analysis tool; estimates above are from manual inspection diff --git a/doc/security-assessment/elsa-workflows-elsa-core-findings-2026-05-20.md b/doc/security-assessment/elsa-workflows-elsa-core-findings-2026-05-20.md new file mode 100644 index 000000000..e62ba9bd2 --- /dev/null +++ b/doc/security-assessment/elsa-workflows-elsa-core-findings-2026-05-20.md @@ -0,0 +1,672 @@ +# Unsandboxed Python.NET execution exposes the host process + +## Details +The Python scripting integration runs workflow-defined code through Python.NET with CLR interop enabled and no sandbox. Equivalent risk to the C# scripting issue. + +## Location +[src/modules/Elsa.Python/Services/PythonEvaluator.cs:1](https://github.com/elsa-workflows/elsa-core/blob/release/3.7.0/src/modules/Elsa.Python/Services/PythonEvaluator.cs#L1) + +## Impact +Workflow author executes arbitrary Python with CLR interop and host-level privileges + +## Reproduction steps +1. Workflow author writes `import clr; clr.AddReference('System.Diagnostics'); from System.Diagnostics import Process; Process.Start('cmd','/c whoami')`. The workflow engine executes the command as the server identity. + +## Recommended fix +Gate the Python script activity behind an elevated permission and warn explicitly that it executes with full host privileges; consider out-of-process execution if untrusted authors are permitted. + +--- +**Severity:** HIGH +**Status:** Open +**Category:** Improper Control of Generation of Code (CWE-94) +**Repository:** elsa-workflows/elsa-core +**Branch:** release/3.7.0 +**Date created:** 2026-05-20 + +--- + +# Path traversal through x-download-id header in ZipManager + +## Details +ZipManager constructs a download path by appending the value of the x-download-id request header to a base directory without canonicalizing or rejecting traversal sequences. A value like `../../../../etc/passwd` escapes the intended directory and discloses arbitrary files readable by the service account. + +## Location +[src/modules/Elsa.Workflows.Api/Files/ZipManager.cs:1](https://github.com/elsa-workflows/elsa-core/blob/release/3.7.0/src/modules/Elsa.Workflows.Api/Files/ZipManager.cs#L1) + +## Impact +Attacker reads arbitrary files from the server filesystem + +## Reproduction steps +1. Attacker sends GET /elsa/api/.../download with header `x-download-id: ../../../../etc/passwd`. The server reads /etc/passwd and returns it. + +## Recommended fix +Validate the download identifier against a whitelist of generated tokens. Reject any value containing path separators or `..` and resolve the canonical path before opening the file. + +--- +**Severity:** HIGH +**Status:** Open +**Category:** Path Traversal (CWE-22) +**Repository:** elsa-workflows/elsa-core +**Branch:** release/3.7.0 +**Date created:** 2026-05-20 + +--- + +# Unauthenticated polymorphic JSON deserialization enables remote code execution + +## Details +The bookmark Resume endpoint is decorated with AllowAnonymous(). When token validation fails at line 30, AddError("Invalid token.") is called but execution is NOT halted with `return`. Control then flows through GetInputFromQueryString() (line 33) which calls _payloadSerializer.Deserialize>(inputJson) on the attacker-controlled `?in=` query string. The payload serializer registers PolymorphicObjectConverterFactory + TypeJsonConverter (JsonPayloadSerializer.cs:82-83). PolymorphicObjectConverter.Read parses an attacker-supplied `_type` discriminator and falls back to Type.GetType(typeAlias) in TypeJsonConverter.cs:50, allowing instantiation of any assembly-qualified .NET type, equivalent to TypeNameHandling.All. Combined with gadgets such as System.Windows.Data.ObjectDataProvider or System.Configuration.Install.AssemblyInstaller this yields arbitrary code execution. Token validation does not block the attack because validation failure does not stop the handler. + +## Location +[src/modules/Elsa.Workflows.Api/Endpoints/Bookmarks/Resume/Endpoint.cs:30](https://github.com/elsa-workflows/elsa-core/blob/release/3.7.0/src/modules/Elsa.Workflows.Api/Endpoints/Bookmarks/Resume/Endpoint.cs#L30) + +## Impact +Unauthenticated attacker achieves remote code execution by instantiating arbitrary CLR types + +## Reproduction steps +1. Attacker POSTs to /elsa/api/bookmarks/{anything}/resume?in={"$values":[{"_type":"System.Windows.Data.ObjectDataProvider, PresentationFramework","MethodName":"Start","ObjectInstance":{"_type":"System.Diagnostics.Process","StartInfo":{"FileName":"cmd.exe","Arguments":"/c calc"}}}]} with no auth header. The handler calls AddError("Invalid token.") but continues; the query JSON is deserialized through the polymorphic converter; the ObjectDataProvider gadget triggers Process.Start during construction, yielding remote code execution as the workflow server user. + +## Recommended fix +After validation failure, terminate the request before any attacker-controlled data is parsed. Replace the polymorphic object/dictionary converter with a strict, schema-bound deserialization model that does not honor a `_type` discriminator on untrusted endpoints, and remove the Type.GetType fallback in TypeJsonConverter so only well-known aliases resolve to safe types. + +--- +**Severity:** HIGH +**Status:** Open +**Category:** Insecure Deserialization (CWE-502) +**Repository:** elsa-workflows/elsa-core +**Branch:** release/3.7.0 +**Date created:** 2026-05-20 + +--- + +# Authenticated polymorphic JSON deserialization in workflow instance import + +## Details +The Import endpoint requires the write:workflow-instances permission and calls _workflowStateSerializer.Deserialize(model.WorkflowState) and _payloadSerializer.Deserialize(payloadElement). Both serializers register PolymorphicObjectConverterFactory and the TypeJsonConverter with Type.GetType fallback. Any imported `_type` value loads and instantiates arbitrary assembly-qualified types, providing the same gadget primitive as the bookmark-resume issue but requiring authentication. + +## Location +[src/modules/Elsa.Workflows.Api/Endpoints/WorkflowInstances/Import/Endpoint.cs:112](https://github.com/elsa-workflows/elsa-core/blob/release/3.7.0/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowInstances/Import/Endpoint.cs#L112) + +## Impact +Authenticated user with import permission achieves remote code execution + +## Reproduction steps +1. An attacker with a low-privilege workflow operator token submits an exported workflow instance containing a properties payload {"_type":"",...} to /workflow-instances/import. During state restoration the polymorphic converter activates the gadget chain, executing attacker code under the server identity. + +## Recommended fix +Restrict workflow-state deserialization to a closed set of types via a strict type registry, drop the Type.GetType fallback, and require an additional administrator role to import workflow state from untrusted sources. + +--- +**Severity:** HIGH +**Status:** Open +**Category:** Insecure Deserialization (CWE-502) +**Repository:** elsa-workflows/elsa-core +**Branch:** release/3.7.0 +**Date created:** 2026-05-20 + +--- + +# AdminApiKeyProvider grants admin to all-zero GUID API key + +## Details +DefaultApiKey is `Guid.Empty.ToString()` and the provider returns an IApiKey with claims `permissions=*` whenever that value is submitted. Any deployment that wires the AdminApiKeyProvider (the default for several samples) grants full administrative privileges to a request that simply presents the all-zero GUID as the API key. + +## Location +[src/modules/Elsa.Identity/Providers/AdminApiKeyProvider.cs:15](https://github.com/elsa-workflows/elsa-core/blob/release/3.7.0/src/modules/Elsa.Identity/Providers/AdminApiKeyProvider.cs#L15) + +## Impact +Anyone holding the empty GUID `00000000-0000-0000-0000-000000000000` becomes admin + +## Reproduction steps +1. Attacker sends `Authorization: ApiKey 00000000-0000-0000-0000-000000000000` to any management endpoint; the provider returns admin claims and the request proceeds with full privileges. + +## Recommended fix +Remove the static fallback. Require explicit provisioning of API keys, store only salted hashes, and refuse to issue an admin key without explicit operator action. + +--- +**Severity:** HIGH +**Status:** Open +**Category:** Use of Hard-coded Credentials (CWE-798) +**Repository:** elsa-workflows/elsa-core +**Branch:** release/3.7.0 +**Date created:** 2026-05-20 + +--- + +# AdminUserProvider returns admin for any username when password is `password` + +## Details +AdminUserProvider hashes the literal string `password` once and returns the admin user from FindAsync regardless of the supplied filter. Coupled with the login flow, any (username, password=`password`) pair authenticates as admin. + +## Location +[src/modules/Elsa.Identity/Providers/AdminUserProvider.cs:19](https://github.com/elsa-workflows/elsa-core/blob/release/3.7.0/src/modules/Elsa.Identity/Providers/AdminUserProvider.cs#L19) + +## Impact +Authentication accepts any username with the static password `password` as admin + +## Reproduction steps +1. Attacker POSTs to /identity/login with username `attacker` and password `password`. The provider returns the admin user; a JWT with permissions=`*` is issued. + +## Recommended fix +Delete the static admin provider. Require operators to seed users explicitly; validate the username against the supplied filter. + +--- +**Severity:** HIGH +**Status:** Open +**Category:** Authentication Bypass by Hard-coded Credentials (CWE-798/CWE-287) +**Repository:** elsa-workflows/elsa-core +**Branch:** release/3.7.0 +**Date created:** 2026-05-20 + +--- + +# LocalHost permission handler grants admin to any request marked local + +## Details +The LocalHostPermissionRequirementHandler succeeds any permission requirement when the HttpContext indicates the connection is local. In containerized or proxy deployments the loopback check trivially passes for traffic forwarded through reverse proxies that do not strip X-Forwarded-For, and for any process colocated with the server. This bypasses all permission checks for those callers. + +## Location +[src/modules/Elsa.Identity/AuthorizationHandlers/LocalHostPermissionRequirementHandler.cs:1](https://github.com/elsa-workflows/elsa-core/blob/release/3.7.0/src/modules/Elsa.Identity/AuthorizationHandlers/LocalHostPermissionRequirementHandler.cs#L1) + +## Impact +Attacker who can spoof loopback gains administrative authorization + +## Reproduction steps +1. Attacker controls a sidecar or compromises any process on the same host. They send requests over loopback (or through a proxy that preserves loopback semantics) and inherit admin authorization without any credentials. + +## Recommended fix +Remove implicit loopback admin entirely, or gate it behind a startup-time opt-in plus a signed local token rather than IP-only check. + +--- +**Severity:** HIGH +**Status:** Open +**Category:** Improper Authorization (CWE-285) +**Repository:** elsa-workflows/elsa-core +**Branch:** release/3.7.0 +**Date created:** 2026-05-20 + +--- + +# Privilege escalation through client-supplied role assignment on Users/Create + +## Details +The Users Create and Update endpoints accept a Roles collection directly from the request body and assign it without verifying that the caller has the authority to grant those roles. A user with `write:users` (intended for user management, not role management) can therefore create a user with the admin role and obtain credentials for it. + +## Location +[src/modules/Elsa.Identity/Endpoints/Users/Create/Endpoint.cs:1](https://github.com/elsa-workflows/elsa-core/blob/release/3.7.0/src/modules/Elsa.Identity/Endpoints/Users/Create/Endpoint.cs#L1) + +## Impact +Low-privilege caller assigns admin role to a newly created user + +## Reproduction steps +1. Attacker with `write:users` POSTs {"name":"backdoor","password":"x","roles":["admin"]}. The new account is created with admin permissions; the attacker logs in as `backdoor`. + +## Recommended fix +Validate role assignments server-side: a caller may only grant roles whose permissions are a subset of their own, and a dedicated `manage:roles` permission must gate admin role assignment. + +--- +**Severity:** HIGH +**Status:** Open +**Category:** Improper Privilege Management (CWE-269) +**Repository:** elsa-workflows/elsa-core +**Branch:** release/3.7.0 +**Date created:** 2026-05-20 + +--- + +# HTTP endpoint trusts ContentLength header for body-size check + +## Details +HttpEndpoint reads HttpContext.Request.ContentLength ?? 0 to enforce a maximum body size and selects a streaming/buffering strategy from it. A chunked-encoded request reports ContentLength = null, so the check is skipped and the full body is read into memory regardless of the configured cap. + +## Location +[src/modules/Elsa.Http/Activities/HttpEndpoint.cs:303](https://github.com/elsa-workflows/elsa-core/blob/release/3.7.0/src/modules/Elsa.Http/Activities/HttpEndpoint.cs#L303) + +## Impact +Attacker bypasses request-size limits with chunked transfer encoding. + +## Reproduction steps +1. Attacker sends a chunked POST to a workflow HTTP-trigger endpoint with body size far larger than the configured limit. The server reads it all, exhausting memory and degrading service. + +## Recommended fix +Use Request.EnableBuffering + a counting stream wrapper, or honor IHttpMaxRequestBodySizeFeature, so the limit is enforced regardless of ContentLength presence. + +--- +**Severity:** HIGH +**Status:** Open +**Category:** Resource Exhaustion / DoS +**Repository:** elsa-workflows/elsa-core +**Branch:** release/3.7.0 +**Date created:** 2026-05-20 + +--- + +# Default admin credentials with reversible password baked into config + +## Details +The Identity.Users section seeds default users (admin, alice, bob) with HashedPassword and Salt values committed in the repository. The hash algorithm is single-round SHA-256 over (UTF-8 password || salt). Cracking HashedPassword='TfKzh9RLix6FPcCNeHLkGrysFu3bYxqzGqduNdi8v1U=' with the committed salt resolves to the plaintext 'password' in milliseconds. Because Elsa.Identity seeds these users into the identity store on first run (and the Docker image ships the same config), every untouched deployment exposes a working administrator login. + +## Location +[src/apps/Elsa.Server.Web/appsettings.json:1](https://github.com/elsa-workflows/elsa-core/blob/release/3.7.0/src/apps/Elsa.Server.Web/appsettings.json#L1) + +## Impact +Unmodified deployments accept login as admin with password 'password' from any client. + +## Reproduction steps +1. 1) Attacker reaches the Identity login endpoint. 2) Attacker submits username=admin, password=password. 3) Login succeeds and returns a JWT (also signed with the hardcoded key from the prior finding). 4) Attacker now has full admin access without prior credentials. + +## Recommended fix +Do not seed users with known credentials. Either require operator-supplied bootstrap credentials, generate a random password at first start and surface it to the operator once, or refuse to seed when running outside an explicit 'demo' profile. + +--- +**Severity:** HIGH +**Status:** Open +**Category:** Auth/access +**Repository:** elsa-workflows/elsa-core +**Branch:** release/3.7.0 +**Date created:** 2026-05-20 + +--- + +# Hardcoded JWT signing key shipped in default server configuration + +## Details +The default appsettings.json shipped with Elsa.Server.Web (the host built by docker/ElsaServer.Dockerfile) contains a literal JWT signing key 'sufficiently-large-secret-signing-key' under Identity.Tokens.SigningKey. The DefaultAccessTokenIssuer reads this key directly from configuration to sign JWT bearer tokens. Because the source is public, any deployment that does not explicitly override this value lets anyone with the published key sign arbitrary JWTs and impersonate any principal (including the seeded admin). The sibling app Elsa.ModularServer.Web correctly uses 'CHANGE_ME_TO_A_SECURE_RANDOM_KEY' as a placeholder, demonstrating the intended pattern was not followed in the server-web app. + +## Location +[src/apps/Elsa.Server.Web/appsettings.json:71](https://github.com/elsa-workflows/elsa-core/blob/release/3.7.0/src/apps/Elsa.Server.Web/appsettings.json#L71) + +## Impact +Any network-reachable attacker can forge valid bearer tokens for any user/role. + +## Reproduction steps +1. 1) Attacker pulls the public Elsa repository or Docker image. 2) Attacker reads SigningKey 'sufficiently-large-secret-signing-key' from appsettings.json. 3) Attacker mints a JWT signed with that key, claiming roles/permissions of the admin tenant. 4) Attacker calls any authenticated endpoint (e.g., POST /workflow-definitions/import) and gains full administrative control over the workflow engine. + +## Recommended fix +Default configuration must not contain a usable signing key. Generate the key at first run, require an explicit operator-provided value, or refuse to start when the key matches a known-default sentinel. Apply the same pattern as Elsa.ModularServer.Web (placeholder that fails fast) to Elsa.Server.Web. + +--- +**Severity:** HIGH +**Status:** Open +**Category:** Auth/access +**Repository:** elsa-workflows/elsa-core +**Branch:** release/3.7.0 +**Date created:** 2026-05-20 + +--- + +# Role overwrite via id collision on Roles/Create + +## Details +The Roles Create endpoint accepts a client-supplied Id and persists the role with that identifier. If the attacker submits an id equal to an existing role (such as the admin role id, which is well known in default seeds), the existing role document is overwritten, replacing its permissions with attacker-controlled values. + +## Location +[src/modules/Elsa.Identity/Endpoints/Roles/Create/Endpoint.cs:1](https://github.com/elsa-workflows/elsa-core/blob/release/3.7.0/src/modules/Elsa.Identity/Endpoints/Roles/Create/Endpoint.cs#L1) + +## Impact +Caller overwrites the admin role document with attacker-defined permissions + +## Reproduction steps +1. Attacker with `write:roles` POSTs {"id":"admin","permissions":["*"]} or replaces the admin role permissions list, then assigns themselves the (now-attacker-controlled) admin role. + +## Recommended fix +Refuse to honor a caller-supplied id on create; always generate a new id server-side and reject updates that target privileged role ids unless the caller has elevated authority. + +--- +**Severity:** HIGH +**Status:** Open +**Category:** Improper Privilege Management (CWE-269) +**Repository:** elsa-workflows/elsa-core +**Branch:** release/3.7.0 +**Date created:** 2026-05-20 + +--- + +# Unsandboxed Roslyn C# script execution exposes the host process + +## Details +The C# scripting feature compiles and runs workflow-author-provided source via Roslyn with reference to system assemblies and no sandbox. Any caller able to define or modify a workflow can execute arbitrary code, including reading secrets, writing files, or shelling out. + +## Location +[src/modules/Elsa.CSharp/Services/RoslynCSharpEvaluator.cs:1](https://github.com/elsa-workflows/elsa-core/blob/release/3.7.0/src/modules/Elsa.CSharp/Services/RoslynCSharpEvaluator.cs#L1) + +## Impact +Workflow author executes arbitrary C# code under the workflow runner identity + +## Reproduction steps +1. Workflow author adds an Inline C# step containing `System.Diagnostics.Process.Start("cmd","/c whoami")`. Running the workflow executes the command as the server identity. + +## Recommended fix +Treat workflow authoring as a privileged trust boundary and document this clearly. If untrusted authors are allowed, restrict the scripting feature behind a separate elevated permission and consider AppDomain/process isolation or removing scripting entirely. + +--- +**Severity:** HIGH +**Status:** Open +**Category:** Improper Control of Generation of Code (CWE-94) +**Repository:** elsa-workflows/elsa-core +**Branch:** release/3.7.0 +**Date created:** 2026-05-20 + +--- + +# Polymorphic JSON deserialization resolves attacker-supplied .NET type via _type discriminator + +## Details +PolymorphicObjectConverter.Read peels a '_type' property off any incoming JSON value typed as object / ExpandoObject / Dictionary / IDictionary, then resolves the string via Type.GetType(typeName) (line 341) with no allowlist and passes the resolved Type into JsonSerializer.Deserialize(ref reader, targetType, newOptions) (line 44) or Activator.CreateInstance(targetType) (line 124). The converter is registered globally by PolymorphicObjectConverterFactory in JsonPayloadSerializer.GetOptions, so every API surface that consumes IApiSerializer / JsonPayloadSerializer is in scope. Concretely, WorkflowDefinitionModel.CustomProperties, activity custom properties, workflow Variables, Inputs, Outputs, and bookmark Resume payloads are all object/IDictionary bags. An attacker with workflow write/import permission can persist a malicious _type tag that fires on every load. + +## Location +[src/modules/Elsa.Workflows.Core/Serialization/Converters/PolymorphicObjectConverter.cs:341](https://github.com/elsa-workflows/elsa-core/blob/release/3.7.0/src/modules/Elsa.Workflows.Core/Serialization/Converters/PolymorphicObjectConverter.cs#L341) + +## Impact +Authenticated workflow author can drive STJ to instantiate arbitrary host-loaded types and run their property setters. + +## Reproduction steps +1. 1) Authenticated user with workflow write permission POSTs /workflow-definitions/import with CustomProperties containing {"x":{"_type":", ", "Prop":"value"}}. 2) On import, JsonPayloadSerializer deserializes the model; PolymorphicObjectConverter calls Type.GetType on the attacker string and JsonSerializer.Deserialize into that type, invoking constructors and property setters. 3) The same payload reactivates on every subsequent load of the definition, giving a persistent gadget surface tied to whatever STJ-reachable side-effecting types are loaded in the host process. + +## Recommended fix +The serializer must refuse unknown _type discriminators rather than falling back to Type.GetType. Restrict polymorphic resolution to an explicit allowlist (e.g., types registered with IWellKnownTypeRegistry and IActivityRegistry). The same constraint must apply on read paths from the database, not only on import. + +--- +**Severity:** MEDIUM +**Status:** Open +**Category:** Deserialization +**Repository:** elsa-workflows/elsa-core +**Branch:** release/3.7.0 +**Date created:** 2026-05-20 + +--- + +# Non-cryptographic randomness used to generate secrets and passwords + +## Details +DefaultRandomStringGenerator uses System.Random, a non-cryptographic PRNG, to produce random strings. DefaultSecretGenerator wraps this generator, and it is consumed by the application-create endpoint (to generate API client secrets) and by UserManager to generate temporary passwords. System.Random is seeded from Environment.TickCount by default; an attacker who can approximate the time a secret was issued can brute-force the seed (small 32-bit space) and recover the exact secret. The hashed value at rest does not protect against this because the attacker reconstructs the plaintext and presents it to the authentication endpoint. + +## Location +[src/modules/Elsa.Identity/Services/DefaultRandomStringGenerator.cs:29](https://github.com/elsa-workflows/elsa-core/blob/release/3.7.0/src/modules/Elsa.Identity/Services/DefaultRandomStringGenerator.cs#L29) + +## Impact +Allows attackers to predict generated application client secrets and reset passwords. + +## Reproduction steps +1. Admin provisions an Elsa Application; the server responds with a freshly generated client secret derived from System.Random. An attacker who observes the approximate time of provisioning (e.g. via timing of an HTTP response, an audit log, or a leaked notification) enumerates Environment.TickCount seeds around that moment, reproduces DefaultRandomStringGenerator's output for each, and locates the matching secret. The attacker then authenticates to the API as that application and exercises its permissions. + +## Recommended fix +Use a cryptographically secure random source (RandomNumberGenerator.GetBytes / GetString) for any value that grants authentication or authorization, including client secrets, API keys, and password resets. + +--- +**Severity:** MEDIUM +**Status:** Open +**Category:** Weak-cryptography +**Repository:** elsa-workflows/elsa-core +**Branch:** release/3.7.0 +**Date created:** 2026-05-20 + +--- + +# Polymorphic deserialization resolves attacker-controlled .NET types + +## Details +TypeJsonConverter resolves a JSON-supplied type alias by first checking the well-known type registry and then falling back to Type.GetType(typeAlias) with no allow-list. PolymorphicObjectConverter then calls JsonSerializer.Deserialize(ref reader, targetType, newOptions) for the resolved type. The same pattern is reached via VariableDefinitionMapper.Map (Type.GetType on source.TypeName at line 23 and on source.StorageDriverTypeName at line 100) and VariableMapper.Map (line 62). These converters are wired into JsonPayloadSerializer and JsonWorkflowStateSerializer (but not SafeSerializer), and both are reachable from the workflow definition/instance import endpoints which only require an authenticated user with workflow write permission. While System.Text.Json does not invoke arbitrary setters as freely as Newtonsoft.Json, attacker-controlled type instantiation still enables type-confusion, denial-of-service via heavy constructors / large allocations, and is a stepping stone toward gadget-based RCE if a suitable type is present in the loaded assembly set. + +## Location +[src/modules/Elsa.Workflows.Core/Serialization/Converters/TypeJsonConverter.cs:50](https://github.com/elsa-workflows/elsa-core/blob/release/3.7.0/src/modules/Elsa.Workflows.Core/Serialization/Converters/TypeJsonConverter.cs#L50) + +## Impact +Authenticated workflow editors can instantiate arbitrary loaded .NET types via crafted import payloads. + +## Reproduction steps +1. An authenticated user with workflow:write submits a crafted workflow definition whose variables or polymorphic payload nodes embed a _type discriminator naming an assembly-qualified .NET type not in the well-known registry. TypeJsonConverter.Read resolves the type via Type.GetType, and PolymorphicObjectConverter (or VariableDefinitionMapper) calls Activator.CreateInstance / JsonSerializer.Deserialize against it. The attacker selects a type whose construction has dangerous side effects in the loaded assembly set (file I/O, process spawn through indirect setters, large allocations, or known gadget chains) to corrupt server state or degrade availability. + +## Recommended fix +Constrain polymorphic deserialization to an explicit allow-list of safe types and reject any _type value not in that allow-list. Do not fall back to Type.GetType on caller-supplied strings. + +--- +**Severity:** MEDIUM +**Status:** Open +**Category:** Deserialization +**Repository:** elsa-workflows/elsa-core +**Branch:** release/3.7.0 +**Date created:** 2026-05-20 + +--- + +# Password and secret hashing uses unsalted single-pass SHA-256 + +## Details +DefaultSecretHasher hashes credentials with a single SHA-256 invocation and no per-credential salt, no KDF, and no iteration count. SHA-256 is a fast hash function evaluable at billions of guesses per second on commodity GPUs, so an attacker who exfiltrates the hash store (via SQL injection, backup theft, insider access, or any database compromise) can recover every weak-to-medium password and any short API secret very rapidly. The lack of a per-record salt also enables rainbow-table reuse across deployments. + +## Location +[src/modules/Elsa.Identity/Services/DefaultSecretHasher.cs:1](https://github.com/elsa-workflows/elsa-core/blob/release/3.7.0/src/modules/Elsa.Identity/Services/DefaultSecretHasher.cs#L1) + +## Impact +Stolen hash database can be brute-forced offline at high speed to recover passwords and API secrets. + +## Reproduction steps +1. An attacker obtains a copy of the Elsa identity database (for example via a backup leak or by exploiting any read primitive against persistence). They run a GPU password cracker against the SHA-256 hashes and recover plaintext user passwords and application secrets, then log in to the Elsa API as those principals. + +## Recommended fix +Hash credentials with a memory-hard or iterated KDF (Argon2id, scrypt, or PBKDF2 with a high iteration count) plus a unique per-credential salt. Migrate existing hashes opportunistically on next successful authentication. + +--- +**Severity:** MEDIUM +**Status:** Open +**Category:** Weak-cryptography +**Repository:** elsa-workflows/elsa-core +**Branch:** release/3.7.0 +**Date created:** 2026-05-20 + +--- + +# Refresh tokens are indistinguishable from access tokens + +## Details +The refresh token issuer signs refresh tokens with the same key, audience and issuer as access tokens, and the bearer middleware accepts both. There is no claim distinguishing refresh vs access, so anyone who exfiltrates a refresh token (logs, browser storage, MITM) can use it as a Bearer access token until expiry. + +## Location +[src/modules/Elsa.Identity/Services/DefaultRefreshTokenIssuer.cs:1](https://github.com/elsa-workflows/elsa-core/blob/release/3.7.0/src/modules/Elsa.Identity/Services/DefaultRefreshTokenIssuer.cs#L1) + +## Impact +A stolen refresh token can be used directly as an access token + +## Reproduction steps +1. Attacker steals a refresh token from a logged client request. They send it as `Authorization: Bearer ` to any API. The bearer middleware validates the signature and authorizes the call. + +## Recommended fix +Add a `typ`/`token_use` claim to distinguish refresh and access tokens, and verify on each request that the claim matches the expected token type for the endpoint. + +--- +**Severity:** MEDIUM +**Status:** Open +**Category:** Insufficient Token Validation (CWE-345) +**Repository:** elsa-workflows/elsa-core +**Branch:** release/3.7.0 +**Date created:** 2026-05-20 + +--- + +# SignalR workflow instance hub allows cross-tenant observation by id + +## Details +WorkflowInstanceHub.ObserveInstanceAsync(string workflowInstanceId) adds the connection to a SignalR group keyed only by the supplied id. There is no authorization check that the caller may observe that instance, so any authenticated client can join the group for any instance id they can guess or enumerate and receive its broadcast events. + +## Location +[src/modules/Elsa.Workflows.Runtime.SignalR/Hubs/WorkflowInstanceHub.cs:1](https://github.com/elsa-workflows/elsa-core/blob/release/3.7.0/src/modules/Elsa.Workflows.Runtime.SignalR/Hubs/WorkflowInstanceHub.cs#L1) + +## Impact +Authenticated user observes workflow instance events belonging to other users/tenants + +## Reproduction steps +1. Attacker (any authenticated user) calls `ObserveInstanceAsync("")`. They begin receiving workflow events (state transitions, incident counts, metadata) for the victim's instance until execution completes. + +## Recommended fix +Before joining the group, verify that the caller has read access to the workflow instance (tenant/owner/permission check). Reject otherwise. + +--- +**Severity:** MEDIUM +**Status:** Open +**Category:** Insecure Direct Object Reference (CWE-639) +**Repository:** elsa-workflows/elsa-core +**Branch:** release/3.7.0 +**Date created:** 2026-05-20 + +--- + +# Dynamic LINQ injection through TimestampFilter.Column + +## Details +TimestampFilters loops over caller-controlled entries and concatenates timestampFilter.Column into Dynamic LINQ predicate strings (`query.Where($"{column} >= @0 ...", ...)`). While the List endpoint applies a column whitelist, other consumers (notably Alterations API filters) do not, allowing the caller to supply an arbitrary expression such as `Id == "a\" || true || \""` and influence which records are returned. Depending on column-name acceptance this can leak data or alter alteration scope. + +## Location +[src/modules/Elsa.Workflows.Management/Filters/WorkflowInstanceFilter.cs:165](https://github.com/elsa-workflows/elsa-core/blob/release/3.7.0/src/modules/Elsa.Workflows.Management/Filters/WorkflowInstanceFilter.cs#L165) + +## Impact +Authenticated caller injects arbitrary EF Core predicates altering query semantics + +## Reproduction steps +1. Attacker posts a TimestampFilter with Column set to a crafted Dynamic LINQ expression; the server compiles it and returns or alters rows beyond the intended scope. + +## Recommended fix +Move the column whitelist into the filter itself so every consumer enforces it, or reject TimestampFilter entries whose Column is not a known property of WorkflowInstance. + +--- +**Severity:** MEDIUM +**Status:** Open +**Category:** Expression Language Injection (CWE-917) +**Repository:** elsa-workflows/elsa-core +**Branch:** release/3.7.0 +**Date created:** 2026-05-20 + +--- + +# SAS token expiration not enforced (broken-by-design issuance) + +## Details +CreateToken(payload, lifetime) protects the payload with `ToTimeLimitedDataProtector().Protect(json, lifetime)`, but DecryptToken calls `Unprotect` on the base (non-time-limited) protector. The two protectors derive different purposes, so all time-limited tokens fail to decrypt. Conversely, the no-lifetime overload (`CreateToken(payload)`) produces tokens that never expire. The net effect is that operators who think they are issuing short-lived SAS tokens are either generating unusable tokens or — if they fall back to the overload — minting eternal capabilities. + +## Location +[src/modules/Elsa.SasTokens/Contracts/DataProtectorTokenService.cs:25](https://github.com/elsa-workflows/elsa-core/blob/release/3.7.0/src/modules/Elsa.SasTokens/Contracts/DataProtectorTokenService.cs#L25) + +## Impact +Tokens issued with a lifetime fail decryption, while feature is silently bypassed + +## Reproduction steps +1. Operator believes a SAS token is valid for 5 minutes. In practice the token cannot be redeemed (broken feature). If they switch to the no-lifetime overload to make redemption work, any token the attacker captures from logs or referer headers remains valid forever. + +## Recommended fix +Make both create and decrypt go through `ToTimeLimitedDataProtector` consistently, and refuse to compile-out the lifetime parameter so tokens always carry an expiration. + +--- +**Severity:** MEDIUM +**Status:** Open +**Category:** Improper Restriction of Authentication Attempts (CWE-307) / Improper Verification of Cryptographic Signature +**Repository:** elsa-workflows/elsa-core +**Branch:** release/3.7.0 +**Date created:** 2026-05-20 + +--- + +# Unauthenticated Resilience SimulateResponse endpoint enables memory exhaustion and unsafe deserialization + +## Details +SimulateResponse is decorated AllowAnonymous(). It parses `codes` from the query string via `JsonSerializer.Deserialize(codesParam)!` without try/catch, then stores per-sessionId state in a process-wide MemoryCache that grows with every new sessionId an attacker supplies. A crafted invalid JSON triggers an uncaught exception; many random session ids exhaust memory. + +## Location +[src/modules/Elsa.Resilience/Endpoints/SimulateResponse/Endpoint.cs:15](https://github.com/elsa-workflows/elsa-core/blob/release/3.7.0/src/modules/Elsa.Resilience/Endpoints/SimulateResponse/Endpoint.cs#L15) + +## Impact +Anonymous attacker forces unbounded cache growth and uncaught exceptions + +## Reproduction steps +1. Attacker scripts thousands of requests with random sessionId values and arbitrary `codes` payloads, growing the MemoryCache until the process OOMs. Sending malformed `codes` JSON throws inside the handler, producing 500s and consuming additional resources. + +## Recommended fix +Require authentication, validate codes input with explicit try/catch, bound the cache by size and TTL, and key entries on authenticated identity rather than client-supplied sessionId. + +--- +**Severity:** MEDIUM +**Status:** Open +**Category:** Allocation of Resources Without Limits (CWE-770) +**Repository:** elsa-workflows/elsa-core +**Branch:** release/3.7.0 +**Date created:** 2026-05-20 + +--- + +# Workflow import persists before authorization check + +## Details +Import.HandleAsync calls ImportSingleWorkflowDefinitionAsync (which persists the workflow via IWorkflowDefinitionImporter.ImportAsync) BEFORE checking the NotReadOnly authorization policy. If the policy fails (read-only mode enabled or target definition is system/readonly), the endpoint returns 403 but the import has already taken effect: the workflow row is created/overwritten in storage. The order is import → authorize → respond, which violates the policy's intent. + +## Location +[src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Import/Endpoint.cs:45](https://github.com/elsa-workflows/elsa-core/blob/release/3.7.0/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Import/Endpoint.cs#L45) + +## Impact +Authenticated user can mutate stored workflow state despite read-only/system protections. + +## Reproduction steps +1. Operator enables IsReadOnlyMode to lock production. A user with write:workflow-definitions permission still uploads a modified definition; the importer writes it to the database, then the endpoint returns 403. The modification persists despite the read-only guard. + +## Recommended fix +Perform the NotReadOnly authorization check (including loading the existing definition by id) BEFORE invoking the importer. Reject the request without mutating storage. + +--- +**Severity:** MEDIUM +**Status:** Open +**Category:** Authorization / TOCTOU +**Repository:** elsa-workflows/elsa-core +**Branch:** release/3.7.0 +**Date created:** 2026-05-20 + +--- + +# ImportFiles endpoint omits per-definition read-only/system check + +## Details +ImportFiles.HandleAsync calls AuthorizeAsync with new NotReadOnlyResource() — i.e. no target WorkflowDefinition supplied. The NotReadOnlyRequirementHandler therefore only evaluates ManagementOptions.IsReadOnlyMode; it never checks whether any of the imported files target an existing system/readonly definition. Each file in the upload is then imported one by one with no per-definition authorization. + +## Location +[src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/ImportFiles/Endpoint.cs:52](https://github.com/elsa-workflows/elsa-core/blob/release/3.7.0/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/ImportFiles/Endpoint.cs#L52) + +## Impact +Authenticated user can overwrite system/read-only workflow definitions via bulk import. + +## Reproduction steps +1. Operator marks a critical workflow as IsSystem=true or IsReadonly=true. A user with write:workflow-definitions uploads a zip containing a modified copy of that definition (same DefinitionId). ImportFiles imports it because the per-definition check is missing. + +## Recommended fix +For each imported workflow, run the NotReadOnly check against the existing definition (looked up by DefinitionId) before persisting it. + +--- +**Severity:** MEDIUM +**Status:** Open +**Category:** Authorization / Missing Check +**Repository:** elsa-workflows/elsa-core +**Branch:** release/3.7.0 +**Date created:** 2026-05-20 + +--- + +# Cross-tenant bookmark resolution in HTTP workflow middleware + +## Details +HttpWorkflowsMiddleware constructs the bookmark filter with `TenantAgnostic = true` when looking up HTTP-triggered workflows. The HTTP path/method hash is therefore matched across every tenant in the deployment. In a multi-tenant install this means a request arriving on tenant A's host header can find and resume a bookmark belonging to tenant B if their HTTP endpoint paths collide, exposing tenant B's workflow output to tenant A and letting tenant A inject input into tenant B's workflow. + +## Location +[src/modules/Elsa.Http/Middleware/HttpWorkflowsMiddleware.cs:152](https://github.com/elsa-workflows/elsa-core/blob/release/3.7.0/src/modules/Elsa.Http/Middleware/HttpWorkflowsMiddleware.cs#L152) + +## Impact +Tenant A can trigger HTTP workflows that belong to tenant B. + +## Reproduction steps +1. Tenant A registers an HTTP workflow at /webhooks/payment. Tenant B (operating in the same Elsa cluster) registers the same path. A request to tenant A's domain at /webhooks/payment can resolve to tenant B's bookmark; the workflow runs under tenant B's context but with input attacker-controlled by tenant A, leaking data back via the response body. + +## Recommended fix +Scope bookmark lookups to the current tenant by default. Only allow TenantAgnostic resolution when the request is explicitly tenant-anonymous (e.g., before tenant resolution runs) and when the workflow definition opts in to cross-tenant invocation. + +--- +**Severity:** MEDIUM +**Status:** Open +**Category:** Broken Access Control +**Repository:** elsa-workflows/elsa-core +**Branch:** release/3.7.0 +**Date created:** 2026-05-20 \ No newline at end of file diff --git a/doc/security-assessment/software-quality-iso25010-assessment.skill b/doc/security-assessment/software-quality-iso25010-assessment.skill new file mode 100644 index 0000000000000000000000000000000000000000..37d157e6b15829251ff2d26b51aacf33d7a18a9a GIT binary patch literal 70500 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 q`|C1%?eO@}ipt-?a)+Cqg&z*i6q|i>t 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 scTrkx!)uIc11c%9d$Byy#^9FC1Dhj8t?n2J zIM(^}fi8pEk~dZ`H9yIK`P})h;VNtWHDG#ESY%QWk@aH;YVUpRmp4P3>hF%KB)82@ zyD~G>(-;W(fjV=Yz|(4bbR2suH6qqn@U_P~P^I)5AGl7{vM<%$!LwSHb5lD{?dMO1 z2VvB2>dGUFa`W@C8;H}i_aCumviKMn3mkHCT0f}=`sMuQGW)zuZYKkOVI~yf54TT3 zEi-mI+uOq#>y57y|L6zvKQ>#RYgHk%eLn@57*L&I1F}(Eb@Tu~ir21^r}NTBUH~B8 z7ytlM-u`|SNBrjwOWAT3obO~_qLcl3F54gJz7=iSGxx@bsMW5CcPZ9(G zc&q^c-9MqsZvC$)7dXlR?C*p0gu#9MQU3)RLLfHNP%`>i-Pk_k;Z#m4@~|%5;<=0t5ggnW-pG{|4~Tu;Ty# literal 0 HcmV?d00001 diff --git a/src/apps/Elsa.ModularServer.Web/Elsa.ModularServer.Web.csproj b/src/apps/Elsa.ModularServer.Web/Elsa.ModularServer.Web.csproj index 138528ec6..ae9991352 100644 --- a/src/apps/Elsa.ModularServer.Web/Elsa.ModularServer.Web.csproj +++ b/src/apps/Elsa.ModularServer.Web/Elsa.ModularServer.Web.csproj @@ -7,6 +7,7 @@ + diff --git a/src/apps/Elsa.ModularServer.Web/appsettings.json b/src/apps/Elsa.ModularServer.Web/appsettings.json index ac49e62c9..107f10bc7 100644 --- a/src/apps/Elsa.ModularServer.Web/appsettings.json +++ b/src/apps/Elsa.ModularServer.Web/appsettings.json @@ -46,6 +46,7 @@ } }, "StructuredLogs": {}, + "ConsoleLogsFeature": {}, "SqliteStructuredLogPersistence": { "ConnectionString": "Data Source=elsa_logs.db;Cache=Shared" }, diff --git a/src/modules/Elsa.Scheduling/ScheduledTasks/ScheduledSpecificInstantTask.cs b/src/modules/Elsa.Scheduling/ScheduledTasks/ScheduledSpecificInstantTask.cs index 71fa87535..51df7f954 100644 --- a/src/modules/Elsa.Scheduling/ScheduledTasks/ScheduledSpecificInstantTask.cs +++ b/src/modules/Elsa.Scheduling/ScheduledTasks/ScheduledSpecificInstantTask.cs @@ -123,4 +123,4 @@ public class ScheduledSpecificInstantTask : IScheduledTask, IDisposable _cancellationTokenSource.Dispose(); _executionSemaphore.Dispose(); } -} \ No newline at end of file +}