Skip to content

Enable ruff security, logging and debugging rules (S, T20, LOG, G, DTZ, ERA, TD, FIX, EXE) - #3915

Draft
anth-volk wants to merge 16 commits into
ruff-exceptionsfrom
ruff-security-logging
Draft

anth-volk wants to merge 16 commits into
ruff-exceptionsfrom
ruff-security-logging

Conversation

@anth-volk

@anth-volk anth-volk commented Oct 13, 2026 •

Copy link
Copy Markdown
Collaborator

Fixes #3914

Also fixes #3909: the country validator's print("Validating country") is removed.

Summary

Phase 1 step 1.8, group 6: security, logging and debugging leftovers. Stacked on #3913.

  • Rules: select gains S, T20, LOG, G, DTZ, ERA, TD, FIX and EXE. Every finding at the base (262, plus 12 in Stage 12A: Sign-in identity and v1-anchored user associations #3891's code after the restack) is fixed, kept with an inline reason, or, in tests, CI/CD scripts and command-line scripts, covered by a file-level exception. One commit per family.
  • File-level exceptions (new):
    • Where they're allowed. A ruff per-file-ignores entry is now allowed only for:
      • tests (a tests or authenticated_tests directory);
      • CI/CD scripts (.github);
      • command-line scripts (a scripts directory, or a module with a top-level if __name__ == "__main__":).
    • How it's checked. The rules guard expands each entry's pattern the way ruff does: * and ? also match /, and a pattern also matches a file by its name. It fails when the pattern covers any other file, is negated, or matches nothing. scripts/rules/file_exceptions.py holds the definitions, docs/engineering/skills/rules-registry.md#file-level-exceptions documents them, and six rules-tool tests prove them.
    • The entries. Each lists exactly the group 6 codes those files had suppressed inline, not whole families, and each has a registered per-file-ignore rule:
      • tests (**/tests/**, extending api.quality.tests-ruff-exemptions): S105, S106, S603, S607, S608, DTZ001, DTZ011, T201;
      • CI/CD scripts (.github/**): S310, T201;
      • command-line scripts (scripts/**): T201, S603, S607, S310, S311, S110, DTZ011;
      • the five modules that also run as programs (catalog initialization, the three migration qualifications, worker_spm_release): T201.
    • What it removed. 122 inline suppressions: 74 in tests, 30 in scripts, 10 in the program modules and 8 in .github. The 36 suppressions in application code are unchanged.
    • Alembic revisions. Alembic revisions aren't any of those kinds of file, so their "migrations/*/versions/*.py" = ["ALL"] entry moves to [tool.ruff.lint] exclude. ruff check . still skips them, and ruff format still checks them. api.quality.alembic-revisions-unused-imports now describes the exclusion.
  • Behaviour changes (three kinds, all deliberate):
    • Debug prints removed (T201): the country validator (validate_country prints "Validating country" on every request #3909), plus the create, get and update prints in the report output and simulation routes. The two update prints become logger.debug calls on a module logger, because their ids come from the request body.
    • Logger name (LOG015): the household calculation routes log their three errors on a module logger instead of the root logger. The message text is unchanged, and production records still reach the root observability handler.
    • Nothing else: error responses, message text and published schemas are unchanged.
  • Asserts outside tests (S101, 11): each type-narrowing assert in the v2 household, policy, user-household and user-policy services, and two in the rules tool, becomes an explicit check that raises TypeError. None runs inside a Pydantic validator, and nothing catches AssertionError. Every check was already guaranteed by an earlier one, so none is expected to fire; the v2 routes and the mirror paths treat either type the same way. No production command runs Python with -O, so these asserts were live.
  • Prints in commands (T201): command-line scripts, CI/CD scripts and the modules that run as programs (including the migration qualification commands' JSON evidence) keep their output under the file-level exceptions. capture_migration_baseline.py, smoke_test_local_boot.py and the rules tool's CLI route output through one _report helper. Application code keeps the api.py startup timings and the gateway-auth warning on stderr, each with an inline reason.
  • Datetimes (DTZ, 21): no value switches between naive and aware. The catalog extraction builds its aware UTC value directly; the other 20 stay naive. Those in tests and scripts, such as the rules tool's calendar-day review dates and test values, are covered by the file-level exceptions. The 4 in application code (naive database columns, the local calendar year) keep an inline reason.
  • Security (S): no vulnerability found.
    • Fixed: hashlib.sha1(..., usedforsecurity=False) (same digest), and the GPT-4 experiment parses embeddings with ast.literal_eval instead of eval.
    • Covered by file-level exceptions:
      • in tests: fake secrets, runs of this repository's scripts and git, and table names from a fixed tuple;
      • in scripts and .github: fixed https and loopback URLs, and a seeded load-test mix.
    • Kept inline with a reason:
    • S110: each try/except/pass already carried a reasoned BLE001 suppression from group 5, which now lists S110 too.
  • Leftovers: 32 lines of commented-out code are deleted (29 in tests/to_refactor/python/test_policy.py). The two TODOs become plain notes; the user-profile one keeps its link to Utilize a technology like Marshmallow to validate inbound requests #2054 and says it was closed. Seven Python scripts with a shebang and a __main__ entry point are now executable.
  • Registry:

Testing

  • Every tip from Enable ruff security, logging and debugging rules (S, T20, LOG, G, DTZ, ERA, TD, FIX, EXE) #3915 to Register weighted-microdata rules, with AST guards and #767 proofs #3938 passes these checks:
    • ruff check and ruff format --check (0.15.12) are clean;
    • the generated rules files are fresh at every commit;
    • the quality guards, the rules guard against each parent, and the rules tool unittest (80 tests) pass;
    • mypy is clean (167 files).
  • This update changes only comments outside the rules tool. Outside scripts/rules/, every changed Python file at every tip differs from the previously pushed tip only in # noqa comments: each version is compared with its noqa comments stripped. The other changes are:
    • pyproject.toml;
    • the registry;
    • the docs;
    • the rules tool's new check and tests.
  • Full suites: tests/unit (2,685 passed) and the to_refactor, contract and dedupe set (97 passed) were run on this branch before the last change, which touched only the rules tool. They were not re-run after it.
  • Every Pydantic model schema (230 models, 100 adapters), the FastAPI OpenAPI with every native router enabled, the /v2/openapi.json preview and the legacy spec are byte-identical to the base.
  • CI/CD: no workflow changed. Four .github scripts changed only in comments, output helpers or file mode, and the environment variables they read are unchanged.
  • CI: this PR is above the bottom of its stack, so it runs only CI complete, which fails and says why (step 0.7, Make CI enforcement trustworthy before the rules registry #3887). Its full run starts once the PRs below it merge.

🤖 Generated with Claude Code

@anth-volk
anth-volk added this pull request to stack #3888 October 13, 2026 17:53
@anth-volk
anth-volk removed this pull request from stack #3888 October 13, 2026 19:26
@anth-volk
anth-volk added this pull request to stack #3917 October 13, 2026 19:26
@anth-volk
anth-volk force-pushed the ruff-security-logging branch from f973af3 to 6c8d2b6 Compare October 14, 2026 13:52
@anth-volk
anth-volk force-pushed the ruff-security-logging branch 2 times, most recently from 747e269 to 899ad06 Compare October 14, 2026 16:59
@anth-volk
anth-volk force-pushed the ruff-security-logging branch 3 times, most recently from c67b555 to 1d13dbe Compare October 14, 2026 20:04
@anth-volk
anth-volk force-pushed the ruff-security-logging branch 7 times, most recently from 509c02c to dfd8c98 Compare October 15, 2026 15:21
@anth-volk
anth-volk force-pushed the ruff-security-logging branch from dfd8c98 to 1f2e57c Compare October 15, 2026 15:45
@anth-volk
anth-volk force-pushed the ruff-security-logging branch from 1f2e57c to 795091f Compare October 15, 2026 17:21
@anth-volk
anth-volk force-pushed the ruff-security-logging branch 2 times, most recently from 7a6c495 to 7008825 Compare October 16, 2026 06:45
anth-volk and others added 16 commits October 16, 2026 11:05
Delete the commented-out TestPolicySearch class in the to-refactor policy
tests, along with the note that introduced it, the commented-out profiler
import in the Flask app and the commented-out service imports in the
services package. The GPT-4 experiment's example reform is reworded as
prose.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Each of these six scripts has a python3 shebang and a __main__ entry
point, so it is meant to be run directly. Git now records them as
executable; running them through python is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The user-profile update note keeps its link to issue #2054, which was
closed as not planned, and now says so. The simulation serializer note
explains that its JSON-in-a-string projection serves only the v1
contract.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The three logging.exception calls in the household routes used the root
logger. They now use a module logger, so the records are named after
policyengine_api.routes.household_routes; the message text and traceback
are unchanged. The module keeps its existing structured logger for
log_struct.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The catalog extractor builds a date's midnight with tzinfo=UTC directly
instead of attaching UTC afterwards; the result is the same aware value.

Every other naive value stays naive, with an inline reason: reform
impact start_time and report run timestamps are naive columns (and the
runtime cache's datetime.min fallback must compare with them), economy
start_time has always held local time, the default year and household
period follow the host's local calendar, and the rules registry checks
review and expiry dates against the local date.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The eight asserts in the v2 household, policy and user-household
services and the two in the rules ci-scripts guard now raise TypeError
when the narrowed value has the wrong type. None runs inside a Pydantic
validator and nothing catches AssertionError: the v2 routes return their
generic 500 and the mirror paths report an "unexpected" failure for
either type, and the rules callers check for a missing [ci_scripts]
table first. The checks also hold under python -O, which strips asserts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
validate_country no longer prints "Validating country" on every request
(#3909). The simulation and report routes no longer print on create and
get, where the URL already carries the country and id; their update
messages, whose id comes from the request body, become debug records on
a module logger with the same text.

The remaining prints are output by design and carry a reason: the CLI
entry points' JSON evidence and results, the startup timings and the
unauthenticated simulation client warning on stderr.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Printing is these scripts' job. The four scripts with more than three
prints (the version bump, the migration baseline capture, the local boot
smoke test and the rules CLI) route their output through one _report
helper. Three of those helpers' prints carry the suppression; the version
bump's helper passes its file argument through, which T201 does not flag,
and its stderr messages still go to stderr. Every other print carries an
inline reason, as does the catalog publication qualification test, whose
printed evidence is its report.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Behaviour-identical fixes: the runtime cache version's sha1 is marked
usedforsecurity=False (the digest is unchanged), and the GPT-4
experiment parses its embedding strings with ast.literal_eval.

Test modules reuse one constant per fake credential or claim token
instead of repeating the literal, and the Cloud Run deploy-script tests
run bash through one helper.

Every other finding keeps its code with an inline reason: try/except/pass
on the deliberate telemetry and logging catch-alls (S110, alongside their
BLE001 reason), environment-variable and Secret Manager names and test
sentinels (S105, S106), fixed repository commands (S603, S607), fixed
https and loopback URLs (S310), the experiment's untimed requests
(S113), the load test's seeded random mix (S311) and fixed table names
in a test query (S608).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Select flake8-datetimez (DTZ), eradicate (ERA), flake8-executable (EXE),
flake8-fixme (FIX), flake8-logging-format (G), flake8-logging (LOG),
flake8-bandit (S), flake8-print (T20) and flake8-todos (TD). Tests
follow all nine: their exemptions stay limited to S101, D1, ANN and
PLR2004.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
api.quality.ruff-lint now lists DTZ, ERA, EXE, FIX, G, LOG, S, T20 and
TD. The rendered rules page also picks up the rules tool's new hash.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…S101)

The user-household and user-policy patch methods asserted that a
supplied legacy id is not None before checking it in v1. They now raise
TypeError instead, as the other v2 services do. The update models
already reject an explicit null legacy id, so neither check can fail for
a validated request; if one did, nothing catches either type and the
route returns its generic 500. The checks also hold under python -O.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- S105: the Auth0 Management secret environment-variable names and the
  invalid-token WWW-Authenticate challenge keep their literals with a
  reason, as do the Auth0 test's Secret Manager path and the route-auth
  test's leak sentinel.
- S106: the Cloud Run deploy-script tests name the Auth0 Management
  secret resource once, as TEST_AUTH0_SECRET_RESOURCE, instead of
  repeating the literal three times.
- S110: the user routes' failure logging keeps its try/except/pass with
  the BLE001 reason.
- T201: the association qualification command's two prints are its JSON
  evidence and keep a reason.
- EXE001: scripts/qualify_v2_user_association_migration.py has a
  python3 shebang and a __main__ entry point like its two siblings, so
  git now records it as executable.

These were the other 10 ruff group 6 findings in the code #3891 added.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…r file

Per-file-ignores are now only for tests, CI/CD scripts and command-line
scripts, so the "migrations/*/versions/*.py" = ["ALL"] entry moves to
[tool.ruff.lint] exclude. The revisions must stay exactly as Alembic
generates them (api.migration.alembic-autogenerated-only), so ruff still
doesn't lint them when it walks the repository, and ruff format still
checks them as before. api.quality.alembic-revisions-unused-imports now
describes the exclusion, with no ruff enforcer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e scripts

A ruff per-file-ignores entry switches rules off for whole files. That is
now acceptable where it's the norm: tests, CI/CD scripts under .github,
and command-line scripts (under a scripts directory, or modules run
through if __name__ == "__main__"). Everywhere else a kept finding still
needs an inline suppression with its reason.

The rules guard already requires a per-file-ignore rule for every entry.
It now also expands each entry's pattern the way ruff does (globset
syntax, where * and ? also match "/", and a pattern also matches a file
by its name alone) and fails when the pattern covers any other file, is
negated, or matches nothing.
scripts/rules/file_exceptions.py holds the definitions, and the registry
doc describes the policy.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Group 6's rules flag patterns that are the norm in tests, CI/CD scripts
and command-line programs: fake credentials, subprocess calls to
repository scripts, prints, naive datetimes, fixed URLs. Those files now
take file-level exceptions, each registered as a per-file-ignore rule,
and the 122 inline suppressions they cover are removed:

- tests (**/tests/**, extending api.quality.tests-ruff-exemptions):
  S105 30, S603 14, DTZ001 13, S607 11, S106 2, S608 2, DTZ011 1, T201 1;
- CI/CD scripts (.github/**): T201 6, S310 2;
- command-line scripts (scripts/**): T201 17, S603 4, S607 2, DTZ011 2,
  S310 2, S110 2 (BLE001 stays on those two lines), S311 1;
- modules that also run as programs (catalog initialization, the three
  migration qualifications and worker_spm_release): T201 10.

Each entry lists exactly the codes these files suppressed, not whole
families, so other security rules still apply to them. Suppressions in
application code are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@anth-volk
anth-volk force-pushed the ruff-security-logging branch from 7008825 to 88bf897 Compare October 16, 2026 07:05

This branch was successfully deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant