cratis-security-review · git:20261013.615e6a9 · 2026-10-13 · sha256 a8af18abd97ad53c
cratis-security-review git:20261013.615e6a9A
Immutable. This exact content never changes and is served at /api/v1/blob/a8af18abd97ad53c.
--- name: cratis-security-review description: Perform a focused security review of changed code in a Cratis application — injection, authentication and authorization, data exposure, secrets, event-sourcing-specific exposure, and the frontend — and report findings by risk. Use when asked for a security review or audit. Do not use to implement authentication and do not report a policy preference as a framework contract. license: MIT --- # Cratis security review Event sourcing changes the shape of a security review. A mistake in an ordinary application is a bug you fix; a secret written into an append is a fact that lives in the log forever and cannot be edited out. The event-sourcing section below is the one that is specific to this stack, and it is the one worth reading first. ## Verified product sources | Package | Purpose | | --- | --- | | `Cratis.Arc.Chronicle` | `[NotAudited]` in `Cratis.Arc.Chronicle.Commands`; the `ARCCHR0009` analyzer | | `Cratis.Chronicle` | `[PII]`, `[Subject]`, redaction, namespace isolation | | `Cratis.Chronicle` | `[Encrypted]` in `Cratis.Chronicle.ProtectedValues`; analyzers `CHR0052`, `CHR0053` | | `Cratis.Arc.Core` | Authorization filters, `CommandResult.Unauthorized` | Every Arc check is cited in references/arc-boundaries.md. Reverify against the owning product repository before asserting a framework guarantee this file does not already state. ## When to run this review without being asked The orchestrating agents and the PR-review prompt run it unconditionally as the last gate. When you are shipping directly — one agent, one branch — run it yourself before committing whenever the diff **adds or removes** a line carrying any of: `[Roles]`, `[Authorize]`, `[AllowAnonymous]`, `[ExecuteCommandsAsSystem]`; `[PII]`, `[Subject]`, `[NotAudited]`; `useIdentity`; or a secret-shaped identifier (`ConnectionString`, `PrivateKey`, `Bearer`, `Password`, `ApiKey`, `AccessToken`). Added *or removed* matters: a deleted `[PII]` or `[Roles]` is the change most worth a second pair of eyes. Match changed lines, not whole files — every slice contains a `[Command]` and an `[EventType]`, and a trigger that fires on every commit is a trigger that gets skipped. A name-based list has the same blind spot as `ARCCHR0009`: a secret with an innocuous property name passes it, so read the command's properties as well as grepping them. ## Route near misses - General correctness and maintainability: use `cratis-code-review`. - Scalability and resource use: use `cratis-performance-review`. - Implementing authentication, authorization or identity: use `cratis-arc-authentication-authorization-and-identity`. This skill reviews; it does not build. - Compliance mechanics — `[PII]`, subject resolution, erasure, redaction: use `cratis-chronicle-compliance`. ## Step 1 — Event sourcing: the permanent-record checks - **Every `[Command]` property holding a secret or credential is marked `[NotAudited]`; a retained operational secret is also `[Encrypted]` on the event, and a credential reaches the event only as a hash or a reference.** A command's property values are written to the causation of every event it appends, and causation is as permanent as the events. Prefer the marking on the concept type so it travels everywhere the value appears. `[NotAudited]` alone leaves the secret in plaintext on events; `[Encrypted]` alone leaves the command input on the causation chain. `ARCCHR0009` catches properties whose *names* read as secrets — so read the properties whose names do not say what they hold, because the analyzer cannot. - **Personal data is `[PII]` alone, a secret is `[Encrypted]` plus `[NotAudited]`.** They are different mechanisms with different consequences: `[NotAudited]` withholds a value from the causation chain; `[Encrypted]` encrypts it at rest without erasure; `[PII]` encrypts, enrolls it in per-subject erasure and withholds it. A third-party API key is `[Encrypted]` + `[NotAudited]`. An email address is `[PII]`. A password is stored as a hash, never as a retained secret. Never combine `[PII]` with `[Encrypted]` (`CHR0053`), and neither goes on an event-source id (`CHR0034`, `CHR0052`). - Credentials used for authentication (passwords, bearer or session tokens) are never stored; keep a hash or a reference. An operational secret the system must retain and use (a third-party API key, a company's bank account number) is stored only as an `[Encrypted]` + `[NotAudited]` concept, and never in a read model returned to clients in plaintext. - Event-source ids are generated server-side, never accepted from an untrusted client where the id grants access to a stream. - Upcasting and event-type migration logic cannot introduce a property the original contract did not carry. - Uniqueness cannot be bypassed by concurrent writes — it is enforced by a Chronicle constraint, not by a read-model pre-check. - Cross-tenant writes cannot bypass a constraint that is scoped per namespace. ## Step 2 — Input validation and injection - Every command property is validated before use — null, empty, range, format. - No raw SQL concatenation; parameterized queries or EF Core only. - No user-supplied value reaches `Path.Combine`, a `File.*` call, a shell command, or process arguments. - No user-supplied value becomes an event-store key without sanitization. ## Step 3 — Authentication and authorization - Every exposed endpoint is either authorized or explicitly anonymous with a stated reason. - Authorization is expressed at the boundary — an attribute, a policy, a command filter — never as an `if` on roles inside `Handle()`. - Tenant isolation holds: no cross-namespace data is reachable without authorization. - Claims are verified before acting on identity-dependent command data. A client must not be able to assert who it is through a command property. - Note that an unauthorized command result maps to HTTP **403**, not 401 — a reviewer reading logs for 401s will miss authorization failures. - Review [Arc-specific security boundaries](references/arc-boundaries.md) when the diff touches hosting, identity, policies, discovery, streams or system actors. It ties the checks to Arc source; do not assume a clean analyzer run proves those deployment boundaries. ## Step 4 — Data exposure - No personal data is returned to a caller that did not supply it. - Query results are scoped to the requesting tenant and user. A query that can return all-tenant data is a finding even when no current caller reaches it. - Response payloads carry only fields the client uses. Over-fetching is an exposure surface, not only a performance one. - A managed read-model document holds one subject's personal data. Mixing several people's data in one document breaks erasure. ## Step 5 — Secrets and configuration - No secret in source, in a configuration file, or in a specification fixture. - Secrets come from environment variables or a secrets manager. - No hard-coded connection string outside test code. ## Step 6 — Frontend - No user-supplied value in `dangerouslySetInnerHTML`. - No token or secret in `localStorage` — use an `httpOnly` cookie or in-memory state. - Command payloads carry only the minimum required fields. - No client-side access control that is not also enforced server-side. A disabled button is a usability affordance, never a control. ## Step 7 — Report Open with one line: > **Security review: No issues / Low-risk findings / Blocking issues found** Group findings by the section that produced them, and classify each: | Risk | Meaning | | --- | --- | | **Critical** | Must be fixed before merge | | **Medium** | Should be fixed soon; state what makes it not-critical | | **Low** | Fix when convenient | Close with a per-section summary table, and **name what you did not review** — the paths, the surfaces, and the axes out of scope. A security report listing only findings reads as a clean bill of health for everything it never opened. ## What breaks - **A finding is a policy preference in framework clothing.** Which roles exist, which data is sensitive, and which retention applies are the product's calls, not the framework's. State them as policy questions for the owner, not as contracts. - **`[NotAudited]` is used for personal data.** The value stays out of causation but is never encrypted and never enrolled in erasure — the opposite of what a subject-rights request needs. - **The review assumes the analyzer covered the secrets.** `ARCCHR0009` matches names. A property called `Value` holding an API key passes it silently. - **A missing check is reported as "verified".** Unknown is not pass. If a surface could not be reached, say so as `indeterminate` rather than omitting it. ## How it is proven The build and specifications are green before the report is written; each finding cites the file and line; and the report states explicitly which surfaces were and were not examined.