docs: add security & quality assessments
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.
This commit is contained in:
parent
d21da0afce
commit
3523804efd
182
.agents/skills/elsa-release-announcements/SKILL.md
Normal file
182
.agents/skills/elsa-release-announcements/SKILL.md
Normal file
|
|
@ -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 <version> 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.
|
||||
|
|
@ -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."
|
||||
Binary file not shown.
Binary file not shown.
283
.agents/skills/elsa-release-announcements/scripts/announcement_pack.py
Executable file
283
.agents/skills/elsa-release-announcements/scripts/announcement_pack.py
Executable file
|
|
@ -0,0 +1,283 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Create a channel-specific Elsa release announcement draft pack."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def main() -> 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,
|
||||
"",
|
||||
"<!-- Review, tighten, and approve before publishing. -->",
|
||||
"",
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
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())
|
||||
194
.agents/skills/elsa-release-announcements/scripts/post_discord.py
Executable file
194
.agents/skills/elsa-release-announcements/scripts/post_discord.py
Executable file
|
|
@ -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())
|
||||
15
Elsa.sln
15
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}
|
||||
|
|
|
|||
126
ROADMAP.md
126
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.
|
||||
|
|
|
|||
280
doc/security-assessment/elsa-core-architecture-patterns.md
Normal file
280
doc/security-assessment/elsa-core-architecture-patterns.md
Normal file
|
|
@ -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<T>` 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<T>()` 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<T>` 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. |
|
||||
1109
doc/security-assessment/elsa-core-iso25010.md
Normal file
1109
doc/security-assessment/elsa-core-iso25010.md
Normal file
File diff suppressed because it is too large
Load diff
375
doc/security-assessment/elsa-core-profile.md
Normal file
375
doc/security-assessment/elsa-core-profile.md
Normal file
|
|
@ -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<TResult>`, 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<T>` |
|
||||
| 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<T>` 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.
|
||||
150
doc/security-assessment/elsa-core-software-quality.md
Normal file
150
doc/security-assessment/elsa-core-software-quality.md
Normal file
|
|
@ -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<TDbContext,TEntity>` (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<TDbContext,TEntity>` (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 | ✅ | `<Nullable>enable</Nullable>` 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<string, object>?` 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<int>` 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<TDbContext, TEntity>` 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
|
||||
|
|
@ -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<IDictionary<string, object>>(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<object>(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":"<gadget>",...} 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<string,object> / IDictionary<string,object>, 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<string,object> 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":"<dangerous .NET type>, <assembly>", "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 <refresh>` 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("<victim-instance-id>")`. 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<int[]>(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
|
||||
Binary file not shown.
|
|
@ -7,6 +7,7 @@
|
|||
<PackageReference Include="Nuplane.Sources.Directory" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\modules\Elsa.Diagnostics.ConsoleLogs\Elsa.Diagnostics.ConsoleLogs.csproj" />
|
||||
<ProjectReference Include="..\..\modules\Elsa.Diagnostics.StructuredLogs.Persistence.Sqlite\Elsa.Diagnostics.StructuredLogs.Persistence.Sqlite.csproj" />
|
||||
<ProjectReference Include="..\..\modules\Elsa.Diagnostics.StructuredLogs\Elsa.Diagnostics.StructuredLogs.csproj" />
|
||||
<ProjectReference Include="..\..\modules\Elsa.Http\Elsa.Http.csproj" />
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@
|
|||
}
|
||||
},
|
||||
"StructuredLogs": {},
|
||||
"ConsoleLogsFeature": {},
|
||||
"SqliteStructuredLogPersistence": {
|
||||
"ConnectionString": "Data Source=elsa_logs.db;Cache=Shared"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -123,4 +123,4 @@ public class ScheduledSpecificInstantTask : IScheduledTask, IDisposable
|
|||
_cancellationTokenSource.Dispose();
|
||||
_executionSemaphore.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue