Skip to content

What a Scorecard zero says about this repository

Status: accepted. docs/adr/0024 is the decision to run Scorecard and publish its score. This is the decision about what to do with the score it returned. Nothing here raises the aggregate on purpose, and one change lowers nothing while making a release page honest.

The first run scored 6.0 - run 34193206416, commit 4c473cdb, 2026-09-08. Six checks came back at zero or below. Three of them are not findings about this repository at all, two are the recorded consequence of decisions already made, and one is a real gap with a blocker in front of it.

At 6.0 Now Check What it is
-1 10 CI-Tests was a 403 on a private repo; resolved by the flip, exactly as predicted
0 0 Maintained the repository is under 90 days old; not a defect
1 1 Signed-Releases every artefact was already signed; the row counts releases, not artefacts
0 0 Code-Review structurally unreachable while one identity does the reviewing
3 3 Branch-Protection capped by the merge-queue arrangement
0 2 CII-Best-Practices the badge project now exists at in_progress; the questionnaire is unfinished
0 0 SAST real; the tool that closes it was blocked and no longer is
0 0 Fuzzing real, and it is #146's, not this record's

Re-measured at 52f7709c (run 34212763873), aggregate 6.1. Two rows moved without anyone touching a check, and both were predicted here: the visibility flip resolved CI-Tests outright, and registering the badge project moved CII-Best-Practices off zero. Nothing else changed, which is the point - the remaining zeros are the ones this record argues are not defects.

The limit that qualifies every number below

The 6.0 run was made while the repository was private. It is now public - .private is false and visibility is public, read 2026-09-08 - so the re-measurement above is the first one that answers about the project rather than about its visibility. The original run's caveats were:

  • CI-Tests failed outright because of it, and nothing else. Confirmed: it is now 10/10, "30 out of 30 merged PRs checked by a CI test". This is the row that proves the caveat was real rather than a hedge.
  • CII-Best-Practices, Code-Review and Maintained are read from APIs that answer for a private repository the same way they answer for a neglected one. Re-taken while public, Code-Review and Maintained did not move - so their zeros were never about visibility, which is what this record claimed and is now measured rather than argued.
  • Signed-Releases and SAST are the two rows that do NOT depend on visibility. They read the release page and the workflow files, and both answer the same way public or private. Every conclusion in this record about those two is therefore load-bearing; every conclusion about the other four is provisional until the score is re-taken while public.

That distinction is the reason this record is worth having rather than waiting: two rows can be settled now, and four cannot be settled by anything in this tree.

Signed-Releases: nothing ships unsigned, and the check does not count artefacts

The reason text is 1 out of the last 5 releases have a total of 1 signed artifacts, and it is crediting one of thirty. v0.4.1 carries 73 assets, 30 of them .sigstore.json bundles. Every binary tarball, every OCI bundle, both SBOM formats, the attribution document and image-digests.txt has one.

Two hypotheses had to be killed before that could be called a measurement artifact, because the alternative - a signature a third party cannot pair to its artefact - would make every bundle decorative.

Hypothesis one: the check does not recognise the .sigstore.json convention. Refuted at the source. ossf/scorecard's probes/releasesAreSigned/impl.go, line 43:

var signatureExtensions = []string{".asc", ".minisig", ".sig", ".sign", ".sigstore", ".sigstore.json"}

.sigstore.json is on the list. It recognises exactly what this repository writes.

Hypothesis two: it recognises them and the pairing is wrong. Refuted twice. First, the probe never pairs at all - it does not read subject names. Lines 65-95 of that file walk the assets, and on the first extension match:

signed = true
break

So the per-release count is capped at one by construction. releaseLookBack = 5, and the evaluation sets releaseMap[release] = 8 for a signed release and 10 for one with provenance, then score = int(math.Floor(float64(score) / float64(totalReleases))). One signed release out of five is floor(8/5) = 1, which is the score observed, exactly. A repository that signed every artefact of all five releases would report 5 signed artifacts and score 8.

