@blamejs/exceptd-skills 0.21.15 → 0.21.16

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,9 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.21.16 — 2026-09-29
4
+
5
+ The skill catalog-facts predeploy gate now also compares three more skill table columns with the catalog. The Active Exploitation column is compared with `active_exploitation`. A Patch column is compared with `patch_available`; its header is Patch, or Patch followed by "/" and Mitigation, Remediation or Workaround, each word singular or plural and the header optionally followed by Available or Availability. A Live Patch column is compared with `live_patch_available`; its header is Live Patch written with a space or a hyphen, optionally ending in -able or -es, and optionally followed by Available or Availability. A bare "Livepatch" header is not compared. Other patch and exploitation headers ("Patch Reboot Required?", "Patch Applied?", "Active Exploitation Actor") are not compared. Each cell is compared only in a small set of unambiguous wordings, and any other wording is not compared. An exploitation cell is compared when the whole cell is a state such as "Confirmed", "Suspected", "Unknown", "Theoretical only", "Confirmed mass exploitation" or "Confirmed exploitation 2024"; a plain none wording such as "None", "None observed" or "No exploitation observed in the wild", which agrees with a none or theoretical entry, as a Theoretical cell agrees with a none entry; or a plain denial of confirmation such as "No confirmed exploitation" or "None known", which differs only from a confirmed entry. "Confirmed PoC", "Suspected (supply-chain)" and "No confirmed mass exploitation" are not compared. A patch cell reads as a patch when the whole cell is a vendor patch, update or fix statement such as "Vendor patch", "Vendor fix released", "Vendor patch + config hardening" or "Mitigation + vendor patch", or a Yes followed by one ("Yes, vendor patch"). A bare Yes reads as a patch in a Patch column only; under a Patch / Mitigation, Remediation or Workaround header it is not compared. A Yes with any other text after it ("Yes (workaround)", "Yes, if on 2.x") is not compared. A patch cell reads as no patch when the whole cell is a denial such as "Vendor patch pending", "Vendor update not yet available", "No", "No vendor patch (EoL)" or "Mitigation only; no vendor patch". A Live Patch cell is compared when it opens with a Yes or No that ends the cell or is followed by a comma, semicolon, period, colon, dash or parenthesis ("Yes (kpatch/livepatch)"), and no qualifying word or second answer follows; "No-reboot hotpatch", "Yes (kpatch pending)" and "No (Ubuntu); Yes (RHEL kpatch)" are not compared. These three columns are compared only on a row that names a single CVE, under an identifier header or, in a table with no identifier header, in the first column, and only when no other row in the same section has that CVE as its own row CVE. A section runs from one "#" or "##" heading to the next, so "###" and deeper sub-headings ("### RHEL 9") stay in their parent section. A row's own CVE is the one its first CVE-bearing cell names alone, not counting a row whose CVE cell only points to one ("No vendor CVE; see CVE-..."); a row that names no CVE and leaves blank the cell under the row above's CVE continues that row. An identifier header is CVE or CVEs, optionally preceded by Evidence and followed by ID or Class, alone or as one "/" part of the header ("Class / CVE"), with a parenthetical note ignored ("CVE (if any)"). A header that holds a related, sibling, similar, see also, example, e.g., analog, analogue, PoC, demo, exploit, note, comment, rationale, reason, description or detail word (singular or plural), in its note or in any "/" part ("CVE (example)", "PoC / CVE"), is not an identifier header, and a first column under such a header, or under any other header that names a CVE ("CVE Number", "Paired CVE"), does not name the row. When a later cell of the row cites another CVE ("Chained with CVE-..."), when the row's only CVE is a case cited in such a column, or when the section has that CVE as the row CVE of several rows (one row, one table or one sub-heading per distribution or product line), the row's CVSS, RWEP, KEV, public-exploit and AI cells are compared and its exploitation, patch and live-patch cells are not.
6
+
3
7
  ## 0.21.15 — 2026-09-28
4
8
 
5
9
  The catalog adds CVE-2026-88772, a DTLS memory overflow in Citrix NetScaler ADC and NetScaler Gateway that can lead to remote code execution or denial of service. CISA added it to KEV on 2026-09-27 with a 2026-09-30 due date and requires forensic triage; Citrix bulletin CTX697096 reports exploitation in the wild. The fixed builds are 14.1-73.37, 13.1-64.23, 14.1-73.37 FIPS and 13.1.37.279, and the upgrade reboots the appliance. The entry puts the Citrix indicator scan and the CTX694799 evidence collection before any upgrade or reboot. It records an appliance with `-dtls OFF` on every SSL VPN virtual server and no virtual server of type DTLS as mitigated with residual risk for this CVE, and states that the setting does not make it safe to defer the upgrade, because the same builds are also affected by CVE-2026-88771, whose KEV facts the entry gives. It scores RWEP 51. The catalog holds 1,759 CVEs.
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "schema_version": "1.1.0",
3
- "generated_at": "2026-09-29T06:54:34.881Z",
3
+ "generated_at": "2026-09-30T10:32:27.761Z",
4
4
  "generator": "scripts/build-indexes.js",
5
5
  "source_count": 64,
6
6
  "source_hashes": {
7
- "manifest.json": "53635b9883d8da988e78ae3ac7609fde37fd9d5b8c2bec438041740f5c5ba472",
7
+ "manifest.json": "f0287eb67bdf0b76c3ea72327ff5a9e117a191248aebaa7f2c41c3849ab10a14",
8
8
  "README.md": "864570c1f7fb221b0176d22d3b0823a8d077d6a7209edc6045b426b1e9ec555d",
9
9
  "data/atlas-ttps.json": "c36c0a27c8de8a37a7070923d7b187345c46db9db778f58e4637f1f81ca6d6b1",
10
10
  "data/attack-techniques.json": "b2acec4cc79b03c77a36b29ece0b8c26304db51ac6ab6a1c26c9b8a3c481dcb9",
package/manifest.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "exceptd-security",
3
- "version": "0.21.15",
3
+ "version": "0.21.16",
4
4
  "description": "AI security skills grounded in mid-2026 threat reality, not stale framework documentation",
5
5
  "homepage": "https://exceptd.com",
6
6
  "license": "Apache-2.0",
@@ -57,7 +57,7 @@
57
57
  ],
58
58
  "last_threat_review": "2026-09-28",
59
59
  "signature": "iFEJzr9ftWZOGEypM+z9quIsJYEn2qp3U/LDsRTueSn6gzhALUbzmwOS3panA23bXo51lWZw2pF2ohifbb2kCA==",
60
- "signed_at": "2026-09-29T06:54:33.344Z",
60
+ "signed_at": "2026-09-30T10:32:26.531Z",
61
61
  "cwe_refs": [
62
62
  "CWE-125",
63
63
  "CWE-362",
@@ -134,7 +134,7 @@
134
134
  ],
135
135
  "last_threat_review": "2026-09-28",
136
136
  "signature": "IqOnC+FpiNIxtOOGRrl58AxE37F9yE2eWpk5GVgwOUMJ2JUBuDA2otvj0rL6Ep/4GXHiZT6E0xD8UoscL6aLAw==",
137
- "signed_at": "2026-09-29T06:54:33.346Z",
137
+ "signed_at": "2026-09-30T10:32:26.532Z",
138
138
  "cwe_refs": [
139
139
  "CWE-1039",
140
140
  "CWE-1426",
@@ -217,7 +217,7 @@
217
217
  ],
218
218
  "last_threat_review": "2026-09-18",
219
219
  "signature": "+zW0ui2tPBoE69HFEuTHzLJkPJD3KLpDCL1murk1N+ALuWEtx/ApgS7eZVH7BsLxVv/uHmmmfw+Uu5cNhrmQAw==",
220
- "signed_at": "2026-09-29T06:54:33.346Z",
220
+ "signed_at": "2026-09-30T10:32:26.533Z",
221
221
  "cwe_refs": [
222
222
  "CWE-22",
223
223
  "CWE-345",
@@ -271,7 +271,7 @@
271
271
  "framework_gaps": [],
272
272
  "last_threat_review": "2026-09-28",
273
273
  "signature": "jvtfxzfMmn9EJa6chjp6JeVuKhMFXU72XtcKJSD1GYqd2eHuJjSAN8YkuGuY4s438Gidx/XoHh2A7lkQsVyqDA==",
274
- "signed_at": "2026-09-29T06:54:33.347Z"
274
+ "signed_at": "2026-09-30T10:32:26.533Z"
275
275
  },
276
276
  {
277
277
  "name": "compliance-theater",
@@ -303,7 +303,7 @@
303
303
  ],
304
304
  "last_threat_review": "2026-09-28",
305
305
  "signature": "tLhoIS1rmBatiNdvS72YKionixlkS2G018VktqGhLghxZIGDIhIw2nkWnj4dd1k3wydhk1YS4EylpFIYhpSQBA==",
306
- "signed_at": "2026-09-29T06:54:33.348Z"
306
+ "signed_at": "2026-09-30T10:32:26.534Z"
307
307
  },
308
308
  {
309
309
  "name": "exploit-scoring",
@@ -334,7 +334,7 @@
334
334
  ],
335
335
  "last_threat_review": "2026-09-28",
336
336
  "signature": "jcplUUQz11YIa3OxWy64ahD9Hy8cLv1Vjv6j0oh8+K4/Rvpcb9iE3ZaJANuf0bGKFTxtEichSGjc19W8OIeKAA==",
337
- "signed_at": "2026-09-29T06:54:33.348Z"
337
+ "signed_at": "2026-09-30T10:32:26.534Z"
338
338
  },
339
339
  {
340
340
  "name": "rag-pipeline-security",
@@ -376,7 +376,7 @@
376
376
  ],
377
377
  "last_threat_review": "2026-09-18",
378
378
  "signature": "NSpytlrXh1+o4sX0EqZ746eAm3+mPfl0hgpgE5YHAMcvosOu1icbZ2aR424GrB5rASr7c/5fo+olZcBA4qx9CQ==",
379
- "signed_at": "2026-09-29T06:54:33.349Z",
379
+ "signed_at": "2026-09-30T10:32:26.535Z",
380
380
  "cwe_refs": [
381
381
  "CWE-1395",
382
382
  "CWE-1426"
@@ -443,7 +443,7 @@
443
443
  ],
444
444
  "last_threat_review": "2026-09-18",
445
445
  "signature": "/bIjyOG3vcL6sMPX0agXTVCO2Wd6/6/PBMQfjALT63ojxXMwBDz4HQTO00OSkNgvCLTOvcUM4nN4H+4yl+roCg==",
446
- "signed_at": "2026-09-29T06:54:33.349Z",
446
+ "signed_at": "2026-09-30T10:32:26.535Z",
447
447
  "cwe_refs": [
448
448
  "CWE-918"
449
449
  ],
@@ -484,7 +484,7 @@
484
484
  "framework_gaps": [],
485
485
  "last_threat_review": "2026-09-28",
486
486
  "signature": "Wu9WnqVUI6D2inIiSi4Q8ruzMeviWPv3p8sKE+5wZujrXSOLo8UZ45eGlr99KiTkRYFa134hoMHVe3cQ1pIuDQ==",
487
- "signed_at": "2026-09-29T06:54:33.350Z",
487
+ "signed_at": "2026-09-30T10:32:26.535Z",
488
488
  "cwe_refs": [
489
489
  "CWE-1188"
490
490
  ],
@@ -519,7 +519,7 @@
519
519
  "framework_gaps": [],
520
520
  "last_threat_review": "2026-09-28",
521
521
  "signature": "0Ihy5ugnEpT8bDGGCzm21W+8gdTzbCXDjj+UmxuvmGaV+hZKax9JcWFYxmkC710Q2HVyTFZ23iXMIsV+9v2vAw==",
522
- "signed_at": "2026-09-29T06:54:33.350Z",
522
+ "signed_at": "2026-09-30T10:32:26.536Z",
523
523
  "forward_watch": [
524
524
  "New AI attack classes as ATLAS v6 publishes",
525
525
  "Post-quantum adversary capability timeline",
@@ -560,7 +560,7 @@
560
560
  "framework_gaps": [],
561
561
  "last_threat_review": "2026-09-09",
562
562
  "signature": "u/3QUr3Nh0q1tjOaHQ3yC4WIJOUoABr+6R8VStCgS4NvNBglb/JcbLYvQmFixqdURu4Qi6xtqvUw0E14b5mtDA==",
563
- "signed_at": "2026-09-29T06:54:33.351Z",
563
+ "signed_at": "2026-09-30T10:32:26.536Z",
564
564
  "d3fend_refs": [
565
565
  "D3-EAL",
566
566
  "D3-EFA",
@@ -602,7 +602,7 @@
602
602
  "framework_gaps": [],
603
603
  "last_threat_review": "2026-09-28",
604
604
  "signature": "qRK6ncDD1o//GLwO/CfVMKK+sppjkuIaql9q5i+GxwMiJxQPPdmTkdvqPmWlV/cnrR90BtrseTBLSbWQB1mGDw==",
605
- "signed_at": "2026-09-29T06:54:33.352Z",
605
+ "signed_at": "2026-09-30T10:32:26.537Z",
606
606
  "forward_watch": [
607
607
  "New CISA KEV entries",
608
608
  "New ATLAS TTP additions in each ATLAS release",
@@ -671,7 +671,7 @@
671
671
  ],
672
672
  "last_threat_review": "2026-08-05",
673
673
  "signature": "r8tW6PJ8G0Sjd9nyr6rWJ11dovdH9llEe6eAldHedUKRl+vdbQd4g5Cefs2Q/xcNzoXGh3C0usNLX8jX4EaWBQ==",
674
- "signed_at": "2026-09-29T06:54:33.353Z",
674
+ "signed_at": "2026-09-30T10:32:26.537Z",
675
675
  "cwe_refs": [
676
676
  "CWE-327"
677
677
  ],
@@ -721,7 +721,7 @@
721
721
  ],
722
722
  "last_threat_review": "2026-09-18",
723
723
  "signature": "R9cbLm2pz1MupiQpWk3CtrnlZ06VQQ5gN5MWlMEbF+aFTHdK7JsSXoyLodZYDo1U+ROQ+4OH2VXo0Z+54k/KBw==",
724
- "signed_at": "2026-09-29T06:54:33.354Z"
724
+ "signed_at": "2026-09-30T10:32:26.538Z"
725
725
  },
726
726
  {
727
727
  "name": "security-maturity-tiers",
@@ -761,7 +761,7 @@
761
761
  ],
762
762
  "last_threat_review": "2026-09-28",
763
763
  "signature": "PE1cz77xEf4rBJghx2cDug6QdyL2qIqn1hoS5NyhLS/5JC+qENLuNBUK4jUVPl+NDv+1+IPBFcPaL+eSCB2YBA==",
764
- "signed_at": "2026-09-29T06:54:33.354Z",
764
+ "signed_at": "2026-09-30T10:32:26.538Z",
765
765
  "cwe_refs": [
766
766
  "CWE-1188"
767
767
  ],
@@ -815,7 +815,7 @@
815
815
  "framework_gaps": [],
816
816
  "last_threat_review": "2026-09-09",
817
817
  "signature": "Lb71p2XDRGmSUe4LGxiMl5HgJQ6gOVvWhjZ24dz8eBt7q+9qb0dLMMXshsfmreTjRU4ArNv50QPLZFFh9l+HCw==",
818
- "signed_at": "2026-09-29T06:54:33.355Z"
818
+ "signed_at": "2026-09-30T10:32:26.539Z"
819
819
  },
820
820
  {
821
821
  "name": "attack-surface-pentest",
@@ -891,7 +891,7 @@
891
891
  "Pwn2Own Berlin 2026 (disclosed 2026-05-14, embargo ends 2026-08-12) — Microsoft Edge 4-bug sandbox escape by Orange Tsai (DEVCORE); forward-watch only (browser sandbox, out of current playbook scope); track Microsoft Edge security advisory and KEV add"
892
892
  ],
893
893
  "signature": "tBzI4lHl8F2+MeDZIOak1IYUBy8rrbgbc7M5uvodJXBKs+Rj9BmC0quk3NEcNRma8eIawUyyjU5ieFMi5MpiBQ==",
894
- "signed_at": "2026-09-29T06:54:33.355Z"
894
+ "signed_at": "2026-09-30T10:32:26.539Z"
895
895
  },
896
896
  {
897
897
  "name": "fuzz-testing-strategy",
@@ -952,7 +952,7 @@
952
952
  "OSS-Fuzz-Gen / AI-assisted harness generation becoming the default expectation for OSS maintainers"
953
953
  ],
954
954
  "signature": "1rpQIBRamlNF6gFGwycmgGLcmNalYeAZjkRKdQ0NWQW1lXQegE4sTObgDFkKCLuLSB+6j3zjzinwSQyxwxL9Dw==",
955
- "signed_at": "2026-09-29T06:54:33.357Z"
955
+ "signed_at": "2026-09-30T10:32:26.539Z"
956
956
  },
957
957
  {
958
958
  "name": "dlp-gap-analysis",
@@ -1030,7 +1030,7 @@
1030
1030
  "Quebec Law 25, India DPDPA, KSA PDPL enforcement actions naming AI-tool prompt data as in-scope personal information"
1031
1031
  ],
1032
1032
  "signature": "wKN08ODL/5Z1Kr1ySnDqj/eWY013y13LRqupqEERjMLjKMxuFiC6cLUaPjUyachML0PZAZrWYlCdfrggbNxkDg==",
1033
- "signed_at": "2026-09-29T06:54:33.357Z"
1033
+ "signed_at": "2026-09-30T10:32:26.540Z"
1034
1034
  },
1035
1035
  {
1036
1036
  "name": "supply-chain-integrity",
@@ -1114,7 +1114,7 @@
1114
1114
  "Pwn2Own Berlin 2026 (disclosed 2026-05-14, embargo ends 2026-08-12) — NVIDIA Megatron Bridge path traversal by haehae; AI training-stack file-system trust boundary; track patch and SBOM-attestation impact"
1115
1115
  ],
