v1.44.0
secure-code-auditor v1.44.0
The three bundled scanners become complete, honest about coverage, and precise.
Three new rules, seven new settings checks, four rules widened to the spellings
they were silently missing, one false positive removed, a redaction, and a new
output contract that makes an empty JSON stream impossible. No reference file
changed. No rule identifier was reused, and no existing severity value moved.
Every change ships with a fixture. The self-test now holds 49 of them, 35
positive and 14 negative, and covers 29 of 29 rules. Every Django default this
release rests on was read off installed source on 20 August 2026 — Django 6.0.7,
cross-checked against 5.2.15 — rather than recalled. The library index date does
not move: no PyPI sweep ran.
What landed
Rules that matched one spelling and passed the rest. DES002 dispatched on
yaml.load alone, so yaml.unsafe_load, yaml.full_load, and the three
*_load_all variants went unreported. DES001 knew pickle, cPickle, and
_pickle, but not dill, cloudpickle, or joblib.load, which carry the same
protocol. TPL002 tested for ast.JoinedStr, so a % operation and the
.format spelling passed while the f-string was caught.
SEC001 read str literals only and did not know PASSWD or PASSPHRASE.
Each gap produced a clean result that looked like a clean tree.
Three new rules. SEC002 — jwt.decode with verify=False or an options
literal carrying verify_signature: False. NET002 — TLS verification disabled
at the ssl layer, through ssl._create_unverified_context(),
check_hostname = False, or verify_mode = ssl.CERT_NONE; the failure NET001
catches at the requests layer, caught where a client builds its own context.
NET003 — a requests/httpx verb or urllib.request.urlopen whose URL
argument derives from request data, resolved through the same taint machinery as
the ORM identifier rules. A URL from a settings value or a module constant is
not reported: who last wrote the value is the SSRF question.
Seven settings checks the references already teach. MIDDLEWARE membership
— CsrfViewMiddleware absent (HIGH), SecurityMiddleware absent (MEDIUM),
XFrameOptionsMiddleware absent (LOW), and SECURE_CSP present with no CSP
middleware (MEDIUM) — judged only on a literal list with no later +=, because
an augmented list may add what the literal lacks. SESSION_ENGINE naming
signed_cookies, where no server-side record exists and no single session can
be revoked. SESSION_COOKIE_SAMESITE and CSRF_COOKIE_SAMESITE weakened, with
the string "None" reported higher when the matching *_SECURE flag is not
True. SECURE_CROSS_ORIGIN_OPENER_POLICY set to None or "unsafe-none".
USE_X_FORWARDED_HOST set to True, on the same trust reasoning as
SECURE_PROXY_SSL_HEADER. A REST_FRAMEWORK block with no
DEFAULT_PERMISSION_CLASSES, or with AllowAny in it, since DRF's own default
is AllowAny. A MySQL/MariaDB alias whose OPTIONS carry no ssl.
A secret that was printed back. SEC001 attached the source line to every
hit, so a hardcoded key appeared in both the text report and the JSON stream.
The snippet for that rule is now the fixed text
<redacted: secret-shaped literal>, and every snippet from every rule is
stripped of its ANSI escapes and control bytes before it reaches a terminal. The
self-test proves it: it scans a canary assignment and asserts the canary reaches
no snippet.
An empty stream can no longer occur. Every --json invocation of all three
scripts now ends with one kind: "summary" record carrying path,
files_discovered, files_scanned, files_unparsed, the per-script emitted
count, and walk_errors. A path that is not a file or a directory writes one
kind: "error" record and the summary instead of nothing at all. Every
os.walk carries an onerror handler, so a directory that cannot be read is
counted rather than skipped in silence, and settings_scan.py no longer raises
on an unreadable package directory. Exit codes are unchanged at 0.
CI runs the scanners. A second job runs --selftest and fails the workflow
on a nonzero exit, then asserts that each of the three scripts, given a missing
path with --json, exits 0 and writes a last line that parses as JSON with
"kind": "summary". Stdlib only, no new dependency.
Behavior worth knowing
--selftestreturns 1 when a check fails. It returned 0 before,
whatever the fixtures did. This is the one mode whose exit code carries a
verdict, andscripts/README.md, "Invariants", now states it as the single
exception to the always-zero rule. Every scanning mode still exits 0. The
documented guidance was always to read the self-test's output rather than
$?, so no described workflow changes; a CI step that reads$?now gets a
real answer.TPL003no longer reportsfrom string import Template. The stdlib
exemption tested only the attribute form, so the bare-import spelling was
reported HIGH. Both spellings are now exempt, resolved through the import map.- PostGIS aliases are judged. The PostgreSQL transport check gated on
"postgresql" in engine, whichdjango.contrib.gis.db.backends.postgisdoes
not contain, so a PostGIS project never had itssslmoderead. OPTIONS["pool"]is judged on its value, not its key. A literal
"pool": Falseis not a pool. Django itself reads the value for truth, in
django/db/backends/postgresql/base.py, and the check now matches it.- An HSTS companion is judged only when HSTS is on.
SECURE_HSTS_INCLUDE_SUBDOMAINSreported its absence even when
SECURE_HSTS_SECONDSwas absent or zero, where it does nothing at all. - An absence whose Django default is already safe is INFO, not LOW.
SECURE_CONTENT_TYPE_NOSNIFFandSESSION_COOKIE_HTTPONLYboth default to
True; the message names the default. Entries whose default is unsafe are
unchanged. - A dynamic
SECRET_KEYis INFO, not OK. Every other dynamic value in the
scanner is reported as verify-by-hand, and this one now reads the same. AccessMixinno longer counts as an authorization declaration. It
configures the failure handling and enforces nothing, so a class carrying only
AccessMixinreads asinheritedrather thandeclared.- A
ProtocolTypeRouterrow is alwaysabsent.AuthMiddlewareStack
supplies the identity, not the authorization: it puts a user in the scope and
admits every consumer it wraps. The stack stays in the row'sstackdetail. - A bare
@taskresolved todjango.taskscarriessystem: django-tasks.
Its default backend,ImmediateBackend, runs the task inline in the caller's
transaction, which is not what theceleryfamily otherwise implies.
Everything else in that family carriessystem: celery. import a.bnow resolves correctly in both scanners. The import map bound
atoa.b, sourllib.request.urlopenresolved to
urllib.request.request.urlopenand the newNET003entry for it could only
have fired through thefrom-import spelling. This was found while adding
that rule and is fixed with a fixture for both spellings.
Verified clean
python3 scripts/dangerous_patterns.py --selftestpasses: 49 fixtures, 0
failures, 29 of 29 rules with a positive fixture, every negative fixture
clean, and the redaction canary absent from every snippet. Exit 0. With a
failing fixture injected, it returns 1.- Every
--jsonmode of every script — missing path, empty directory,
unreadable directory, a file target, a directory target,--min-severity, and
an unknown--kind— writes only valid JSON Lines, ends with asummary
record, and exits 0. Fourteen invocations checked. - All three scripts report zero findings against this repository's own tree.
- The document-integrity checks pass: 24 reference files, every link resolves,
no orphans, balanced fences,SKILL.mdat 33,618 of 40,960 bytes. - Changed files carry no citation artifacts.
Deferred, and why
references/01-audit-workflow.mdstill says the self-test "exits 0 whether
or not the fixtures pass." That clause is now stale. The file belongs to no
change set in this repair except the final whole-repository checkpoint, so it
was left for that pass rather than edited across an ownership boundary. The
authority it cites,scripts/README.md, "Invariants", is correct as of this
release.SKILL.md's router row for that file carries the same phrasing.data-layer-and-database.mddoes not yet teach the MySQL/MariaDB transport
rule the newDATABASEScheck routes to. The check is grounded — Django
readsOPTIONS["ssl"]["ca"]indjango/db/backends/mysql/client.py— but the
reference prose belongs to another change set and was not written here.- The library index was not re-dated. Nothing in this release rests on a new
PyPI sweep. - One item stands open from the 13 August 2026 audit, unchanged here: the
gevent>=24.10.1floor recorded for CVE-2023-41419 in
deployment-and-runtime.mdstill wants confirmation against the advisory.
Two commits are released here for the first time: the SKILL.md frontmatter
validation, and the reference link, orphan, fence, and size check.
Full Changelog: v1.43.0...v1.44.0
Full Changelog: v1.43.0...v1.44.0