pii

package
v0.5.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 11, 2026 License: Apache-2.0 Imports: 18 Imported by: 0

Documentation

Overview

Package pii decides which result cells must be masked: column rules kept in .locksql/pii.toml, rules proposed from the schema, value detectors (email, phone, IBAN, card, national ids) and the masking itself.

Index

Constants

View Source
const (
	// ModeRedact replaces the whole value: "<redacted>" (the default).
	ModeRedact = "redact"
	// ModePartial keeps the first character and the length: "j***(12)".
	ModePartial = "partial"
	// ModeEmail keeps the first character and the domain of an address:
	// "j***@example.com" (other values are masked as partial).
	ModeEmail = "email"
)

Mask modes.

View Source
const DetectLimit = 64 << 10

DetectLimit is how much of a text cell the detectors scan. A longer cell is cut there, and ends with "…", before detection: nothing past the scanned part can reach the output. The renderer cuts cells much earlier (max_cell_chars), so the cut is normally invisible; it bounds the time a huge value can cost. The engines already cut a cell at engine.MaxCellBytes, which is no more.

View Source
const QuasiLimit = "masking blocks range filters, LIKE and ORDER BY on this column"

QuasiLimit is what masking a quasi-identifier costs, shown when the scan proposes one.

View Source
const Redacted = "<redacted>"

Redacted is the cell of a column masked in mode redact.

View Source
const RulesFile = ".locksql/pii.toml"

RulesFile is the rules file, relative to the project root.

Variables

This section is empty.

Functions

func AliasViolation

func AliasViolation(st sqlclass.Statement, r Rules, d sqlclass.Dialect) error

AliasViolation is the fallback for result columns without an origin: it refuses a READ select in which a rule-matched column (matched by name, see Rules.MatchesName) is renamed or used inside an expression anywhere in a select list, since its values would then reach a label that no rule matches. Renaming covers "AS x", an implicit alias, a later UNION arm whose first arm labels the position differently, CTE column lists over a body that mentions a matched column or selects * from a source that may hold one, a * in a later arm over such a source, and table alias column lists ("t(x, y)"), which can rename any column. A PostgreSQL whole-row reference to a table that may hold such a column ("SELECT u", "row_to_json(u)", "to_jsonb(users.*)") is refused too, see wholeRow. A matched column or whole-row reference inside a VALUES list or the arguments of a FROM-clause function, and a TABLE arm after the first, are refused as well. Every named relation counts as a source that may hold a matched column, since it may be a view over a rule table. COUNT(...) is allowed. The error is a *sqlclass.Refusal that explains how to write the query.

A statement that is not a plain READ select is checked the same way when it may return rows: a RETURNING list counts as a select list, and a statement led by SELECT, WITH, VALUES or TABLE that the classifier made WRITE (a data-modifying CTE) is checked whole. A write without RETURNING is only checked by writtenValues and columnLists. Other statements (SHOW, DESCRIBE, PRAGMA, DDL) pass. The console runs the write analysis of package sqlast instead of the select-list checks when sqlast.NeedsWriteAnalysis says so (see WriteValuesViolation).

On PostgreSQL, "rel.f" may be the call f(rel) on the whole row of rel when rel has no column f (attribute notation: b.row_to_json, t.name, big.record_out). Since a statement alone cannot tell such a call from a column, a qualified reference "rel.x" to a FROM-clause relation is refused wherever its value may reach the result (select lists, RETURNING, VALUES, FROM-clause function arguments), see attrCall. ResultAliasViolation accepts a plain qualified item of the outer select list whose result column carries an origin, which proves it is a base-table column.

A write, with or without RETURNING, must not copy a rule-matched column's values into another column: a rule-matched column, a whole row or a star over a source that may hold one is refused anywhere its value may be written (SET values, the select list or VALUES of an INSERT source), see writtenValues.

func CellText added in v0.1.2

func CellText(v any) string

CellText is the text of a cell value, as compared and substituted.

func Detects added in v0.1.2

func Detects(s string, ds []Detector) bool

Detects reports whether a detector would mask something in s.

func MaskOutputs

func MaskOutputs(res *engine.Result, outs []sqlast.Output, r Rules, ds []Detector, cell func(row, col int, v any) string, origin bool) error

MaskOutputs masks res in place following the analysed outputs of its statement: each column whose resolved sources are under a rule is masked in the output's mode. As a second line of defence a column whose engine- reported origin matches a rule is masked too. The other text and integer cells go through the detectors.