1116
1116
  "signature": "nAxLtc0jWsb5QuMQBRxAax05YFw4lLHGtl/xoX0mzMjPfvSAXJrSxMtX5CzDYuvTPKUuopwZDfCltKL+pRQoDw==",
1117
- "signed_at": "2026-09-29T06:54:33.358Z"
1117
+ "signed_at": "2026-09-30T10:32:26.540Z"
1118
1118
  },
1119
1119
  {
1120
1120
  "name": "defensive-countermeasure-mapping",
@@ -1171,7 +1171,7 @@
1171
1171
  ],
1172
1172
  "last_threat_review": "2026-06-10",
1173
1173
  "signature": "W8kg2Okd/VF521VmeVFGK2TmXkFPycuwgYdyNxLU2fMN+sR9Aals/X+Xqr/prrGEsqWnZsCInSq1TYzFr0wiDg==",
1174
- "signed_at": "2026-09-29T06:54:33.358Z"
1174
+ "signed_at": "2026-09-30T10:32:26.540Z"
1175
1175
  },
1176
1176
  {
1177
1177
  "name": "identity-assurance",
@@ -1245,7 +1245,7 @@
1245
1245
  ],
1246
1246
  "last_threat_review": "2026-05-11",
1247
1247
  "signature": "UV3458QXSkEpenzrOmdlTTfPHUD4hNyKMDHoeZDq/kiFb4mAG0ghQGTTgI9Ru8cJbSmYM1++m9N5TFIJ6JJPBg==",
1248
- "signed_at": "2026-09-29T06:54:33.358Z"
1248
+ "signed_at": "2026-09-30T10:32:26.541Z"
1249
1249
  },
1250
1250
  {
1251
1251
  "name": "ot-ics-security",
@@ -1304,7 +1304,7 @@
1304
1304
  "d3fend_refs": [],
1305
1305
  "last_threat_review": "2026-09-18",
1306
1306
  "signature": "2pmqMIfqL1VAK5ZzPY4l1wllI7tnl5ne7ML6u9qQnA3SScRR1++TPUjEaB3iLZeRanCKGgkMj8LmU96dbqYYDA==",
1307
- "signed_at": "2026-09-29T06:54:33.359Z"
1307
+ "signed_at": "2026-09-30T10:32:26.541Z"
1308
1308
  },
1309
1309
  {
1310
1310
  "name": "coordinated-vuln-disclosure",
@@ -1364,7 +1364,7 @@
1364
1364
  "NYDFS 23 NYCRR 500 amendments potentially adding explicit CVD program requirements"
1365
1365
  ],
1366
1366
  "signature": "FfzagIXtJNegSNPjLSI11fnQqxXJ4nSfPcv+IdbD3XmIfDTxnV7nTAR0tCQKqOGKpsyYEwAZP5jQzA3tIuJJBw==",
1367
- "signed_at": "2026-09-29T06:54:33.359Z"
1367
+ "signed_at": "2026-09-30T10:32:26.541Z"
1368
1368
  },
1369
1369
  {
1370
1370
  "name": "threat-modeling-methodology",
@@ -1418,7 +1418,7 @@
1418
1418
  "PASTA v2 updates incorporating AI/ML application threats"
1419
1419
  ],
1420
1420
  "signature": "0njaUBtRgD+5ys5xh1x+daPI2xhnHzZP+pielUdqEndR2/uDAuf3XIDUWnhC8LKr+33HSX3pXmqfgbXJpyqxBQ==",
1421
- "signed_at": "2026-09-29T06:54:33.360Z"
1421
+ "signed_at": "2026-09-30T10:32:26.542Z"
1422
1422
  },
1423
1423
  {
1424
1424
  "name": "webapp-security",
@@ -1501,7 +1501,7 @@
1501
1501
  ],
1502
1502
  "last_threat_review": "2026-09-18",
1503
1503
  "signature": "YhdXy2fMZubdoGemwvt8XCPFaMpcA3uVzGlGH7j+3koPuPl99VqkD1Q2gsJjs894V3CiRSWuRU75q4nIGwUAAQ==",
1504
- "signed_at": "2026-09-29T06:54:33.360Z",
1504
+ "signed_at": "2026-09-30T10:32:26.542Z",
1505
1505
  "forward_watch": [
1506
1506
  "NGINX Rift CVE-2026-42945 (disclosed 2026-05-13, source depthfirst) — KEV-watch predicted CISA KEV listing by 2026-05-29; AI-assisted discovery angle; track for active-exploitation confirmation and patch advisory affecting front-door web app deployments"
1507
1507
  ]
@@ -1562,7 +1562,7 @@
1562
1562
  ],
1563
1563
  "last_threat_review": "2026-05-15",
1564
1564
  "signature": "+x25rPi9K6Yo39Nj9ZN+PV4y0yQ9z9mNmOrBISYLsWrjqVTACCUFI+mp4w/nV6sZueEMoiar+inwE/fucbuiDw==",
1565
- "signed_at": "2026-09-29T06:54:33.361Z"
1565
+ "signed_at": "2026-09-30T10:32:26.542Z"
1566
1566
  },
1567
1567
  {
1568
1568
  "name": "sector-healthcare",
@@ -1630,7 +1630,7 @@
1630
1630
  ],
1631
1631
  "last_threat_review": "2026-09-18",
1632
1632
  "signature": "XMI/ZJn07vHRmN+zRCbYh+JH0bUaRL0KpthMVfRXGz3ELqQqeLG7eUrrnBWnJ5GhsWTuYY/dGCyDdsVyBKtNDA==",
1633
- "signed_at": "2026-09-29T06:54:33.361Z"
1633
+ "signed_at": "2026-09-30T10:32:26.543Z"
1634
1634
  },
1635
1635
  {
1636
1636
  "name": "sector-financial",
@@ -1719,7 +1719,7 @@
1719
1719
  "TIBER-EU framework v2.0 alignment with DORA TLPT RTS (JC 2024/40); cross-recognition with CBEST and iCAST"
1720
1720
  ],
1721
1721
  "signature": "gFUmQCkAfdW++Mb+UcwHTATJcZuszFA3dQSD+v3asEdYYsSFt63L03p09Y3JWN0c4JS4jAC1x08WsgYSklSCBQ==",
1722
- "signed_at": "2026-09-29T06:54:33.362Z"
1722
+ "signed_at": "2026-09-30T10:32:26.543Z"
1723
1723
  },
1724
1724
  {
1725
1725
  "name": "sector-federal-government",
@@ -1796,7 +1796,7 @@
1796
1796
  "Australia PSPF 2024 revision and ISM quarterly updates — track for Essential Eight Maturity Level requirements for federal entities"
1797
1797
  ],
1798
1798
  "signature": "Nc6G/hJJmvzFL628xQnpEjOOLvhm3JVv9Xbh0KMiBPChZqKqmY2Eebk67Iz2ywM/bcwTUWEVAL8LQ9lcqGbpDA==",
1799
- "signed_at": "2026-09-29T06:54:33.362Z"
1799
+ "signed_at": "2026-09-30T10:32:26.543Z"
1800
1800
  },
1801
1801
  {
1802
1802
  "name": "sector-energy",
@@ -1870,7 +1870,7 @@
1870
1870
  "ICS-CERT advisory feed (https://www.cisa.gov/news-events/cybersecurity-advisories/ics-advisories) for vendor CVEs in Siemens, Rockwell, Schneider Electric, ABB, GE Vernova, Hitachi Energy, AVEVA / OSIsoft PI"
1871
1871
  ],
1872
1872
  "signature": "tpbR4Azrmbh9YSByEBqsb40/c6e17bKw6RDYzdW1P1gK31g+It5wOCgVvoacCv4F5SjQWIQnlrm1QWTMYYJWAA==",
1873
- "signed_at": "2026-09-29T06:54:33.363Z"
1873
+ "signed_at": "2026-09-30T10:32:26.544Z"
1874
1874
  },
1875
1875
  {
1876
1876
  "name": "sector-telecom",
@@ -1956,7 +1956,7 @@
1956
1956
  "O-RAN SFG / WG11 security specifications"
1957
1957
  ],
1958
1958
  "signature": "G7WrqBaR/Nqf4zhYdS69gBsunZJolx0saEAAd6Y+3ev8iN7mDiB5H0W+ow+YZjD74hhsJr4ks3RD/yKulavXCQ==",
1959
- "signed_at": "2026-09-29T06:54:33.363Z"
1959
+ "signed_at": "2026-09-30T10:32:26.544Z"
1960
1960
  },
1961
1961
  {
1962
1962
  "name": "api-security",
@@ -2035,7 +2035,7 @@
2035
2035
  ],
2036
2036
  "last_threat_review": "2026-09-18",
2037
2037
  "signature": "y6LfKfsHtpPT+UBrfkXVR4xO139xvzrWOGTdyeEAna1kecWDJIqKKMdSpHqR4T7KG2HCAmgLCV2cmf3B3jnkAg==",
2038
- "signed_at": "2026-09-29T06:54:33.364Z",
2038
+ "signed_at": "2026-09-30T10:32:26.545Z",
2039
2039
  "forward_watch": [
2040
2040
  "NGINX Rift CVE-2026-42945 (disclosed 2026-05-13, source depthfirst) — KEV-watch predicted CISA KEV listing by 2026-05-29; track for active-exploitation confirmation and patch advisory affecting API gateway / reverse-proxy deployments",
2041
2041
  "Pwn2Own Berlin 2026 (disclosed 2026-05-14, embargo ends 2026-08-12) — LiteLLM 3-bug SSRF + Code Injection chain by k3vg3n; LLM-proxy API surface; track upstream patch and CVE assignments",
@@ -2131,7 +2131,7 @@
2131
2131
  "CISA KEV additions for cloud-control-plane CVEs (IMDSv1 abuses, federation token mishandling, cross-tenant boundary failures); CISA Cybersecurity Advisories for cross-cloud advisories"
2132
2132
  ],
2133
2133
  "signature": "f7b0+D4TJwd9c9ISGMFquvePueywoO92QZ172etyayBcDwYVTIiGYII67+NyaLl2+u0QG49d4QY41UPbRflrDw==",
2134
- "signed_at": "2026-09-29T06:54:33.365Z"
2134
+ "signed_at": "2026-09-30T10:32:26.545Z"
2135
2135
  },
2136
2136
  {
2137
2137
  "name": "container-runtime-security",
@@ -2203,7 +2203,7 @@
2203
2203
  ],
2204
2204
  "last_threat_review": "2026-09-28",
2205
2205
  "signature": "XT0iHNb0zTgBewACN1jXOfiXzVC3BpD7KoJgi9KcvTWPFE8Y2TzrqMmkftoUbyyCyNSecFB17z9ydoPswMWyDA==",
2206
- "signed_at": "2026-09-29T06:54:33.366Z",
2206
+ "signed_at": "2026-09-30T10:32:26.546Z",
2207
2207
  "forward_watch": [
2208
2208
  "Pwn2Own Berlin 2026 (disclosed 2026-05-14, embargo ends 2026-08-12) — NVIDIA Container Toolkit container escape ($50K award) by chompie / IBM X-Force XOR; high-severity container/hypervisor boundary break; track patch and KEV add post-embargo"
2209
2209
  ]
@@ -2284,7 +2284,7 @@
2284
2284
  "MITRE ATLAS release 2026.06 (June 2026) added AML.T0113 (Steal Web Session Cookie), AML.T0091.001 (Use Alternate Authentication Material: Web Session Cookie) and AML.T0114 (AI Service Web Interface) to release 2026.05 and removed no technique ids. Earlier releases shipped the AML.T0010 sub-technique expansion and the agentic-AI techniques this forecast tracked (\"Publish Poisoned AI Agent Tool\", \"Escape to Host\"). Forward watch: subsequent monthly ATLAS releases, for updates to agentic-AI TTPs and MLOps-pipeline-specific techniques"
2285
2285
  ],
2286
2286
  "signature": "+DWL+I6VipjzZpvjZptUNk7jrCYhOBQqFy1ejThc4Gdr2BJ5vWYuRWclUUd4RhC58iv4S3NAgatuZOcXTOkRCg==",
2287
- "signed_at": "2026-09-29T06:54:33.366Z"
2287
+ "signed_at": "2026-09-30T10:32:26.546Z"
2288
2288
  },
2289
2289
  {
2290
2290
  "name": "incident-response-playbook",
@@ -2359,7 +2359,7 @@
2359
2359
  "NYDFS 23 NYCRR 500.17 amendments tightening ransom-payment 24h disclosure operationalization"
2360
2360
  ],
2361
2361
  "signature": "GXEqUic3Dg65QODe/2+AA5PJ2gkRLf39/AbFaZDt19GzknfhHlBxQf7LrpDX2S87JKPHzHPShRAFRxJSW90LAA==",
2362
- "signed_at": "2026-09-29T06:54:33.367Z"
2362
+ "signed_at": "2026-09-30T10:32:26.547Z"
2363
2363
  },
2364
2364
  {
2365
2365
  "name": "ransomware-response",
@@ -2439,7 +2439,7 @@
2439
2439
  ],
2440
2440
  "last_threat_review": "2026-09-18",
2441
2441
  "signature": "ULB2eBGPwh5vgOTK9m9i7Do25p48npVDpyDqjRYOfUUHoYvXkG+XxF0DdJLrs27ngQ2TyETn6X9xKp4310RGBA==",
2442
- "signed_at": "2026-09-29T06:54:33.367Z"
2442
+ "signed_at": "2026-09-30T10:32:26.547Z"
2443
2443
  },
2444
2444
  {
2445
2445
  "name": "email-security-anti-phishing",
@@ -2507,7 +2507,7 @@
2507
2507
  ],
2508
2508
  "last_threat_review": "2026-05-18",
2509
2509
  "signature": "FVBn4ex2qPIo9SHMVJ6tntoz4tVwjbIq3m6wDjjZyv2JODlS+90GBYCOkNamxxkmw/6de6SMs0YHQiF/xjo/DQ==",
2510
- "signed_at": "2026-09-29T06:54:33.368Z"
2510
+ "signed_at": "2026-09-30T10:32:26.547Z"
2511
2511
  },
2512
2512
  {
2513
2513
  "name": "age-gates-child-safety",
@@ -2582,7 +2582,7 @@
2582
2582
  "US state adult-site age-verification laws — 19+ states by mid-2026 (TX HB 18 upheld by SCOTUS June 2025 in Free Speech Coalition v. Paxton); track ongoing challenges in remaining states"
2583
2583
  ],
2584
2584
  "signature": "rlHzyruu9OhkcVrYfHnMJvQf9z3rnDh5VbDQr4qCmqFXTzPqyQz9u1GvKdNiElSNXv3Qv3afvUFsvcHxOxX4DQ==",
2585
- "signed_at": "2026-09-29T06:54:33.368Z"
2585
+ "signed_at": "2026-09-30T10:32:26.548Z"
2586
2586
  },
2587
2587
  {
2588
2588
  "name": "cloud-iam-incident",
@@ -2662,7 +2662,7 @@
2662
2662
  ],
2663
2663
  "last_threat_review": "2026-09-28",
2664
2664
  "signature": "JzYf4O9qu4rJV3FZFDl0u/PgBqhwvrlJt8SM9lAEcJf8ZpGKaCmQ2cYIKZ94zzxH6G1KiUADuHUMCXEYJLAPDA==",
2665
- "signed_at": "2026-09-29T06:54:33.369Z",
2665
+ "signed_at": "2026-09-30T10:32:26.548Z",
2666
2666
  "forward_watch": [
2667
2667
  "AWS IAM Identity Center session-policy refresh and step-up-on-admin enforcement (anticipated 2026-H2 release)",
2668
2668
  "GCP Workload Identity Federation principal-set attribute mapping tightening (post-2026 Q3 Federation hardening guide)",
@@ -2756,7 +2756,7 @@
2756
2756
  ],
2757
2757
  "last_threat_review": "2026-09-28",
2758
2758
  "signature": "RqfshMse1fZQ5oeObQypeRFtc0kioh1DdqICbFSX2OiAjku2JJFj90ld7Hhal86o0DtSJ0bSA8uoQquJYbUbCQ==",
2759
- "signed_at": "2026-09-29T06:54:33.370Z",
2759
+ "signed_at": "2026-09-30T10:32:26.549Z",
2760
2760
  "forward_watch": [
2761
2761
  "Entra ID conditional access evolution post-Midnight Blizzard — Microsoft's 2025-2026 commitments on legacy-tenant MFA enforcement and OAuth-app consent gating",
2762
2762
  "Okta IPSIE (Interoperability Profile for Secure Identity in the Enterprise) OpenID Foundation working-group output and adoption timeline",
@@ -2828,7 +2828,7 @@
2828
2828
  "d3fend_refs": [],
2829
2829
  "last_threat_review": "2026-06-02",
2830
2830
  "signature": "dOFgANMtHm64uSFRjMfeA0fWZS9ISwMTt59I2CbjaF1PgTtbtb8yg/f+3pC4eXcXIET0KPGxRRmENycfIz3rAw==",
2831
- "signed_at": "2026-09-29T06:54:33.370Z"
2831
+ "signed_at": "2026-09-30T10:32:26.549Z"
2832
2832
  },
2833
2833
  {
2834
2834
  "name": "mail-server-hardening",
@@ -2887,7 +2887,7 @@
2887
2887
  "d3fend_refs": [],
2888
2888
  "last_threat_review": "2026-06-02",
2889
2889
  "signature": "5x8RGzKo/T2IbYnFkYMZs16ThD9a5FEdrKHsnwvtMuGxdyyfZsKNIJVWw2I8O0um0UOXwwkl17Z3zhch26ymCQ==",
2890
- "signed_at": "2026-09-29T06:54:33.370Z"
2890
+ "signed_at": "2026-09-30T10:32:26.549Z"
2891
2891
  },
2892
2892
  {
2893
2893
  "name": "network-trust",
@@ -2943,7 +2943,7 @@
2943
2943
  "d3fend_refs": [],
2944
2944
  "last_threat_review": "2026-06-02",
2945
2945
  "signature": "C/RdmnGjXKreKiGkwJfUESIxUwQzisGJ9A7KBdE9cBRBHBYHkAcScV89L+Z3kOG032Qax6LeIQai7Lr3nrQBCA==",
2946
- "signed_at": "2026-09-29T06:54:33.371Z"
2946
+ "signed_at": "2026-09-30T10:32:26.549Z"
2947
2947
  },
2948
2948
  {
2949
2949
  "name": "audit-log-integrity",
@@ -2998,7 +2998,7 @@
2998
2998
  "d3fend_refs": [],
2999
2999
  "last_threat_review": "2026-06-02",
3000
3000
  "signature": "T7sFREX7P2QYrau+eTNXKkrU2aJ91dsX6GQWVJeexJ9SqbyFHtAnIKo4r6F6VdELADyVjaRXKk/fLuyUaCkGBA==",
3001
- "signed_at": "2026-09-29T06:54:33.371Z"
3001
+ "signed_at": "2026-09-30T10:32:26.550Z"
3002
3002
  },
3003
3003
  {
3004
3004
  "name": "self-update-integrity",
@@ -3052,7 +3052,7 @@
3052
3052
  "d3fend_refs": [],
3053
3053
  "last_threat_review": "2026-06-02",
3054
3054
  "signature": "OKVmNgVoGGDS8kwezWrtFk62xgYkU3B7lhGRd7xO7wfSQPbETk+aeLmkpoS8sTryzrAg7JYhJ0qDofbSQpOTCg==",
3055
- "signed_at": "2026-09-29T06:54:33.371Z"
3055
+ "signed_at": "2026-09-30T10:32:26.550Z"
3056
3056
  },
3057
3057
  {
3058
3058
  "name": "multitenancy-isolation",
@@ -3110,7 +3110,7 @@
3110
3110
  "d3fend_refs": [],
3111
3111
  "last_threat_review": "2026-06-02",
3112
3112
  "signature": "MWDxwIfhUvGAet5F1LBJnBApTBHafuW+NwysdaSYh8DmPM70bcg6v7f7qwv+zADXYc1LHdF4FoqiAcFy8TA7DA==",
3113
- "signed_at": "2026-09-29T06:54:33.372Z"
3113
+ "signed_at": "2026-09-30T10:32:26.550Z"
3114
3114
  },
3115
3115
  {
3116
3116
  "name": "decompression-dos",
@@ -3168,7 +3168,7 @@
3168
3168
  "d3fend_refs": [],
3169
3169
  "last_threat_review": "2026-06-02",
3170
3170
  "signature": "iDsEYQo/OWYUI8TbBNsS9A60Kt5XEcZ1WYKg52wuSAzgctn4W68dgMeaoNbPXq2Bon1amFKIH5uxUtU8q7I/Dw==",
3171
- "signed_at": "2026-09-29T06:54:33.372Z"
3171
+ "signed_at": "2026-09-30T10:32:26.550Z"
3172
3172
  },
3173
3173
  {
3174
3174
  "name": "log-injection-telemetry",
@@ -3223,7 +3223,7 @@
3223
3223
  "d3fend_refs": [],
3224
3224
  "last_threat_review": "2026-06-02",
3225
3225
  "signature": "1Qn1LUzL2qCvG+jEaif7uMVL/cDwyMVueZAhu5uSroQzfw1V5X2vm/t69fExGKoLnVdkfta71aZWI4Gz0tfiDQ==",
3226
- "signed_at": "2026-09-29T06:54:33.372Z"
3226
+ "signed_at": "2026-09-30T10:32:26.550Z"
3227
3227
  },
3228
3228
  {
3229
3229
  "name": "privacy-consent-ops",
@@ -3278,11 +3278,11 @@
3278
3278
  "d3fend_refs": [],
3279
3279
  "last_threat_review": "2026-06-02",
3280
3280
  "signature": "UIzZfFwZP/7Gg5IdwQQTv6oDZyzjID8lXWYL1SgiSu4JRPJYzt/vqQO1ztfEaaRdbZBi0Vao3td2aFixnhw6AQ==",
3281
- "signed_at": "2026-09-29T06:54:33.373Z"
3281
+ "signed_at": "2026-09-30T10:32:26.551Z"
3282
3282
  }
3283
3283
  ],
3284
3284
  "manifest_signature": {
3285
3285
  "algorithm": "Ed25519",
3286
- "signature_base64": "RF6c9zrUGSKZFl60JBDPL4dzddePj1hT1X+F5qLdPwaKgfukSNNXUb3++b6bsZk6wzWVspXfFyxxjQfiOaiYAQ=="
3286
+ "signature_base64": "3andaihAjaRAIL4RO5fTRajdZc5vEDrB1bW2etTRb8ECA4heFFIhgmPlLV1Ib1CCFSumoJYZXJnJZXr//YQABQ=="
3287
3287
  }
3288
3288
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/exceptd-skills",
3
- "version": "0.21.15",
3
+ "version": "0.21.16",
4
4
  "description": "AI security skills grounded in mid-2026 threat reality, not stale framework documentation. 51 skills, 11 catalogs (1759 CVEs / 241 CWEs / 794 ATT&CK + ICS / 174 ATLAS / 468 D3FEND / 8889 RFCs), 35 jurisdictions, 10-class catalog gap detector + budget gate, real XML parser + canonical-form diff + content-pattern regression detection, Ed25519-signed.",
