@blamejs/exceptd-skills 0.15.44 → 0.15.46
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/AGENTS.md +1 -1
- package/CHANGELOG.md +65 -39
- package/CONTEXT.md +1 -1
- package/README.md +5 -5
- package/bin/exceptd.js +166 -124
- package/data/_indexes/_meta.json +7 -7
- package/data/_indexes/activity-feed.json +8 -8
- package/data/_indexes/catalog-summaries.json +1 -1
- package/data/_indexes/section-offsets.json +41 -41
- package/data/_indexes/token-budget.json +32 -32
- package/data/cve-catalog.json +2 -2
- package/lib/flag-suggest.js +6 -10
- package/lib/lint-skills.js +18 -19
- package/lib/playbook-runner.js +11 -3
- package/lib/refresh-external.js +4 -2
- package/lib/source-osv.js +3 -1
- package/lib/validate-catalog-meta.js +6 -5
- package/lib/validate-cve-catalog.js +8 -8
- package/lib/validate-playbooks.js +14 -14
- package/manifest.json +47 -47
- package/orchestrator/index.js +26 -4
- package/orchestrator/scanner.js +1 -1
- package/package.json +1 -1
- package/sbom.cdx.json +50 -50
- package/scripts/check-test-count.js +11 -4
- package/skills/attack-surface-pentest/skill.md +6 -6
- package/skills/cloud-iam-incident/skill.md +2 -2
- package/skills/sector-financial/skill.md +1 -1
|
@@ -1865,7 +1865,7 @@
|
|
|
1865
1865
|
},
|
|
1866
1866
|
"attack-surface-pentest": {
|
|
1867
1867
|
"path": "skills/attack-surface-pentest/skill.md",
|
|
1868
|
-
"total_bytes":
|
|
1868
|
+
"total_bytes": 33158,
|
|
1869
1869
|
"total_lines": 388,
|
|
1870
1870
|
"frontmatter": {
|
|
1871
1871
|
"line_start": 1,
|
|
@@ -1888,25 +1888,25 @@
|
|
|
1888
1888
|
"normalized_name": "framework-lag-declaration",
|
|
1889
1889
|
"line": 106,
|
|
1890
1890
|
"byte_start": 6410,
|
|
1891
|
-
"byte_end":
|
|
1892
|
-
"bytes":
|
|
1891
|
+
"byte_end": 10953,
|
|
1892
|
+
"bytes": 4543,
|
|
1893
1893
|
"h3_count": 0
|
|
1894
1894
|
},
|
|
1895
1895
|
{
|
|
1896
1896
|
"name": "TTP Mapping (MITRE ATLAS v5.6.0 + MITRE ATT&CK v19.0)",
|
|
1897
1897
|
"normalized_name": "ttp-mapping",
|
|
1898
1898
|
"line": 124,
|
|
1899
|
-
"byte_start":
|
|
1900
|
-
"byte_end":
|
|
1901
|
-
"bytes":
|
|
1899
|
+
"byte_start": 10953,
|
|
1900
|
+
"byte_end": 13162,
|
|
1901
|
+
"bytes": 2209,
|
|
1902
1902
|
"h3_count": 0
|
|
1903
1903
|
},
|
|
1904
1904
|
{
|
|
1905
1905
|
"name": "Exploit Availability Matrix",
|
|
1906
1906
|
"normalized_name": "exploit-availability-matrix",
|
|
1907
1907
|
"line": 143,
|
|
1908
|
-
"byte_start":
|
|
1909
|
-
"byte_end":
|
|
1908
|
+
"byte_start": 13162,
|
|
1909
|
+
"byte_end": 15617,
|
|
1910
1910
|
"bytes": 2455,
|
|
1911
1911
|
"h3_count": 0
|
|
1912
1912
|
},
|
|
@@ -1914,17 +1914,17 @@
|
|
|
1914
1914
|
"name": "Analysis Procedure",
|
|
1915
1915
|
"normalized_name": "analysis-procedure",
|
|
1916
1916
|
"line": 160,
|
|
1917
|
-
"byte_start":
|
|
1918
|
-
"byte_end":
|
|
1919
|
-
"bytes":
|
|
1917
|
+
"byte_start": 15617,
|
|
1918
|
+
"byte_end": 25273,
|
|
1919
|
+
"bytes": 9656,
|
|
1920
1920
|
"h3_count": 4
|
|
1921
1921
|
},
|
|
1922
1922
|
{
|
|
1923
1923
|
"name": "Output Format",
|
|
1924
1924
|
"normalized_name": "output-format",
|
|
1925
1925
|
"line": 281,
|
|
1926
|
-
"byte_start":
|
|
1927
|
-
"byte_end":
|
|
1926
|
+
"byte_start": 25273,
|
|
1927
|
+
"byte_end": 29270,
|
|
1928
1928
|
"bytes": 3997,
|
|
1929
1929
|
"h3_count": 9
|
|
1930
1930
|
},
|
|
@@ -1932,8 +1932,8 @@
|
|
|
1932
1932
|
"name": "Compliance Theater Check",
|
|
1933
1933
|
"normalized_name": "compliance-theater-check",
|
|
1934
1934
|
"line": 363,
|
|
1935
|
-
"byte_start":
|
|
1936
|
-
"byte_end":
|
|
1935
|
+
"byte_start": 29270,
|
|
1936
|
+
"byte_end": 31586,
|
|
1937
1937
|
"bytes": 2316,
|
|
1938
1938
|
"h3_count": 0
|
|
1939
1939
|
},
|
|
@@ -1941,9 +1941,9 @@
|
|
|
1941
1941
|
"name": "Defensive Countermeasure Mapping (D3FEND)",
|
|
1942
1942
|
"normalized_name": "defensive-countermeasure-mapping",
|
|
1943
1943
|
"line": 377,
|
|
1944
|
-
"byte_start":
|
|
1945
|
-
"byte_end":
|
|
1946
|
-
"bytes":
|
|
1944
|
+
"byte_start": 31586,
|
|
1945
|
+
"byte_end": 33158,
|
|
1946
|
+
"bytes": 1572,
|
|
1947
1947
|
"h3_count": 0
|
|
1948
1948
|
}
|
|
1949
1949
|
]
|
|
@@ -2984,7 +2984,7 @@
|
|
|
2984
2984
|
},
|
|
2985
2985
|
"sector-financial": {
|
|
2986
2986
|
"path": "skills/sector-financial/skill.md",
|
|
2987
|
-
"total_bytes":
|
|
2987
|
+
"total_bytes": 50285,
|
|
2988
2988
|
"total_lines": 401,
|
|
2989
2989
|
"frontmatter": {
|
|
2990
2990
|
"line_start": 1,
|
|
@@ -3034,16 +3034,16 @@
|
|
|
3034
3034
|
"normalized_name": "analysis-procedure",
|
|
3035
3035
|
"line": 182,
|
|
3036
3036
|
"byte_start": 26352,
|
|
3037
|
-
"byte_end":
|
|
3038
|
-
"bytes":
|
|
3037
|
+
"byte_end": 35068,
|
|
3038
|
+
"bytes": 8716,
|
|
3039
3039
|
"h3_count": 10
|
|
3040
3040
|
},
|
|
3041
3041
|
{
|
|
3042
3042
|
"name": "Output Format",
|
|
3043
3043
|
"normalized_name": "output-format",
|
|
3044
3044
|
"line": 271,
|
|
3045
|
-
"byte_start":
|
|
3046
|
-
"byte_end":
|
|
3045
|
+
"byte_start": 35068,
|
|
3046
|
+
"byte_end": 38475,
|
|
3047
3047
|
"bytes": 3407,
|
|
3048
3048
|
"h3_count": 15
|
|
3049
3049
|
},
|
|
@@ -3051,8 +3051,8 @@
|
|
|
3051
3051
|
"name": "Compliance Theater Check",
|
|
3052
3052
|
"normalized_name": "compliance-theater-check",
|
|
3053
3053
|
"line": 335,
|
|
3054
|
-
"byte_start":
|
|
3055
|
-
"byte_end":
|
|
3054
|
+
"byte_start": 38475,
|
|
3055
|
+
"byte_end": 42813,
|
|
3056
3056
|
"bytes": 4338,
|
|
3057
3057
|
"h3_count": 0
|
|
3058
3058
|
},
|
|
@@ -3060,8 +3060,8 @@
|
|
|
3060
3060
|
"name": "Defensive Countermeasure Mapping",
|
|
3061
3061
|
"normalized_name": "defensive-countermeasure-mapping",
|
|
3062
3062
|
"line": 370,
|
|
3063
|
-
"byte_start":
|
|
3064
|
-
"byte_end":
|
|
3063
|
+
"byte_start": 42813,
|
|
3064
|
+
"byte_end": 46532,
|
|
3065
3065
|
"bytes": 3719,
|
|
3066
3066
|
"h3_count": 0
|
|
3067
3067
|
},
|
|
@@ -3069,8 +3069,8 @@
|
|
|
3069
3069
|
"name": "Hand-Off / Related Skills",
|
|
3070
3070
|
"normalized_name": "hand-off",
|
|
3071
3071
|
"line": 385,
|
|
3072
|
-
"byte_start":
|
|
3073
|
-
"byte_end":
|
|
3072
|
+
"byte_start": 46532,
|
|
3073
|
+
"byte_end": 50285,
|
|
3074
3074
|
"bytes": 3753,
|
|
3075
3075
|
"h3_count": 0
|
|
3076
3076
|
}
|
|
@@ -4112,7 +4112,7 @@
|
|
|
4112
4112
|
},
|
|
4113
4113
|
"cloud-iam-incident": {
|
|
4114
4114
|
"path": "skills/cloud-iam-incident/skill.md",
|
|
4115
|
-
"total_bytes":
|
|
4115
|
+
"total_bytes": 44433,
|
|
4116
4116
|
"total_lines": 416,
|
|
4117
4117
|
"frontmatter": {
|
|
4118
4118
|
"line_start": 1,
|
|
@@ -4162,16 +4162,16 @@
|
|
|
4162
4162
|
"normalized_name": "analysis-procedure",
|
|
4163
4163
|
"line": 185,
|
|
4164
4164
|
"byte_start": 22803,
|
|
4165
|
-
"byte_end":
|
|
4166
|
-
"bytes":
|
|
4165
|
+
"byte_end": 30423,
|
|
4166
|
+
"bytes": 7620,
|
|
4167
4167
|
"h3_count": 12
|
|
4168
4168
|
},
|
|
4169
4169
|
{
|
|
4170
4170
|
"name": "Output Format",
|
|
4171
4171
|
"normalized_name": "output-format",
|
|
4172
4172
|
"line": 275,
|
|
4173
|
-
"byte_start":
|
|
4174
|
-
"byte_end":
|
|
4173
|
+
"byte_start": 30423,
|
|
4174
|
+
"byte_end": 32621,
|
|
4175
4175
|
"bytes": 2198,
|
|
4176
4176
|
"h3_count": 15
|
|
4177
4177
|
},
|
|
@@ -4179,8 +4179,8 @@
|
|
|
4179
4179
|
"name": "Compliance Theater Check",
|
|
4180
4180
|
"normalized_name": "compliance-theater-check",
|
|
4181
4181
|
"line": 338,
|
|
4182
|
-
"byte_start":
|
|
4183
|
-
"byte_end":
|
|
4182
|
+
"byte_start": 32621,
|
|
4183
|
+
"byte_end": 37220,
|
|
4184
4184
|
"bytes": 4599,
|
|
4185
4185
|
"h3_count": 0
|
|
4186
4186
|
},
|
|
@@ -4188,17 +4188,17 @@
|
|
|
4188
4188
|
"name": "Defensive Countermeasure Mapping",
|
|
4189
4189
|
"normalized_name": "defensive-countermeasure-mapping",
|
|
4190
4190
|
"line": 374,
|
|
4191
|
-
"byte_start":
|
|
4192
|
-
"byte_end":
|
|
4193
|
-
"bytes":
|
|
4191
|
+
"byte_start": 37220,
|
|
4192
|
+
"byte_end": 41260,
|
|
4193
|
+
"bytes": 4040,
|
|
4194
4194
|
"h3_count": 0
|
|
4195
4195
|
},
|
|
4196
4196
|
{
|
|
4197
4197
|
"name": "Hand-Off / Related Skills",
|
|
4198
4198
|
"normalized_name": "hand-off",
|
|
4199
4199
|
"line": 396,
|
|
4200
|
-
"byte_start":
|
|
4201
|
-
"byte_end":
|
|
4200
|
+
"byte_start": 41260,
|
|
4201
|
+
"byte_end": 44433,
|
|
4202
4202
|
"bytes": 3173,
|
|
4203
4203
|
"h3_count": 0
|
|
4204
4204
|
}
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
"schema_version": "1.0.0",
|
|
4
4
|
"tokenizer_note": "Character-density approximation: 1 token ≈ 4 chars. This is the canonical rule-of-thumb for OpenAI tokenizers on English+technical text. Claude's tokenizer is typically more efficient on prose; treat this as an upper-bound budget for both. Consumers with stricter precision needs should re-tokenize with their own tokenizer.",
|
|
5
5
|
"approx_chars_per_token": 4,
|
|
6
|
-
"total_chars":
|
|
7
|
-
"total_approx_tokens":
|
|
6
|
+
"total_chars": 1673692,
|
|
7
|
+
"total_approx_tokens": 418426,
|
|
8
8
|
"skill_count": 42
|
|
9
9
|
},
|
|
10
10
|
"skills": {
|
|
@@ -1080,10 +1080,10 @@
|
|
|
1080
1080
|
},
|
|
1081
1081
|
"attack-surface-pentest": {
|
|
1082
1082
|
"path": "skills/attack-surface-pentest/skill.md",
|
|
1083
|
-
"bytes":
|
|
1084
|
-
"chars":
|
|
1083
|
+
"bytes": 33158,
|
|
1084
|
+
"chars": 33017,
|
|
1085
1085
|
"lines": 388,
|
|
1086
|
-
"approx_tokens":
|
|
1086
|
+
"approx_tokens": 8254,
|
|
1087
1087
|
"approx_chars_per_token": 4,
|
|
1088
1088
|
"sections": {
|
|
1089
1089
|
"threat-context": {
|
|
@@ -1092,14 +1092,14 @@
|
|
|
1092
1092
|
"approx_tokens": 1173
|
|
1093
1093
|
},
|
|
1094
1094
|
"framework-lag-declaration": {
|
|
1095
|
-
"bytes":
|
|
1096
|
-
"chars":
|
|
1097
|
-
"approx_tokens":
|
|
1095
|
+
"bytes": 4543,
|
|
1096
|
+
"chars": 4533,
|
|
1097
|
+
"approx_tokens": 1133
|
|
1098
1098
|
},
|
|
1099
1099
|
"ttp-mapping": {
|
|
1100
|
-
"bytes":
|
|
1101
|
-
"chars":
|
|
1102
|
-
"approx_tokens":
|
|
1100
|
+
"bytes": 2209,
|
|
1101
|
+
"chars": 2188,
|
|
1102
|
+
"approx_tokens": 547
|
|
1103
1103
|
},
|
|
1104
1104
|
"exploit-availability-matrix": {
|
|
1105
1105
|
"bytes": 2455,
|
|
@@ -1107,9 +1107,9 @@
|
|
|
1107
1107
|
"approx_tokens": 610
|
|
1108
1108
|
},
|
|
1109
1109
|
"analysis-procedure": {
|
|
1110
|
-
"bytes":
|
|
1111
|
-
"chars":
|
|
1112
|
-
"approx_tokens":
|
|
1110
|
+
"bytes": 9656,
|
|
1111
|
+
"chars": 9606,
|
|
1112
|
+
"approx_tokens": 2402
|
|
1113
1113
|
},
|
|
1114
1114
|
"output-format": {
|
|
1115
1115
|
"bytes": 3997,
|
|
@@ -1122,9 +1122,9 @@
|
|
|
1122
1122
|
"approx_tokens": 576
|
|
1123
1123
|
},
|
|
1124
1124
|
"defensive-countermeasure-mapping": {
|
|
1125
|
-
"bytes":
|
|
1126
|
-
"chars":
|
|
1127
|
-
"approx_tokens":
|
|
1125
|
+
"bytes": 1572,
|
|
1126
|
+
"chars": 1570,
|
|
1127
|
+
"approx_tokens": 393
|
|
1128
1128
|
}
|
|
1129
1129
|
}
|
|
1130
1130
|
},
|
|
@@ -1735,10 +1735,10 @@
|
|
|
1735
1735
|
},
|
|
1736
1736
|
"sector-financial": {
|
|
1737
1737
|
"path": "skills/sector-financial/skill.md",
|
|
1738
|
-
"bytes":
|
|
1739
|
-
"chars":
|
|
1738
|
+
"bytes": 50285,
|
|
1739
|
+
"chars": 50120,
|
|
1740
1740
|
"lines": 401,
|
|
1741
|
-
"approx_tokens":
|
|
1741
|
+
"approx_tokens": 12530,
|
|
1742
1742
|
"approx_chars_per_token": 4,
|
|
1743
1743
|
"sections": {
|
|
1744
1744
|
"threat-context": {
|
|
@@ -1762,9 +1762,9 @@
|
|
|
1762
1762
|
"approx_tokens": 724
|
|
1763
1763
|
},
|
|
1764
1764
|
"analysis-procedure": {
|
|
1765
|
-
"bytes":
|
|
1766
|
-
"chars":
|
|
1767
|
-
"approx_tokens":
|
|
1765
|
+
"bytes": 8716,
|
|
1766
|
+
"chars": 8686,
|
|
1767
|
+
"approx_tokens": 2172
|
|
1768
1768
|
},
|
|
1769
1769
|
"output-format": {
|
|
1770
1770
|
"bytes": 3407,
|
|
@@ -2395,10 +2395,10 @@
|
|
|
2395
2395
|
},
|
|
2396
2396
|
"cloud-iam-incident": {
|
|
2397
2397
|
"path": "skills/cloud-iam-incident/skill.md",
|
|
2398
|
-
"bytes":
|
|
2399
|
-
"chars":
|
|
2398
|
+
"bytes": 44433,
|
|
2399
|
+
"chars": 44275,
|
|
2400
2400
|
"lines": 416,
|
|
2401
|
-
"approx_tokens":
|
|
2401
|
+
"approx_tokens": 11069,
|
|
2402
2402
|
"approx_chars_per_token": 4,
|
|
2403
2403
|
"sections": {
|
|
2404
2404
|
"threat-context": {
|
|
@@ -2422,9 +2422,9 @@
|
|
|
2422
2422
|
"approx_tokens": 842
|
|
2423
2423
|
},
|
|
2424
2424
|
"analysis-procedure": {
|
|
2425
|
-
"bytes":
|
|
2426
|
-
"chars":
|
|
2427
|
-
"approx_tokens":
|
|
2425
|
+
"bytes": 7620,
|
|
2426
|
+
"chars": 7596,
|
|
2427
|
+
"approx_tokens": 1899
|
|
2428
2428
|
},
|
|
2429
2429
|
"output-format": {
|
|
2430
2430
|
"bytes": 2198,
|
|
@@ -2437,9 +2437,9 @@
|
|
|
2437
2437
|
"approx_tokens": 1146
|
|
2438
2438
|
},
|
|
2439
2439
|
"defensive-countermeasure-mapping": {
|
|
2440
|
-
"bytes":
|
|
2441
|
-
"chars":
|
|
2442
|
-
"approx_tokens":
|
|
2440
|
+
"bytes": 4040,
|
|
2441
|
+
"chars": 4032,
|
|
2442
|
+
"approx_tokens": 1008
|
|
2443
2443
|
},
|
|
2444
2444
|
"hand-off": {
|
|
2445
2445
|
"bytes": 3173,
|
package/data/cve-catalog.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"_meta": {
|
|
3
3
|
"schema_version": "1.0.0",
|
|
4
|
-
"last_updated": "2026-05-
|
|
4
|
+
"last_updated": "2026-05-30",
|
|
5
5
|
"source": "NVD + CISA KEV + vendor advisories — see sources/index.json",
|
|
6
6
|
"required_fields": [
|
|
7
7
|
"type",
|
|
@@ -90,7 +90,7 @@
|
|
|
90
90
|
],
|
|
91
91
|
"note": "Catalog keys are CVE-* by default. For pre-CVE-assignment advisories under active operational impact, the project accepts OSV-native identifier shapes as the canonical key, with cross-references retained in `aliases`: MAL-* (OSSF Malicious Packages dataset — published into OSV.dev; primary key for malicious-package compromises), GHSA-* (GitHub Advisory Database; primary key when the package is on GitHub and no CVE has issued yet), and SNYK-* (Snyk advisory dataset; primary key for advisories Snyk catalogued before OSV/GHSA ingested them). When MITRE issues a CVE, the entry is renamed in lockstep with the matching zeroday-lessons key; the previous identifier is retained in `aliases` so historical references continue to resolve. Precedent: MAL-2026-3083 added 2026-05-13 (the elementary-data PyPI worm, 1.1M monthly downloads, OSV/OSSF-cataloged before any CVE issued). EPSS coverage does not extend to non-CVE identifiers; epss_score is null with a documenting epss_note on such entries. Upstream pull from OSV.dev: `exceptd refresh --source osv` (added v0.12.10)."
|
|
92
92
|
},
|
|
93
|
-
"last_threat_review": "2026-05-
|
|
93
|
+
"last_threat_review": "2026-05-30"
|
|
94
94
|
},
|
|
95
95
|
"CVE-2025-0282": {
|
|
96
96
|
"ai_assisted_weaponization": false,
|
package/lib/flag-suggest.js
CHANGED
|
@@ -10,8 +10,10 @@
|
|
|
10
10
|
* distance ≤ 2 AND ≤ floor(flag.length / 2).
|
|
11
11
|
*
|
|
12
12
|
* Per-verb allowlists are the canonical CLI surface. Adding a new flag to a
|
|
13
|
-
* verb means appending to the allowlist here AND updating the
|
|
14
|
-
* block;
|
|
13
|
+
* verb means appending to the allowlist here AND updating the
|
|
14
|
+
* printPlaybookVerbHelp block; keep the two in sync. doctor maintains its own
|
|
15
|
+
* KNOWN_DOCTOR_FLAGS set in bin/exceptd.js — keep VERB_FLAG_ALLOWLIST.doctor
|
|
16
|
+
* aligned with it (tests/lib-flag-suggest.test.js pins the shared flags).
|
|
15
17
|
*/
|
|
16
18
|
|
|
17
19
|
function editDistance(a, b) {
|
|
@@ -66,7 +68,7 @@ function suggestFlag(flag, allowlist) {
|
|
|
66
68
|
* (e.g. `pretty`, `json`, `help`) live under '_global'.
|
|
67
69
|
*/
|
|
68
70
|
const VERB_FLAG_ALLOWLIST = Object.freeze({
|
|
69
|
-
_global: ['help', 'pretty', 'json', 'verbose'],
|
|
71
|
+
_global: ['help', 'pretty', 'json', 'verbose', 'quiet'],
|
|
70
72
|
run: [
|
|
71
73
|
'evidence', 'evidence-dir', 'session-id', 'force-overwrite', 'attestation-root',
|
|
72
74
|
'mode', 'air-gap', 'force-stale', 'operator', 'ack', 'csaf-status',
|
|
@@ -96,12 +98,6 @@ const VERB_FLAG_ALLOWLIST = Object.freeze({
|
|
|
96
98
|
'mode', 'force-stale', 'tlp',
|
|
97
99
|
'bundle-deterministic', 'bundle-epoch',
|
|
98
100
|
],
|
|
99
|
-
ingest: [
|
|
100
|
-
'evidence', 'session-id', 'force-overwrite', 'attestation-root', 'operator',
|
|
101
|
-
'ack', 'csaf-status', 'publisher-namespace', 'air-gap', 'force-stale',
|
|
102
|
-
'strict-preconditions',
|
|
103
|
-
'bundle-deterministic', 'bundle-epoch',
|
|
104
|
-
],
|
|
105
101
|
brief: ['all', 'scope', 'directives', 'flat', 'phase'],
|
|
106
102
|
discover: ['scan-only', 'scope', 'cwd'],
|
|
107
103
|
ask: [],
|
|
@@ -112,7 +108,7 @@ const VERB_FLAG_ALLOWLIST = Object.freeze({
|
|
|
112
108
|
reattest: [
|
|
113
109
|
'playbook', 'since', 'latest', 'force-replay', 'attestation-root',
|
|
114
110
|
],
|
|
115
|
-
doctor: ['signatures', 'cves', 'rfcs', 'fix', 'registry-check', 'exit-codes', 'shipped-tarball', 'ai-config', 'currency', 'collectors'],
|
|
111
|
+
doctor: ['signatures', 'cves', 'rfcs', 'fix', 'registry-check', 'exit-codes', 'shipped-tarball', 'ai-config', 'currency', 'collectors', 'air-gap'],
|
|
116
112
|
lint: ['evidence'],
|
|
117
113
|
collect: ['cwd', 'attest-ownership', 'resolve'],
|
|
118
114
|
refresh: [
|
package/lib/lint-skills.js
CHANGED
|
@@ -91,14 +91,13 @@ const REQUIRED_SECTIONS = [
|
|
|
91
91
|
|
|
92
92
|
// L3 — Defensive Countermeasure Mapping became a required section for skills
|
|
93
93
|
// reviewed on or after this cutoff (documented in AGENTS.md). Pre-cutoff
|
|
94
|
-
// skills remain exempt to preserve patch-class compatibility
|
|
95
|
-
// broaden the cutoff.
|
|
94
|
+
// skills remain exempt to preserve patch-class compatibility.
|
|
96
95
|
const COUNTERMEASURE_SECTION = 'Defensive Countermeasure Mapping';
|
|
97
96
|
const COUNTERMEASURE_CUTOFF = '2026-05-11';
|
|
98
97
|
|
|
99
98
|
// L1 — Minimum number of words of body text between a section heading and the
|
|
100
99
|
// next heading (or EOF) for the section to count as populated. Header-only
|
|
101
|
-
// sections surface as
|
|
100
|
+
// sections surface as warnings; promoted to failures under --strict.
|
|
102
101
|
const MIN_SECTION_BODY_WORDS = 20;
|
|
103
102
|
|
|
104
103
|
const PLACEHOLDER_PATTERNS = [
|
|
@@ -396,8 +395,8 @@ function validateFrontmatter(fm, skillName) {
|
|
|
396
395
|
* in the body (case-insensitive). Hard failure.
|
|
397
396
|
* - headerOnly[] — sections whose heading exists but whose body between
|
|
398
397
|
* that heading and the next heading is shorter than
|
|
399
|
-
* MIN_SECTION_BODY_WORDS words.
|
|
400
|
-
*
|
|
398
|
+
* MIN_SECTION_BODY_WORDS words. A warning by default;
|
|
399
|
+
* promoted to an error under --strict. */
|
|
401
400
|
function findMissingSections(body, requiredSections) {
|
|
402
401
|
const sections = requiredSections || REQUIRED_SECTIONS;
|
|
403
402
|
const lines = body.split(/\r?\n/);
|
|
@@ -563,16 +562,16 @@ function lintSkill(entry, ctx) {
|
|
|
563
562
|
}
|
|
564
563
|
}
|
|
565
564
|
|
|
566
|
-
// L2 — attack_refs cross-catalog resolution. Surface as
|
|
567
|
-
//
|
|
568
|
-
//
|
|
569
|
-
//
|
|
570
|
-
//
|
|
565
|
+
// L2 — attack_refs cross-catalog resolution. Surface as warnings by
|
|
566
|
+
// default (preserving patch-class compatibility); promoted to hard
|
|
567
|
+
// failures under --strict (the predeploy gate). If
|
|
568
|
+
// data/attack-techniques.json is missing entirely the ctx.attackKeys set
|
|
569
|
+
// is null — skip the check (the gate degrades gracefully).
|
|
571
570
|
if (Array.isArray(fm.attack_refs) && ctx.attackKeys) {
|
|
572
571
|
for (const ref of fm.attack_refs) {
|
|
573
572
|
if (!ctx.attackKeys.has(ref)) {
|
|
574
573
|
skillWarnings.push(
|
|
575
|
-
`attack_refs: "${ref}" not present in data/attack-techniques.json (
|
|
574
|
+
`attack_refs: "${ref}" not present in data/attack-techniques.json (an error under --strict)`,
|
|
576
575
|
);
|
|
577
576
|
}
|
|
578
577
|
}
|
|
@@ -617,18 +616,18 @@ function lintSkill(entry, ctx) {
|
|
|
617
616
|
|
|
618
617
|
// L3 — Defensive Countermeasure Mapping is required for skills reviewed
|
|
619
618
|
// on or after COUNTERMEASURE_CUTOFF. Pre-cutoff skills are exempt. The
|
|
620
|
-
// section's absence on a post-cutoff skill is a
|
|
621
|
-
// existing skills can add the section gradually;
|
|
622
|
-
//
|
|
619
|
+
// section's absence on a post-cutoff skill is a warning by default so
|
|
620
|
+
// existing skills can add the section gradually; promoted to a hard
|
|
621
|
+
// failure under --strict.
|
|
623
622
|
const { missing, headerOnly } = findMissingSections(body, REQUIRED_SECTIONS);
|
|
624
623
|
for (const s of missing) {
|
|
625
624
|
skillErrors.push(`body: missing required section "${s}"`);
|
|
626
625
|
}
|
|
627
626
|
for (const ho of headerOnly) {
|
|
628
|
-
// L1 — Header-only sections are
|
|
629
|
-
//
|
|
627
|
+
// L1 — Header-only sections are warnings by default; promoted to a
|
|
628
|
+
// failure under --strict.
|
|
630
629
|
skillWarnings.push(
|
|
631
|
-
`body: section "${ho.section}" has only ${ho.wordCount} words of body text (need >= ${MIN_SECTION_BODY_WORDS});
|
|
630
|
+
`body: section "${ho.section}" has only ${ho.wordCount} words of body text (need >= ${MIN_SECTION_BODY_WORDS}); an error under --strict`,
|
|
632
631
|
);
|
|
633
632
|
}
|
|
634
633
|
if (
|
|
@@ -639,12 +638,12 @@ function lintSkill(entry, ctx) {
|
|
|
639
638
|
const cmResult = findMissingSections(body, [COUNTERMEASURE_SECTION]);
|
|
640
639
|
if (cmResult.missing.length > 0) {
|
|
641
640
|
skillWarnings.push(
|
|
642
|
-
`body: missing required section "${COUNTERMEASURE_SECTION}" (required for skills with last_threat_review >= ${COUNTERMEASURE_CUTOFF};
|
|
641
|
+
`body: missing required section "${COUNTERMEASURE_SECTION}" (required for skills with last_threat_review >= ${COUNTERMEASURE_CUTOFF}; an error under --strict)`,
|
|
643
642
|
);
|
|
644
643
|
} else {
|
|
645
644
|
for (const ho of cmResult.headerOnly) {
|
|
646
645
|
skillWarnings.push(
|
|
647
|
-
`body: section "${ho.section}" has only ${ho.wordCount} words of body text (need >= ${MIN_SECTION_BODY_WORDS});
|
|
646
|
+
`body: section "${ho.section}" has only ${ho.wordCount} words of body text (need >= ${MIN_SECTION_BODY_WORDS}); an error under --strict`,
|
|
648
647
|
);
|
|
649
648
|
}
|
|
650
649
|
}
|
package/lib/playbook-runner.js
CHANGED
|
@@ -74,7 +74,7 @@ try {
|
|
|
74
74
|
|
|
75
75
|
// Probe the catalog (parse it, surface any load error) LAZILY on first need
|
|
76
76
|
// rather than at module load. The probe parses the ~2.6MB CVE catalog (~8.5ms);
|
|
77
|
-
// doing it eagerly charged that to every cheap verb (brief/
|
|
77
|
+
// doing it eagerly charged that to every cheap verb (brief/ask/lint/
|
|
78
78
|
// discover) that never analyzes. run() calls this before the analyze path, so
|
|
79
79
|
// a corrupt catalog still surfaces as blocked_by:'catalog_corrupt' before
|
|
80
80
|
// analyze — just not on verbs that don't touch the catalog. Memoized: probes
|
|
@@ -1829,7 +1829,11 @@ function close(playbookId, directiveId, analyzeResult, validateResult, agentSign
|
|
|
1829
1829
|
// Severity ladder for active_exploitation. The worst-of reduction lets
|
|
1830
1830
|
// analyzeFindingShape report the most-exploited CVE in the matched set, not
|
|
1831
1831
|
// the first-encountered one. Higher index = worse.
|
|
1832
|
-
|
|
1832
|
+
// `theoretical` (PoC exists, no in-the-wild use) must rank between `none` and
|
|
1833
|
+
// `unknown`; omitting it made `?? -1` lose to the -1 start, so an all-theoretical
|
|
1834
|
+
// matched set wrongly reduced to 'unknown' and a theoretical+none set dropped
|
|
1835
|
+
// the theoretical entry entirely. This vocabulary is first-class in scoring.js.
|
|
1836
|
+
const ACTIVE_EXPLOITATION_RANK = { none: 0, theoretical: 1, unknown: 2, suspected: 3, confirmed: 4 };
|
|
1833
1837
|
|
|
1834
1838
|
function worstActiveExploitation(matchedCves) {
|
|
1835
1839
|
let worst = null;
|
|
@@ -1840,7 +1844,9 @@ function worstActiveExploitation(matchedCves) {
|
|
|
1840
1844
|
const rank = ACTIVE_EXPLOITATION_RANK[v] ?? -1;
|
|
1841
1845
|
if (rank > worstRank) { worst = v; worstRank = rank; }
|
|
1842
1846
|
}
|
|
1843
|
-
|
|
1847
|
+
// Empty / all-unrecognized matched set → 'none' (a draft must not assert
|
|
1848
|
+
// 'unknown' exploitation it never observed).
|
|
1849
|
+
return worst || 'none';
|
|
1844
1850
|
}
|
|
1845
1851
|
|
|
1846
1852
|
// Severity ladder derived from rwep_adjusted. Playbooks reference
|
|
@@ -3838,4 +3844,6 @@ module.exports = {
|
|
|
3838
3844
|
_acquireLockDiagnostic: acquireLockDiagnostic,
|
|
3839
3845
|
_releaseLock: releaseLock,
|
|
3840
3846
|
_lockFilePath: lockFilePath,
|
|
3847
|
+
_vulnIdToUrn: vulnIdToUrn,
|
|
3848
|
+
_worstActiveExploitation: worstActiveExploitation,
|
|
3841
3849
|
};
|
package/lib/refresh-external.js
CHANGED
|
@@ -206,7 +206,8 @@ Sources (default = all):
|
|
|
206
206
|
osv (v0.12.10) OSV.dev aggregator — OSSF Malicious Packages (MAL-*) + Snyk
|
|
207
207
|
+ GHSA + RustSec + Mageia + Go Vuln DB + Ubuntu USN. Unauthenticated.
|
|
208
208
|
Use --advisory MAL-* / RUSTSEC-* / SNYK-* / USN-* to seed a single
|
|
209
|
-
draft.
|
|
209
|
+
draft. One advisory ID per invocation; there is no bulk or
|
|
210
|
+
package-watchlist import.
|
|
210
211
|
|
|
211
212
|
Air-gap workflow:
|
|
212
213
|
1. On a connected host: \`exceptd refresh --prefetch\`
|
|
@@ -595,7 +596,8 @@ const GHSA_SOURCE = {
|
|
|
595
596
|
if (ctx.fixtures?.ghsa) return synthesizeFromFixture(ctx, "ghsa");
|
|
596
597
|
if (ctx.cacheDir) {
|
|
597
598
|
// Cache parity: ghsa cache layout is .cache/upstream/ghsa/<published-date>.json
|
|
598
|
-
//
|
|
599
|
+
// GHSA has no cache layer yet — fall through to live fetch. (Unlike NVD
|
|
600
|
+
// and RFC, GHSA does not honor the air-gap --from-cache path.)
|
|
599
601
|
}
|
|
600
602
|
const ghsa = require("./source-ghsa");
|
|
601
603
|
return ghsa.buildDiff(ctx);
|
package/lib/source-osv.js
CHANGED
|
@@ -372,7 +372,9 @@ async function fetchAdvisoryById(id, opts = {}) {
|
|
|
372
372
|
|
|
373
373
|
/**
|
|
374
374
|
* List advisories for a package, optionally filtered to a specific version.
|
|
375
|
-
*
|
|
375
|
+
* The single-package network path is implemented; there is no bulk /
|
|
376
|
+
* package-watchlist import caller — advisories are seeded one at a time via
|
|
377
|
+
* `refresh --advisory <id>`.
|
|
376
378
|
*/
|
|
377
379
|
async function fetchAdvisoriesForPackage(name, ecosystem, version, opts = {}) {
|
|
378
380
|
if (!name || !ecosystem) {
|
|
@@ -62,7 +62,7 @@ function parseArgs(argv) {
|
|
|
62
62
|
'Usage: node lib/validate-catalog-meta.js [--quiet] [--strict]\n' +
|
|
63
63
|
'\n' +
|
|
64
64
|
' --quiet Suppress per-catalog PASS output; show failures only.\n' +
|
|
65
|
-
' --strict Promote
|
|
65
|
+
' --strict Promote freshness warnings to errors (used by the predeploy gate).\n',
|
|
66
66
|
);
|
|
67
67
|
process.exit(0);
|
|
68
68
|
} else {
|
|
@@ -165,11 +165,12 @@ function validateMeta(catalogPath, opts) {
|
|
|
165
165
|
|
|
166
166
|
/* freshness enforcement. When both meta.last_updated and
|
|
167
167
|
* freshness_policy.stale_after_days are present, surface a warning if
|
|
168
|
-
* (now - last_updated) > stale_after_days.
|
|
169
|
-
*
|
|
168
|
+
* (now - last_updated) > stale_after_days. Emitted at WARN level by
|
|
169
|
+
* default (does not fail validation).
|
|
170
170
|
*
|
|
171
171
|
* Optional `opts.strict` (or `opts.errorOnStale`) promotes the warning
|
|
172
|
-
* to an error
|
|
172
|
+
* to an error; the predeploy gate runs --strict, plain validation keeps
|
|
173
|
+
* the warning posture.
|
|
173
174
|
*/
|
|
174
175
|
if (
|
|
175
176
|
typeof meta.last_updated === 'string' &&
|
|
@@ -185,7 +186,7 @@ function validateMeta(catalogPath, opts) {
|
|
|
185
186
|
const msg =
|
|
186
187
|
`_meta freshness: last_updated ${meta.last_updated} is ${ageDays} days old ` +
|
|
187
188
|
`(stale_after_days = ${fp.stale_after_days}); refresh the catalog or bump _meta.last_updated. ` +
|
|
188
|
-
`
|
|
189
|
+
`Promoted to an error under --strict.`;
|
|
189
190
|
if (opts && (opts.strict || opts.errorOnStale)) {
|
|
190
191
|
errors.push(msg);
|
|
191
192
|
} else {
|