@coo-quack/sensitive-canary 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,44 @@
1
1
  # Changelog
2
2
 
3
+ ## v0.7.0 (2026-08-04)
4
+
5
+ ### Features
6
+
7
+ - Add multi-region PII detection rules (25 PII rules, up from 7)
8
+ - National IDs with checksum validation: Japanese My Number, French NIR, Italian Codice Fiscale, German Steuer-IdNr., Spanish DNI/NIE, Korean RRN and BRN, Chinese Resident Identity Card
9
+ - Phone numbers for JP, US, FR, IT, DE, ES, KR, CN
10
+ - Postal codes for JP, US/EU/KR (5/9-digit), and CN (6-digit)
11
+ - Public IPv4 and IPv6 addresses (reserved ranges excluded)
12
+ - Add context gating for noisy rules
13
+ - Rules with `requireContext` only fire when a nearby context word (phone, ZIP, IP, etc.) is found within a small window around the match (default: 3 tokens ≈ 24 characters)
14
+ - Reduces false positives on bare digit sequences without sacrificing detection when labels are present
15
+ - Move all rule definitions to JSON (`src/lib/default-config.json`)
16
+ - Rules are now data, not code — the full set can be inspected and modified without editing TypeScript
17
+ - Checksum validators remain in code and are referenced by name from the config
18
+ - Add user-defined custom rules via config file
19
+ - Create `~/.config/sensitive-canary/config.json` or set `SENSITIVE_CANARY_CONFIG` to a custom path
20
+ - Add new rules, override built-in rules by id, and set a custom `contextWindow`
21
+ - Invalid rules are skipped with a warning; the rest of the config loads normally
22
+ - Expand secret detection coverage (39 secret rules, up from 24)
23
+ - AI services: Replicate, Hugging Face, Groq, OpenRouter, xAI (Grok), Perplexity
24
+ - Cloud / IaaS: DigitalOcean PAT, Supabase PAT
25
+ - Payment: Square access token
26
+ - SaaS / Dev tools: Mapbox, Sentry (user + org tokens), Atlassian, Linear, Postman
27
+
28
+ ### Fixes
29
+
30
+ - Fix My Number checksum: when the weighted-sum remainder is 0 or 1, the check digit is 0 (not invalid). Valid My Numbers ending in 0 were previously rejected.
31
+ - Correct spec source abbreviation: JIPTEC → J-LIS (地方公共団体情報システム機構)
32
+ - Harden `compileRule`: force `g` flag on regex, validate `regex` field, warn on unknown validator name
33
+ - Add strict schema validation for user-defined rules (required fields, optional field types, cross-field constraints)
34
+ - Pass `secretValue` (not full match) to validator so `secretGroup` + `validate` works in user rules
35
+
36
+ ### Dependencies
37
+
38
+ - Update pnpm to v11.19.0 and refresh the lockfile
39
+
40
+ ---
41
+
3
42
  ## v0.6.0 (2026-08-02)
4
43
 
5
44
  ### Features
package/README.md CHANGED
@@ -25,9 +25,10 @@ Claude Code is a powerful development tool, but file reads and command execution
25
25
  | `echo $API_KEY` with live key ❌ | Env var value scanned and blocked ✅ |
26
26
 
27
27
  - **Two hooks** — `UserPromptSubmit` and `PreToolUse` cover both directions of risk
28
- - **31 detection rules** — sourced from gitleaks and TruffleHog detector definitions
28
+ - **64 detection rules** — sourced from gitleaks and TruffleHog detector definitions
29
+ - **Checksum validation** — credit cards (Luhn) and national ID numbers (JP My Number, FR NIR, IT Codice Fiscale, DE Steuer-IdNr., ES DNI/NIE, KR RRN/BRN, CN Resident ID)
30
+ - **Context gating** — phone numbers, postal codes, and public IP addresses require a nearby label, reducing false positives on bare digit sequences
29
31
  - **Entropy filtering** — reduces false positives on low-entropy values
30
- - **Luhn validation** — credit card numbers are validated, not just pattern-matched
31
32
  - **Local only** — all scanning runs in your terminal; nothing is sent anywhere