5
5
  "keywords": [
6
6
  "ai-security",
package/sbom.cdx.json CHANGED
@@ -1,22 +1,22 @@
1
1
  {
2
2
  "bomFormat": "CycloneDX",
3
3
  "specVersion": "1.6",
4
- "serialNumber": "urn:uuid:f0d7e722-9418-4c65-abfa-825ae68d170e",
4
+ "serialNumber": "urn:uuid:d7447d56-ce40-4f85-9d96-520994216720",
5
5
  "version": 1,
6
6
  "metadata": {
7
- "timestamp": "2026-09-28T00:00:00.000Z",
7
+ "timestamp": "2026-09-29T00:00:00.000Z",
8
8
  "tools": [
9
9
  {
10
10
  "vendor": "blamejs",
11
11
  "name": "scripts/refresh-sbom.js",
12
- "version": "0.21.15"
12
+ "version": "0.21.16"
13
13
  }
14
14
  ],
15
15
  "component": {
16
- "bom-ref": "pkg:npm/@blamejs/exceptd-skills@0.21.15",
16
+ "bom-ref": "pkg:npm/@blamejs/exceptd-skills@0.21.16",
17
17
  "type": "application",
18
18
  "name": "@blamejs/exceptd-skills",
19
- "version": "0.21.15",
19
+ "version": "0.21.16",
20
20
  "description": "AI security skills grounded in mid-2026 threat reality, not stale framework documentation. 51 skills, 11 catalogs (1759 CVEs / 241 CWEs / 794 ATT&CK + ICS / 174 ATLAS / 468 D3FEND / 8889 RFCs), 35 jurisdictions, 10-class catalog gap detector + budget gate, real XML parser + canonical-form diff + content-pattern regression detection, Ed25519-signed.",
21
21
  "licenses": [
22
22
  {
@@ -25,17 +25,17 @@
25
25
  }
26
26
  }
27
27
  ],
28
- "purl": "pkg:npm/%40blamejs/exceptd-skills@0.21.15",
28
+ "purl": "pkg:npm/%40blamejs/exceptd-skills@0.21.16",
29
29
  "hashes": [
30
30
  {
31
31
  "alg": "SHA-256",
32
- "content": "2ecf804c3bf533da1b2cf8b4e3daa604709fdaab17840fc26d1e1c1e70015f14"
32
+ "content": "8f94c4d5bcc8821d45ded4dcec65c625e93f375d666c813ce0d254e92eabc5c8"
33
33
  }
34
34
  ],
35
35
  "externalReferences": [
36
36
  {
37
37
  "type": "distribution",
38
- "url": "https://www.npmjs.com/package/@blamejs/exceptd-skills/v/0.21.15"
38
+ "url": "https://www.npmjs.com/package/@blamejs/exceptd-skills/v/0.21.16"
39
39
  },
40
40
  {
41
41
  "type": "vcs",
@@ -124,11 +124,11 @@
124
124
  "hashes": [
125
125
  {
126
126
  "alg": "SHA-256",
127
- "content": "aaa45a410b82a8d3708df333280de3f7201d3ee1a5387bd5a51b12db105f30d4"
127
+ "content": "6d59921d08dc1a0658477c5b0017050e37a50eff5ead3a70cde13f74a5a8a244"
128
128
  },
129
129
  {
130
130
  "alg": "SHA3-512",
131
- "content": "508d20fec91b4c4c2918dc6c5fa0111418f107e0f066863526d43b18d49c7ec8a3ced9466c725fc1d383b2175ce8b9c4dc02714148436f2734e58d22b2f942cf"
131
+ "content": "6e1f30b18a7ab313d8a633e0f38644f6e8e00c47e5f372ca292064a50a100f194011f7ac3cad275601694273fdc539d692631ddc5cf412cd2f37bfef45e3b21b"
132
132
  }
133
133
  ]
134
134
  },
@@ -1954,11 +1954,11 @@
1954
1954
  "hashes": [
1955
1955
  {
1956
1956
  "alg": "SHA-256",
1957
- "content": "53635b9883d8da988e78ae3ac7609fde37fd9d5b8c2bec438041740f5c5ba472"
1957
+ "content": "f0287eb67bdf0b76c3ea72327ff5a9e117a191248aebaa7f2c41c3849ab10a14"
1958
1958
  },
1959
1959
  {
1960
1960
  "alg": "SHA3-512",
1961
- "content": "1b1392e66ac787173916986fed9774cf3f8f9ad8c4ceb76f4884ae860a69bec10abd9cbf469a5db7a999b4712698830f6b28a15222a4e776a97077ed4f0cad75"
1961
+ "content": "94461fa5cf19fd37a41e5e839616aed28eb7160768e80859b3c77c6e18a61d9efd39bc4e1ccc01c0fcfe4ed44f68c915f760d4c8689b9dad22edb0ce7c258e45"
1962
1962
  }
1963
1963
  ]
1964
1964
  },
@@ -2074,11 +2074,11 @@
2074
2074
  "hashes": [
2075
2075
  {
2076
2076
  "alg": "SHA-256",
2077
- "content": "aeb04f3de193ce0a43956b566e846d21098e24e6de4f7dced4ec21078a8d2fd6"
2077
+ "content": "337787992a8a8d1e495fe8ee51f6d2586e9b6fc02efb17175e3a90ef1ef0aaea"
2078
2078
  },
2079
2079
  {
2080
2080
  "alg": "SHA3-512",
2081
- "content": "234919ce2ead2e3a1b8382dbc05eaff95d764b05fa255c48c761512e572e81727bbb2a479432919ada841b572994ae9a69dd8eb8efd0e374de39733783716904"
2081
+ "content": "0e377fb752f09c575cb2c48a6d212fe24a987a304ed2f745d479bc68f0fc8fc37d64fa73e831ea0ce6b03109da5f43f4e769a45562623fc8f8e7778bf90a9991"
2082
2082
  }
2083
2083
  ]
2084
2084
  },
@@ -2539,11 +2539,11 @@
2539
2539
  "hashes": [
2540
2540
  {
2541
2541
  "alg": "SHA-256",
2542
- "content": "f068f3ce319f7f302a6f33924ab77140b379b92d1f54ddaab5aa74ed26813164"
2542
+ "content": "2c513a828bb76d0cb25009ff5d45e0663210bfa56bb0319890f5f383ff8d29c7"
2543
2543
  },
2544
2544
  {
2545
2545
  "alg": "SHA3-512",
2546
- "content": "19da9c54538c46136460bf06260c29e95e824b467112c4d0a7176060e221e66060458db2a2a4b80a08480df7aefc450ce9589ab8691544eb98211dc3f2e13198"
2546
+ "content": "ea328da6fca702a680cb00ce4729d644002190311a80328b5c97b661834822c747882a6d3f3bd73f1fc993d1c2d79befe81f67a5ea3d3c36a224a17fa5841c71"
2547
2547
  }
2548
2548
  ]
2549
2549
  },
@@ -10,7 +10,23 @@
10
10
  * cell gives one), a public-exploit column (Yes/No), and an AI column. An
11
11
  * "AI-Discovered" column is compared with ai_discovered; an AI column that also
12
12
  * covers acceleration, enablement or weaponization is compared with