Second, and this is the result worth more than anything about the score: a consumer can pair and verify them. Measured on 2026-09-08 against the published v0.4.1 asset, not asserted:

$ cosign verify-blob --bundle sutura-attribution.md.sigstore.json \
    --certificate-oidc-issuer https://token.actions.githubusercontent.com \
    --certificate-identity https://github.com/telekom/sutura/.github/workflows/release.yml@refs/tags/v0.4.1 \
    sutura-attribution.md
Verified OK

Exit 0, from the downloaded bytes and the bundle beside them, with the identity guessed from the convention rather than looked up. The signatures are usable by someone who has never seen this repository.

What the row will do on its own, and the one thing that would move it further

The five-release window is v0.4.1, v0.2.4, v0.2.3, v0.2.2 and v0.2.1, and every one but the first predates signing. That is history, and it ages out. Nothing needs to be done for this row to reach 8.

The last two points need releasesHaveProvenance, whose extension list is one entry: .intoto.jsonl. This repository does produce SLSA provenance - actions/attest-build-provenance over every subject - but it lands in GitHub's attestation API, not on the release page, so the probe cannot see it. Publishing the provenance bundle as a release asset is a real improvement and not a badge move, for the same reason docs/adr/0024 gives for signing blobs at all: gh attestation verify needs the forge and an authenticated client, so provenance was not verifiable from mirrored bytes the way the signatures were.

DONE, and this paragraph used to say it was not. It was deferred here because it is a change to the release path that no gate in this tree can exercise, and shipping an unverifiable change to release signing to gain two points is the trade this record exists to refuse. #463 carried the open questions and #505 answered them rather than assuming: .github/actions/attest-and-sign makes FIVE attest-build-provenance calls and reads bundle-path from each, and cargo xtask collect-provenance validates the five records and the exact unique subject/digest set against the frozen release inputs before writing them as dist/sutura-provenance.intoto.jsonl - the one extension the probe reads. release.yml's Publish step refuses to create the draft unless that asset is non-empty. So a mirror can now check WHERE the bytes came from, not only what they are.

The limits that remain, because the deferral's reasoning did not stop being true. Signed-Releases reaches 10 only once v0.2.x ages out of the five-release window. And no gate in this tree runs a tagged release: the export is held by collect-provenance's own tests, not by an observed publish, so the first real tag is still where the wiring is seen end to end.

Thirteen unsigned checksums: signed, not dropped

Of the 43 non-signature assets on v0.4.1, 13 had no bundle, and every one was a .sha256 sidecar. The action excluded them deliberately, and its argument was correct: a bundle over foo.tar.gz already commits to that file's digest, so signing a file whose entire content is that digest proves nothing new.

Reversed, on ergonomics rather than cryptography. The absence of a signature is read by a person, and on a page holding 30 bundles the 13 files without one are indistinguishable from an oversight. Worse, it is a trap with a direction: someone who verifies foo.tar.gz.sha256 and stops has checked integrity against a file an attacker able to replace the tarball could replace too, while believing they checked provenance. The signature they needed was one file over.

So .github/actions/attest-and-sign/action.yml now signs every file that reaches the release page. Signed rather than dropped, and the case for dropping deserves stating because it is not weak - removing the sidecars shrinks the release surface and removes the trap outright. It loses to two things: the sidecars are a published interface a consumer's script may already read, and every published byte is signed is a rule a reader can check, where every published byte except the ones we judged uninteresting is not. That second sentence is the argument the same file already makes for including sidecars in provenance; the two rules are now consistent. The cost is 13 more certificates and transparency-log entries per release, against 30 already minted.

SAST: add CodeQL for Rust, and it cannot run yet