32
33
 
33
34
  ---
@@ -254,9 +255,98 @@ This is a persistent filter, unlike allow tags which apply per prompt. The categ
254
255
 
255
256
  ---
256
257
 
258
+ ## Custom Rules
259
+
260
+ All detection rules are defined in `src/lib/default-config.json` as data, not code. You can add your own rules or override built-in ones by creating a config file.
261
+
262
+ ### Config file location
263
+
264
+ Create `~/.config/sensitive-canary/config.json`, or point to a custom path with the `SENSITIVE_CANARY_CONFIG` environment variable. Set it in the `env` block of your Claude Code `settings.json`:
265
+
266
+ ```json
267
+ {
268
+ "env": {
269
+ "SENSITIVE_CANARY_CONFIG": "/path/to/my-rules.json"
270
+ }
271
+ }
272
+ ```
273
+
274
+ or export it in your shell:
275
+
276
+ ```sh
277
+ export SENSITIVE_CANARY_CONFIG=/path/to/my-rules.json
278
+ ```
279
+
280
+ ### Adding a rule
281
+
282
+ Each rule is a JSON object with an `id`, `description`, `regex` (source string), and `category` (`"secret"` or `"pii"`):
283
+
284
+ ```json
285
+ {
286
+ "rules": [
287
+ {
288
+ "id": "custom-api-key",
289
+ "description": "My Service API Key",
290
+ "regex": "MYSVC-[A-Za-z0-9]{32}",
291
+ "category": "secret"
292
+ }
293
+ ]
294
+ }
295
+ ```
296
+
297
+ ### Overriding a built-in rule
298
+
299
+ A user rule with the same `id` as a built-in rule replaces it. For example, to tighten the email regex:
300
+
301
+ ```json
302
+ {
303
+ "rules": [
304
+ {
305
+ "id": "pii-email",
306
+ "description": "Internal Email",
307
+ "regex": "[A-Za-z0-9]+@internal\\.corp\\.(com|org)",
308
+ "category": "pii"
309
+ }
310
+ ]
311
+ }
312
+ ```
313
+
314
+ ### Context gating and validators
315
+
316
+ User rules support the same fields as built-in rules:
317
+
318
+ | Field | Type | Description |
319
+ |---|---|---|
320
+ | `requireContext` | boolean | Only fire when a nearby context word is found |
321
+ | `contextWords` | string[] | Words that satisfy the context requirement |
322
+ | `contextWindow` | number | Override the global context window (default: 3 tokens) |
323
+ | `entropyThreshold` | number | Skip matches below this Shannon entropy |
324
+ | `secretGroup` | number | Capture group index containing the secret (default: 0 = full match) |
325
+ | `validate` | string | Name of a built-in checksum validator (see below) |
326
+ | `flags` | string | Regex flags (default: `"g"`) |
327
+
328
+ Available validators (referenced by name in the `validate` field):
329
+
330
+ `luhn`, `mynumber-jp`, `nir-fr`, `codice-fiscale-it`, `steuer-id-de`, `dni-nie-es`, `rrn-kr`, `brn-kr`, `resident-id-cn`, `public-ipv4`, `public-ipv6`
331
+
332
+ ### Overriding the context window globally
333
+
334
+ Set `contextWindow` at the top level to change how many tokens of surrounding text are scanned for context words (default: 3):
335
+
336
+ ```json
337
+ {
338
+ "contextWindow": 5,
339
+ "rules": []
340
+ }
341
+ ```
342
+
343
+ Invalid rules (bad regex, wrong types, missing required fields) are skipped with a warning on stderr. The rest of the config still loads. Each rule is validated against a strict schema before compilation — `requireContext: true` without `contextWords` is also rejected, since empty `contextWords` would silently disable context gating and make the rule fire on every match.
344
+
345
+ ---
346
+
257
347
  ## Detection rules
258
348
 
259
- ### Secrets (24 rules)
349
+ ### Secrets (39 rules)
260
350
 
261
351
  | Rule ID | Description |
262
352
  |---|---|
@@ -280,25 +370,64 @@ This is a persistent filter, unlike allow tags which apply per prompt. The categ
280
370
  | `openai-key` | OpenAI API Key (legacy format) |
281
371
  | `openai-project-key` | OpenAI Project API Key (`sk-proj-` prefix) *(entropy ≥ 3.5)* |
282
372
  | `anthropic-key` | Anthropic API Key |
373
+ | `replicate-token` | Replicate API Token |
374
+ | `huggingface-token` | Hugging Face Access Token |
375
+ | `groq-key` | Groq API Key |
376
+ | `openrouter-key` | OpenRouter API Key |
377
+ | `xai-key` | xAI (Grok) API Key |
378
+ | `perplexity-key` | Perplexity API Key |
379
+ | `digitalocean-pat` | DigitalOcean Personal Access Token |
380
+ | `square-access-token` | Square Access Token |
381
+ | `mapbox-token` | Mapbox Token |
382
+ | `sentry-user-token` | Sentry User Auth Token |
383
+ | `sentry-org-token` | Sentry Organization Auth Token |
384
+ | `atlassian-token` | Atlassian API Token |
385
+ | `linear-key` | Linear API Key |
386
+ | `postman-key` | Postman API Key |
387
+ | `supabase-key` | Supabase Personal Access Token |
283
388
  | `jwt` | JSON Web Token (JWT) |
284
389
  | `generic-secret` | Generic API key / secret assignment *(entropy ≥ 3.5)* |
285
390
  | `env-assignment` | `.env`-style secret assignment *(entropy ≥ 3.0)* |
286
391
  | `connection-string` | Database connection string with embedded credentials |
287
392
 
288
- ### PII (7 rules)
393
+ ### PII (25 rules)
289
394
 
290
395
  | Rule ID | Description | Validation |
291
396
  |---|---|---|
292
397
  | `pii-email` | Email address | — |
293
398
  | `pii-credit-card` | Credit card number | Luhn check |
399
+ | `pii-ipv4` | IPv4 address (RFC 1918 private ranges only) | — |
294
400
  | `pii-ssn` | US Social Security Number | Invalid prefix exclusion |
401
+ | `pii-mynumber-jp` | Japanese Individual Number (My Number) | Checksum (weighted mod 11) |
402
+ | `pii-nir-fr` | French NIR / Social Security Number | Check key (mod 97) |
403
+ | `pii-codice-fiscale-it` | Italian Codice Fiscale | Control character (mod 26) |
404
+ | `pii-steuer-id-de` | German Steuer-Identifikationsnummer | MOD 11,10 |
405
+ | `pii-dni-nie-es` | Spanish DNI / NIE | Control letter (mod 23) |
295
406
  | `pii-phone-us` | US phone number | — |
296
407
  | `pii-phone-jp` | Japanese phone number | — |
408
+ | `pii-phone-fr` | French phone number | Context-gated |
409
+ | `pii-phone-it` | Italian phone number | Context-gated |
410
+ | `pii-phone-de` | German phone number | Context-gated |
411
+ | `pii-phone-es` | Spanish phone number | Context-gated |
297
412
  | `pii-postal-jp` | Japanese postal code (`〒` prefix required) | — |
298
- | `pii-ipv4` | IPv4 address (RFC 1918 private ranges only) | — |
413
+ | `pii-postal-code` | Postal code (US ZIP / EU / KR) | Context-gated |
414
+ | `pii-rrn-kr` | Korean Resident Registration Number | Checksum (weighted mod 11) |
415
+ | `pii-brn-kr` | Korean Business Registration Number | Checksum (NTS standard algorithm) |
416
+ | `pii-resident-id-cn` | Chinese Resident Identity Card | Check digit (GB 11643 MOD 11-2) |
417
+ | `pii-phone-kr` | Korean phone number | Context-gated |
418
+ | `pii-phone-cn` | Chinese phone number | Context-gated |
419
+ | `pii-postal-cn` | Chinese postal code (6-digit) | Context-gated |
420
+ | `pii-ipv4-public` | Public IPv4 address | Context-gated, reserved ranges excluded |
421
+ | `pii-ipv6` | IPv6 address | Context-gated, reserved ranges excluded |
299
422
 
300
423
  Detection patterns are based on rule definitions from [gitleaks](https://github.com/gitleaks/gitleaks) and [TruffleHog](https://github.com/trufflesecurity/trufflehog).
301
424
 
425
+ National ID checksum algorithms follow the official specs from each issuing authority: 地方公共団体情報システム機構 (J-LIS) for My Number, INSEE for NIR, Agenzia delle Entrate for Codice Fiscale, Bundeszentralamt für Steuern for Steuer-IdNr., the Ministerio del Interior for DNI/NIE, the Ministry of the Interior and Safety for the Korean RRN, GB 11643-1999 for the Chinese Resident Identity Card, and the NTS (Hometax) standard algorithm for the Korean BRN.
426
+
427
+ ### Context gating
428
+
429
+ Phone numbers (IT, DE, FR, ES, KR, CN), bare 5/9-digit and Chinese 6-digit postal codes, and public IP addresses produce too many false positives on digit-only patterns. These rules carry a list of context words (phone, ZIP, PLZ, CAP, IP, etc. in the relevant languages) and only fire when one of those words appears near the match. National ID numbers rely on their checksums instead and do not need context. Japanese postal codes keep their `〒` prefix requirement, which is a stricter form of the same idea.
430
+
302
431
  ---
303
432
 
304
433
  ## How It Works
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@coo-quack/sensitive-canary",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "Claude Code hooks that block secrets and PII before they reach the Anthropic API",
5
5
  "homepage": "https://coo-quack.github.io/sensitive-canary/",
6
6
  "type": "module",
@@ -162,6 +162,7 @@ describe("applyAllowTags", () => {
162
162
  category: "secret",
163
163
  matchRedacted: "AKIA****",
164
164
  secretValue: "AKIATEST",
165
+ score: 1,
165
166
  },
166
167
  {
167
168
  ruleId: "pii-email",
@@ -169,6 +170,7 @@ describe("applyAllowTags", () => {
169
170
  category: "pii",
170
171
  matchRedacted: "user****",
171
172
  secretValue: "user@example.com",
173
+ score: 1,
172
174
  },
173
175
  ];
174
176
 
@@ -213,6 +215,7 @@ describe("dedupeFindings", () => {
213
215
  category: "secret",
214
216
  matchRedacted: "AKIA****",
215
217
  secretValue: "AKIATEST",
218
+ score: 1,
216
219
  },
217
220
  {
218
221
  ruleId: "aws-access-key",
@@ -220,6 +223,7 @@ describe("dedupeFindings", () => {
220
223
  category: "secret",
221
224
  matchRedacted: "AKIA****",
222
225
  secretValue: "AKIATEST",
226
+ score: 1,
223
227
  },
224
228
  ];
225
229
  expect(dedupeFindings(findings)).toHaveLength(1);
@@ -233,6 +237,7 @@ describe("dedupeFindings", () => {
233
237
  category: "secret",
234
238
  matchRedacted: "AKIA****",
235
239
  secretValue: "AKIATEST1",
240
+ score: 1,
236
241
  },
237
242
  {
238
243
  ruleId: "aws-access-key",
@@ -240,6 +245,7 @@ describe("dedupeFindings", () => {
240
245
  category: "secret",
241
246
  matchRedacted: "AKIA****",
242
247
  secretValue: "AKIATEST2",
248
+ score: 1,
243
249
  },
244
250
  ];
245
251
  expect(dedupeFindings(findings)).toHaveLength(2);
@@ -257,6 +263,7 @@ describe("findingsToLines", () => {
257
263
  category: "secret",
258
264
  matchRedacted: "AKIA****MPLE",
259
265
  secretValue: "AKIAIOSFODNN7EXAMPLE",
266
+ score: 1,
260
267
  },
261
268
  ];
262
269
  const lines = findingsToLines(findings);
@@ -273,6 +280,7 @@ describe("findingsToLines", () => {
273
280
  category: "pii",
274
281
  matchRedacted: "user****",
275
282
  secretValue: "user@example.com",
283
+ score: 1,
276
284
  },
277
285
  ];
278
286
  const lines = findingsToLines(findings);