13
- * ai_discovered OR ai_assisted_weaponization. A prose line that names exactly one
13
+ * ai_discovered OR ai_assisted_weaponization. The Active Exploitation, Patch and
14
+ * Live Patch columns (the headers columnKind lists) are compared only when no
15
+ * cell in the row names another CVE, the row's CVE is under an identifier
16
+ * header (ID_HEADER_PART, with no ROLE_HEADER word) or, in a table with no
17
+ * identifier column, in the first column under a header that names no CVE and
18
+ * no role, and no other row in the same section
19
+ * (from one "#" or "##" heading outside a fenced code block to the next) has
20
+ * that CVE as its own row CVE (a row that names no CVE and leaves blank the
21
+ * cell under the row above's CVE continues that row): Active Exploitation in the
22
+ * STATE_FORMS,
23
+ * NONE_FORMS or NOT_CONFIRMED_FORMS wording ("No confirmed exploitation"
24
+ * differs only from Confirmed, and None agrees with Theoretical); Patch in one
25
+ * of the PATCH_FORMS wordings, or as a Yes alone or followed by a PATCH_FORMS
26
+ * patch statement (a bare Yes only in a plain Patch column); Live Patch as a Yes
27
+ * or No that ends the cell or is followed by a comma, semicolon, period, colon,
28
+ * dash or parenthesis, with no qualifying word or second answer after it. A
29
+ * prose line that names exactly one
14
30
  * catalog CVE is compared on "RWEP <n>", "CVSS <n.n>", "KEV-listed <date>" and
15
31
  * "not KEV-listed". On any line, the parentheses that follow a CVE id are read
16
32
  * as that CVE's values, so a list such as "CVE-A (90), CVE-B (Dirty Frag, 38,
@@ -63,12 +79,137 @@ function columnKind(header) {
63
79
  if (/\bpoc\b|public exploit/.test(h)) return "poc";
64
80
  if (/^ai[- ]discovered$/.test(h)) return "ai_discovered";
65
81
  if (/^ai\b/.test(h)) return "ai_any";
82
+ // The exploitation, patch and live-patch columns are read only under these
83
+ // headers. Any other header ("Patch Reboot Required?", "Patch Downtime",
84
+ // "Active Exploitation Actor", "Patch / Live Patch") is not compared.
85
+ if (/^active exploitation$/.test(h)) return "active";
86
+ // "Live Patch" or "Live-Patch"; a bare "Livepatch" column names Canonical
87
+ // Livepatch alone in a per-vendor matrix.
88
+ if (/^live[- ]patch(?:able|es)?(?:\s+availab(?:le|ility))?$/.test(h)) return "live_patch";
89
+ if (/^patch(?:es)?(?:\s+availab(?:le|ility))?$/.test(h)) return "patch";
90
+ if (/^patch(?:es)?\s*\/\s*(?:mitigation|remediation|workaround)s?(?:\s+availab(?:le|ility))?$/.test(h)) return "patch_or_mitigation";
66
91
  return null;
67
92
  }
68
93
 
94
+ /**
95
+ * The vendor-patch phrase: "Vendor patch", "Vendor updates", "Vendor IDE update".
96
+ * The one qualifier word is a closed list, so a status word in that slot
97
+ * ("Vendor declined fix", "Vendor never patches") never forms the phrase.
98
+ */
99
+ const PHRASE = "vendor(?: (?:ide|saas|firmware|security|product|os|kernel|browser|app|agent|server|client|cloud|library|platform))? (?:patch|update|fix)(?:e?s)?";
100
+ // A word that can qualify or reverse the statement it follows ("Yes (kpatch
101
+ // pending)", "Vendor patch + hardening still pending"). A live-patch cell whose text after
102
+ // its leading Yes or No holds one, and a cell in a qualifiable PATCH_FORMS
103
+ // wording that holds one, is not compared.
104
+ const QUALIFYING = /\b(?:not|no|none|never|n\/a|tbd|tba|eta|pending|planned|expected|unavailable|unreleased|awaiting|awaited|outstanding|delayed|forthcoming|upcoming|coming|unknown|soon|later|next|progress|development|scheduled|future|until|needed|required|necessary|still|yet|q[1-4]|if|unless|except|but|although|however|withdrawn|revoked|reverted|pulled|superseded)\b/i;
105
+
106
+ /**
107
+ * The patch cells the gate reads, each matched against the whole cell, with the
108
+ * value it reads and whether a qualifying word leaves it not compared. Any other
109
+ * wording is not compared.
110
+ */
111
+ const PATCH_FORMS = [
112
+ // "Vendor patch", "Vendor updates available", "Vendor patches shipped 2099-01-01".
113
+ [new RegExp(`^${PHRASE}(?:\\s+(?:is\\s+|are\\s+)?(?:available|released|shipped|published|issued|applied)(?:\\s+\\d{4}-\\d{2}-\\d{2})?)?\\s*\\.?$`, "i"), true, false], // allow:dynamic-regex — built from the static PHRASE literal
114
+ // "Vendor patch + config hardening", "Vendor IDE update + manifest signing".
115
+ // Each "+" item starts and ends on a non-space character and stops at "+", ";",
116
+ // "," or a parenthesis, so the spaces around a "+" can only match `\s*` and a
117
+ // failing match backtracks at most linearly.
118
+ [new RegExp(`^${PHRASE}(?:\\s*\\+\\s*[^+;,()\\s](?:[^+;,()]*[^+;,()\\s])?)+$`, "i"), true, true], // allow:dynamic-regex — static PHRASE
119
+ // "Mitigation + vendor patch".
120
+ [new RegExp(`^[a-z][a-z -]{0,40}\\+\\s*${PHRASE}\\s*\\.?$`, "i"), true, true], // allow:dynamic-regex — static PHRASE
121
+ // "Vendor patch pending", "Vendor fix unavailable", "Vendor update not yet available".
122
+ [new RegExp(`^${PHRASE}\\s+(?:is\\s+|are\\s+)?(?:unavailable|unreleased|pending|planned|not (?:yet )?(?:available|released|shipped|published|issued))\\s*\\.?$`, "i"), false, false], // allow:dynamic-regex — static PHRASE
123
+ // "No", "None", "No patch", "No vendor fix available", "No vendor patch (product EoL)".
124
+ [new RegExp(`^(?:no|none)(?:\\s+(?:${PHRASE}|patch(?:es)?|fix(?:es)?|updates?)(?:\\s+(?:is\\s+|are\\s+)?available)?)?(?:\\s+\\((?:product\\s+)?(?:eol|end[- ]of[- ]life|unsupported)\\))?\\s*\\.?$`, "i"), false, false], // allow:dynamic-regex — static PHRASE
125
+ // "Mitigation only; no vendor patch", "Workaround only, no vendor fix available".
126
+ [new RegExp(`^(?:mitigations?|workarounds?|compensating controls?)(?:\\s+only)?\\s*[;,]\\s*no\\s+${PHRASE}(?:\\s+available)?\\s*\\.?$`, "i"), false, false], // allow:dynamic-regex — static PHRASE
127
+ ];
128
+
129
+ /**
130
+ * The value of the first PATCH_FORMS entry that matches the whole cell, or null
131
+ * when no entry matches or when that entry is qualifiable and the cell holds a
132
+ * QUALIFYING word.
133
+ */
134
+ function patchForm(cell) {
135
+ for (const [re, value, qualifiable] of PATCH_FORMS) {
136
+ if (re.test(cell)) return qualifiable && QUALIFYING.test(cell) ? null : value;
137
+ }
138
+ return null;
139
+ }
140
+
141
+ /**
142
+ * A leading Yes or No that ends the cell or is followed by a comma, semicolon,
143
+ * period, colon, dash or parenthesis ("Yes (kpatch/livepatch)", "No — kpatch
144
+ * RHEL-only"), or null when another word or a hyphen follows it ("No-reboot
145
+ * hotpatch", "No reboot"), when a qualifying word comes after it ("Yes (kpatch
146
+ * pending)"), or when a later Yes follows it ("No (Ubuntu); Yes (RHEL
147
+ * kpatch)").
148
+ */
149
+ function plainAnswer(cell) {
150
+ const m = /^(yes|no)(?=\s*(?:$|[,;.:(—–]|-\s))/i.exec(cell);
151
+ if (!m) return null;
152
+ const rest = cell.slice(m[0].length);
153
+ if (QUALIFYING.test(rest) || /\byes\b/i.test(rest)) return null;
154
+ return m[1].toLowerCase() === "yes";
155
+ }
156
+
157
+ /**
158
+ * A Patch column cell's claim. A Yes reads as a patch when a PATCH_FORMS patch
159
+ * statement follows it ("Yes, vendor patch"), or when it is the whole cell in a
160
+ * plain Patch column; in a "Patch / Mitigation", "Patch / Remediation" or "Patch
161
+ * / Workaround" column a bare Yes can answer for the mitigation and is not
162
+ * compared. A Yes with any other text after it ("Yes (workaround)", "Yes, if on
163
+ * 2.x") is not compared. Any other cell is read only when the whole cell is one
164
+ * of PATCH_FORMS.
165
+ */
166
+ function patchCellValue(cell, kind) {
167
+ if (/^yes\b/i.test(cell)) {
168
+ // "Yes (vendor IDE update)" is read on the text inside the parentheses.
169
+ const wrapped = /^yes\s*\(([^()]*)\)\s*\.?$/i.exec(cell);
170
+ const rest = wrapped ? wrapped[1].trim() : cell.replace(/^yes\b[\s,;:—–-]*/i, "");
171
+ if (/^\.?$/.test(rest)) return kind === "patch" ? true : null;
172
+ return patchForm(rest) === true ? true : null;
173
+ }
174
+ return patchForm(cell);
175
+ }
176
+
177
+ /** The cells that read as no exploitation: "None", "None observed", "No exploitation observed in the wild". */
178
+ const NONE_FORMS = /^(?:none|no)(?:\s+(?:in[- ]the[- ]wild\s+)?exploitation)?(?:\s+(?:observed|recorded|reported|seen|detected))?(?:\s+in[- ]the[- ]wild)?\s*\.?$/i;
179
+ /** The cells that deny confirmation: "No confirmed exploitation", "No exploitation confirmed", "None known", "No known in-the-wild use". */
180
+ const NOT_CONFIRMED_FORMS = /^(?:(?:no|none)\s+(?:yet\s+)?(?:publicly\s+)?(?:confirmed|known)(?:\s+(?:in[- ]the[- ]wild\s+)?(?:exploitation|use|attacks?))?(?:\s+in[- ]the[- ]wild)?|no\s+(?:in[- ]the[- ]wild\s+)?exploitation\s+(?:yet\s+)?(?:publicly\s+)?(?:confirmed|known))\s*\.?$/i;
181
+ /** The cells that name a state: "Confirmed", "Confirmed mass exploitation", "Confirmed exploitation 2024", "Theoretical only". */
182
+ const STATE_FORMS = /^(confirmed|suspected|unknown|theoretical)(?:\s+(?:mass\s+|active\s+)?exploitation)?(?:\s+\d{4}(?:\s*[-–]\s*\d{4})?)?(?:\s+only)?\s*\.?$/i;
183
+
184
+ /**
185
+ * Active-exploitation cell, read only when the whole cell is one of STATE_FORMS,
186
+ * NONE_FORMS or NOT_CONFIRMED_FORMS. "No confirmed exploitation" (or "None
187
+ * known") denies confirmation, not exploitation: it is true of every state but
188
+ * confirmed, so it returns "not_confirmed", which differs only from a confirmed
189
+ * entry. Any other wording ("Confirmed PoC", "No confirmed mass exploitation",
190
+ * "Suspected (supply-chain)", "No data") is not compared.
191
+ */
192
+ function exploitationValue(cell) {
193
+ if (NOT_CONFIRMED_FORMS.test(cell)) return "not_confirmed";
194
+ if (NONE_FORMS.test(cell)) return "none";
195
+ const m = STATE_FORMS.exec(cell);
196
+ return m ? m[1].toLowerCase() : null;
197
+ }
198
+
69
199
  /** A cell's text with Markdown emphasis and code marks removed. */
70
200
  const plain = (cell) => cell.replace(/[*_`]/g, "").trim();
71
201
 
202
+ /** The column kinds compared only on a row that names a single CVE. */
203
+ const ROW_CVE_KINDS = new Set(["active", "patch", "patch_or_mitigation", "live_patch"]);
204
+ /**
205
+ * One "/" part of an identifier column's header, with parenthetical notes
206
+ * removed: CVE or CVEs, optionally preceded by Evidence and followed by ID or
207
+ * Class ("CVE", "CVE ID", "Evidence CVE", "CVE Class", "CVE (if any)").
208
+ */
209
+ const ID_HEADER_PART = /^(?:evidence\s+)?cves?(?:\s+(?:id|class))?$/i;
210
+ /** A header word that marks a column as describing or citing a case, not naming the row. */
211
+ const ROLE_HEADER = /\b(?:related|siblings?|similar|see[- ]also|examples?|e\.g|analog(?:ue)?s?|pocs?|demos?|exploits?|notes?|comments?|rationales?|reasons?|descriptions?|details?)\b/i;
212
+
72
213
  function compareCell(kind, raw, e, say) {
73
214
  const cell = plain(raw);
74
215
  if (kind === "cvss") {
@@ -97,6 +238,20 @@ function compareCell(kind, raw, e, say) {
97
238
  const v = aiValue(cell, /^ai[- ](?:discovered|assisted|accelerated|enabled|weaponi[sz]ed)\b/i);
98
239
  const expected = Boolean(e.ai_discovered) || Boolean(e.ai_assisted_weaponization);
99
240
  if (v !== null && v !== expected) say(`AI "${cell.slice(0, 40)}", catalog ai_discovered ${e.ai_discovered} / ai_assisted_weaponization ${e.ai_assisted_weaponization}`);
241
+ } else if (kind === "active") {
242
+ const v = exploitationValue(cell);
243
+ const actual = String(e.active_exploitation || "").toLowerCase();
244
+ // The catalog's theoretical state is a public PoC with no exploitation seen,
245
+ // so a None cell agrees with it, and a Theoretical cell with a none entry.
246
+ const quiet = (s) => s === "none" || s === "theoretical";
247
+ const differs = v === "not_confirmed" ? actual === "confirmed" : v !== actual && !(quiet(v) && quiet(actual));
248
+ if (v && actual && differs) say(`active exploitation "${cell.slice(0, 40)}", catalog ${actual}`);
249
+ } else if (kind === "patch" || kind === "patch_or_mitigation") {
250
+ const v = patchCellValue(cell, kind);
251
+ if (v !== null && v !== Boolean(e.patch_available)) say(`patch "${cell.slice(0, 40)}", catalog patch_available ${e.patch_available}`);
252
+ } else if (kind === "live_patch") {
253
+ const v = plainAnswer(cell);
254
+ if (v !== null && v !== Boolean(e.live_patch_available)) say(`live patch "${cell.slice(0, 40)}", catalog live_patch_available ${e.live_patch_available}`);
100
255
  }
101
256
  }
102
257
 
@@ -216,7 +371,34 @@ function compareFactorRow(cells, rawHeader, e, say) {
216
371
  function checkSkill(file, catalog) {
217
372
  const seen = new Set();
218
373
  const failures = [];
219
- const push = (msg) => { if (!seen.has(msg)) { seen.add(msg); failures.push(msg); } };
374
+ // Each failure carries the order in which it was found, so failures held back
375
+ // until the section ends are still reported in line order.
376
+ let seq = 0;
377
+ const push = (msg, at = seq++) => { if (!seen.has(msg)) { seen.add(msg); failures.push([at, msg]); } };
378
+ // Exploitation, patch and live-patch failures are held until the section ends
379
+ // (the next "#" or "##" heading, or the end of the file) and kept only for a
380
+ // CVE that is the row CVE of exactly one row in that section. A per-distribution
381
+ // or per-version table ("RHEL 9 | Yes", "Ubuntu 24.04 | No"), one table or one
382
+ // "###" sub-heading per distribution, or a continuation row that leaves the CVE
383
+ // cell blank answers for one row at a time, not for the CVE.
384
+ let held = [];
385
+ let rowCves = new Map();
386
+ let lastRowCve = null;
387
+ let lastRowCol = -1;
388
+ const flush = () => {
389
+ for (const [at, cve, msg] of held) if (rowCves.get(cve) === 1) push(msg, at);
390
+ held = [];
391
+ rowCves = new Map();
392
+ lastRowCve = null;
393
+ };
394
+ // A "#" line inside a fenced code block does not end the section for the
395
+ // exploitation, patch and live-patch row count. A fence closes only on a line
396
+ // of its own character, at least as long as the opening run, with nothing
397
+ // after it. A nested code block that uses the other character or a shorter run
398
+ // leaves the outer fence open; a bare run of the same character at least as
399
+ // long as the outer one closes it.
400
+ let fence = null;
401
+ const countRow = (cve) => rowCves.set(cve, (rowCves.get(cve) || 0) + 1);
220
402
  let compared = 0;
221
403
  let header = null;
222
404
  let rawHeader = null;
@@ -224,33 +406,68 @@ function checkSkill(file, catalog) {
224
406
  const lines = fs.readFileSync(file, "utf8").split(/\r?\n/);
225
407
  lines.forEach((line, i) => {
226
408
  const where = `${path.relative(ROOT, file).replace(/\\/g, "/")}:${i + 1}`;
409
+ const fenceLine = /^\s*(`{3,}|~{3,})(.*)$/s.exec(line);
410
+ if (fenceLine && !fence) fence = fenceLine[1];
411
+ else if (fenceLine && fenceLine[1][0] === fence[0] && fenceLine[1].length >= fence.length && !fenceLine[2].trim()) fence = null;
227
412
  if (/^#{1,6}\s/.test(line)) {
413
+ // A "###" or deeper heading ("### RHEL 9", "### Ubuntu 24.04") stays in its
414
+ // parent section for the row count.
415
+ if (!fence && /^#{1,2}\s/.test(line)) flush();
228
416
  const headingIds = uniqueCves(line);
229
417
  sectionCve = headingIds.length === 1 && catalog[headingIds[0]] ? headingIds[0] : null;
230
418
  }
231
419
  compared += compareParentheticals(line, catalog, (id, msg) => push(`${where} ${id}: ${msg}`));
232
420
  if (/^\s*\|/.test(line)) {
233
421
  const cells = splitRow(line);
234
- if (header === null) { rawHeader = cells; header = cells.map(columnKind); return; }
422
+ if (header === null) { rawHeader = cells; header = cells.map(columnKind); lastRowCve = null; return; }
235
423
  if (isDivider(cells)) return;
424
+ // A row that names no CVE and leaves blank the cell under the row above's
425
+ // CVE continues that row.
426
+ const continues = lastRowCve && !plain(cells[lastRowCol] || "") && uniqueCves(line).length === 0;
236
427
  if (sectionCve && uniqueCves(line).length === 0) {
428
+ if (continues) countRow(lastRowCve);
429
+ else lastRowCve = null;
237
430
  if (compareFactorRow(cells, rawHeader, catalog[sectionCve], (msg) => push(`${where} ${sectionCve}: ${msg}`))) compared++;
238
431
  return;
239
432
  }
240
- const idCell = cells.find((c) => uniqueCves(c).length > 0) || "";
241
- if (/^(?:no|n\/a)\b/i.test(idCell) || /\bsee\s+CVE-/i.test(idCell)) return;
433
+ const idCol = cells.findIndex((c) => uniqueCves(c).length > 0);
434
+ const idCell = cells[idCol] || "";
435
+ if (/^(?:no|n\/a)\b/i.test(idCell) || /\bsee\s+CVE-/i.test(idCell)) { lastRowCve = null; return; }
242
436
  const ids = uniqueCves(idCell);
243
437
  if (ids.length > 1) {
438
+ lastRowCve = null;
244
439
  const known = ids.filter((id) => catalog[id]);
245
440
  const scored = header.some((kind, k) => (kind === "cvss" || kind === "rwep") && /^\d/.test(plain(cells[k] || "")));
246
441
  if (known.length && scored) push(`${where} ${ids.join(", ")}: ${UNATTRIBUTED}`);
247
442
  return;
248
443
  }
249
- if (ids.length !== 1 || !catalog[ids[0]]) return;
444
+ if (continues) { countRow(lastRowCve); return; }
445
+ if (ids.length !== 1 || !catalog[ids[0]]) { lastRowCve = null; return; }
250
446
  const e = catalog[ids[0]];
251
447
  compared++;
448
+ countRow(ids[0]);
449
+ lastRowCve = ids[0];
450
+ lastRowCol = idCol;
451
+ // The exploitation, patch and live-patch cells are read against the row's
452
+ // CVE only when no cell names another CVE and the CVE sits under an
453
+ // identifier header (ID_HEADER_PART), or, in a table with no identifier
454
+ // column, in the first column under a header that names no CVE and no role
455
+ // (ROLE_HEADER). A row whose CVE is a related
456
+ // case, cited in a PoC, notes, "Related CVE", "CVE (example)" or similar
457
+ // column, does not have them compared with that entry.
458
+ const single = cells.every((c) => uniqueCves(c).every((id) => id === ids[0]));
459
+ const norm = (h) => (h || "").replace(/[?*`_]/g, " ");
460
+ const idHeader = (h) => !ROLE_HEADER.test(h) && h.replace(/\([^)]*\)/g, " ").split("/").some((part) => ID_HEADER_PART.test(part.trim()));
461
+ const first = norm(rawHeader[0]);
462
+ const idColumn = rawHeader.some((h) => idHeader(norm(h)));
463
+ const named = idHeader(norm(rawHeader[idCol])) || (idCol === 0 && !idColumn && !/\bcves?\b/i.test(first) && !ROLE_HEADER.test(first));
252
464
  header.forEach((kind, k) => {
253
- if (kind && cells[k] !== undefined) compareCell(kind, cells[k], e, (msg) => push(`${where} ${ids[0]}: ${msg}`));
465
+ if (!kind || cells[k] === undefined) return;
466
+ if (!ROW_CVE_KINDS.has(kind)) {
467
+ compareCell(kind, cells[k], e, (msg) => push(`${where} ${ids[0]}: ${msg}`));
468
+ } else if (single && named) {
469
+ compareCell(kind, cells[k], e, (msg) => held.push([seq++, ids[0], `${where} ${ids[0]}: ${msg}`]));
470
+ }
254
471
  });
255
472
  return;
256
473
  }
@@ -266,7 +483,8 @@ function checkSkill(file, catalog) {
266
483
  compared++;
267
484
  compareProse(line, catalog[ids[0]], (msg) => push(`${where} ${ids[0]}: ${msg}`));
268
485
  });
269
- return { failures, compared };
486
+ flush();
487
+ return { failures: failures.sort((a, b) => a[0] - b[0]).map(([, msg]) => msg), compared };
270
488
  }
271
489
 
272
490
  function check(skillsDir, catalog) {