The zero is honest about a tool and dishonest about a property. Verbatim: "SAST tool is not run on all commits -- score normalized to 0. 0 commits out of 30 are checked with a SAST tool." What Scorecard looks for is a fixed list, read from checks/raw/sast.go on 2026-09-08: uses: steps matching ^github/codeql-action/analyze$, ^snyk/actions/.*, ^facebook/pysa-action$, ^JetBrains/qodana-action$, ^hadolint/hadolint-action$, or the check runs github-advanced-security, github-code-scanning, lgtm-com, sonarcloud, sonarqubecloud. None is configured here, so the zero is a true statement about that list.

It is not a true statement about static analysis. Inside the one required ci context this repository runs clippy at --workspace --all-targets --all-features -- -D warnings, with the whole restriction and nursery categories on, unsafe_code = "forbid", and unwrap_used / panic / indexing_slicing denied; zizmor and actionlint over every workflow; shellcheck over every script; cargo-deny against RustSec; and the 34 hygiene gates just hygiene runs, of 56 registered cargo xtask tasks.

And there is still a gap, which is why the recommendation is to add one rather than to accept the zero. Not one of those mechanisms tracks a value from an untrusted source to a sink. That matters here specifically: this codebase compiles SQL from caller input - -semantic turns a query into a plan, -sql turns a plan into a statement per dialect - and SECURITY.md already names the failure class as one it treats as a defect: injection where text was interpolated instead of bound, and an unquoted identifier. Today that class is held by newtypes and review, not by a tool. Interprocedural taint analysis to an injection sink is exactly what CodeQL does and exactly what no lint does, and CodeQL's Rust support has been generally available since October 2025. That is a real check that can fail on a real risk in this tree, which is the bar #459 set.

Why it is not in this change

It was blocked when this record was written, and the block has since lifted. At 6.0 the API answered advanced_security.status: "disabled" with private: true, so a SARIF upload needed GitHub Advanced Security and a CodeQL workflow would have failed on every run - a red job nobody can fix being worse than a recorded zero. The repository is now public, advanced_security is no longer reported at all (the field is absent for public repositories, where code scanning is free), and the obstacle is gone.

So this is a deferral whose reason is spent, not a refusal: #464 is the change, it is now actionable, and the only reason it is not in this commit is that adding a scanner is its own reviewable change rather than a rider on a record. The runner-label restriction that breaks Scorecard's own publication does not apply - that is api.scorecard.dev verifying the producing workflow, #451/#456's territory, and unrelated to whether CodeQL may run on a self-hosted runner.

And when it is added, deliberately not for the maximum score

Scorecard's evaluation weights the check codeQlWeight = 7 against sastWeight = 3, the second being the per-commit ratio. So a present, enabled CodeQL workflow carries 7 of the 10 points whatever the ratio is, and the remaining 3 are bought by analysing every pull request.

Take the 7. Run CodeQL on pushes to main and on a weekly cron - not per pull request. A buildless Rust analysis of this workspace is minutes, not seconds, and the runners are self-hosted, so the cost is not billed minutes but contention on the same runners ci needs; adding it to every pull request spends that on every push to buy three points. This is the point in the record where the badge-maximising choice is refused on purpose.

Two limits, stated because an overstated control is the defect. It would be a report, not a gate - the only required context is ci, and code-scanning alerts block nothing unless somebody makes them a required check. And CodeQL is a second authority on findings outside the nix pin, the same cost docs/adr/0024 accepts for the Scorecard action itself.

What holds this record

cargo xtask check-workflows gained the rule in xtask/src/workflows/sast.rs, and its subject is this record's own claim rather than the score. It refuses three things: flake.nix dropping -D warnings from cargoClippyExtraArgs; a tree where nothing CI reaches builds checks.<system>.clippy; and a workflow adding a Scorecard-recognised SAST tool while this file still says none is configured. The last direction is deliberate - adding CodeQL is a good change that makes this section wrong the moment it lands, so the gate refuses the combination and names the file to edit. Without it, the stand-in argument above was held by recall.

CII-Best-Practices: one repository setting and about ten sentences

Not a code problem and not this record's to close. Registration plus a self-certification questionnaire is the whole mechanism, exactly the shape the REUSE registration turned out to be in docs/adr/0024's amendment.

