@softspark/ai-toolkit 4.22.1 → 4.23.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
|
@@ -7,6 +7,35 @@ Versioning follows [Semantic Versioning](https://semver.org/).
|
|
|
7
7
|
|
|
8
8
|
---
|
|
9
9
|
|
|
10
|
+
## v4.23.0 — a compliance scanner that says nothing must say why (2026-08-06)
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- **`hipaa-validate` scanned a manifest-less project silently clean.** Category
|
|
15
|
+
1, 3, 4 and 7 patterns are language-tagged and fire only for a detected
|
|
16
|
+
language, and detection read manifest files only. A directory of Python with
|
|
17
|
+
PHI in its loggers therefore reported `HIGH: 0` while every Python rule sat
|
|
18
|
+
unused — the worst possible output for a compliance tool, because zero reads
|
|
19
|
+
as compliant. Found by the post-release SOP on v4.22.1.
|
|
20
|
+
|
|
21
|
+
Detection now falls back to the extensions of the files being scanned when no
|
|
22
|
+
manifest is present. **Manifests still win**, so a project that already
|
|
23
|
+
declared itself keeps exactly the behaviour it had.
|
|
24
|
+
|
|
25
|
+
### Changed
|
|
26
|
+
|
|
27
|
+
- **The scan summary reports how the language was decided.** A new
|
|
28
|
+
`language_detection` field says `manifest`, `file extensions (no manifest
|
|
29
|
+
found)`, or `none`. When it is `none` the text report writes to stderr that
|
|
30
|
+
language-tagged patterns did not run and that a zero-finding result means
|
|
31
|
+
unscanned rather than compliant.
|
|
32
|
+
|
|
33
|
+
**Upgrade note:** a project scanned without a manifest may now report HIGH
|
|
34
|
+
findings where v4.22.x reported none. Those findings were always there. A CI
|
|
35
|
+
job gating on the exit code can start failing on real PHI exposure.
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
10
39
|
## v4.22.1 — nine skills, not four, could not find their own scripts (2026-08-06)
|
|
11
40
|
|
|
12
41
|
v4.22.0 claimed to fix four skills whose documented script path never resolved.
|
package/README.md
CHANGED
|
@@ -6,7 +6,16 @@
|
|
|
6
6
|
[](LICENSE)
|
|
7
7
|
[](app/skills/)
|
|
8
8
|
[](app/agents/)
|
|
9
|
-
[](tests/)
|
|
10
|
+
|
|
11
|
+
## What's New in v4.23.0
|
|
12
|
+
|
|
13
|
+
**v4.23.0** — `hipaa-validate` scanned a project without a manifest silently
|
|
14
|
+
clean: its language-tagged patterns never ran, and the report said `HIGH: 0`.
|
|
15
|
+
Zero reads as compliant. Detection now falls back to file extensions, the summary
|
|
16
|
+
says how the language was decided, and an undecidable run warns that a zero means
|
|
17
|
+
unscanned. Manifest-declaring projects are unaffected. Found by the post-release
|
|
18
|
+
SOP, not by a user.
|
|
10
19
|
|
|
11
20
|
## What's New in v4.22.1
|
|
12
21
|
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"name": "ai-toolkit",
|
|
4
4
|
"displayName": "AI Toolkit",
|
|
5
5
|
"description": "Professional-grade engineering skills, agents, rules, and lifecycle guardrails for Claude Code, Claude Chat, and Cowork.",
|
|
6
|
-
"version": "4.
|
|
6
|
+
"version": "4.23.0",
|
|
7
7
|
"author": {
|
|
8
8
|
"name": "SoftSpark",
|
|
9
9
|
"url": "https://github.com/softspark"
|
|
@@ -161,6 +161,9 @@ This distinction helps compliance officers prioritize immediate remediation (def
|
|
|
161
161
|
|
|
162
162
|
## Gotchas
|
|
163
163
|
|
|
164
|
+
- **Check `language_detection` in the summary before trusting a zero.** Category 1, 3, 4 and 7 patterns are language-tagged and only fire for a detected language. Manifests decide first (`pyproject.toml`, `package.json`, `go.mod`, …); without one the scanner falls back to file extensions. If it reports `languages: ["any"]` with `language_detection: "none"`, the language rules never ran and `HIGH: 0` means *unscanned*, not *compliant* — the scanner prints that warning to stderr, so a run whose stderr is discarded loses it.
|
|
165
|
+
- Scanning a monorepo package or a subdirectory can put you below the manifest. The extension fallback covers the common case, but a directory of `.sql`, `.yaml` or templates resolves to no language at all — scan from the level that holds the manifest.
|
|
166
|
+
|
|
164
167
|
- Test fixtures and seed data often contain **synthetic** PHI that looks real (SSN-shaped IDs, formatted phone numbers, sample email addresses). Flag them but lower severity — production code handling the same patterns is the actual risk.
|
|
165
168
|
- HIPAA §164.312(b) requires audit logging but does not specify a format. "Logs exist" is not evidence of compliance — the logs must capture WHO (authenticated user), WHAT (action), WHEN (timestamp), WHERE (resource), and they must be immutable (append-only or write-once storage).
|
|
166
169
|
- Encryption-at-rest varies silently by storage layer. RDS auto-encrypts new volumes since 2017, but older DB snapshots may not be; S3 bucket policies can override instance-level encryption. Treat "encryption enabled" as a claim to verify with the cloud provider, not a state to trust.
|
|
@@ -302,8 +302,24 @@ def context_gate(files: list[Path], keywords: list[str],
|
|
|
302
302
|
# Language detection (Step 1)
|
|
303
303
|
# ---------------------------------------------------------------------------
|
|
304
304
|
|
|
305
|
-
def detect_languages(root: Path) -> set[str]:
|
|
306
|
-
"""Detect project languages
|
|
305
|
+
def detect_languages(root: Path, files: list[Path] | None = None) -> tuple[set[str], str]:
|
|
306
|
+
"""Detect project languages. Returns (languages, how_they_were_detected).
|
|
307
|
+
|
|
308
|
+
Manifest files decide when present. When none is found, fall back to the
|
|
309
|
+
extensions of the files actually being scanned.
|
|
310
|
+
|
|
311
|
+
The fallback exists because language-tagged patterns fire only when the
|
|
312
|
+
project language includes the tag, so a manifest-less directory of Python
|
|
313
|
+
scanned clean while every Python rule sat unused — and the report said
|
|
314
|
+
`HIGH: 0`, which reads as compliant rather than unscanned. Silence is the
|
|
315
|
+
worst possible output for a compliance tool. A monorepo package, a scan
|
|
316
|
+
pointed at a subdirectory, or a service whose manifest lives one level up
|
|
317
|
+
all hit this.
|
|
318
|
+
|
|
319
|
+
Manifests still win: a project that declares itself keeps exactly the
|
|
320
|
+
behaviour it had, so upgrading changes nothing for anyone who was already
|
|
321
|
+
being scanned properly.
|
|
322
|
+
"""
|
|
307
323
|
langs = set()
|
|
308
324
|
for indicator, lang in LANGUAGE_INDICATORS.items():
|
|
309
325
|
if "*" in indicator:
|
|
@@ -314,7 +330,20 @@ def detect_languages(root: Path) -> set[str]:
|
|
|
314
330
|
else:
|
|
315
331
|
if (root / indicator).exists():
|
|
316
332
|
langs.add(lang)
|
|
317
|
-
|
|
333
|
+
if langs:
|
|
334
|
+
return langs, "manifest"
|
|
335
|
+
|
|
336
|
+
if files:
|
|
337
|
+
ext_to_lang: dict[str, set[str]] = {}
|
|
338
|
+
for lang, exts in LANG_EXTENSIONS.items():
|
|
339
|
+
for ext in exts:
|
|
340
|
+
ext_to_lang.setdefault(ext, set()).add(lang)
|
|
341
|
+
for f in files:
|
|
342
|
+
langs |= ext_to_lang.get(f.suffix, set())
|
|
343
|
+
if langs:
|
|
344
|
+
return langs, "file extensions (no manifest found)"
|
|
345
|
+
|
|
346
|
+
return {"any"}, "none — language-tagged patterns were skipped"
|
|
318
347
|
|
|
319
348
|
|
|
320
349
|
# ---------------------------------------------------------------------------
|
|
@@ -616,7 +645,8 @@ def format_text_report(findings: list[dict], mode: str,
|
|
|
616
645
|
|
|
617
646
|
def format_json_report(findings: list[dict], mode: str,
|
|
618
647
|
phi_count: int, scanned_count: int,
|
|
619
|
-
categories_run: list[int], languages: list[str]
|
|
648
|
+
categories_run: list[int], languages: list[str],
|
|
649
|
+
language_source: str = "manifest") -> str:
|
|
620
650
|
"""Format findings as structured JSON for CI integration."""
|
|
621
651
|
high_count = sum(1 for f in findings if f["severity"] == "HIGH")
|
|
622
652
|
warn_count = sum(1 for f in findings if f["severity"] == "WARN")
|
|
@@ -628,6 +658,7 @@ def format_json_report(findings: list[dict], mode: str,
|
|
|
628
658
|
"files_scanned": scanned_count,
|
|
629
659
|
"categories_run": sorted(categories_run),
|
|
630
660
|
"languages": sorted(languages),
|
|
661
|
+
"language_detection": language_source,
|
|
631
662
|
"high": high_count,
|
|
632
663
|
"warn": warn_count,
|
|
633
664
|
"total": len(findings),
|
|
@@ -723,8 +754,9 @@ def main() -> None:
|
|
|
723
754
|
file=sys.stderr)
|
|
724
755
|
print("Consider narrowing the scan path for targeted results.", file=sys.stderr)
|
|
725
756
|
|
|
726
|
-
# Step 1: Detect languages
|
|
727
|
-
|
|
757
|
+
# Step 1: Detect languages. Categories 1-2 scan everything, so the extension
|
|
758
|
+
# fallback looks at all collected files, not just the PHI-adjacent subset.
|
|
759
|
+
languages, lang_source = detect_languages(root, all_files)
|
|
728
760
|
|
|
729
761
|
# Step 2: Run categories
|
|
730
762
|
findings: list[dict] = []
|
|
@@ -789,10 +821,18 @@ def main() -> None:
|
|
|
789
821
|
if args.output == "json":
|
|
790
822
|
print(format_json_report(findings, args.mode, phi_count,
|
|
791
823
|
scanned_count, categories_run,
|
|
792
|
-
sorted(languages)))
|
|
824
|
+
sorted(languages), lang_source))
|
|
793
825
|
else:
|
|
794
826
|
print(format_text_report(findings, args.mode, phi_count,
|
|
795
827
|
scanned_count, categories_run))
|
|
828
|
+
if languages == {"any"}:
|
|
829
|
+
# Never let an unscanned run read as a clean one.
|
|
830
|
+
print("\nWARNING: no project language identified "
|
|
831
|
+
f"({lang_source}).", file=sys.stderr)
|
|
832
|
+
print("Language-tagged patterns (Python, JS/TS, Go, Java, Ruby, C#) "
|
|
833
|
+
"did NOT run.", file=sys.stderr)
|
|
834
|
+
print("A zero-finding result here means unscanned, not compliant. "
|
|
835
|
+
"Scan from the directory holding the manifest.", file=sys.stderr)
|
|
796
836
|
|
|
797
837
|
# Exit code: non-zero if HIGH findings
|
|
798
838
|
high_count = sum(1 for f in findings if f["severity"] == "HIGH")
|
package/manifest.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@softspark/ai-toolkit",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.23.0",
|
|
4
4
|
"description": "AI coding toolkit: 109 skills, 44 agents, 12 developer-tool integrations, recoverable native tool-output filtering, Claude Chat/Cowork export, safety constitution, SARIF audit, and signed npm provenance.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"claude",
|