Luciq PII Masking Linter
A release gate that statically catches unmasked PII — card numbers, SSNs, emails, passwords, and more — in your app that integrated with Luciq-SDK before it ships, across the surfaces the SDK protects: Screenshots, Session Replay, and Network logs.
On iOS the linter is a command-line tool written in Python (stdlib only, zero third-party dependencies). It scans your Swift (.swift) source and can run:
Inline in Xcode as warnings/errors via a Run Script build phase.
In CI as a gate that fails the job on a masking gap.
From your terminal for an ad-hoc check.
Requirements
Python
3.8+
Dependencies
None (standard library only).
Luciq SDK
≥ 14.2.0 recommended — network auto-masking is on by default from this version. Older versions are flagged.
Install
Use pipx so the command lands in ~/.local/bin — a stable location the Xcode build can find:
pipx install luciq-masking-linterThis exposes the luciq-masking-linter command. Plain pip install luciq-masking-linter also works (useful in CI).
Inline in Xcode
To see findings inline in Xcode, add a Run Script build phase. Xcode build phases run with a minimal PATH, so prepend ~/.local/bin before calling the command — otherwise the build reports the tool as "not installed" even when it is:
With --format xcode, findings appear as native Xcode warnings (or errors) on the exact source line.
Usage
path is the project root to scan (defaults to the current directory). A typical run:
Flags
--mode
warn
warn reports and exits 0; enforce exits 1 on a blocking gap (the gate).
--compliance
from luciq.yml
none / soc2 / pci / gdpr / hipaa — overrides the file and the LUCIQ_COMPLIANCE env var fallback.
--format
text
text, xcode (inline in Xcode), github (PR annotations), sarif (Code Scanning).
--exclude
—
A path glob or directory name to skip; repeatable. Merged with exclude: in luciq.yml.
Compliance resolution order: --compliance flag → LUCIQ_COMPLIANCE env var → luciq.yml → default none.
Output formats
text (default)
Local terminal runs
A human-readable report with a summary and verdict.
xcode
Xcode Run Script phase
`file:line:col: warning
github
GitHub Actions PR annotations
::warning file=…,line=…::[check] message workflow commands.
sarif
GitHub Code Scanning / security dashboards
SARIF 2.1.0 JSON; each check id becomes a SARIF rule id.
In IDE/annotation formats a finding shows as an error only when it would actually block (a fail under --mode enforce); otherwise it is a warning.
A text run looks like:
What it checks
Project posture
auto-mask-config
Visual
No auto-mask configured (setAutoMaskScreenshots / autoMaskScreenshotOptions absent), or set to MASK_NOTHING / maskNothing, or — under HIPAA — MEDIA is missing from the configured types.
network-disabled
Network
Network auto-masking is explicitly turned off via NetworkLogger.autoMaskingEnabled = false (or IBGNetworkLogger.autoMaskingEnabled = false).
network-sdk-version
Network
The detected Luciq SDK version is < 14.2.0 (hard fail); or it could not be determined (warning).
consent-gate
Defense
Under GDPR/HIPAA: SessionReplay.enabled = true is not guarded by a consent check.
Per-field coverage (unmasked-field)
unmasked-field)For each candidate field the linter asks, in order:
Is it a PII candidate? A text input or display label whose name matches a built-in or custom keyword family. Candidacy is name-driven by default — a
TextField/SecureFieldis flagged only when its name matches a family, never merely for being an input. (Exception: the GDPR/HIPAA input floor, below.)Is it masked? Covered by auto-mask of its type, an inline private marker, or a reference in
addPrivateViews(...)/setPrivateView(...).Is it waived? An inline
// luciq-mask-ignorecomment.If none of the above → finding.
Candidate detection runs on the comment-stripped source, so a field or widget name appearing only in a comment is never flagged, and a commented-out marker or config call never counts as real, active masking.
How to fix a finding (mask the field)
Mask the flagged field with any of these:
Or clear a whole class of fields in bulk by configuring auto-mask at init:
A clean setup (passes the gate) looks like:
Skip a finding (waivers)
If a flagged field isn't really PII, skip it with a comment:
// luciq-mask-ignoreon the field's line (or the line just above it) — waives that one field.// luciq-mask-ignore-fileanywhere in a file — waives every candidate in that file.Add a reason for the audit trail:
// luciq-mask-ignore: masked upstream. The reason is surfaced in the finding.
The legacy aliases // luciq-ignore and // luciq-ignore-file still work.
Card fields can't be waived under
pci. A card-family waiver (line- or file-level) is refused and reported as a hard failure when compliance ispci.
Configure — luciq.yml
luciq.ymlPut one optional file at your project root. It holds the only two things the linter cannot read from your code: the compliance level and custom keyword families. The CLI and the Android Lint check both read it. If PyYAML is installed it is used; otherwise a built-in minimal YAML reader handles the subset this file needs.
compliance may also come from --compliance or the LUCIQ_COMPLIANCE env var, both of which override the file.
Compliance levels — what blocks vs. warns
The level does not add new checks; it turns dials on the existing ones.
none / soc2
card, SSN, credentials
pci
same, and card waivers are refused
gdpr
+ email, phone, name, address, DOB, every text input (input floor); consent required
hipaa
all of the above + health; MEDIA auto-mask + consent required
The GDPR/HIPAA input floor. Under gdpr and hipaa, masking all user input is mandatory, so the synthetic input family kicks in: every text input is a candidate regardless of name. PII inputs are normally cleared in bulk via .textInputs auto-mask.
Keyword families
Candidacy is name-driven against these built-in families. Each family's default severity is promoted to a hard fail by the levels in the last column.
card
PAN, CVV, bank account, IBAN
cardNumber, pan, cvv, iban, accountNumber
all levels (no waiver under pci)
ssn
National / tax / social-security ids
ssn, nationalId, taxId
all levels
credentials
Passwords, PINs, OTPs, secrets, tokens
password, pin, otp, apiKey, accessToken
all levels
health
PHI — diagnosis, MRN, medication
diagnosis, mrn, medication, patientId
hipaa
dob
Date of birth
dob, birthDate, dateOfBirth
gdpr, hipaa
email
Email addresses
email, userEmail
gdpr, hipaa
phone
Phone / mobile numbers
phone, msisdn, mobileNumber
gdpr, hipaa
address
Postal / home address
homeAddress, street, postalCode
gdpr, hipaa
name
Person names
firstName, lastName, patientName
gdpr, hipaa
input
Synthetic: any unnamed text input
every SecureField / TextField
gdpr, hipaa
Custom keywords. Add app-specific stems under the closest built-in family so compliance severity applies. Matching is case- and separator-insensitive: a stem matches camelCase, snake_case, kebab-case, and spaced spellings alike — fiscalCode catches userFiscalCode, fiscal_code, fiscal-code, and fiscalCodeNumber. Keep custom keywords as stems, never frozen exact names.
Custom families are always
warn— even under HIPAA. Severity is promoted only for families a preset lists by name. To make an app-specific keyword block, nest its stem under a built-in family (e.g.ssn) rather than inventing a new family name.
Full unmasked-field severity matrix
unmasked-field severity matrixfail blocks under --mode enforce; warn only reports; — means the field is not a candidate at that level.
card
fail
fail
fail
fail
fail
ssn
fail
fail
fail
fail
fail
credentials
fail
fail
fail
fail
fail
health
warn
warn
warn
warn
fail
dob
warn
warn
warn
fail
fail
email
warn
warn
warn
fail
fail
phone
warn
warn
warn
fail
fail
address
warn
warn
warn
fail
fail
name
warn
warn
warn
fail
fail
input (unnamed input)
—
—
—
fail
fail
custom family
warn
warn
warn
warn
warn
Posture severity matrix (non-field checks)
auto-mask-config (absent / MASK_NOTHING)
fail
fail
fail
fail
fail
auto-mask-config (MEDIA required)
—
—
—
—
fail
card waiver refused
—
—
fail
—
—
network-disabled
fail
fail
fail
fail
fail
network-sdk-version (< 14.2.0)
fail
fail
fail
fail
fail
network-sdk-version (unknown)
warn
warn
warn
warn
warn
consent-gate
—
—
—
fail
fail
Exclusions
Skip paths with repeatable --exclude <glob> or an exclude: list under pii_masking in luciq.yml. A pattern matches a whole relative path (Generated/*) or any single path segment (Tests skips every directory named Tests). The tool also always skips build, Pods, node_modules, .git, DerivedData, and Carthage.
CI
Install the tool, then run it where you want a gate. GitHub Actions:
Any CI works the same: install, then luciq-masking-linter . --mode enforce. For inline PR annotations add --format github; for GitHub Code Scanning add --format sarif and upload the SARIF output.
Exit codes
0— clean, or running in--mode warn.1—--mode enforceand at least one blocking gap.
No static gate catches everything. It will not catch PII held in a generically-named field (a value property holding an SSN), whether a custom network key is masked server-side, or whether a region actually renders black at runtime. The last is irreducibly human. This gate raises the floor; it does not replace the pre-production visual check.
Last updated