The project now exists and the row has already moved. bestpractices.dev project 14542 answers with the correct repo_url and one match, at badge_level: "in_progress", and Scorecard now reports CII-Best-Practices = 2, "badge detected: InProgress". That is not a rounding artefact: checks/evaluation/cii_best_practices.go maps the levels to fixed scores - inProgressScore = 2, passingScore = 5, silverScore = 7, gold 10. So finishing the questionnaire is worth three more points, not ten, and reaching gold is a different project again.

And the live form is not the criteria set measured below. Project 14542 carries the newer OSPS set (AC/BR/DO/GV/LE/QA), not the ~67-item passing list, and 174 of its 196 criteria are unanswered on bestpractices.dev today. The per-criterion breakdown is visible only to the registering (owner) account, so it is not re-verified here. The sweep in the next section was made against that older list, so treat it as an evidence inventory rather than a percentage: it says which properties this repository can prove and where the proof lives, and most OSPS items are answerable straight from it. It does not predict the badge percentage, and the 28% showing today is mostly unanswered, not unmet.

Measured against the 67 passing-level criteria on 2026-09-08: 54 MET with citable evidence, 5 met but undocumented, 4 not met, 4 N/A. Per category:

Category MET undocumented NOT MET N/A
Basics 11 1 1 0
Change Control 7 0 1 1
Reporting 4 3 1 0
Quality 13 0 0 0
Security 15 0 0 1
Analysis 4 1 1 2

The two categories that usually cost a project weeks are the two that are already complete. Quality is 13/13 on the strength of 2738 #[test] plus 122 #[tokio::test] across 276 files, a coverage-derived CRAP gate, and clippy at restriction + nursery under -D warnings. Security is 15/15 and one N/A on signed releases with SLSA provenance and CycloneDX SBOMs, unsafe_code = "forbid", rustls with no TLS below 1.2, a signing-algorithm parser that refuses symmetric and none, weekly cargo-deny, and gitleaks on every commit.

Three of the four genuinely unmet were one thing, and that thing has happened. repo_public failed because the repository was private, and report_archive and discussion failed because of that alone - Issues and Discussions were already enabled and well used, simply not publicly readable. The flip converted all three. Secret scanning is the one part it did not carry: secret_scanning still reads disabled, so that remains a setting somebody has to turn on.

The fourth is dynamic_analysis, which is SUGGESTED rather than MUST - it can be answered Unmet, tracked as #146 without losing the badge, and #146 is where fuzzing belongs.

The five undocumented items are prose, and two are the same three lines: interact and report_process both want README.md to link the issue tracker, CONTRIBUTING.md and SECURITY.md. report_responses and enhancement_responses want one honest sentence that no external report has arrived yet.

The verdict the owner asked for: an afternoon of form-filling, and the organisational decision it was waiting on is already made. Not ten minutes - 174 of 196 OSPS criteria are unanswered, and even at a minute each with the evidence already in hand that is a sitting, not a coffee break. But it is answering, not building: the sweep above found 54 of 67 old-list criteria provable from files that already exist, Quality and Security complete, and the three MUST failures that needed a disclosure review have been converted by the flip.

What it is worth, stated so nobody over-invests: finishing the questionnaire to passing moves Scorecard's row from 2 to 5 - three points of one check - and the aggregate by a fraction of that. Do it because the answers are a useful public inventory of what this repository can prove, not for the number.

Two things worth fixing whether or not the badge is pursued, found by the same sweep: SECURITY.md is absent from mkdocs.yml's nav, so the vulnerability-reporting process is undiscoverable on the published site, and cliff.toml has no security commit group, so release_notes_vulns has no mechanism the first time it applies.

Code-Review = 0: unreachable, not neglected

Verbatim: "Found 0/30 approved changesets -- score normalized to 0."

