Let the security scan install, then say why each finding is not one

The lane never scanned anything. `pip --break-system-packages` cannot
install semgrep on the runner image: semgrep wants PyJWT, the image has
PyJWT 2.7.0 from its distro, a distro package carries no RECORD, so pip
cannot uninstall what it needs to replace and stops. The flag grants
permission to touch that environment; it does not make the uninstall
possible. uv gives semgrep its own environment and shims the binary onto
PATH — the same fix the insights lane took, so both read alike now.

With the install working the scan reports four, and all four are the
code being careful rather than careless. Each carries the reason at the
line, because a bare suppression is indistinguishable from someone
silencing a real one:

  alembic/env.py     the advisory-lock key is an int on both branches —
                     a sha256 digest reduced mod 2**63-1, or a literal
  auth/iam.py        decoded unverified only to read `iss` and pick a
                     validator; a token claiming our issuer then goes to
                     validate(), which checks the signature and raises
  auth/service.py    reads a claim without authenticating it, safe while
                     nothing authorises on the result — today only this
                     module's tests call it
  arxiv.py           imports the Element TYPE for annotations; parsing is
                     defusedxml.fromstring, two lines below

Placement matters and cost a round trip: the directive has to sit on the
line the rule anchors to, which for the jwt call is the `options=` line
carrying verify_signature, not the line opening the call.

Co-authored-by: Hanzo Dev <[email protected]>
This commit is contained in:
2026-08-22 22:01:32 -07:00
co-authored by Hanzo Dev
parent 879a6154c1
commit 4f11077b6c
5 changed files with 33 additions and 1 deletions
+20 -1
View File
@@ -50,7 +50,26 @@ jobs:
# Pinned. A scanner that changes rules underneath the build turns an
# unrelated commit red and teaches people to ignore it.
- name: install semgrep
run: python3 -m pip install --break-system-packages --quiet semgrep==1.167.0
# `uv tool install`, not `pip --break-system-packages`. Semgrep depends on
# PyJWT, the runner image has PyJWT 2.7.0 from its DISTRO, and a distro
# package carries no RECORD — so pip cannot uninstall what it needs to
# replace and stops:
#
# ERROR: Cannot uninstall PyJWT 2.7.0, RECORD file not found.
#
# --break-system-packages grants permission to touch that environment; it
# does not make the uninstall possible. uv puts semgrep in its own
# environment and shims the binary onto PATH, so nothing is replaced and
# the pin above is still exact. It is also what every other python lane
# in this fleet now uses.
run: |
command -v uv >/dev/null 2>&1 || {
curl -fsSL https://astral.sh/uv/install.sh | sh
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
export PATH="$HOME/.local/bin:$PATH"
}
uv tool install semgrep==1.167.0
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
# The whole tree, not a named list — an allowlist stops covering whatever
# is added after it is written. semgrep walks only git-tracked files, so
+3
View File
@@ -114,6 +114,9 @@ def _do_run_migrations(connection):
logger.info(f"Using default migration lock_key: {lock_key}")
connection.execute(text("SET LOCAL lock_timeout = '180s';"))
# lock_key is an int either way above — a sha256 digest reduced mod 2**63-1,
# or the literal default. There is no string here to inject into.
# nosemgrep: python.sqlalchemy.security.audit.avoid-sqlalchemy-text.avoid-sqlalchemy-text
connection.execute(text(f"SELECT pg_advisory_xact_lock({lock_key});"))
context.run_migrations()
@@ -142,7 +142,11 @@ def try_validate(token: str, *, validator: HanzoIAMValidator | None = None) -> I
# Peek the issuer unverified: only tokens claiming OUR issuer are IAM tokens; anything
# else is a local token and must fall back (not error).
try:
# Unverified ON PURPOSE and only to read `iss`, which decides WHICH validator
# the token belongs to. Nothing is trusted from this: a token claiming our
# issuer goes to v.validate() below, which verifies the signature and raises.
unverified = jwt.decode(
# nosemgrep: python.jwt.security.unverified-jwt-decode.unverified-jwt-decode
token, options={"verify_signature": False, "verify_exp": False, "verify_aud": False}
)
except jwt.InvalidTokenError:
@@ -617,6 +617,10 @@ class AuthService(BaseAuthService):
not verified as this is a utility function, not an authentication function.
"""
try:
# Reads a claim without authenticating it, which is safe only while nothing
# authorises on the result — today the only callers are this module's tests.
# If a request path ever needs the subject, verify the token instead.
# nosemgrep: python.jwt.security.unverified-jwt-decode.unverified-jwt-decode
claims = jwt.decode(token, options={"verify_signature": False})
user_id = claims["sub"]
return UUID(user_id)
@@ -1,4 +1,6 @@
import urllib
# The TYPE only, for annotations. Parsing is defusedxml.fromstring, imported below.
# nosemgrep: python.lang.security.use-defused-xml.use-defused-xml
from xml.etree.ElementTree import Element
from defusedxml.ElementTree import fromstring