Service audit, data cards

Last updated

Checks one running service: its API, failure handling, limits, and security. This page holds the cards for the data context. The Service audit page lists every card. Report each check as PASS, FAIL, or N/A with file:line evidence. A FAIL carries the card's severity. Any block FAIL means request changes. A should-fix or discuss FAIL goes in the review but does not block.

Verdict: request changes - change, function - 2 block, 1 should-fix FAIL, 1 PASS, 14 N/A
FAIL [block] chg-expand-contract - src/a.py:42 - renames the column and drops the old one in one migration
FAIL [block] svc-sqli-authn - src/db.py:88 - proven: builds the WHERE clause with an f-string from request.args
FAIL [should-fix] chg-one-concern - src/b.py:7 - a logging rewrite rides along with the bug fix
PASS chg-breaking-change-taxonomy - src/c.py:15 - no signature or return shape changes
N/A: 14 cards, applies_when does not hold

Filters

Concerns
Topics

Checklist

Back to filters

What the badges mean

Cards

svc-export-scope

blockstablesecurity

Applies when: An endpoint or job produces a file, archive or stream of many records.

Rule. Scope an export to what the requesting principal may read.

Why. An export is a read that returns many rows at once. The per-object check on the detail page never runs, so a SELECT * with no tenant or access predicate hands the caller the table. Apply the same filter the list endpoint uses, including any role or ownership predicate and not only the tenant, and apply it inside the export query rather than on rows already fetched. OWASP's examples put the tenant scope in the query itself. Test it with two tenants. A restore asks the same question in reverse: a shared backup holds several tenants and old memberships, so restore one tenant into a separate resource and re-apply current access and retention before anyone can read it. A recovered environment also needs the production controls, not the ones it had when the backup was taken.

FAIL
GET /export  ->  SELECT * FROM orders; streamed to any authenticated caller
PASS
def export(user):
    rows = session.query(Order).filter(Order.tenant_id == user.tenant_id)
    return csv_stream(rows)

Counterpoint. "Some exports deliberately include everything." Some products ship an admin XML export that includes pages the exporter cannot view. That path is an administrative backup, so gate it on an admin permission and audit it. The user-facing exports in the same products contain only what the exporter can view, which is the rule here. Check which export path the change touches before calling a missing filter a bug.

Tool gate: A test that exports as a second tenant and asserts the other tenant's rows are absent.

svc-soft-delete-default

should-fixstablesecuritycorrectness

Applies when: A table or model marks a row as deleted instead of removing it.

Rule. Filter soft-deleted rows out of every read path by default, and make a read that includes them an explicit call.

Why. A soft delete sets a column. It does not remove the row, so any query that forgets the filter still returns the record. One raw query or one new endpoint is enough to expose it. Put the filter in the model's default scope, so new queries inherit it, and keep the escape hatch visible. A deletion policy has to reach caches, exports, replicas and object versions too, or the record lives on in a copy.

FAIL
modelBuilder.Entity<Document>();                 // no HasQueryFilter
var doc = await context.Documents.FindAsync(42); // returns the soft-deleted row
PASS
modelBuilder.Entity<Document>().HasQueryFilter(d => d.DeletedAt == null);

// The one admin report that needs deleted rows, behind an admin check:
var all = await context.Documents.IgnoreQueryFilters().ToListAsync();

Counterpoint. "The admin console has to see deleted rows." Keep that one call explicit, name it for what it does, and check the caller is an admin. The default stays closed. Raw SQL and reporting replicas are the usual bypass, so route them through the same helper or audit them. A service that hard-deletes does not need the filter, but backups and object versions can still hold the old copy.

Tool gate: A test per read path: soft-delete a row, then assert the endpoint returns 404 or leaves it out of the list.

Backed by 4 organisations, backing unjudged: