Full-Blog PDPA Audit — Scanning 202 Posts for PII Leaks
I’ve been running bb pdpa-scan on every new post since I
integrated the pdpa-sg-clj toolkit
into my build pipeline. But I’d never run it across the entire
backlog — all 202 posts, every markdown file, 239,352 words
spanning kernel debugging, APU architecture, BSV protocols, and AI agent
design.
Today I did.
The audit scope
| Metric | Value |
|---|
.md files |Total words | 239,352 |
Total lines | 42,798 |
Total size | 1,664,889 bytes (~1.66 MB) |
Avg post size | 8,242 bytes (~8 KB) |
Scan duration | < 2 seconds |
Final verdict | ✅ PASS — zero critical or high findings |
Methodology: What the scanner checks
The scan pipeline runs four layers of detection, each with its own data source and confidence model:
Layer 1: NRIC structural pattern
\b[STFGM]\d{7}[A-Z]\b
This matches the Singapore NRIC/FIN format: one of S,
T, F, G, M (the
prefix encodes century and citizenship), followed by 7 digits and a
checksum letter. But structural patterns generate false positives — hex
strings, git hashes, BSV transaction IDs all match this shape.
Findings: 15 structural matches across 4 files. All in PDPA documentation posts.
Layer 2: Phone numbers and API keys
# Singapore mobile numbers
\+65\s?[89]\d{7}
# API key patterns
sk_live_[A-Za-z0-9]{16,} # Stripe live keys
ghp_[A-Za-z0-9]{36} # GitHub personal access tokens
AKIA[0-9A-Z]{16} # AWS access keys
-----BEGIN.*PRIVATE KEY # PEM private key blocks
Findings: Zero +65 phone matches. 8 API
key pattern matches — all in documentation posts explaining the
scanner’s regex rules or discussing security practices. No actual leaked
keys.
Layer 3: Email addresses
[A-Za-z0-9._%+\-]+@[A-Za-z0-9.\-]+\.[A-Za-z]{2,}
Filtered against a domain allowlist — @nurazhar.com,
@example.com, @google.com,
@github.com, @cloudflare.com,
@amd.com, @acer.com, and
@realtek.com are excluded as known-safe documentation
domains.
Findings: 5 matches outside the allowlist —
[email protected], [email protected],
[email protected], [email protected],
[email protected]. All are intentional educational examples
in PDPA and Cloudflare Access posts.
Layer 4: Mod-11 checksum validation
This is the differentiating layer. The scanner doesn’t just regex — it runs the Singapore ICA Modulo-11 algorithm on every structural NRIC match:
This is what separates a critical finding (valid
NRIC — blocks the pipeline) from a medium finding
(structural pattern that fails checksum — allowed through). A hex string
like deadbeefF matches the regex but fails the checksum, so
it’s ignored.
The findings breakdown
By severity
| Severity | Count | Source files | Nature |
|---|---|---|---|
| Critical | 0 | — | No valid NRICs leaked |
| High | 0 | — | No phones, no API keys leaked |
| Medium | 0 | — | All structural NRIC matches failed checksum (educational) or were in excluded directories |
| Low | 0 | — | All emails are intentional examples |
The 4 files with NRIC matches
Every single NRIC match in the 202-post corpus is deliberately placed in PDPA documentation posts — test vectors used to explain the Mod-11 algorithm, the scanner’s validation logic, and the pre-commit hook behavior. None are accidental leaks.
The API key “matches”
| File | Match | Actual content |
|---|---|---|
pdpa-sg-clj-architecture.md |
-----BEGIN.*PRIVATE KEY |
Regex rule in source code listing showing how the scanner detects PEM blocks |
pdpa-sg-clj-architecture.md |
sk_live_ |
Table row:
"Stripe live key | high | Regex sk_live_[A-Za-z0-9]{16,}" |
pdpa-sg-compliance-blog-pipeline.md |
sk_live_, ghp_ |
Severity classification table — documenting detection rules |
ai-ecommerce-store-zero-to-one.md |
ghp_ |
Security advice: “sweep the staging tree for AIza...,
sk-..., ghp_... patterns” |
All are meta-references — the scanner’s own rule definitions documented in blog posts about the scanner. Not actual secrets.
The email “matches”
| File | Address | Context |
|---|---|---|
cloudflare-access-free-oauth.md |
[email protected] |
Cloudflare Access policy example |
cloudflare-access-free-oauth.md |
[email protected] |
Cloudflare Access policy example |
cloudflare-access-free-oauth.md |
[email protected] |
OAuth policy tutorial |
pdpa-sg-clj-launches-today... |
[email protected] |
Scanner example: “User S0100000J contacted us at…” |
pdpa-sg-clj/README.md |
[email protected] |
DPO config example in README |
All are documentation examples using placeholder domains or generic names — not real people’s contact information.
What this audit proved
1. The scanner is precise
The Mod-11 checksum validation eliminated every false
positive from structural NRIC patterns. Without checksum
validation, a naive regex scanner would have flagged every hex string,
git hash, and BSV transaction ID that happened to match
S\d{7}[A-Z]. That would be dozens of false alarms — enough
to train a human to ignore the scanner entirely.
With checksum validation, it zeroed in on only the deliberately
placed educational NRICs — and correctly avoided flagging them as
critical because they were in the pdpa-sg-clj/
exclusion.
2. The blog is structurally clean
No +65 phone numbers anywhere. No committed
.env files. No real API keys. No accidental email exposure.
The pre-commit hook and the build-time scan are both doing their jobs —
but the data also suggests the blog was already clean before these tools
existed.
3. Educational content needs an escape hatch
The four PDPA posts contain deliberate test NRICs. They exist to teach — to show readers what a valid NRIC looks like, how the checksum works, and what the scanner catches. But they trigger the scanner just like a real leak would.
The solution is not to remove the educational
content. It’s to use git commit --no-verify when pushing
PDPA documentation posts — a deliberate, conscious override that says “I
know this contains NRIC patterns, and I’ve verified they are
educational.”
Lessons from scanning a 200-post corpus
Lesson 1: Structural patterns without validation are noise
A regex-only NRIC scanner on this blog would have produced 15+ false
positives. Every post with a hex hash, every BSV transaction ID, every
git commit reference that happened to match
[STFGM]\d{7}[A-Z] — all false alarms. The Mod-11 checksum
is not optional; it’s what makes the scanner usable.
Lesson 2: Educational content is different from leaks
Posts about PDPA contain NRICs by definition. Posts about security contain API key patterns. Posts about Cloudflare Access contain example email addresses. A scanner that can’t distinguish these from real leaks is a scanner nobody will use.
The right model: block by default, override with
intention. The --no-verify flag is a conscious act
— you’re telling the system “yes, I know this is here, and it belongs
here.”
Lesson 3: Zero is a valid result
When I first ran bb pdpa-scan on the full corpus and saw
critical=0 high=0 medium=0 low=0, my first instinct was: is
the scanner broken? Did I miss something?
But the data checks out. Every match is documented, every pattern is explained, every educational NRIC is accounted for. The blog is clean — not because the scanner is lenient, but because the content was already written with care.
Lesson 4: Audit transparency is security
Publishing this audit — showing every match, explaining every
finding, documenting the false positives and the intentional placements
— is itself a security practice. It says: “Here’s what I checked, here’s
what I found, here’s the methodology.” You can verify it yourself by
cloning the repo and running bb pdpa-scan.
Commands to reproduce
# Clone the blog
git clone [email protected]:nurazhar/homepage.git
cd homepage
# Run the full scan (same as what I ran)
bb pdpa-scan
# Raw ripgrep for NRIC patterns (no checksum, expect false positives)
rg --no-heading -n '\b[STFGM]\d{7}[A-Z]\b' -g '*.md' -g '!pdpa-sg-clj/' -g '!builds/'
# Raw ripgrep for phone numbers
rg --no-heading -n '\+65\s?[89]\d{7}' -g '*.md'
# Raw ripgrep for API keys
rg --no-heading -n 'sk_live_|ghp_|AKIA[0-9A-Z]{16}|-----BEGIN.*PRIVATE KEY' -g '*.md' -g '!pdpa-sg-clj/'
# Count everything
ls *.md | wc -l
wc -w *.md | tail -1
The verdict
| Category | Status |
|---|
Phone leaks | ✅ Clean — zero matches |
API key leaks | ✅ Clean — patterns only in documentation |
Email exposure | ✅ Clean — all examples/placeholders |
.env files | ✅ Clean — none committed
|Pre-commit hook | ✅ Active — blocks real PII |
Build-time scan | ✅ Active —
bb pdpa-scan
runs on publish |202 posts. 239,352 words. Zero PII leaks.
The PDPA pipeline works — not just on new posts, but retroactively across the entire history of this blog. That’s the difference between “we scan what we publish today” and “we’ve verified everything that’s ever been published.”