This is arithmetic on a recorded decision, and no configuration change reaches it. The main ruleset sets required_approving_review_count: 0 - read from gh api repos/telekom/sutura/rules/branches/main on 2026-09-08, which needs no admin:org. It is 0 because review capacity is one person plus agent sessions that all authenticate as the same account, and GitHub will not let an account approve its own pull request. So no changeset in this repository can carry an approval, and Scorecard divides by the changesets it found.

It will read 0 until a second identity exists. That is a decision about who reviews, not a setting, and it is deliberately not proposed here. What is recorded is that the zero is a consequence and not an omission - because an unexplained zero beside a published score reads as neglect, and this one is a capacity constraint somebody already reasoned about.

The same ruleset read shows what is held: deletion and non_fast_forward both refused, dismiss_stale_reviews_on_push: true, required_linear_history, a squashing merge queue, and ci as the one required status context.

Branch-Protection = 3: admin enforcement would break the merge queue

The informational lines confirm deletion and force-push are disabled and stale-review dismissal is on. The warning is "'branch protection settings apply to administrators' is disabled on branch 'main'".

Enabling it is not free here, and the reason is in the ruleset rather than in an opinion. The main ruleset carries a merge_queue with min_entries_to_merge: 3 and grouping_strategy: ALLGREEN, and the repository's own operating notes record that a required merge queue nobody can bypass removes the override path entirely. Bypass is per-ruleset, not per-rule: the same actor list that lets a maintainer land a stacked pull request out of the queue is what admin enforcement withdraws. So the trade is three points of a published score against the ability to recover the default branch when the queue itself is what is broken - and with one maintainer, the queue jamming and the only person who can unjam it being bound by it is not a hypothetical.

This record does not decide it, because it is an availability trade the owner owns. It states it so the gap is a stated one. What removes the trade is the same second identity Code-Review needs.

The two that are not findings

CI-Tests = -1 is an error, not a low score. Verbatim: internal error: Client.Repositories.ListStatuses: GET .../commits/8c8c19b8.../statuses: 403 Resource not accessible. The commit-statuses API refused because the repository was private. Nothing was broken, and re-measuring settled it: the row is now 10/10, "30 out of 30 merged PRs checked by a CI test". A negative score is Scorecard's own signal for this check did not run, and reading it as a finding is the mistake this row invites.

Maintained = 0 is a young-project warning. Verbatim: "project was created within the last 90 days. Please review its contents carefully." The repository was created 2026-08-24. It merged 59 pull requests in three days. It resolves with the calendar and nothing else.

Neither is actionable, and both are written down here for one reason: a reader of the score who finds four rows explained and two silent will assume the silent two are the bad news.

Fuzzing = 0

Real, and #146 already holds it - no parser that reads untrusted input is fuzzed, and panic = abort makes a panic process death. Not duplicated here.

What this record does not do

It does not raise the aggregate, and it would be a worse record if it did. Three of the six low rows were a failed measurement, a young repository and a capacity constraint; the fourth is a questionnaire; and the two with substance were a release page that was already correct and a taint analysis that was blocked. The one change that ships signs 13 files that were already covered by a signature one file over, which buys nothing on the score at all - the .sha256 sidecars are invisible to a probe that breaks on its first match - and closes a trap a person could walk into.

And the re-measurement is the argument for having written it down rather than acted. Between 6.0 and 6.1 two rows moved and neither was touched by a change: CI-Tests went from -1 to 10 because the repository became public, and CII-Best-Practices from 0 to 2 because somebody registered a project. Both were predicted here as not defects. Had they been treated as defects, the work would have been spent on rows that fixed themselves.

