Summary#
Reports where your app falls short of a production-grade bar. Compiling tells you the app is correct; osy lint
tells you whether it is finished — starting with the category that hurts most when it is not: security.
Signature#
osy lint [path] [--json] [--strict]Description#
Findings come in three tiers:
MUST — a production app is broken or exposed without it. Every rule at this tier compiles and type-checks — which is exactly why they need a linter. Most are security; the other is a number that comes back wrong.
A total that is quietly short. A function creates rows, does not commit, and then sums, averages or takes the min or max over that entity (Sum / Average / Min / Max / Count). The database computes the aggregate over committed rows only, so the answer silently omits the rows just created — it is short by exactly the work the function just did. Nothing fails: the code compiles, runs, and hands back a wrong number. Call
UnitOfWork.Commit();before the aggregate.A query that filters on an edit you have not committed. Change a property on a row, do not commit, then query that entity with a filter on the property you changed. The filter runs in the database, against the committed value — the one from before your edit. So the query misses the row your edit would now match, and still returns the row it no longer matches; and that row then reads back with your new value, contradicting the very filter that selected it. Commit before the query, or filter the rows you already hold in memory. (Reading the value straight off the row you edited is fine — that always shows your edit. It is only the filter that is computed on the old value, and only for a property you actually changed.)
The login nobody tests. The app has an
[AuthMethod]and no test proves it both ways — that a right credential is accepted and that a wrong one is refused. Every access rule you wrote sits behind this one function, and it is the one that fails invisibly: a login which handed a ticket to anybody passes a suite that only signs in successfully, and the app behaves exactly as it does now until the wrong person is holding a ticket. Test the refusal too — and include an address with no account, which must fail exactly like a wrong password, or the form tells an attacker which addresses are registered.A credential is handed out. An entity grants reads and a sensitive field (
PasswordHash,*Token,*Secret) has no field-leveldeny read, so it goes to everyone who can read the row.A role grant can be written by its own subject. A role grants (and the first admin) table whose writes are ungated — or guarded by a
whererow-filter, which here says "you may write your own privileges" — lets a caller hand themselves whatever role the rest of the app trusts. Every other rule you wrote is then decoration. The same hole is reported when someone mayupdateordeletea grant they could not havecreated: editing aMembergrant into anAdminone is the identical escalation through another verb.The auth flow is denied its own credential. An
[AuthMethod]runs as the ephemeral auth principal, which bears a role and has no user — working out the user is what it was called to do. An entity it must read, granted only by awhererow-filter, therefore denies it: there is no user id to match, so the login cannot read the record it exists to check and fails every time. Your tests still pass, because they run as a real signed-in user for whom that filter works perfectly (auth bootstrap (login, before anyone is signed in)).A secret is compared with
==.storedHash == Security.HashPassword(password)is not merely insecure — it can never be true, because a hash is salted; the login cannot succeed (useSecurity.VerifyPassword, Security.* — hashing, verifying, tickets, random ids). And==on an HMAC tag leaks, through how long the check takes to fail, how much of a guess was right — enough to forge one (useCrypto.FixedTimeEquals, Crypto.HmacSha256Hex and Crypto.FixedTimeEquals).An integer-only format on a Double.
someDouble.ToString("X")or"D"— those specifiers are integer-only, so .NET throws aFormatExceptionat runtime. It compiles (ToStringtakes any string), so only the crash tells you. Format the Double withN/F/C/P, or convert to an integer type first.
SHOULD — expected, and worth flagging.
A login that says WHICH half of the credential was wrong. Answering "no account with that address" and "wrong password" differently lets anybody discover which addresses are registered, without ever guessing a password — the input to credential-stuffing and to targeted phishing. It never looks like a security decision while you are writing it; it looks like a helpful error message. Answer every rejection identically, and put the specific message where it is safe to be specific: the sign-up page, or a reset flow that emails the address rather than telling the browser.
An entity with no
security { }block is safe (deny-all means it grants nothing to anyone) but is usually a grant someone forgot to write — the app cannot read its own data.A credential written to the log.
Log.*(… someHash, someToken …)— a log line is not private (it ships to a sink, is retained, often indexed), and nothing redacts it for you. Log an id or an email that identifies the record, never the credential field itself. (Only fields derived as a credential or ending inHash/Token/Secret/… are flagged — an innocently-namedTokenCountis left alone.)An app built to be multi-user, with no way to log in. It declares a
[Principal], a[Role]enum, and a gated surface — a secure-by-default page or a role-grant table — so it plainly means to have users; but not one function is an[AuthMethod], so there is no login path at all. Nobody can authenticate, so nobody can pass the deny-all gate those pages and grants sit behind: the app compiles, its data model is complete, and not one real user can get in. Add a login[AuthMethod]and wire it inapp.AuthBootstrap(auth bootstrap (login, before anyone is signed in)). A fully public app — no[Principal]— is a valid choice and is never flagged; nor is a[Principal]modelled as plain data with no roles or gated pages yet.A rule nobody ever tried to break. An entity whose rules can deny, and no test acting as a principal proves they do; or an invariant no
Assert.Throwsproves bites. A rule you have not tested is a rule you only believe you wrote — and an untested invariant does not fail loudly when it rots: the day someone deletes it to make an import work, the suite is still green. A[Test]that asserts nothing at all is reported for the same reason.The two refusals are proved differently, and the difference matters. A denied write throws, so
Assert.Deniedsettles it on its own. A denied read does not throw — row security is part of the query, so the row was never in the result set — and you prove it with an empty result. But an empty result is evidence of a denial only if there was something there to deny: on its own it passes just as happily when the rule refuses everyone, when the table is empty, or when the row was never created. So pair it — show that somebody can see a row of that entity, right beside the principal who cannot. Unpaired, the assertion is reported, because it does not yet prove what it appears to.A rule that cannot say why. An invariant or a
[Pattern]with no message refuses the user with the rule itself — and a pattern's rule is a regular expression, which explains nothing.An unbounded
string. No[MaxLength]means the caller decides how much you store. Fine for prose; wrong for a code, a name or a status.An enum value that reads as a run-together word. Without a
[Label]label, an enum member is shown by its own name — perfect forDraft, and wrong the moment there are two words: the grid cell says "InProgress". Only multi-word members are reported; a single-word one needs no label.Work that only shows up on real data. A query materialized with no
Takefetches however many rows happen to exist; aSkipwith noOrderBylets page 2 repeat a row from page 1; reading a child collection in a loop over parents is one query per parent; and the same query run twice does the work twice — and may give two different answers. All four are correct, fast on a laptop, and the reason an app that worked in development falls over in production.A remote call that assumes it works. An outbound call (Http.*) has two failure modes and they need two different answers. The network throws — a timeout, a DNS failure, a refused connection — and with no
trythat kills the function outright, so the caller sees an internal error instead of the failure you meant to handle. The response does not throw: a 404 or a 500 comes back as an ordinary result, so code that never looks atIsSuccesscarries on and uses the error page's body as though it were the answer — wrong data, no stack trace, no log line.A function that reaches the network through something it calls. The call may be nowhere in the function's own text — a helper makes it — and the function still has no
try. A timeout down there kills this one exactly as dead. Nothing you can read in it warns you, which is the whole reason the linter looks past the source and at what the code actually does.A
catchthat says nothing. A caught exception with no log is a failure the app decided to survive and then forgot. In production it is invisible: no trace, no count, and no way to answer why the numbers are off.
CONSIDER — a candidate for your judgment, reported with its evidence. Never an error.
- A
[Unique]or[Pattern]field with no[Required]: every constraint except[Required]lets null through, so two rows may both have no value and not collide. If the field is genuinely optional that is exactly right — and if you read[Unique]as "every row has one", it does not say that. Only you know which was meant, so the linter asks rather than asserts. - A routed page with no title. A page declares its name with
[Title("…")](the chrome/route name a breadcrumb reads) or ameta { title = "…"; }block (the SEO<title>). A routed page with neither is a nameless browser tab and an accessibility gap — a screen reader announces a page by its title on navigation. A title-less route can be deliberate (a redirect-only page), so it is a candidate, not an error. - An editable field under a rule the browser can't pre-empt, in a form that catches nothing. Plain field rules
(
[Required]/[MaxLength]/[Pattern]/…) are surfaced for you — the input carries them as native attributes, so the browser blocks bad input and paints the invalid state without any app code. Two rules can't work that way and still throw aValidationExceptionon save: a cross-fieldinvariant(Paid <= Total), which the client can't evaluate; and[Unique], enforced by the atomic DB index — a client "is this taken?" check races with the index, so the check is a UX nicety and the catch is the real guard. If a form edits such a field and catches no error, the user sees a raw failure — catch it where the form saves and show the message. If the component handles errors at all, it is not flagged. - A class method that quietly hands off to the server. A class method is client-runnable code, so a call it makes to a function or method that runs on the server is a network round trip — and nothing at the call site shows it; whether the callee stays on the client is a fact about its body, not the call. Now that almost everything runs on the client, a server hop is the notable exception. If it is intended, leave it; if the method was meant to stay client-side, keep the server-only work off its path.
- A number/date format that runs on the server.
value.ToString(format[, culture])formats in the browser only for the specifiers the client reproduces byte-identically; anything else fails closed to the server — a round trip, invisible at the call site. A Double outsideN/F/C/P, a custom pattern over a Double (which also rounds differently), a runtime-built format or culture, an unsupported specifier under a culture, or an unsupported date format all round-trip. The finding names which, and the fix (switch the specifier, use aDecimal, make the format or culture literal). If the round trip is fine, leave it.
- A
Each finding names the rule, what it is about, and how to close it.
--strict makes any MUST-tier finding fail the run, so it can gate a ship. --json writes the findings as JSON, for
a coding agent or a CI step.
This is a growing rule set, not a finished one — rules are added as patterns emerge. To see what the app is rather than what it is missing, use Understanding your app.
Which rules exist?#
Every rule the linter knows, from the compiler's own catalogue. This list is generated when the page is built, so
it cannot name a rule this release does not have; osy lint --rules prints the same list from the binary. A count
on its own would be a vanity number — the names are the point: each one says what it catches.
A rule id is a search term. Type the id the linter printed into osy docs and it answers: the page that
documents the rule, or — for a rule no page discusses on its own — this section, with the rule's own line above it
so you learn what it catches before anything opens.
⚠ The example below uses a placeholder rather than a real id ON PURPOSE — naming a specific undocumented rule here would make THIS page the one page that mentions it, which is enough to make the lookup treat this page as the rule's OWN documentation instead of falling through to this catalogue. That is a real trap (it happened once — a worked example in this exact spot broke its own guard), and a placeholder cannot fall into it.
$ osy docs <a rule id no page discusses on its own>
matched on lint rule <that id> (<TIER>): <what it catches> — no page documents it on its own, so the linter's
catalogue → local-checking-your-app129 rules — 33 MUST, 75 SHOULD, 21 CONSIDER — grouped by what they judge.
Security · 32 rules
| Rule | Tier | Catches |
|---|---|---|
security-auth-role-tests-nothing | MUST | an armed auth role that no guard anywhere ever tests |
security-authmethod-entity-row-filtered | MUST | the login's own entity granted only by a row filter the auth principal cannot satisfy |
security-credential-mask-hides-it-from-the-login | MUST | an unconditional deny on the credential also hides it from the login that must read it |
security-grant-edit-without-create | MUST | a caller who may update or delete a role grant they could not have created |
security-grant-write-unguarded | MUST | a role-grant table whose writes are open, so a caller can hand themselves a role |
security-hmac-compared-non-constant-time | MUST | an HMAC tag compared with ==, which leaks how much of a guess was right |
security-jwt-issued-unverified | MUST | a JWT issued before the credential was verified |
security-login-enumerates-users | MUST | a login whose refusal reveals which addresses have an account |
security-oauth-signup-no-existing-check | MUST | an OAuth sign-up that creates a user without checking for an existing one |
security-partial-exposes-platform-credential | MUST | a partial security block on a platform entity that opens a credential column |
security-password-compared-directly | MUST | a stored hash compared with == to a fresh hash, which can never be true |
security-password-echoed-in-clear | MUST | a password field rendered back as visible text |
security-principal-credential-client-exposed | MUST | a principal's credential field that can reach the browser |
security-principal-login-field-not-unique | MUST | the field a login looks users up by is not unique |
security-reset-token-never-delivered | MUST | a reset secret minted but never delivered, so nobody can finish the flow |
security-sensitive-field-exposed | MUST | a readable row carries a password hash, token or secret with no field-level deny |
security-signup-cannot-create-the-principal | MUST | a sign-up that runs as a principal with no create grant on the user entity |
security-weak-random | MUST | a token or secret drawn from a seedable Random |
security-anon-page-calls-gated-function | SHOULD | an anonymous page that calls a function anonymous callers cannot reach |
security-anon-page-reads-ungranted-entity | SHOULD | an anonymous page that reads an entity nobody anonymous may read |
security-app-creates-what-its-block-denies | SHOULD | a function that creates rows its entity's security block denies to every caller |
security-authz-without-authn | SHOULD | a principal, roles and gated pages, but no login function at all |
security-callback-url-widens-an-authorize | SHOULD | a CallbackUrl minted for an event that declares Authorize, which the link then bypasses |
security-classified-field-audited-unredacted | SHOULD | a classified field written to the audit trail unredacted |
security-concurrency-check-without-its-trail | SHOULD | a concurrency check on an entity whose audit trail is off |
security-entity-no-block | SHOULD | an entity with no security block, which grants nobody anything — usually a forgotten grant |
security-integration-role-granted-by-signup-order | SHOULD | a privileged role handed to whoever signs up first, in an app that mints per-user API keys |
security-login-reveals-which-credential-failed | SHOULD | a login that answers a wrong address and a wrong password differently |
security-password-typed-in-clear | SHOULD | a password bound to a plain text field instead of a password field |
security-secret-in-log | SHOULD | a credential field written to the log |
security-signup-no-password-policy | SHOULD | a sign-up that accepts any password at all |
security-auth-trail-disabled | CONSIDER | the authentication audit trail switched off |
Authentication · 1 rule
| Rule | Tier | Catches |
|---|---|---|
auth-bootstrap-without-app-auth | CONSIDER | an AuthBootstrap with no app.Auth, so only its own methods can sign in |
Data model · 14 rules
| Rule | Tier | Catches |
|---|---|---|
data-category-derived-from-free-text | MUST | a category list built from the rows' own free-text values, which fragments on the first typo |
data-root-dialog-cannot-confirm | MUST | a root dialog that never calls Dialog.Confirm, so nothing it edits can be saved |
data-unique-swapped-within-one-commit | MUST | two rows that swap a unique value inside one commit, which the index refuses |
data-write-never-committed | MUST | a write that is never committed |
data-caught-write-fault-left-staged | SHOULD | a caught write fault whose failed changes are left staged instead of discarded |
data-detached-child-query | SHOULD | children fetched by a standalone query instead of the parent's collection |
data-enum-member-no-label | SHOULD | a multi-word enum member with no Label, shown as one run-together word |
data-required-without-a-message | SHOULD | a Required field with no message for the refusal |
data-rule-without-message | SHOULD | an invariant or pattern with no message, so a refusal shows the rule itself |
data-unbounded-string | SHOULD | a string with no MaxLength, so the caller decides how much you store |
data-uniqueness-guarded-only-in-an-action | SHOULD | uniqueness checked in an action instead of declared on the field |
data-constraint-lets-null-through | CONSIDER | a Unique or Pattern field that is not Required, so null satisfies it |
data-row-compared-by-id | CONSIDER | rows compared by Id by hand where == already is row identity |
data-unique-editable-unchecked | CONSIDER | a Unique field edited in a form that catches no error on save |
Correctness · 10 rules
| Rule | Tier | Catches |
|---|---|---|
correctness-aggregate-over-pending-writes | MUST | an aggregate over rows written but not yet committed, so the total is short |
correctness-call-has-no-sql-form | MUST | a call inside a query with no SQL form |
correctness-nullable-tested-for-zero | MUST | a nullable tested for zero, which null passes |
correctness-parse-that-answers-zero | MUST | a parse that answers zero on bad input instead of failing |
correctness-egress-that-nothing-calls | SHOULD | an outbound call declared that nothing in the app ever invokes |
correctness-external-value-replaced-by-a-literal | SHOULD | a missing external value replaced by a literal that looks like real data |
correctness-freshness-stamp-with-no-source | SHOULD | a FetchedAt-style field in an app that has no egress to have fetched anything from |
correctness-member-read-off-a-nullable | SHOULD | a member read off a nullable that may be null |
correctness-null-substituted-for-a-value | SHOULD | a null replaced in arithmetic by a value indistinguishable from a real one |
correctness-pool-slot-credited-to-its-assignee | SHOULD | a pool slot's own arm reading its Assignee, which is nothing unless somebody claimed — actor is who acted |
Cost · 8 rules
| Rule | Tier | Catches |
|---|---|---|
cost-child-read-without-include | SHOULD | a child collection read with no Include on the parent query |
cost-index-of-in-its-own-loop | SHOULD | an IndexOf inside the loop over the same list |
cost-n-plus-one | SHOULD | a child read inside a loop over parents, one query per parent |
cost-page-reads-the-whole-table | SHOULD | a page that reads the whole table |
cost-paging-without-an-order | SHOULD | a Skip with no OrderBy, so page 2 can repeat page 1 |
cost-the-same-query-twice | SHOULD | the same query run twice in one function |
cost-unbounded-read | SHOULD | a query materialized with no Take |
cost-clause-calls-the-server-per-element | CONSIDER | a clause that calls the server once per element |
Reliability · 3 rules
| Rule | Tier | Catches |
|---|---|---|
reliability-http-result-unchecked | SHOULD | an HTTP result used without checking IsSuccess |
reliability-outbound-call-unguarded | SHOULD | an outbound call with no try, so a timeout kills the function or re-runs the workflow body |
reliability-reaches-the-network-unguarded | SHOULD | a function or workflow body that reaches the network through a helper, with no try |
Observability · 1 rule
| Rule | Tier | Catches |
|---|---|---|
observability-catch-without-log | SHOULD | a catch that logs nothing |
Workflows · 8 rules
| Rule | Tier | Catches |
|---|---|---|
workflow-dead-end-state | MUST | a non-terminal state with no way out |
workflow-ambient-clock-in-a-durable-body | SHOULD | the ambient clock read inside a durable body, which replays wrong |
workflow-clock-without-route | SHOULD | a deadline that passes with nothing routed to happen |
workflow-initial-state-never-observed | SHOULD | an Initial state whose Start body always redirects, so no run is ever in it |
workflow-no-success-terminal | SHOULD | a workflow whose every ending is a cancel or an error |
workflow-slot-open-to-everyone | SHOULD | a slot offered to everyone |
workflow-unreachable-state | SHOULD | a state nothing can ever enter |
workflow-repeated-completion-condition | CONSIDER | the same completion condition repeated at the end of several route arms |
UI · 37 rules
| Rule | Tier | Catches |
|---|---|---|
ui-app-has-no-home-page | MUST | no page serving the app's front door |
ui-toggle-handler-writes-it-again | MUST | a toggle whose handler writes the value the toggle already wrote |
ui-toggle-written-as-a-button | MUST | a boolean written as a button instead of a toggle |
ui-action-never-invoked | SHOULD | an action nothing in any render can invoke |
ui-atom-where-the-kit-has-a-control | SHOULD | a raw atom hand-built where the kit ships the control |
ui-button-indistinguishable-from-a-text-field | SHOULD | a button styled so it reads as a text field |
ui-component-reimplements-a-kit-control | SHOULD | a component that hand-rolls what a bundled kit control already does |
ui-control-call-without-an-accessible-name | SHOULD | a control call that passes no accessible name |
ui-currency-without-a-culture | SHOULD | a currency formatted with no culture |
ui-date-kept-as-a-string | SHOULD | a date kept as a string and bound to a plain text box |
ui-draft-field-ghosts-its-own-list | SHOULD | a draft row created at mount on a component that lists the same entity, so it appears in its own list |
ui-enum-rendered-without-its-label | SHOULD | an enum rendered by its member name instead of its label |
ui-guard-and-action-disagree-about-the-list | SHOULD | a guard and its action reading two different lists |
ui-inert-affordance | SHOULD | a control that accepts the click and does nothing |
ui-input-without-an-accessible-name | SHOULD | an input with no accessible name |
ui-key-read-with-no-key-surface | SHOULD | Keyboard.Down asked about a key no element declares, so it is false forever |
ui-label-drawn-twice | SHOULD | a label drawn twice for one control |
ui-nondeterministic-render-slot | SHOULD | a render-slot value whose behaviour depends on what else its expression reads |
ui-page-root-flush-against-the-viewport | SHOULD | a page root that paints a surface and sits welded to the viewport edge |
ui-page-server-read-with-no-skeleton | SHOULD | a page that reads from the server with nothing shown while it waits |
ui-row-guard-reads-the-unfiltered-list | SHOULD | a per-row guard that reads the unfiltered list |
ui-spacing-step-looks-like-pixels | SHOULD | a spacing argument that reads as pixels but is steps on the 0.25rem scale |
ui-state-nothing-reads | SHOULD | a state field nothing reads |
ui-text-field-bound-to-a-number | SHOULD | a text field bound to a number |
ui-theme-primary-collides-with-a-tone | SHOULD | a theme's Primary is too close to a semantic tone the app also paints, so a destructive action looks ordinary |
ui-theme-token-shadows-nothing | SHOULD | a theme token named to shadow a kit token that does not exist |
ui-control-state-not-announced | CONSIDER | a control whose state a screen reader is never told |
ui-control-without-an-accessible-name | CONSIDER | a control with no accessible name |
ui-data-read-declared-inside-render | CONSIDER | a data read declared inside a render instead of as a field |
ui-date-rendered-without-a-format | CONSIDER | a date rendered with no format |
ui-editable-field-no-error-surface | CONSIDER | a field under a rule the browser cannot pre-empt, in a form that catches nothing |
ui-editable-field-write-policy-unreflected | CONSIDER | an editable field whose write is gated by a declared policy the input does not reflect |
ui-mount-hook-is-a-fetch | CONSIDER | an on-mount hook that only loads data a field could declare |
ui-page-no-title | CONSIDER | a routed page with neither a Title nor a meta title |
ui-rank-rendered-as-a-position | CONSIDER | a stored rank drawn as the reader's position in a loop over a filtered list, so it reads 1, 3 |
ui-raw-style-literal-repeated | CONSIDER | the same raw style literal repeated where a token belongs |
ui-rendered-list-query-not-live | CONSIDER | a rendered list bound to a query that is not live |
Formatting · 3 rules
| Rule | Tier | Catches |
|---|---|---|
format-throws-on-double | MUST | an integer-only format on a Double, which throws at runtime |
format-double-custom-rounds-differently | CONSIDER | a custom pattern on a Double that rounds differently on the client |
format-runs-on-the-server | CONSIDER | a format the browser cannot reproduce, so it round-trips to the server |
Client · 1 rule
| Rule | Tier | Catches |
|---|---|---|
client-server-hop-in-class-method | CONSIDER | a class method that quietly makes a server round trip |
Testing · 11 rules
| Rule | Tier | Catches |
|---|---|---|
testing-login-untested | MUST | a login no test proves both ways |
testing-scope-names-text-not-a-container | MUST | a test scope that names text rather than a container |
testing-app-has-no-tests | SHOULD | an app that ships no tests at all |
testing-denial-provable-by-its-setup | SHOULD | a denial the setup alone would prove, with nothing there to deny |
testing-gated-read-outside-runas | SHOULD | a gated read asserted outside a runas, so a denial passes as empty |
testing-invariant-untested | SHOULD | an invariant no Assert.Throws proves bites |
testing-page-never-driven | SHOULD | a routed page no test ever visits |
testing-security-rule-untested | SHOULD | a rule that can deny, and no test acting as a principal proves it does |
testing-test-without-assertion | SHOULD | a test that asserts nothing |
testing-visible-on-a-number | SHOULD | an Assert.Visible on a bare number, a contains-check the whole page can satisfy |
testing-write-denial-unproven | SHOULD | a denied write with nothing anywhere proving the same write can succeed for anybody |
What does it find in the sample apps?#
osy lint run over every sample app in the repository when this page was built — the same call a downloader
makes, on the same source. A rule set is only as credible as what it says about the apps its authors ship, so the
result is published whether or not it is clean.
36 apps, 302 files: 1 MUST, 599 SHOULD and 117 CONSIDER findings.
| App | Files | MUST | SHOULD | CONSIDER | Distinct rules |
|---|---|---|---|---|---|
agent-expenses | 18 | 0 | 46 | 9 | 10 |
arcade | 28 | 0 | 51 | 22 | 11 |
auth-demo | 6 | 0 | 0 | 0 | 0 |
chart-demo | 8 | 0 | 5 | 4 | 5 |
chat-demo | 6 | 0 | 10 | 5 | 8 |
chat-room | 6 | 0 | 16 | 2 | 9 |
concurrency | 9 | 0 | 13 | 3 | 9 |
dialog-demo | 6 | 0 | 17 | 1 | 8 |
docs-site | 9 | 0 | 13 | 2 | 10 |
dropdown-demo | 5 | 0 | 9 | 3 | 10 |
ember | 15 | 0 | 38 | 8 | 13 |
entity-inheritance | 9 | 0 | 35 | 0 | 9 |
file-manager | 11 | 0 | 15 | 5 | 12 |
generic-grid | 5 | 0 | 11 | 9 | 10 |
gestures | 2 | 0 | 1 | 2 | 3 |
gridprobe | 3 | 0 | 4 | 1 | 3 |
hello-osy | 4 | 0 | 2 | 1 | 2 |
kanban | 5 | 0 | 18 | 4 | 13 |
markdown-demo | 6 | 0 | 0 | 0 | 0 |
media-demo | 4 | 0 | 4 | 1 | 5 |
memory-lab | 7 | 0 | 13 | 1 | 6 |
motion | 3 | 0 | 1 | 0 | 1 |
shell-arrangements | 7 | 1 | 11 | 1 | 4 |
shell-showcase | 10 | 0 | 15 | 0 | 1 |
shop | 6 | 0 | 12 | 1 | 5 |
tabbed_admin_paused | 18 | 0 | 40 | 20 | 17 |
template-stretch | 5 | 0 | 9 | 6 | 9 |
todo | 3 | 0 | 0 | 0 | 0 |
wf-approvals | 9 | 0 | 24 | 1 | 9 |
wf-expense-hitl | 9 | 0 | 20 | 0 | 7 |
wf-fanout-quorum | 11 | 0 | 27 | 0 | 7 |
wf-nightly-digest | 7 | 0 | 12 | 0 | 5 |
wf-order-saga | 9 | 0 | 31 | 0 | 7 |
wf-signup-invite | 11 | 0 | 20 | 1 | 12 |
wf-supplier-dispatch | 6 | 0 | 21 | 0 | 6 |
wf-support-sla | 16 | 0 | 35 | 4 | 13 |
Examples#
osy lint # what this app is missing
osy lint --strict # fail the run on any MUST-tier finding
osy lint --json # findings as JSON
osy lint --rules # every rule: id, tier, what it catchesSee also#
Understanding your app — the resolved model: what the app is.
Explaining your app's security — who can do what, in plain English; --with-findings folds these findings into it.
secure by default (deny-all) — the deny-all default the security rules are written against.
security { } — how to declare an entity's access rules.