Skip to content

Commit d6bbe6d

Browse files
committed
feat: Enhance PII classification and improve ISMS checks for secret handling
1 parent e6cf1ab commit d6bbe6d

4 files changed

Lines changed: 50 additions & 0 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,17 @@
2121
categories (ifsc, upi, cvv, iban, imei, caste, bank+account, blood+group, …).
2222
- Per-category summary in Check 1, with a GDPR Art. 9 note when
2323
health-biometric fields are present.
24+
- **"default secret fallback" rule**: catches `process.env.JWT_SECRET ||
25+
'your-secret-key'` — a placeholder used as the runtime default is the bug.
2426
- `PRIVCHECK_MODEL` env var to override the Check 2 model.
2527
- Runnable demo in `examples/` with its generated evidence pack.
2628
- Test workflow on push/PR (ubuntu/windows × Python 3.10/3.12).
2729

2830
### Fixed
31+
- Check 4 precision (validated on a real codebase): advice-like patterns
32+
(TLS/crypto/config) inside comment lines no longer flag; generic
33+
secret-named assignments in test files are treated as fixtures (vendor
34+
tokens still flag everywhere — a real key in a test is still a leak).
2935
- Single-line `CREATE TABLE (...);` no longer poisons SQL scanner state.
3036
- Skipped directories (`node_modules`, …) are pruned before descent instead
3137
of walked and filtered — large JS repos scan dramatically faster.

‎privcheck/isms.py‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,8 +74,21 @@
7474
|NODE_TLS_REJECT_UNAUTHORIZED\W{0,3}0
7575
|sslmode=disable
7676
|SSL_VERIFYPEER\W{1,4}(false|0))"""), False),
77+
# env-var lookup falling back to a literal default secret (the classic
78+
# `process.env.JWT_SECRET || 'your-secret-key'`) — the placeholder filter
79+
# doesn't apply here: a placeholder USED as the runtime default is the bug
80+
("default secret fallback", "A.5.17 Authentication information",
81+
re.compile(r"""(?ix)\benv(?:\.|\[["'])\w*(secret|token|key|passw)\w*(?:["'\]])?
82+
\s*(?:\|\||\?\?)\s*["'][^"']{6,}["']"""), True),
7783
]
7884
_DEBUG_TRUE = re.compile(r"^\s*DEBUG\s*=\s*True\b") # Django settings*.py only
85+
# comment leaders — advice-like patterns (TLS/crypto/config) in comments are
86+
# docs, not deviations; secrets stay flagged even in comments (commented-out
87+
# creds are still a leak)
88+
_COMMENT = re.compile(r"^\s*(#|//|--|\*|<!--|;)")
89+
# generic secret-named assignments in test code are fixtures, not leaks;
90+
# fixed-prefix vendor tokens still flag everywhere (a real key in a test IS a leak)
91+
_TEST_PATH = re.compile(r"(?i)(^|/)(tests?|__tests__|fixtures)/|\.(test|spec)\.|(^|/)test_|_test\.")
7992

8093

8194
@dataclass
@@ -122,10 +135,16 @@ def check(root: str | Path) -> IsmsResult:
122135
continue
123136
result.files_scanned += 1
124137
is_settings = name.startswith("settings") and p.suffix == ".py"
138+
is_test = bool(_TEST_PATH.search(rel))
125139
for n, line in enumerate(p.read_text(errors="replace").splitlines(), 1):
126140
if "privcheck-ignore" in line:
127141
continue
142+
in_comment = bool(_COMMENT.match(line))
128143
for issue, control, rx, redact in RULES:
144+
if in_comment and not redact:
145+
continue # advice patterns in comments are docs, not code
146+
if issue == "hardcoded secret" and is_test:
147+
continue # test fixtures; vendor-token rules still apply
129148
if m := rx.search(line):
130149
if issue == "hardcoded secret" and _PLACEHOLDER.match(m.group(2)):
131150
continue

‎privcheck/pii.py‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
"msisdn": "contact", "imei": "network-identifier",
3434
"nationality": "demographics", "marital": "demographics",
3535
"religion": "demographics", "caste": "demographics",
36+
"avatar": "identity", "password": "credential",
3637
}
3738
# pairs that flag only in combination (avoids "pan" in "company", bare "name")
3839
PAIR_CATEGORIES = {
@@ -45,6 +46,9 @@
4546
("bank", "account"): "financial", ("credit", "card"): "financial",
4647
("card", "number"): "financial", ("blood", "group"): "health-biometric",
4748
("national", "id"): "government-id", ("mac", "address"): "network-identifier",
49+
("user", "agent"): "network-identifier",
50+
("google", "id"): "identity", ("facebook", "id"): "identity",
51+
("github", "id"): "identity", ("oauth", "id"): "identity",
4852
}
4953

5054
_CAMEL = re.compile(r"(?<=[a-z0-9])(?=[A-Z])")

‎privcheck/test_checks.py‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@ def test_classify_tokens():
2525
assert pii.classify("creditCardNumber") == "financial"
2626
assert pii.classify("blood_group") == "health-biometric"
2727
assert pii.classify("maritalStatus") == "demographics"
28+
assert pii.classify("avatarUrl") == "identity"
29+
assert pii.classify("googleId") == "identity"
30+
assert pii.classify("userAgent") == "network-identifier"
31+
assert pii.classify("password") == "credential"
2832
# non-PII must not flag
2933
assert pii.classify("company") is None # contains "pan" as substring only
3034
assert pii.classify("name") is None # bare name is too noisy
@@ -169,6 +173,23 @@ def test_isms_check(tmp_path):
169173
assert all(f.control.startswith("A.") for f in r.findings)
170174

171175

176+
def test_isms_precision(tmp_path):
177+
# advice patterns in comments are docs, not deviations
178+
_write(tmp_path, "docker-compose.yml", "# do NOT use NODE_TLS_REJECT_UNAUTHORIZED=0\n")
179+
# generic secrets in test files are fixtures; vendor tokens still flag there
180+
_write(tmp_path, "src/auth.test.ts", textwrap.dedent("""\
181+
const password = "P@ssw0rd@123";
182+
const key = "AIza""" + "A" * 35 + '";\n'))
183+
# env fallback to a literal default secret is the bug, placeholder or not
184+
_write(tmp_path, "src/config.js",
185+
"const secret = process.env.JWT_SECRET || 'your-secret-key';\n")
186+
got = {(f.file, f.issue) for f in isms.check(tmp_path).findings}
187+
assert ("docker-compose.yml", "TLS verification disabled") not in got
188+
assert ("src/auth.test.ts", "hardcoded secret") not in got
189+
assert ("src/auth.test.ts", "Google API key") in got
190+
assert ("src/config.js", "default secret fallback") in got
191+
192+
172193
def test_sql_single_line_create_does_not_leak(tmp_path):
173194
_write(tmp_path, "m.sql", """\
174195
CREATE TABLE t (id INT);

0 commit comments

Comments
 (0)