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 is integrated with Luciq-SDK before it ships, across the surfaces the SDK protects: Screenshots, Session Replay, and Network logs.

On Android the linter ships as a native Android Lint check (a Kotlin lint.jar). You add it as a single lintChecks(...) dependency — no Python required. From that one dependency you get:

  • Live squiggles in Android Studio as you type on unmasked PII fields.

  • A build/CI gate: ./gradlew :app:lint (and check) fail on a masking gap because the hard-fail issues default to ERROR and Lint's abortOnError is on by default.

  • The full engine — compliance dialing, custom luciq.yml keywords, the GDPR/HIPAA input floor, and whole-project posture checks — identical to the cross-platform CLI.


Requirements

Requirement
Detail

Android Gradle Plugin / Lint

Built against Lint API 32.0.1 (AGP 9.x; convention: lint version = AGP version + 23.0.0).

JDK

17 (the Lint 32.x line targets JDK 17).

Luciq SDK

≥ 14.2.0 recommended — network auto-masking is on by default from this version. Older versions are flagged.


Install

1. Add JitPack to settings.gradle.kts:

dependencyResolutionManagement {
    repositories {
        google()
        mavenCentral()
        maven { url = uri("https://jitpack.io") }   // ← add this
    }
}

2. Add the check to app/build.gradle.kts:


How it runs

The check operates in two complementary passes so findings never double-report:

  1. Per-field, live (as-you-type). A UAST file handler scans the in-memory editor buffer for the file you are editing, so Android Studio shows the squiggle immediately — no save or full build required. This pass emits unmasked-field findings.

  2. Project posture, whole-project (batch). Auto-mask configuration, network masking, FLAG_SECURE, consent gating, and SDK version depend on the whole project, so they run once per project during ./gradlew lint.

Findings are reported with a [check-id] prefix in the message (for example [unmasked-field] card field is not masked …), surfaced under one of the four Lint issue ids below.


The four issue ids

Use these ids in lint { } / lint.xml to tune the check.

Issue id
Default
What it covers

LuciqUnmaskedPii

error (blocks)

A PII field (input or label) that is not masked and not waived.

LuciqMaskingConfig

error (blocks)

Project posture: missing auto-mask or MASK_NOTHING, network masking disabled, FLAG_SECURE overridden, missing consent gate, or an old SDK.

LuciqUnmaskedPiiAdvisory

warning

A field finding that does not block at the current compliance level.

LuciqMaskingConfigAdvisory

warning

A non-blocking posture advisory.

Severity → issue mapping inside the engine:

  • hard-fail field → LuciqUnmaskedPii (ERROR)

  • soft-warn field → LuciqUnmaskedPiiAdvisory (WARNING)

  • hard-fail posture → LuciqMaskingConfig (ERROR)

  • soft-warn posture → LuciqMaskingConfigAdvisory (WARNING)

All four issues are in the SECURITY category, vendor "Luciq".


Tuning — Lint settings

Out of the box the check blocks: ./gradlew :app:lint (and CI) fail on a masking gap. You tune everything in app/build.gradle.kts, inside android { lint { … } }.

Most common: show findings as warnings only (squiggles, but never fail the build).

You still get the editor squiggles and report entries, but the build stays green.

Other knobs in the same lint { } block:

Want to…
Add this

Turn a check off completely

disable += "LuciqUnmaskedPii"

Keep errors but don't fail the build (project-wide)

abortOnError = false

Block only new findings

baseline = file("lint-baseline.xml")

Write SARIF for GitHub Code Scanning

sarifReport = true

abortOnError is Lint's own default-on switch: an ERROR finding fails lint / check / CI unless you disable it. Per-issue severity (warning +=, disable +=) and lint.xml also work as with any Lint check.


What it checks

Project posture (LuciqMaskingConfig / …Advisory)

Check id
Layer
Fires when

auto-mask-config

Visual

No auto-mask configured (setAutoMaskScreenshotsTypes absent), or it is set to MASK_NOTHING, or — under HIPAA — MEDIA is missing from the configured types.

network-disabled

Network

Network auto-masking is explicitly turned off via Luciq.setNetworkAutoMaskingState(Feature.State.DISABLED).

network-sdk-version

Network

The detected Luciq SDK version is < 14.2.0 (hard fail); or the version could not be determined (warning).

flag-secure

Defense

ignoreFlagSecure(true) is present — it overrides FLAG_SECURE, so secure windows get captured.

consent-gate

Defense

Under GDPR/HIPAA: SessionReplay.enabled = true is not guarded by a consent check.

Per-field coverage (LuciqUnmaskedPii / …Advisory)

For each candidate field the engine 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 / EditText 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 that appears 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. Both this Lint check and the CLI read it. The check looks for luciq.yml (or luciq.yaml) in the nearest ancestor directory of the module.

On Android, compliance comes only from luciq.yml. The CLI's --compliance flag and LUCIQ_COMPLIANCE env var are command-line concepts and do not apply to the Lint check.

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 TEXT_INPUTS 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 EditText / 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; 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

flag-secure

fail

fail

fail

fail

fail

consent-gate

fail

fail


Exclusions

Skip paths with an exclude: list under pii_masking in luciq.yml. A pattern matches a whole relative path (app/generated/*) or any single path segment (Tests skips every directory named Tests). The check also always skips build, Pods, node_modules, .git, DerivedData, and Carthage.


CI

On Android, CI needs no Python — just run Lint:

For GitHub Code Scanning, enable sarifReport = true in the lint { } block and upload the generated SARIF.

./gradlew :app:lint fails the build on a blocking gap the same way the CLI's --mode enforce does — unless you set abortOnError = false.

Last updated