cell, when not nil, stores the value of a cell masked in mode redact and returns its reference name, or "" for none. It is called only for a plain column the analysis masks in mode redact, whose values cannot be literals of the statement and all of whose sources are under a mask rule (referenceable): anywhere else the agent may know the value behind the reference (a constant, an unmasked UNION arm, an aggregate) and could look up one row on it without the k-anonymity check. A column masked by its engine-reported origin alone gets plain <redacted> too.

The result must have exactly the analysed columns, and the columns the analysis expects by name (plain references, aliases, star expansions) must carry that label: otherwise masking by position could hit the wrong column, and the result is refused.

func MaskResult

func MaskResult(res *engine.Result, r Rules, ds []Detector, origin bool)

MaskResult masks res in place.

A column is masked whole when its origin matches a rule. Engines report an origin only when the catalog confirms it is a base-table column; a view, a derived table or a CTE alias never counts. A view column is therefore masked only by its own name: a view that renames a rule column (CREATE VIEW v AS SELECT firstname AS contact ...) needs a rule for that name (*.*.contact or db.v.contact). When origin is false (the session reports no origins), or a column has none (an expression, a UNION, an untrusted origin the engine blanked), the column label is matched by name instead, see Rules.MatchesName; the console must then also refuse renamed uses with AliasViolation (see NeedsAliasCheck). A column under a rule is masked in the rule's mode (Rules.Mode, Rules.ModeByName; several rules with different modes give redact), as MaskValue does: redact gives plain "<redacted>" with no reference, since a write keeps no result in the reference store. A binary cell under a partial or email rule becomes "<masked bytes:N>"; NULL stays NULL.

Every other text cell, and every integer (a national number or a card can be stored as one), goes through the detectors in place.

func MaskValue

func MaskValue(v any, mode string) any

MaskValue masks one non-NULL cell in mode.

func NeedsAliasCheck

func NeedsAliasCheck(res engine.Result, origin bool) bool

NeedsAliasCheck reports whether the statement behind res must pass AliasViolation (ResultAliasViolation, given res.Columns) before its rows may be shown: the session reports no origins, or some result column came back without one.

func PlanCheck

func PlanCheck(st sqlclass.Statement, r Rules, d sqlclass.Dialect, origin bool) error

PlanCheck is the PII check a statement must pass before it runs, unless it is an unmask run: StatsViolation, then AliasViolation for a statement that is not a plain READ select (it runs before the write, whose result may carry no origin) or when the session reports no origins. A plain READ select on an origin-reporting session is checked after it runs, only when some result column has no origin (see NeedsAliasCheck).

func Propose

func Propose(cols []engine.ColumnInfo) []string

Propose returns the "db.table.column" patterns for the columns that look like personal data from their name or type, sorted and de-duplicated. A name that would break the pattern syntax becomes '*'.

func ProposeQuasi added in v0.1.2

func ProposeQuasi(cols []engine.ColumnInfo) []string

ProposeQuasi returns the "db.table.column" patterns for the columns that look like quasi-identifiers and not like personal data, sorted and de-duplicated.

func RedactMessage

func RedactMessage(msg, sql string, ds []Detector) string

RedactMessage removes from a database error message the row values it may quote: a failed cast or function call in a WHERE clause echoes the value it choked on ("invalid input syntax for type integer: \"Smith\""), and that value reaches no result row, so no rule or detector masks it.

Everything from the first quote character to the last one is replaced, unless each quoted part is text of the statement itself (a syntax error, an unknown column) or a known server phrase, joined only by known server words: a value cannot shift the pairing of quotes, since its text would then sit in a gap. A single stray quote redacts the rest of the message. The detectors then run over what is left; an unquoted value they cannot recognise is not caught.

func RedactedRef added in v0.1.2

func RedactedRef(name string) string

RedactedRef is a redacted cell that carries a reference to its value, which the console alone keeps.

func ResultAliasViolation

func ResultAliasViolation(st sqlclass.Statement, r Rules, d sqlclass.Dialect, cols []engine.ResultColumn) error

ResultAliasViolation is AliasViolation once the statement has run, with the result columns it returned: a plain qualified PostgreSQL item of the outer select list ("SELECT b.id, ...") is accepted when its result column has an origin. With cols nil, every such item is refused.

func SaveRules

func SaveRules(root string, r Rules) error

SaveRules writes the rules to <root>/.locksql/pii.toml, sorted and de-duplicated, replacing the file atomically.

func SaveRulesFile added in v0.1.2

func SaveRulesFile(path string, r Rules) error

SaveRulesFile writes the rules to path, sorted and de-duplicated, replacing the file atomically. Missing directories are created.