Amendment (2026-09-08, #464): what the CodeQL-for-Rust recommendation ran into

The recommendation above says a CodeQL Rust taint check "can fail on a real risk in this tree", and #464 is the change, previously blocked on the visibility flip. The repository is public now, so the SARIF upload can work - and the change ran head-first into a second blocker, this one inside the tool. It is recorded here because it changes the recommendation's standing: the specific taint flow #464 exists to hold is not reported by the buildless Rust database a CodeQL job here would build, so a job added today would be green while reporting nothing on exactly the defect it was for. That is the "runs and analyses nothing" shape this repository refuses, so the workflow is deliberately not added yet.

The measurement. CodeQL CLI 2.26.4 with codeql/rust-all 0.2.20 and codeql/rust-queries 0.1.41 - the bundle codeql-bundle-v2.26.4, which is what the action resolves to as of this date - was run over a buildless Rust database (--build-mode=none, the mode a job here would use) built from a probe of the exact shape this tree compiles SQL in: a caller-supplied value is interpolated into a statement and executed by rusqlite::Connection::execute - interpolated instead of bound, the failure class SECURITY.md names. The full default Rust query suite found no rust/sql-injection finding, and rust/summary/query-sinks and rust/summary/taint-sources confirm both endpoints are modelled in that database.

The cause, isolated by re-measurement rather than assumed. Six variants, taint traced from a caller-supplied value to the first argument of execute:

Variant Reaches the sink
value passed straight to execute yes
value bound to a local, then passed yes
through format! into a local, then passed no
format! inline at the call no
through + string concat no
through .to_string() no

.to_string() failing rules out anything specific to string formatting: what the four failing variants share is that the executed value is an owned String produced by a standard-library function. The shipped library models exactly that step - codeql/rust/frameworks/stdlib carries alloc::fmt::format as Argument[0] to ReturnValue, taint, manual - and the row is loaded. It is inert: the database contains no function whose canonical path is alloc::fmt::format, and none beginning alloc:: at all, because a buildless database extracts the crate's own dependencies but not alloc/std. The summary has no callable to attach to, so the step silently does not exist. Every cargo dependency, rusqlite included, is extracted - which is why the sink is recognised and the direct variant flows.

The first reading of this was wrong, and the correction is the part worth keeping. It was first recorded here as a missing edge in the query library - no step from a format_args! node to the String it produces, in a cached predicate no pack could extend. Both halves are false at this version: the step is modelled, and summaryModel is extensible, so a pack can supply models. The failure mode is worse than a missing query: a taint summary that names a callable the database does not contain produces no finding and no error. A SAST job can therefore be green because its models were inert, and nothing in the run says so - the same "looks like coverage" shape this record refuses, one layer further down.

What this means for the recommendation. The recommendation to add CodeQL for Rust stands, and the next step is now an experiment rather than a wait: build the database with a real build mode, or with dependency-and-standard-library extraction, and re-run the probe. If the flow is then reported, a shippable job exists today and the only cost is build time in CI; if it still is not, the gap is in the library after all and the release to watch is the one whose notes mention Rust taint through owned-String construction. Until one of those is measured, adding the job would trade a recorded zero for a green run that looks like coverage and isn't, which this record already treats as the worse error.

What this does not say. It does not say CodeQL Rust cannot track this class - only that it did not, in a buildless database at the version named, for the reason isolated above. The variant table was measured with a caller-supplied parameter as the taint source, which is not itself a modelled remote source; it isolates propagation, not whether rust/sql-injection fires end-to-end. And nothing here is held by a gate: xtask's SAST rule refuses a Scorecard-recognised scanner while this record still accepts the zero, so the absence of the workflow is enforced - the reason for the absence is only written down.

Second amendment (2026-09-09, #464): the experiment the first one asked for, run

The amendment above ends by naming two next steps and asking for one of them to be measured. Both have now been settled, and the answer does not change the decision: the workflow is still not added. What changes is that the deferral is no longer waiting on an experiment - the experiment is done, and one of the two branches it named never existed.

Which branch died, and why. The first was build the database with a real build mode. There is no such mode: rust/codeql-extractor.yml declares build_modes: with none as the entire list, so no change to a workflow, a runner or a toolchain can buy one. That branch was never available and should not be offered to the next reader. The second was dependency-and-standard-library extraction, and that one was real, testable and has now been tested.

The measurement. CodeQL CLI 2.27.0 with codeql/rust-all 0.2.21 and codeql/rust-queries 0.1.42 - bundle codeql-bundle-v2.27.0, read from the action's own src/defaults.json rather than assumed - over a buildless database built with --extractor-option=extract_dependencies_as_source=true and a toolchain carrying rust-src, so the sysroot source was present and discoverable. Two things are different from the earlier run and both make this probe strictly stronger. The sink is <tokio_postgres::client::Client>::prepare, the call this tree actually makes - the earlier table was measured against rusqlite::Connection::execute, a sink in a crate no adapter here uses. And the taint source is an axum route handler, which is a modelled remote source, so this measures whether rust/sql-injection fires end to end rather than only whether a value propagates.

The positive control fired, which is the only thing that separates a working analysis from an inert one in a tree that should have no findings. Shapes, all against prepare:

Shape Sink recognised Reported by rust/sql-injection
value passed straight to prepare yes yes
bound to a local, then passed yes yes
through format! yes no
through + concatenation yes no
through .to_string() yes no
through .as_str() into query yes no
format! into a struct field, read back through an accessor yes no

The last row is the one that matters, because it is the shape this repository compiles SQL in.

The earlier diagnosis was right in consequence and wrong in mechanism, and the correction is the part worth keeping. It recorded that a buildless database "extracts the crate's own dependencies but not alloc/std". The sysroot is in fact extracted: 3016 sysroot files and 98903 functions from them are in the database. Only 2497 of those functions carry a canonical path, and no alloc::, core:: or std:: canonical path exists at all - against 25257 of 148253 functions overall. The extractor says why, over those files: semantic analyzer unavailable (failed to determine rust edition). So the standard library is not absent, it is unresolved, and a taint summary naming alloc::fmt::format still has nothing to bind to. Turning dependency-as-source extraction on does not reach it, because the failure is not about which files are read.

A third instance of the same cause, found by accident and worth the warning. A sink whose receiver sits behind Arc is not recognised as a sink at all - method resolution needs the same missing core::/alloc:: callables. Three fixture cases were invalidated this way and were discarded rather than counted as negatives. This tree is not exposed to it today, because the adapter holds client: tokio_postgres::Client as a direct field, but a future refactor putting a client behind a smart pointer would silently remove the sink rather than the finding.

What this tree would actually get, which is the cost argument. Nothing today, and not because the analysis is broken: sutura-sql renders SQL from a typed AST, values travel as $n bind parameters, and the only format! reaching statement text interpolates two newtypes. A correctly working scanner reports zero here. So the job's whole value would be catching a future regression - and the regression class it would need to catch is precisely the one measured above as not caught. A scanner that finds nothing today because it is working and one that finds nothing because its models are inert are indistinguishable from the outside, which is why the control, not the finding count, is the thing to look at.

Cost, measured rather than estimated. On one contended developer host: database creation 659-770 s for a ten-function fixture, producing a 692 MiB database; suite analysis 111-189 s at --threads=4. Roughly a quarter of an hour for a fixture, against ci's 20-37 minutes - enough to say that a per-pull-request job would not be free, and not enough to price a job over this whole workspace, which was not measured.

What stays true. No secret is involved in any of this: code scanning on a public repository needs only security-events: write, so nothing here bears on docs/adr/0026. The absence of the workflow remains held by xtask's SAST rule rather than by this text - that rule refuses a Scorecard-recognised scanner while this record accepts the zero, and it is what would make adding CodeQL fail until this record is rewritten.

What this does not say. It does not say CodeQL Rust cannot track this class, and it does not say a later release will not. It is one fixture, one host, one version. Not measured: any other sink family end to end, the analysis cost over this workspace rather than a fixture, and whether a first-party model pack supplying the missing summaries against the paths the database does contain would close the gap - that is the one untried lever left, and it is a larger piece of work than a workflow. The release to watch is one whose notes mention Rust taint through owned-String construction, or canonical paths for sysroot crates in a buildless database.