Taint-shape scanner: finds functions where a value reaches a sink (SQL, shell, file write, ...) without evidence that the security boundary the sink implies (tenant, auth, privilege, filesystem, process) was checked at the sink itself, as opposed to only upstream. Emits deidentified JSON (file/function/line/category, no source or SQL text) so findings can be handed to a fix pass without shipping code off-box. Candidates for manual review, per Gate 1.5, same discipline as confinement-scan/stub-scan.
| Tier | tools |
| Role | unclassified (baselined) |
| Path | crates/tools/boundary-scan |
| Edition | 2021 |
| Targets | tools-boundary-scan, tools_boundary_scan |
| Public items | 42 across 4 modules |
| Tests | 24 |
What it is for
Taint-shape scanner.
Models a request's path as Source -> Transformation* -> SecurityBoundary* -> Sink and looks for functions where a Sink is reached without evidence that the SecurityBoundary a caller-controlled value implies was checked AT THE SINK, as opposed to only somewhere upstream in the same function. That gap — a boundary asserted once, then not re-checked where the dangerous operation actually happens — is what let list_relationships_for_party_for_tenant (crates/identity/parties/src/repo.rs) read across tenants until it was fixed this session: party_belongs_to_tenant checked the ANCHOR party in code, but the paginated SQL itself never mentioned tenant_id, so the OTHER endpoint of the relationship was never constrained. Now fixed (a parties join on the other endpoint); see the_relationships_list_gap_stays_fixed below.
Only one concrete rule is implemented: Sink::Sql + SecurityBoundary::Tenant, triggered when a function's signature declares a tenant_id-shaped parameter and its body calls sqlx::query*. The rest of the taxonomy below is modeled as data (so findings from a future rule slot into the same shape) but has no detector yet — do not read a variant's presence in the enum as a claim that this scanner checks it.
# Deidentified output
A Finding carries file/function/line and taxonomy tags — never source text or SQL text. The scanner is meant to hand findings to a fix pass (human or model) that re-reads the named function from the file itself; nothing about the code needs to leave the box in the finding.
Candidate list for manual review, like tools-confinement-scan/tools-stub-scan — not a hard gate.
Capabilities
Finding
Taint-shape scanner.
| Item |
|---|
pub struct Finding |
Finding :: fn to_envelope(& self) -> report::Envelope |
fn scan_file(path : & Path, rel : & str) -> Result <Vec <Finding>> |
fn scan(root : & Path) -> Result <Vec <Finding>> |
FnSpan
Taint-shape scanner.
| Item |
|---|
pub struct FnSpan |
fn find_functions(chars : & char, code : & str) -> Vec <FnSpan> |
Kind
Taint-shape scanner.
| Item |
|---|
pub enum Kind |
fn classify(src : & str) -> Vec <Kind> |
fn code_only(chars : & char, kinds : & Kind) -> String |
SecurityBoundary
Taint-shape scanner.
| Item |
|---|
pub enum SecurityBoundary |
Sink
Taint-shape scanner.
| Item |
|---|
pub enum Sink |
Source
Taint-shape scanner.
| Item |
|---|
pub enum Source |
Status
Taint-shape scanner.
| Item |
|---|
pub enum Status |
Transformation
Taint-shape scanner.
| Item |
|---|
pub enum Transformation |
pattern (other)
The learning-loop half of tools-boundary-scan: capturing why a repair was
| Item |
|---|
pub const CANONICAL_BOUNDARIES: & & str |
pub const CANONICAL_SINKS: & & str |
AnalysisLevel
The learning-loop half of tools-boundary-scan: capturing why a repair was
| Item |
|---|
pub enum AnalysisLevel |
Instance
The learning-loop half of tools-boundary-scan: capturing why a repair was
| Item |
|---|
pub struct Instance |
Pattern
The learning-loop half of tools-boundary-scan: capturing why a repair was
| Item |
|---|
pub struct Pattern |
PatternRegistry
The learning-loop half of tools-boundary-scan: capturing why a repair was
| Item |
|---|
fn lint_unknown_vocabulary(registry : & PatternRegistry) -> Vec <String> |
pub struct PatternRegistry |
PatternRegistry :: fn load(path : & std::path::Path) -> anyhow::Result <Self> |
PatternRegistry :: fn save(& self, path : & std::path::Path) -> anyhow::Result <()> |
RepairRationale
The learning-loop half of tools-boundary-scan: capturing why a repair was
| Item |
|---|
pub struct RepairRationale |
ScannerRuleStatus
The learning-loop half of tools-boundary-scan: capturing why a repair was
| Item |
|---|
pub enum ScannerRuleStatus |
pseudonym (other)
Deterministic identifier pseudonymization, scoped per finding.
| Item |
|---|
fn code_mask(kinds : & crate::Kind) -> Vec <bool> |
PseudoMap
Deterministic identifier pseudonymization, scoped per finding.
| Item |
|---|
pub struct PseudoMap |
PseudoMap :: fn new(finding_id : & str) -> Self |
PseudoMap :: fn load_or_new(cache_dir : & Path, finding_id : & str) -> Result <Self> |
PseudoMap :: fn save(& self, cache_dir : & Path) -> Result <()> |
PseudoMap :: fn assign(& mut self, real : & str, slot : Slot) -> String |
PseudoMap :: fn real_of(& self, pseudonym_or_real : & str) -> String |
fn pseudonymize_slice(raw : & str, code_mask : & bool, param_names : & String, fn_real_name : & str, map : & mut PseudoMap,) -> String |
Slot
Deterministic identifier pseudonymization, scoped per finding.
| Item |
|---|
pub enum Slot |
repair_log (other)
Durable, append-only log of the shard-repair lifecycle — shared by every
| Item |
|---|
fn default_log_path(workspace_root : & Path) -> std::path::PathBuf |
fn now_unix_seconds() -> u64 |
Event
Durable, append-only log of the shard-repair lifecycle — shared by every
| Item |
|---|
pub enum Event |
LogEntry
Durable, append-only log of the shard-repair lifecycle — shared by every
| Item |
|---|
pub struct LogEntry |
fn append(log_path : & Path, entry : & LogEntry) -> Result <()> |
fn read_all(log_path : & Path) -> Result <Vec <LogEntry>> |
report (other)
One reporting shape shared by every scanner in this family
| Item |
|---|
fn finding_id(prefix : & str, file : & str, function : & str, line : usize) -> String |
Envelope
One reporting shape shared by every scanner in this family
| Item |
|---|
pub struct Envelope |
How to use it
No examples/ target and no doctest in this crate's rustdoc. The tests listed under Verification are the closest executable usage.
Module structure
tools_boundary_scan
patternpseudonymrepair_logreport
flowchart TD n_tools_boundary_scan["tools_boundary_scan"] n_tools_boundary_scan --> n_pattern["pattern"] n_tools_boundary_scan --> n_pseudonym["pseudonym"] n_tools_boundary_scan --> n_repair_log["repair_log"] n_tools_boundary_scan --> n_report["report"]
Public surface
`crate root`
| Item | What it is |
|---|---|
pub enum Source | — |
pub enum Transformation | — |
pub enum SecurityBoundary | — |
pub enum Sink | — |
pub enum Status | Where the boundary evidence was found relative to the sink, or that it wasn't found at all. |
pub struct Finding | — |
Finding :: fn to_envelope(& self) -> report::Envelope | The standardized cross-tool reporting shape — see report module. |
pub enum Kind | — |
fn classify(src : & str) -> Vec <Kind> | Classify every char in src as code, comment, or string content |
fn code_only(chars : & char, kinds : & Kind) -> String | A code-only view (comments AND strings blanked to spaces) for brace matching and identifier search — same length as src.chars() |
pub struct FnSpan | — |
fn find_functions(chars : & char, code : & str) -> Vec <FnSpan> | — |
fn scan_file(path : & Path, rel : & str) -> Result <Vec <Finding>> | — |
fn scan(root : & Path) -> Result <Vec <Finding>> | — |
`pattern`
| Item | What it is |
|---|---|
pub struct RepairRationale | What the model determined, for one finding, after producing a fix |
pub enum AnalysisLevel | How much program understanding a detector needs — cheapest first |
pub enum ScannerRuleStatus | — |
pub const CANONICAL_BOUNDARIES: & & str | Canonical boundary/sink values in current use, for the optional lint below — NOT a hard enum |
pub const CANONICAL_SINKS: & & str | — |
fn lint_unknown_vocabulary(registry : & PatternRegistry) -> Vec <String> | Best-effort, informational only -- returns unrecognized boundary/sink strings found in the registry, never a hard failure |
pub struct Instance | One detector's manifestation of a Pattern -- boundary/sink are free strings (see CANONICAL_BOUNDARIES/CANONICAL_SINKS), deliberately NOT crate::SecurityBoundary/crate::Sink (those stay detector-internal to tools-boundary-scan; the registry must not force every future detector to share tools-boundary-scan's own enum). |
pub struct Pattern | Gate 1.5 decision (2026-08-11, Q1): GUC-name-drift and the advisory-lock check are NOT instances of PAT-001 -- different failure mechanism (activation/coordination failure, not "missing at the sink") and different evidence/remediation |
pub struct PatternRegistry | — |
PatternRegistry :: fn load(path : & std::path::Path) -> anyhow::Result <Self> | — |
PatternRegistry :: fn save(& self, path : & std::path::Path) -> anyhow::Result <()> | — |
`pseudonym`
| Item | What it is |
|---|---|
pub enum Slot | — |
pub struct PseudoMap | — |
PseudoMap :: fn new(finding_id : & str) -> Self | — |
PseudoMap :: fn load_or_new(cache_dir : & Path, finding_id : & str) -> Result <Self> | — |
PseudoMap :: fn save(& self, cache_dir : & Path) -> Result <()> | — |
PseudoMap :: fn assign(& mut self, real : & str, slot : Slot) -> String | Get-or-assign the pseudonym for real in slot |
PseudoMap :: fn real_of(& self, pseudonym_or_real : & str) -> String | Resolve a pseudonym back to its real name, if known to this map. |
fn pseudonymize_slice(raw : & str, code_mask : & bool, param_names : & String, fn_real_name : & str, map : & mut PseudoMap,) -> String | Rename identifiers in raw (a code slice, chars, code_mask same length as raw.chars(), code_maski true where i is real code, not string/comment content) using map, seeding parameter names from param_names as Slot::Arg before the general pass. |
fn code_mask(kinds : & crate::Kind) -> Vec <bool> | Code-region mask for a chars, kinds slice — true where kindsi is Kind::Code (as opposed to string or comment content). |
`repair_log`
| Item | What it is |
|---|---|
pub enum Event | — |
pub struct LogEntry | — |
fn default_log_path(workspace_root : & Path) -> std::path::PathBuf | — |
fn append(log_path : & Path, entry : & LogEntry) -> Result <()> | — |
fn now_unix_seconds() -> u64 | — |
fn read_all(log_path : & Path) -> Result <Vec <LogEntry>> | — |
`report`
| Item | What it is |
|---|---|
fn finding_id(prefix : & str, file : & str, function : & str, line : usize) -> String | Deterministic, content-addressed finding id: same (file, function, line) always produces the same id, independent of scan order or which index a site happened to be at in a particular run |
pub struct Envelope | — |
No pub use re-exports: every item above is declared in this crate.
Boundary
Depends on no other workspace tier.
Shares tier tools with 84 other crates: tools-advisory-reach, tools-archive-guard, tools-artifact-scaffold, tools-ask-ai-core, tools-ask-ais, tools-ask-gemini, tools-book, tools-book-report, … (84 total).
_What this crate deliberately does NOT own is a judgment. No committed registry records one for it, so none is stated here._
Where it sits
| Tier (ontology) | tools |
| Architectural role (taxonomy) | unclassified (baselined) |
| Location | crates/tools/boundary-scan |
| Vocabulary in force (lexicon) | current |
Dependencies
Runtime, from outside the workspace.
| Crate | Requirement | Features | Optional | Only on |
|---|---|---|---|---|
anyhow | ^1 | — | no | always |
serde | ^1 | derive | no | always |
serde_json | ^1 | — | no | always |
Development. None.
Build. None.
Depended on by. 1 workspace crate.
Signal flow — what reaches this crate, and what it reaches.
flowchart LR n_tools_advisory_reach["tools-advisory-reach"] -->|uses| SELF SELF["tools-boundary-scan"] classDef self fill:#1f883d,stroke:#1f883d,color:#fff; class SELF self;
Feature flags
No Cargo features are defined: every capability is unconditional, so no consumer can receive a half-wired crate.
Targets
| Kind | Name | Source |
|---|---|---|
| bin | tools-boundary-scan | `src/main.rs` |
| lib | tools_boundary_scan | `src/lib.rs` |
Error model
No public error type was detected: no public item declares a type named *Error, and no public signature returns one.
Operational characteristics
| Property | Evidence |
|---|---|
| async public surface | none detected |
| async runtime | none detected |
| database access | none detected |
| network I/O | none detected |
| unsafe code | none detected |
| environment variables | yes |
No unsafe block, unsafe fn, unsafe impl or unsafe trait was found by the parser anywhere in this crate's source.
Configuration
| Variable | Read in |
|---|---|
CARGO_MANIFEST_DIR | src/lib.rs |
Related capabilities
1 workspace crate depends on this one: tools-advisory-reach.
Verification
| Kind | Count |
|---|---|
| Unit tests | 24 |
| Integration tests | 0 |
| Examples | 0 |
| Doctests | 0 |
Evidence by module. How often each public module is named by something executable.
| Module | Tests | Examples | Consumers |
|---|---|---|---|
crate root | 13 | 0 | 4 |
pattern | 9 | 0 | 0 |
pseudonym | 4 | 0 | 4 |
repair_log | 6 | 0 | 5 |
report | 2 | 0 | 2 |
What the tests establish, by name:
a_non_sql_string_literal_does_not_unshield_a_disqualified_finding—src/lib.rsabsent_boundary_is_flagged—src/lib.rsbypassable_sibling_is_flagged—src/lib.rsguc_binding_call_is_not_flagged—src/lib.rsid_equals_lookup_on_a_non_tenants_table_is_still_flagged—src/lib.rsinterpolated_const_without_tenant_id_is_still_flagged—src/lib.rsmissing_filter_query_is_flagged_even_alongside_an_unrelated_guc_bind—src/lib.rspatterns_json_matches_the_registry_schema—src/lib.rspub_crate_sibling_is_not_flagged_as_bypassable—src/lib.rsregistry_table_self_lookup_is_not_flagged—src/lib.rssink_enforced_is_not_flagged—src/lib.rstenant_id_in_an_interpolated_const_is_sink_enforced—src/lib.rsthe_relationships_list_gap_stays_fixed—src/lib.rsupstream_only_boundary_is_flagged—src/lib.rsbare_first_party_call_is_renamed_but_method_calls_are_not—src/pseudonym.rsbuiltins_and_keywords_are_never_renamed—src/pseudonym.rsdifferent_names_get_increasing_pseudonyms—src/pseudonym.rsround_trips_through_disk—src/pseudonym.rssame_real_name_gets_same_pseudonym_every_time—src/pseudonym.rsappend_then_read_round_trips—src/repair_log.rsreading_a_missing_log_is_empty_not_an_error—src/repair_log.rsdifferent_locations_get_different_ids—src/report.rsprefix_distinguishes_tools_sharing_the_same_location_shape—src/report.rssame_location_always_gets_the_same_id—src/report.rs
Documentation coverage
| Measure | Documented | Total |
|---|---|---|
| Public items with rustdoc | 15 | 42 |
Public modules with a //! block | 4 | 4 |
pie showData
title Public items with rustdoc
"Documented" : 15
"No rustdoc detected" : 27
Metrics
| Metric | Value |
|---|---|
| Rust source files | 6 |
| Source lines | 1927 |
| Code lines | 1442 |
| Public API items | 42 |
| Public modules | 4 |
| Tests | 24 |
| Examples | 0 |
| Cargo features | 0 |
| Direct runtime dependencies | 3 |
| Workspace reverse dependencies | 1 |
pie showData
title Public API by kind
"constant" : 2
"enum" : 10
"function" : 13
"method" : 8
"struct" : 9
pie showData
title Rust source composition
"Code" : 1442
"Blank or comment" : 485
Generation
Rendered by tools-corpus corpus readme from repository evidence alone, renderer schema 2, lexicon current. No model, network service or database was consulted. Regenerate with tools-corpus corpus readme --write; verify with --check.