elsa-core/doc/security-assessment/elsa-core-software-quality.md
Sipke Schoorstra 5862bb84e3
fix(build): make ConfigureAwait.Fody weaving actually take effect (#7983)
* fix(build): make ConfigureAwait.Fody weaving actually take effect

ConfigureAwait.Fody only rewrites awaits when it is handed an explicit
ContinueOnCapturedContext value. A bare <ConfigureAwait /> element parses
cleanly, emits no warning, and weaves nothing.

Of the 98 FodyWeavers.xml files under src/, only 22 set the attribute. The
other 76 carried a bare element, so those projects compiled with no weaving
at all while looking correctly configured. Verified on Debug net10.0 builds:
Elsa.Secrets (attribute set) referenced ConfiguredTaskAwaitable, while
Elsa.Alterations (bare element) did not.

Elsa ships as a library and can be hosted where a SynchronizationContext
exists, so weave everywhere rather than dropping the packages.

Fody reads the WeaverConfiguration MSBuild property in preference to any
FodyWeavers.xml, so the directive now lives in a single file, src/Fody.props,
alongside the package references it belongs with. All 98 per-project XML files
are deleted; they would otherwise be dead and misleading.

src/apps has its own props root that does not chain up to
src/Directory.Build.props, so it imports src/Fody.props directly instead of
redeclaring the Fody package references. This second gap was found by the
guard below, not by inspection.

Guard: Directory.Build.targets fails the build for any project that references
ConfigureAwait.Fody without an effective directive (ELSA0001) or that
reintroduces a FodyWeavers.xml alongside it (ELSA0002). Both were verified to
fire, including on the exact original bug shape.

The 22 already-weaving projects are unaffected: their effective directive is
identical before and after, and that set is disjoint from the four projects
holding explicit .ConfigureAwait( calls. All 25 such calls pass false, matching
what the weaver now applies, so they become redundant rather than contradictory
and are left in place.

Also repoints two security-assessment claims that cited the presence of
FodyWeavers.xml as evidence of weaving — the inference that masked this bug.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(build): drop FodyWeavers.xml from the new UserTasks modules

Merging main brought in eight new projects. Elsa.UserTasks carried a bare
<ConfigureAwait /> — the same latent no-op this branch removes elsewhere, added
while the fix was in review. Its seven persistence siblings set the attribute.

The guard caught it: ELSA0002 failed CI on the PR merge commit for all three
TFMs, on a file that never existed in the branch's own worktree.

All eight are redundant now that src/Fody.props supplies the directive.
Verified Elsa.UserTasks resolves it and its net10.0 build references
ConfiguredTaskAwaitable.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 01:37:23 +02:00

151 lines
24 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 — `src/Fody.props` declares a single `ContinueOnCapturedContext="false"` directive that reaches every props root under `src/`, and `Directory.Build.targets` fails the build (`ELSA0001`/`ELSA0002`) for any project that references the weaver without an effective directive. `ConfigureAwait(false)` is injected automatically at build time; manual calls are not required, and the 25 explicit calls that remain in `src/` are redundant rather than load-bearing. The presence of a `FodyWeavers.xml` is *not* evidence of weaving — a bare `<ConfigureAwait />` element parses cleanly and rewrites nothing — so the directive and the build guard are what to check. 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