For the complete documentation index, see llms.txt. This page is also available as Markdown.

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

Requirement
Detail

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-linter

This 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

Flag
Default
What it does

--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

Format
Use for
Shape

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

Check id
Layer
Fires when

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)

For each candidate field the linter asks, in order:

  1. 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 / SecureField is flagged only when its name matches a family, never merely for being an input. (Exception: the GDPR/HIPAA input floor, below.)

  2. Is it masked? Covered by auto-mask of its type, an inline private marker, or a reference in addPrivateViews(...) / setPrivateView(...).

  3. Is it waived? An inline // luciq-mask-ignore comment.

  4. 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-ignore on the field's line (or the line just above it) — waives that one field.

  • // luciq-mask-ignore-file anywhere 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 is pci.


Configure — luciq.yml

Put 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.

Level
Blocks on

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.

Family
Covers
Example name matches
Hard-fail under

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

fail blocks under --mode enforce; warn only reports; means the field is not a candidate at that level.

Family
none
soc2
pci
gdpr
hipaa

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)

Check
none
soc2
pci
gdpr
hipaa

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 enforce and at least one blocking gap.


message-exclamation

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