func StatementTextRelation added in v0.1.2

func StatementTextRelation(d sqlclass.Dialect, rel string) bool

StatementTextRelation reports whether rel, a relation name as written ("name", "schema.name"), holds the text of past statements, and so the values the console substituted into them. On PostgreSQL these are pg_stat_statements and pg_stat_activity. On MySQL and MariaDB they are information_schema.PROCESSLIST, every performance_schema and sys relation (threads, events_statements_*, session, statement_analysis, x$...), and mysql.general_log and slow_log.

func StatsViolation

func StatsViolation(st sqlclass.Statement, r Rules, d sqlclass.Dialect) error

StatsViolation refuses, while mask rules exist, a statement that names a planner statistics relation (pg_stats, pg_statistic, mysql.column_stats, information_schema.COLUMN_STATISTICS, sqlite_stat4, ...): they hold real values of the columns, rule columns included, which no rule can match. It refuses a statement-text relation (see StatementTextRelation) the same way.

func ValidMode

func ValidMode(m string) bool

ValidMode reports whether m is a mask mode.

func WriteValuesViolation added in v0.5.0

func WriteValuesViolation(st sqlclass.Statement, r Rules, d sqlclass.Dialect) error

WriteValuesViolation is the check by column name of a write that the console also analyses in full (sqlast.NeedsWriteAnalysis): it refuses statistics and statement-text relations (StatsViolation), copies and planted values (writtenValues) and column lists that rename columns by position (columnLists). Its RETURNING list and the outer SELECT of its data-modifying CTEs are left to the analysis, which resolves and masks them like a read's outputs.

Types

type Detector

type Detector interface {
	Name() string
	// Mask returns s with every value the detector recognises replaced by
	// its masked form (first character + "***" + "(length)").
	Mask(s string) string
}

Detector masks personal values found inside a text, in place.

func Detectors

func Detectors(names []string) ([]Detector, error)

Detectors returns the named detectors, de-duplicated, in their fixed run order. Names: email, phone, iban, card, be_niss, fr_nir, nl_bsn, us_ssn.

type Rules

type Rules struct {
	Mask  []string
	Allow []string
	// Modes maps a Mask pattern to its mode when it is not ModeRedact.
	Modes map[string]string
}

Rules are the column rules. Each pattern is "db.table.column", where any segment may be '*'. For PostgreSQL the db segment is the schema. Matching ignores case. An Allow pattern beats every Mask pattern.

func LoadRules

func LoadRules(projectRoot string) (Rules, error)

LoadRules reads <projectRoot>/.locksql/pii.toml. A missing file gives empty rules and no error.

func LoadRulesFile added in v0.1.2

func LoadRulesFile(path string) (Rules, error)

LoadRulesFile reads the rules file at path. A missing file gives empty rules and no error.

func (*Rules) Add

func (r *Rules) Add(pattern string) error

Add adds a mask pattern after validating it. Adding an existing pattern is a no-op.

func (*Rules) AddAllow added in v0.1.2

func (r *Rules) AddAllow(pattern string) error

AddAllow adds an allow pattern after validating it. Adding an existing pattern is a no-op.

func (*Rules) AddMode

func (r *Rules) AddMode(pattern, mode string) error

AddMode adds a mask pattern with a mode ("" for the default). Adding an existing pattern sets its mode.

func (Rules) Covered

func (r Rules) Covered(db, table, column string) bool

Covered reports whether a mask or an allow pattern names the column: the human already decided about it.

func (Rules) Matches

func (r Rules) Matches(db, table, column string) bool

Matches reports whether the column db.table.column is masked: some Mask pattern matches it and no Allow pattern does.

func (Rules) MatchesName

func (r Rules) MatchesName(column string) bool

MatchesName reports whether a column known only by its name may be under a rule: some Mask pattern has that column segment (whatever its db and table) and no Allow pattern exempts the name everywhere ("*.*.name"). It is the conservative test used when a result column has no origin.

Servers resolve identifiers with their own collation (MySQL compares column names by collation weight, so "fírstname" and "fİrstname" name the column firstname), which no Go case folding reproduces. A name with a non-ASCII character therefore matches as soon as any Mask pattern exists, and a non-ASCII character of a pattern matches any character.

func (Rules) Mode

func (r Rules) Mode(db, table, column string) (string, bool)

Mode returns the mask mode of the column db.table.column and whether a rule masks it. Several matching rules with different modes give ModeRedact.

func (Rules) ModeByName

func (r Rules) ModeByName(column string) (string, bool)

ModeByName is Mode for a column known by its name only (MatchesName).

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL