@blamejs/exceptd-skills 0.21.13 → 0.21.14
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 +6 -0
- package/data/_indexes/_meta.json +17 -17
- package/data/_indexes/activity-feed.json +98 -98
- package/data/_indexes/currency.json +28 -28
- package/data/_indexes/section-offsets.json +200 -200
- package/data/_indexes/summary-cards.json +14 -14
- package/data/_indexes/token-budget.json +99 -99
- package/manifest.json +81 -81
- package/package.json +1 -1
- package/sbom.cdx.json +58 -43
- package/scripts/check-skill-catalog-facts.js +304 -0
- package/scripts/predeploy.js +8 -0
- package/skills/ai-attack-surface/skill.md +2 -2
- package/skills/cloud-iam-incident/skill.md +6 -6
- package/skills/compliance-theater/skill.md +6 -6
- package/skills/container-runtime-security/skill.md +2 -2
- package/skills/exploit-scoring/skill.md +16 -14
- package/skills/framework-gap-analysis/skill.md +5 -5
- package/skills/idp-incident-response/skill.md +4 -4
- package/skills/kernel-lpe-triage/skill.md +15 -15
- package/skills/policy-exception-gen/skill.md +4 -4
- package/skills/sector-federal-government/skill.md +3 -3
- package/skills/security-maturity-tiers/skill.md +5 -5
- package/skills/supply-chain-integrity/skill.md +3 -3
- package/skills/threat-model-currency/skill.md +6 -6
- package/skills/zeroday-gap-learn/skill.md +8 -8
package/sbom.cdx.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"bomFormat": "CycloneDX",
|
|
3
3
|
"specVersion": "1.6",
|
|
4
|
-
"serialNumber": "urn:uuid:
|
|
4
|
+
"serialNumber": "urn:uuid:61e23ad7-9237-4fef-a585-e4c30e681cd9",
|
|
5
5
|
"version": 1,
|
|
6
6
|
"metadata": {
|
|
7
7
|
"timestamp": "2026-09-28T00:00:00.000Z",
|
|
@@ -9,14 +9,14 @@
|
|
|
9
9
|
{
|
|
10
10
|
"vendor": "blamejs",
|
|
11
11
|
"name": "scripts/refresh-sbom.js",
|
|
12
|
-
"version": "0.21.
|
|
12
|
+
"version": "0.21.14"
|
|
13
13
|
}
|
|
14
14
|
],
|
|
15
15
|
"component": {
|
|
16
|
-
"bom-ref": "pkg:npm/@blamejs/exceptd-skills@0.21.
|
|
16
|
+
"bom-ref": "pkg:npm/@blamejs/exceptd-skills@0.21.14",
|
|
17
17
|
"type": "application",
|
|
18
18
|
"name": "@blamejs/exceptd-skills",
|
|
19
|
-
"version": "0.21.
|
|
19
|
+
"version": "0.21.14",
|
|
20
20
|
"description": "AI security skills grounded in mid-2026 threat reality, not stale framework documentation. 51 skills, 11 catalogs (1758 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.
|
|
28
|
+
"purl": "pkg:npm/%40blamejs/exceptd-skills@0.21.14",
|
|
29
29
|
"hashes": [
|
|
30
30
|
{
|
|
31
31
|
"alg": "SHA-256",
|
|
32
|
-
"content": "
|
|
32
|
+
"content": "26e43d9e6f73cba6831642a781162d4b7ef63f6774b74d607349772cc28d536a"
|
|
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.
|
|
38
|
+
"url": "https://www.npmjs.com/package/@blamejs/exceptd-skills/v/0.21.14"
|
|
39
39
|
},
|
|
40
40
|
{
|
|
41
41
|
"type": "vcs",
|
|
@@ -124,11 +124,11 @@
|
|
|
124
124
|
"hashes": [
|
|
125
125
|
{
|
|
126
126
|
"alg": "SHA-256",
|
|
127
|
-
"content": "
|
|
127
|
+
"content": "e5f861bc69052d3c66de2b59e2a188a2fdf8033ce612a16bae98c25ef7602377"
|
|
128
128
|
},
|
|
129
129
|
{
|
|
130
130
|
"alg": "SHA3-512",
|
|
131
|
-
"content": "
|
|
131
|
+
"content": "0aeda331116a12462a9ef5c42035f9231164c2e9c175cbe53df29c204e2753696fbb77b3d8c16dc38d51aee26d8662e90689dc04f4b9c2989b2b7cfaed941077"
|
|
132
132
|
}
|
|
133
133
|
]
|
|
134
134
|
},
|
|
@@ -1954,11 +1954,11 @@
|
|
|
1954
1954
|
"hashes": [
|
|
1955
1955
|
{
|
|
1956
1956
|
"alg": "SHA-256",
|
|
1957
|
-
"content": "
|
|
1957
|
+
"content": "de9184697ed6593cde0bb16f3be6a99297e3cbd2fdc1e4a4eaaa94cdfb75ff74"
|
|
1958
1958
|
},
|
|
1959
1959
|
{
|
|
1960
1960
|
"alg": "SHA3-512",
|
|
1961
|
-
"content": "
|
|
1961
|
+
"content": "c8fd09b5b1a1fb2ec9cc8f7cb4f58e68b6a328f49c5f00a9ac821a5bd3b467a67be11d8f43fa8b81a4feec84df39f790d8364773c5cf37abbbb16746d54c5ae8"
|
|
1962
1962
|
}
|
|
1963
1963
|
]
|
|
1964
1964
|
},
|
|
@@ -2074,11 +2074,11 @@
|
|
|
2074
2074
|
"hashes": [
|
|
2075
2075
|
{
|
|
2076
2076
|
"alg": "SHA-256",
|
|
2077
|
-
"content": "
|
|
2077
|
+
"content": "6235ee92496261098205966f7af831c9951434ec68f0bcdf5176f7884aa3568c"
|
|
2078
2078
|
},
|
|
2079
2079
|
{
|
|
2080
2080
|
"alg": "SHA3-512",
|
|
2081
|
-
"content": "
|
|
2081
|
+
"content": "e9d4487682133ca1231323bf2ac55fa07116a4e1dc1f298a1de03f1607026aa52be63f225b4f2a4398385b7319313567a765a77eb9b0e23063362e68772d7f78"
|
|
2082
2082
|
}
|
|
2083
2083
|
]
|
|
2084
2084
|
},
|
|
@@ -2532,6 +2532,21 @@
|
|
|
2532
2532
|
}
|
|
2533
2533
|
]
|
|
2534
2534
|
},
|
|
2535
|
+
{
|
|
2536
|
+
"bom-ref": "file:scripts/check-skill-catalog-facts.js",
|
|
2537
|
+
"type": "file",
|
|
2538
|
+
"name": "scripts/check-skill-catalog-facts.js",
|
|
2539
|
+
"hashes": [
|
|
2540
|
+
{
|
|
2541
|
+
"alg": "SHA-256",
|
|
2542
|
+
"content": "f068f3ce319f7f302a6f33924ab77140b379b92d1f54ddaab5aa74ed26813164"
|
|
2543
|
+
},
|
|
2544
|
+
{
|
|
2545
|
+
"alg": "SHA3-512",
|
|
2546
|
+
"content": "19da9c54538c46136460bf06260c29e95e824b467112c4d0a7176060e221e66060458db2a2a4b80a08480df7aefc450ce9589ab8691544eb98211dc3f2e13198"
|
|
2547
|
+
}
|
|
2548
|
+
]
|
|
2549
|
+
},
|
|
2535
2550
|
{
|
|
2536
2551
|
"bom-ref": "file:scripts/check-test-count.js",
|
|
2537
2552
|
"type": "file",
|
|
@@ -2674,11 +2689,11 @@
|
|
|
2674
2689
|
"hashes": [
|
|
2675
2690
|
{
|
|
2676
2691
|
"alg": "SHA-256",
|
|
2677
|
-
"content": "
|
|
2692
|
+
"content": "0944ae40249fa6c2412d25992627fa0d85c1ba5abb4d3267ccb4aaeaf9b43c10"
|
|
2678
2693
|
},
|
|
2679
2694
|
{
|
|
2680
2695
|
"alg": "SHA3-512",
|
|
2681
|
-
"content": "
|
|
2696
|
+
"content": "2f4108d69b57805f4bf4dd8213964f4f792c6c2e1eb66fe05c2c3b3a50723cd2a8b8ee07cdd8ec8d787d6389ab0187a2097e5f37bd4c000c08582666aa22c096"
|
|
2682
2697
|
}
|
|
2683
2698
|
]
|
|
2684
2699
|
},
|
|
@@ -2929,11 +2944,11 @@
|
|
|
2929
2944
|
"hashes": [
|
|
2930
2945
|
{
|
|
2931
2946
|
"alg": "SHA-256",
|
|
2932
|
-
"content": "
|
|
2947
|
+
"content": "8419f1792c9fe66666afd364d8e593cb3a48096e12563efba109e8a94a8fcada"
|
|
2933
2948
|
},
|
|
2934
2949
|
{
|
|
2935
2950
|
"alg": "SHA3-512",
|
|
2936
|
-
"content": "
|
|
2951
|
+
"content": "4959ec722520f1aded1fa2dc55842edc49f5b8c38def88a405e3b83994e4d9944cf5b2dfc158a520d2e70a4bc11d9aae758f9742588fb0b3fe3466f164ca8e45"
|
|
2937
2952
|
}
|
|
2938
2953
|
]
|
|
2939
2954
|
},
|
|
@@ -3019,11 +3034,11 @@
|
|
|
3019
3034
|
"hashes": [
|
|
3020
3035
|
{
|
|
3021
3036
|
"alg": "SHA-256",
|
|
3022
|
-
"content": "
|
|
3037
|
+
"content": "102a79be85d08b60b5550f50775201ca499577909842f740a9140fc82b6d159d"
|
|
3023
3038
|
},
|
|
3024
3039
|
{
|
|
3025
3040
|
"alg": "SHA3-512",
|
|
3026
|
-
"content": "
|
|
3041
|
+
"content": "bf5e32d78350aef82003890df5888d8f3fadacc364b80b16152ea8c96247a145c3ae4cc806e2f4c5fc7161f439fbf61e15e6f34194fadc05e50e55697d56f746"
|
|
3027
3042
|
}
|
|
3028
3043
|
]
|
|
3029
3044
|
},
|
|
@@ -3049,11 +3064,11 @@
|
|
|
3049
3064
|
"hashes": [
|
|
3050
3065
|
{
|
|
3051
3066
|
"alg": "SHA-256",
|
|
3052
|
-
"content": "
|
|
3067
|
+
"content": "6aa6bbd11a57471ba53622d64d07b219571f1fc767198f9c2ffccfc6e14104fd"
|
|
3053
3068
|
},
|
|
3054
3069
|
{
|
|
3055
3070
|
"alg": "SHA3-512",
|
|
3056
|
-
"content": "
|
|
3071
|
+
"content": "6af4f4d6f63d27be99ca075eee197c68e527de189eabaaf85a2ec9ba08282d524dea37332ded25c90943938387dd0df091a8b422feca3c49aa3285273047998b"
|
|
3057
3072
|
}
|
|
3058
3073
|
]
|
|
3059
3074
|
},
|
|
@@ -3064,11 +3079,11 @@
|
|
|
3064
3079
|
"hashes": [
|
|
3065
3080
|
{
|
|
3066
3081
|
"alg": "SHA-256",
|
|
3067
|
-
"content": "
|
|
3082
|
+
"content": "28b075b44c53b88d09a52770f9160a139afa530124cf3762636a9f3e7a36b8cc"
|
|
3068
3083
|
},
|
|
3069
3084
|
{
|
|
3070
3085
|
"alg": "SHA3-512",
|
|
3071
|
-
"content": "
|
|
3086
|
+
"content": "d03f60fb42c3cd3ad924e2aaaecf9cbfbb482b98d6ff319d72d7d49dae803099c2c55b2f49792d27cf47027e72fa5263c279576ab0afad875dc06db07c49d6eb"
|
|
3072
3087
|
}
|
|
3073
3088
|
]
|
|
3074
3089
|
},
|
|
@@ -3154,11 +3169,11 @@
|
|
|
3154
3169
|
"hashes": [
|
|
3155
3170
|
{
|
|
3156
3171
|
"alg": "SHA-256",
|
|
3157
|
-
"content": "
|
|
3172
|
+
"content": "59473887bc7b2a21de657729db3202a3f20521dbf1010fa38290f797b1b873ae"
|
|
3158
3173
|
},
|
|
3159
3174
|
{
|
|
3160
3175
|
"alg": "SHA3-512",
|
|
3161
|
-
"content": "
|
|
3176
|
+
"content": "0bfa8a70a27ce27e836f1ec20311b8a8a3520fb8629e19b4bff2b75882e27f3c4f2d23f5b4994fe0deafa580d54537b57bbe83d3d902aeb151a8ed64e440868b"
|
|
3162
3177
|
}
|
|
3163
3178
|
]
|
|
3164
3179
|
},
|
|
@@ -3169,11 +3184,11 @@
|
|
|
3169
3184
|
"hashes": [
|
|
3170
3185
|
{
|
|
3171
3186
|
"alg": "SHA-256",
|
|
3172
|
-
"content": "
|
|
3187
|
+
"content": "27024d6dc79627befd621d4e8d0cd05ce78f76291a1d5c60d8dd337cbc6d9eb6"
|
|
3173
3188
|
},
|
|
3174
3189
|
{
|
|
3175
3190
|
"alg": "SHA3-512",
|
|
3176
|
-
"content": "
|
|
3191
|
+
"content": "2c32bac86cd7aee4f0ad36aaa0c6f7a5cbd273f17fa9123b90dd55209153b660a3e92c401f3a92b45d6745bd585a95d75ca6eee21bc851a7f9fb08cab6b58e61"
|
|
3177
3192
|
}
|
|
3178
3193
|
]
|
|
3179
3194
|
},
|
|
@@ -3229,11 +3244,11 @@
|
|
|
3229
3244
|
"hashes": [
|
|
3230
3245
|
{
|
|
3231
3246
|
"alg": "SHA-256",
|
|
3232
|
-
"content": "
|
|
3247
|
+
"content": "c08f53ec55f8a2704008e8c289f9a66bd3e7f7d47a7b0a246f22ecfcaacaa7f4"
|
|
3233
3248
|
},
|
|
3234
3249
|
{
|
|
3235
3250
|
"alg": "SHA3-512",
|
|
3236
|
-
"content": "
|
|
3251
|
+
"content": "6136fbd4475b28a21f0122e279d12133eba031b464455413b2b93a44aa60eccb24195b6576e62f416e1ad93ef1c68a60beb765f954f98239f2a2ea912af47b0f"
|
|
3237
3252
|
}
|
|
3238
3253
|
]
|
|
3239
3254
|
},
|
|
@@ -3259,11 +3274,11 @@
|
|
|
3259
3274
|
"hashes": [
|
|
3260
3275
|
{
|
|
3261
3276
|
"alg": "SHA-256",
|
|
3262
|
-
"content": "
|
|
3277
|
+
"content": "8cbc65dab6b50b01bdddb8f042f360ebf3bb4f2d38ee7f4d4e9c080d179ca95b"
|
|
3263
3278
|
},
|
|
3264
3279
|
{
|
|
3265
3280
|
"alg": "SHA3-512",
|
|
3266
|
-
"content": "
|
|
3281
|
+
"content": "3ab4ea003f86f9b888be288eb79f8d85d2579786c01c26962d07756ff86f62070602ec9ef770cd16499fd78d3c82fba4a18edfc4d17d818c2f787996da0c973e"
|
|
3267
3282
|
}
|
|
3268
3283
|
]
|
|
3269
3284
|
},
|
|
@@ -3379,11 +3394,11 @@
|
|
|
3379
3394
|
"hashes": [
|
|
3380
3395
|
{
|
|
3381
3396
|
"alg": "SHA-256",
|
|
3382
|
-
"content": "
|
|
3397
|
+
"content": "072aeac64d77b7d0d65e2882c60b0980e5883dd0e27f9cd7b62c0f26cbbb835f"
|
|
3383
3398
|
},
|
|
3384
3399
|
{
|
|
3385
3400
|
"alg": "SHA3-512",
|
|
3386
|
-
"content": "
|
|
3401
|
+
"content": "119434a503e192316ff72fcb80658f11fabee51a3a6df46ca3c208c51cea7eb84bda7df9a2f57ac7d2cbba3d1623044194dff4e8b7e3eb1b30ee4c13cb3e00f5"
|
|
3387
3402
|
}
|
|
3388
3403
|
]
|
|
3389
3404
|
},
|
|
@@ -3484,11 +3499,11 @@
|
|
|
3484
3499
|
"hashes": [
|
|
3485
3500
|
{
|
|
3486
3501
|
"alg": "SHA-256",
|
|
3487
|
-
"content": "
|
|
3502
|
+
"content": "b6357b23119374ea5caed2873babbf8d158da0a54fc566e3c5c8559a28b7b79d"
|
|
3488
3503
|
},
|
|
3489
3504
|
{
|
|
3490
3505
|
"alg": "SHA3-512",
|
|
3491
|
-
"content": "
|
|
3506
|
+
"content": "9239a20c968755fd8c279189b5f2e507d7c99f5f786323f472e3f11939a79c1e2f14cbff1ad157f0539473a1aa364adceb1264968294135846f00297ce5cf84b"
|
|
3492
3507
|
}
|
|
3493
3508
|
]
|
|
3494
3509
|
},
|
|
@@ -3544,11 +3559,11 @@
|
|
|
3544
3559
|
"hashes": [
|
|
3545
3560
|
{
|
|
3546
3561
|
"alg": "SHA-256",
|
|
3547
|
-
"content": "
|
|
3562
|
+
"content": "b5046e711003123bf2ec31c1ddc51281ae1349747ea65a7d787287a5d33aefb2"
|
|
3548
3563
|
},
|
|
3549
3564
|
{
|
|
3550
3565
|
"alg": "SHA3-512",
|
|
3551
|
-
"content": "
|
|
3566
|
+
"content": "98abdac02d680a5f822acec22e50a9d60e91c4bc5cf1ca530df1da97368705100347d35b4c83cdee9bac4c16f2258f6d5ab4312cee4402272c2c8f6890f14f1c"
|
|
3552
3567
|
}
|
|
3553
3568
|
]
|
|
3554
3569
|
},
|
|
@@ -3589,11 +3604,11 @@
|
|
|
3589
3604
|
"hashes": [
|
|
3590
3605
|
{
|
|
3591
3606
|
"alg": "SHA-256",
|
|
3592
|
-
"content": "
|
|
3607
|
+
"content": "f7694c0a0e335f98631afb7e414baab0528e68830cfd02be9d76075ee50899fa"
|
|
3593
3608
|
},
|
|
3594
3609
|
{
|
|
3595
3610
|
"alg": "SHA3-512",
|
|
3596
|
-
"content": "
|
|
3611
|
+
"content": "3888bb2565075b86d6995c320e75be9a2fe99c98479a909f181b117e4d3d3a5ffde24e4133d91b267d3be9139cdb2a310369874257d666cfea1e1ec6c1c033e1"
|
|
3597
3612
|
}
|
|
3598
3613
|
]
|
|
3599
3614
|
},
|
|
@@ -3604,11 +3619,11 @@
|
|
|
3604
3619
|
"hashes": [
|
|
3605
3620
|
{
|
|
3606
3621
|
"alg": "SHA-256",
|
|
3607
|
-
"content": "
|
|
3622
|
+
"content": "058b67901e836c20f8730d0c09eabb4bd9600ee0b720d8b6a48bcee5fb08cb7e"
|
|
3608
3623
|
},
|
|
3609
3624
|
{
|
|
3610
3625
|
"alg": "SHA3-512",
|
|
3611
|
-
"content": "
|
|
3626
|
+
"content": "b94b9f6c97b7b24c520cd7aa354da49a9a7ed63d321e360408804eaeb7e079011ddaf32c882ce85dae0a8e34626b1d9d3e5d026b4de397c2c9ac339a600a6e01"
|
|
3612
3627
|
}
|
|
3613
3628
|
]
|
|
3614
3629
|
},
|
|
@@ -3664,11 +3679,11 @@
|
|
|
3664
3679
|
"hashes": [
|
|
3665
3680
|
{
|
|
3666
3681
|
"alg": "SHA-256",
|
|
3667
|
-
"content": "
|
|
3682
|
+
"content": "7733233b8eef00b4592d9ad4336cbd815250565bcd54c4b476fbdfe9a73ad9c8"
|
|
3668
3683
|
},
|
|
3669
3684
|
{
|
|
3670
3685
|
"alg": "SHA3-512",
|
|
3671
|
-
"content": "
|
|
3686
|
+
"content": "4fea76683b42928e67867c6e4a030f210319a1332d6ae075377658204b7d8804cbcbe1fbd00fe0b7064b4d49b62421693c88076ee9bcc91df451f55d1fa42104"
|
|
3672
3687
|
}
|
|
3673
3688
|
]
|
|
3674
3689
|
},
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Skill catalog-facts gate. Exit 0 consistent, 1 when a skill states a CVE fact
|
|
6
|
+
* that differs from data/cve-catalog.json.
|
|
7
|
+
*
|
|
8
|
+
* A markdown table row is compared when the first cell that names a CVE names
|
|
9
|
+
* exactly one catalog CVE. Its cells are compared by column header: CVSS, RWEP, a KEV column (Yes/No, and the listing date when the
|
|
10
|
+
* cell gives one), a public-exploit column (Yes/No), and an AI column. An
|
|
11
|
+
* "AI-Discovered" column is compared with ai_discovered; an AI column that also
|
|
12
|
+
* covers acceleration, enablement or weaponization is compared with
|
|
13
|
+
* ai_discovered OR ai_assisted_weaponization. A prose line that names exactly one
|
|
14
|
+
* catalog CVE is compared on "RWEP <n>", "CVSS <n.n>", "KEV-listed <date>" and
|
|
15
|
+
* "not KEV-listed". On any line, the parentheses that follow a CVE id are read
|
|
16
|
+
* as that CVE's values, so a list such as "CVE-A (90), CVE-B (Dirty Frag, 38,
|
|
17
|
+
* CVSS 7.8)" is compared CVE by CVE. A cell or line that states no comparable value is skipped,
|
|
18
|
+
* and so is a CVE the catalog does not hold, a row whose CVE cell only points
|
|
19
|
+
* to one ("No vendor CVE; see CVE-..."), and a value the text marks as superseded.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
const fs = require("node:fs");
|
|
23
|
+
const path = require("node:path");
|
|
24
|
+
|
|
25
|
+
const ROOT = path.resolve(__dirname, "..");
|
|
26
|
+
const CVE = /CVE-\d{4}-\d{4,}/g;
|
|
27
|
+
const DATE = /\d{4}-\d{2}-\d{2}/;
|
|
28
|
+
|
|
29
|
+
const splitRow = (line) => line.trim().replace(/^\|/, "").replace(/\|$/, "").split("|").map((c) => c.trim());
|
|
30
|
+
const isDivider = (cells) => cells.every((c) => /^:?-{2,}:?$/.test(c));
|
|
31
|
+
const uniqueCves = (text) => [...new Set(text.match(CVE) || [])];
|
|
32
|
+
|
|
33
|
+
/** Leading Yes or No of a cell; anything else ("Partial", "varies") is not compared. */
|
|
34
|
+
function yesNo(cell) {
|
|
35
|
+
if (/^yes\b/i.test(cell)) return true;
|
|
36
|
+
if (/^no\b/i.test(cell)) return false;
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* An AI cell's claim: a leading Yes/No, a declarative form the column counts as
|
|
42
|
+
* yes ("AI-discovered", "AI-weaponized"), or "Human-discovered" as no.
|
|
43
|
+
*/
|
|
44
|
+
function aiValue(cell, yesForm) {
|
|
45
|
+
const v = yesNo(cell);
|
|
46
|
+
if (v !== null) return v;
|
|
47
|
+
if (yesForm.test(cell)) return true;
|
|
48
|
+
if (/^human[- ](?:discovered|found)\b/i.test(cell)) return false;
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** The CVSS major version a text names ("CVSS 4.0", "CVSS v3.1"), or null. */
|
|
53
|
+
function cvssVersion(vector) {
|
|
54
|
+
const m = /^CVSS:(\d)/.exec(vector || "");
|
|
55
|
+
return m ? m[1] : null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function columnKind(header) {
|
|
59
|
+
const h = header.toLowerCase().replace(/[`*?]/g, "").trim();
|
|
60
|
+
if (/^cvss\b/.test(h)) return "cvss";
|
|
61
|
+
if (/^rwep\b/.test(h)) return "rwep";
|
|
62
|
+
if (/\bkev\b/.test(h)) return "kev";
|
|
63
|
+
if (/\bpoc\b|public exploit/.test(h)) return "poc";
|
|
64
|
+
if (/^ai[- ]discovered$/.test(h)) return "ai_discovered";
|
|
65
|
+
if (/^ai\b/.test(h)) return "ai_any";
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** A cell's text with Markdown emphasis and code marks removed. */
|
|
70
|
+
const plain = (cell) => cell.replace(/[*_`]/g, "").trim();
|
|
71
|
+
|
|
72
|
+
function compareCell(kind, raw, e, say) {
|
|
73
|
+
const cell = plain(raw);
|
|
74
|
+
if (kind === "cvss") {
|
|
75
|
+
const m = /^(\d{1,2}(?:\.\d)?)\b/.exec(cell);
|
|
76
|
+
if (m && Number(m[1]) !== e.cvss_score) say(`CVSS ${m[1]}, catalog ${e.cvss_score}`);
|
|
77
|
+
} else if (kind === "rwep") {
|
|
78
|
+
const m = /^(\d{1,3})(?![\d+–-]|\.\d)/.exec(cell);
|
|
79
|
+
if (m && Number(m[1]) !== e.rwep_score) say(`RWEP ${m[1]}, catalog ${e.rwep_score}`);
|
|
80
|
+
} else if (kind === "kev") {
|
|
81
|
+
const v = yesNo(cell);
|
|
82
|
+
if (v === null) return;
|
|
83
|
+
if (v !== Boolean(e.cisa_kev)) say(`KEV "${cell.slice(0, 40)}", catalog ${e.cisa_kev ? `listed ${e.cisa_kev_date}` : "not listed"}`);
|
|
84
|
+
else if (v) {
|
|
85
|
+
const d = DATE.exec(cell);
|
|
86
|
+
if (d && e.cisa_kev_date && d[0] !== e.cisa_kev_date) say(`KEV date ${d[0]}, catalog ${e.cisa_kev_date}`);
|
|
87
|
+
}
|
|
88
|
+
} else if (kind === "poc") {
|
|
89
|
+
// "Partial" is the catalog's own granular value for a conceptual or chain-only
|
|
90
|
+
// exploit, which it scores as poc_available true.
|
|
91
|
+
const v = /^partial\b/i.test(cell) ? true : yesNo(cell);
|
|
92
|
+
if (v !== null && v !== Boolean(e.poc_available)) say(`public exploit "${cell.slice(0, 40)}", catalog poc_available ${e.poc_available}`);
|
|
93
|
+
} else if (kind === "ai_discovered") {
|
|
94
|
+
const v = aiValue(cell, /^ai[- ](?:discovered|assisted)\b/i);
|
|
95
|
+
if (v !== null && v !== Boolean(e.ai_discovered)) say(`AI-discovered "${cell.slice(0, 40)}", catalog ai_discovered ${e.ai_discovered}`);
|
|
96
|
+
} else if (kind === "ai_any") {
|
|
97
|
+
const v = aiValue(cell, /^ai[- ](?:discovered|assisted|accelerated|enabled|weaponi[sz]ed)\b/i);
|
|
98
|
+
const expected = Boolean(e.ai_discovered) || Boolean(e.ai_assisted_weaponization);
|
|
99
|
+
if (v !== null && v !== expected) say(`AI "${cell.slice(0, 40)}", catalog ai_discovered ${e.ai_discovered} / ai_assisted_weaponization ${e.ai_assisted_weaponization}`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** A value the text marks as superseded ("the initial CVSS 9.8 was withdrawn"). */
|
|
104
|
+
function historical(line, m) {
|
|
105
|
+
const before = line.slice(Math.max(0, m.index - 24), m.index);
|
|
106
|
+
const after = line.slice(m.index + m[0].length, m.index + m[0].length + 24);
|
|
107
|
+
return /\b(?:initial|original|previous(?:ly)?|earlier|former|prior)\s*$/i.test(before) || /^\s*(?:was|were|is)\s+(?:withdrawn|superseded|corrected|replaced)\b/i.test(after);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function compareProse(text, e, say) {
|
|
111
|
+
const line = plain(text);
|
|
112
|
+
for (const m of line.matchAll(/RWEP(?: score)?(?:\s*(?:of|is|=|:))?\s*(\d{1,3})(?![\d+–-]|\.\d)/gi)) {
|
|
113
|
+
if (historical(line, m)) continue;
|
|
114
|
+
if (Number(m[1]) !== e.rwep_score) say(`RWEP ${m[1]}, catalog ${e.rwep_score}`);
|
|
115
|
+
}
|
|
116
|
+
for (const m of line.matchAll(/CVSS(?:\s*v?(\d)(?:\.\d)?\b)?(?:\s*(?:score|base score))?(?:\s*(?:of|is|=|:))?\s*(\d{1,2}\.\d)\b/gi)) {
|
|
117
|
+
const version = m[1] || null;
|
|
118
|
+
if (version && version !== cvssVersion(e.cvss_vector)) continue;
|
|
119
|
+
if (historical(line, m)) continue;
|
|
120
|
+
if (Number(m[2]) !== e.cvss_score) say(`CVSS ${m[2]}, catalog ${e.cvss_score}`);
|
|
121
|
+
}
|
|
122
|
+
for (const m of line.matchAll(/\bKEV[- ]listed(?: on)? (\d{4}-\d{2}-\d{2})/g)) {
|
|
123
|
+
if (e.cisa_kev_date && m[1] !== e.cisa_kev_date) say(`KEV date ${m[1]}, catalog ${e.cisa_kev_date}`);
|
|
124
|
+
if (!e.cisa_kev) say(`says KEV-listed ${m[1]}, catalog not listed`);
|
|
125
|
+
}
|
|
126
|
+
if (/\bnot (?:yet )?(?:CISA )?KEV[- ]listed\b|\bnot (?:yet )?(?:on|in) (?:the )?(?:CISA )?KEV\b|\bKEV(?: listing)?:? pending\b|\bpending (?:CISA )?KEV\b/i.test(line) && e.cisa_kev) {
|
|
127
|
+
say(`says not KEV-listed, catalog listed ${e.cisa_kev_date}`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Values in the parentheses that follow a CVE id, or that open with one, belong
|
|
133
|
+
* to that CVE: "CVE-X (Copy Fail, RWEP 90, CVSS 7.8)", "CVE-X (Dirty Frag, 38,
|
|
134
|
+
* CVSS 7.8)" or "(CVE-X, CVSS 7.8 / AV:L)". A bare 1-3 digit item is read as
|
|
135
|
+
* RWEP when the line mentions RWEP.
|
|
136
|
+
*/
|
|
137
|
+
const PER_CVE = /(CVE-\d{4}-\d{4,})\s*\(([^()]*)\)|\((CVE-\d{4}-\d{4,})[,;:]\s*([^()]*)\)/g;
|
|
138
|
+
|
|
139
|
+
const UNATTRIBUTED = 'states a score for several CVEs outside per-CVE parentheses; write each as "CVE-X (name, RWEP n, CVSS n.n)"';
|
|
140
|
+
|
|
141
|
+
/** A score statement: "RWEP 53", "CVSS 8.8", "**RWEP:** 53/100". */
|
|
142
|
+
const SCORE = /RWEP(?: score)?\*{0,2}(?:\s*(?:of|is|=|:))?\*{0,2}\s*\d{1,3}(?![\d+–-]|\.\d)|CVSS(?:\s*v?\d(?:\.\d)?\b)?(?:\s*(?:score|base score))?\*{0,2}(?:\s*(?:of|is|=|:))?\*{0,2}\s*\d{1,2}\.\d\b|\b\d{1,3}\/100\b/i;
|
|
143
|
+
|
|
144
|
+
function compareParentheticals(line, catalog, say) {
|
|
145
|
+
const rwepContext = /\bRWEP\b/i.test(line);
|
|
146
|
+
let found = 0;
|
|
147
|
+
for (const raw of line.matchAll(PER_CVE)) {
|
|
148
|
+
const m = raw[1] ? [raw[0], raw[1], raw[2]] : [raw[0], raw[3], raw[4]];
|
|
149
|
+
const e = catalog[m[1]];
|
|
150
|
+
if (!e) continue;
|
|
151
|
+
found++;
|
|
152
|
+
const inner = m[2];
|
|
153
|
+
const tell = (msg) => say(m[1], msg);
|
|
154
|
+
compareProse(inner, e, tell);
|
|
155
|
+
if (rwepContext && !/\bRWEP\b/i.test(inner)) {
|
|
156
|
+
for (const item of inner.split(/[,;]/).map((s) => s.trim())) {
|
|
157
|
+
if (/^\d{1,3}$/.test(item) && Number(item) !== e.rwep_score) tell(`RWEP ${item}, catalog ${e.rwep_score}`);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return found;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** Factor-table row labels and the rwep_factors key each one scores. */
|
|
165
|
+
const FACTOR_KEYS = [
|
|
166
|
+
[/^cisa kev$/, "cisa_kev"],
|
|
167
|
+
[/^poc public$|^public poc$/, "poc_available"],
|
|
168
|
+
[/^ai[- ](?:assisted|discovered|factor)$/, "ai_factor"],
|
|
169
|
+
[/^active exploitation$/, "active_exploitation"],
|
|
170
|
+
[/^blast radius$/, "blast_radius"],
|
|
171
|
+
[/^patch available$/, "patch_available"],
|
|
172
|
+
[/^live patch available$/, "live_patch_available"],
|
|
173
|
+
[/^reboot required$/, "reboot_required"],
|
|
174
|
+
];
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* A "Factor | Value | Points" row under a heading that names one catalog CVE:
|
|
178
|
+
* the points must equal that CVE's rwep_factors entry, and the RWEP row must
|
|
179
|
+
* equal its rwep_score.
|
|
180
|
+
*/
|
|
181
|
+
/** Factor rows whose Value cell states a catalog boolean, with how to read it. */
|
|
182
|
+
const FACTOR_VALUES = [
|
|
183
|
+
[/^cisa kev$/, (e) => Boolean(e.cisa_kev), "cisa_kev"],
|
|
184
|
+
[/^poc public$|^public poc$/, (e) => Boolean(e.poc_available), "poc_available"],
|
|
185
|
+
[/^ai[- ]discovered$/, (e) => Boolean(e.ai_discovered), "ai_discovered"],
|
|
186
|
+
[/^ai[- ](?:assisted|factor)$/, (e) => Boolean(e.ai_discovered) || Boolean(e.ai_assisted_weaponization), "ai_discovered / ai_assisted_weaponization"],
|
|
187
|
+
[/^patch available$/, (e) => Boolean(e.patch_available), "patch_available"],
|
|
188
|
+
[/^live patch available$/, (e) => Boolean(e.live_patch_available), "live_patch_available"],
|
|
189
|
+
[/^reboot required$/, (e) => Boolean(e.patch_required_reboot), "patch_required_reboot"],
|
|
190
|
+
];
|
|
191
|
+
|
|
192
|
+
function compareFactorRow(cells, rawHeader, e, say) {
|
|
193
|
+
const pointsCol = rawHeader.findIndex((h) => /^\s*points\s*$/i.test(h));
|
|
194
|
+
if (pointsCol < 1) return false;
|
|
195
|
+
const label = (cells[0] || "").replace(/\*/g, "").trim().toLowerCase();
|
|
196
|
+
const valueCol = rawHeader.findIndex((h) => /^\s*value\s*$/i.test(h));
|
|
197
|
+
const valueRule = FACTOR_VALUES.find(([re]) => re.test(label));
|
|
198
|
+
if (valueCol > 0 && valueRule) {
|
|
199
|
+
const cell = plain(cells[valueCol] || "");
|
|
200
|
+
const v = /^partial\b/i.test(cell) && valueRule[2] === "poc_available" ? true : yesNo(cell);
|
|
201
|
+
if (v !== null && v !== valueRule[1](e)) say(`factor table ${cells[0].replace(/\*/g, "").trim()} value "${cell.slice(0, 40)}", catalog ${valueRule[2]} ${valueRule[1](e)}`);
|
|
202
|
+
}
|
|
203
|
+
const m = /^\*{0,2}\s*([+\-−]?\d{1,3})\b/.exec(cells[pointsCol] || "");
|
|
204
|
+
if (!m) return false;
|
|
205
|
+
const points = Number(m[1].replace("−", "-"));
|
|
206
|
+
if (/^rwep(?: total)?$/.test(label)) {
|
|
207
|
+
if (points !== e.rwep_score) say(`factor table RWEP ${points}, catalog ${e.rwep_score}`);
|
|
208
|
+
return true;
|
|
209
|
+
}
|
|
210
|
+
const hit = FACTOR_KEYS.find(([re]) => re.test(label));
|
|
211
|
+
if (!hit || !e.rwep_factors || typeof e.rwep_factors[hit[1]] !== "number") return false;
|
|
212
|
+
if (points !== e.rwep_factors[hit[1]]) say(`factor table ${cells[0].replace(/\*/g, "").trim()} ${points}, catalog rwep_factors.${hit[1]} ${e.rwep_factors[hit[1]]}`);
|
|
213
|
+
return true;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function checkSkill(file, catalog) {
|
|
217
|
+
const seen = new Set();
|
|
218
|
+
const failures = [];
|
|
219
|
+
const push = (msg) => { if (!seen.has(msg)) { seen.add(msg); failures.push(msg); } };
|
|
220
|
+
let compared = 0;
|
|
221
|
+
let header = null;
|
|
222
|
+
let rawHeader = null;
|
|
223
|
+
let sectionCve = null;
|
|
224
|
+
const lines = fs.readFileSync(file, "utf8").split(/\r?\n/);
|
|
225
|
+
lines.forEach((line, i) => {
|
|
226
|
+
const where = `${path.relative(ROOT, file).replace(/\\/g, "/")}:${i + 1}`;
|
|
227
|
+
if (/^#{1,6}\s/.test(line)) {
|
|
228
|
+
const headingIds = uniqueCves(line);
|
|
229
|
+
sectionCve = headingIds.length === 1 && catalog[headingIds[0]] ? headingIds[0] : null;
|
|
230
|
+
}
|
|
231
|
+
compared += compareParentheticals(line, catalog, (id, msg) => push(`${where} ${id}: ${msg}`));
|
|
232
|
+
if (/^\s*\|/.test(line)) {
|
|
233
|
+
const cells = splitRow(line);
|
|
234
|
+
if (header === null) { rawHeader = cells; header = cells.map(columnKind); return; }
|
|
235
|
+
if (isDivider(cells)) return;
|
|
236
|
+
if (sectionCve && uniqueCves(line).length === 0) {
|
|
237
|
+
if (compareFactorRow(cells, rawHeader, catalog[sectionCve], (msg) => push(`${where} ${sectionCve}: ${msg}`))) compared++;
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
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;
|
|
242
|
+
const ids = uniqueCves(idCell);
|
|
243
|
+
if (ids.length > 1) {
|
|
244
|
+
const known = ids.filter((id) => catalog[id]);
|
|
245
|
+
const scored = header.some((kind, k) => (kind === "cvss" || kind === "rwep") && /^\d/.test(plain(cells[k] || "")));
|
|
246
|
+
if (known.length && scored) push(`${where} ${ids.join(", ")}: ${UNATTRIBUTED}`);
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
if (ids.length !== 1 || !catalog[ids[0]]) return;
|
|
250
|
+
const e = catalog[ids[0]];
|
|
251
|
+
compared++;
|
|
252
|
+
header.forEach((kind, k) => {
|
|
253
|
+
if (kind && cells[k] !== undefined) compareCell(kind, cells[k], e, (msg) => push(`${where} ${ids[0]}: ${msg}`));
|
|
254
|
+
});
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
header = null;
|
|
258
|
+
const ids = uniqueCves(line);
|
|
259
|
+
if (ids.length > 1) {
|
|
260
|
+
const rest = plain(line.replace(PER_CVE, " "));
|
|
261
|
+
const m = SCORE.exec(rest);
|
|
262
|
+
if (ids.some((id) => catalog[id]) && m && !historical(rest, m)) push(`${where} ${ids.join(", ")}: ${UNATTRIBUTED}`);
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
if (ids.length !== 1 || !catalog[ids[0]]) return;
|
|
266
|
+
compared++;
|
|
267
|
+
compareProse(line, catalog[ids[0]], (msg) => push(`${where} ${ids[0]}: ${msg}`));
|
|
268
|
+
});
|
|
269
|
+
return { failures, compared };
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function check(skillsDir, catalog) {
|
|
273
|
+
const failures = [];
|
|
274
|
+
let compared = 0;
|
|
275
|
+
let files = 0;
|
|
276
|
+
for (const name of fs.readdirSync(skillsDir).sort()) {
|
|
277
|
+
const file = path.join(skillsDir, name, "skill.md");
|
|
278
|
+
if (!fs.existsSync(file)) continue;
|
|
279
|
+
files++;
|
|
280
|
+
const r = checkSkill(file, catalog);
|
|
281
|
+
failures.push(...r.failures);
|
|
282
|
+
compared += r.compared;
|
|
283
|
+
}
|
|
284
|
+
return { failures, compared, files };
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function main() {
|
|
288
|
+
const skillsDir = process.argv[2] || path.join(ROOT, "skills");
|
|
289
|
+
const catalogFile = process.argv[3] || path.join(ROOT, "data", "cve-catalog.json");
|
|
290
|
+
const catalog = JSON.parse(fs.readFileSync(catalogFile, "utf8"));
|
|
291
|
+
const { failures, compared, files } = check(skillsDir, catalog);
|
|
292
|
+
if (failures.length) {
|
|
293
|
+
console.error("Skill catalog facts: FAIL");
|
|
294
|
+
for (const f of failures) console.error(` ${f}`);
|
|
295
|
+
console.error(`\n${failures.length} skill ${failures.length === 1 ? "statement differs" : "statements differ"} from data/cve-catalog.json. Update the skill text to the catalog value.`);
|
|
296
|
+
process.exitCode = 1;
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
console.log(`Skill catalog facts: PASS — ${compared} CVE rows and lines across ${files} skills match data/cve-catalog.json`);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (require.main === module) main();
|
|
303
|
+
|
|
304
|
+
module.exports = { check, checkSkill, columnKind, yesNo };
|
package/scripts/predeploy.js
CHANGED
|
@@ -218,6 +218,14 @@ const GATES = [
|
|
|
218
218
|
args: [path.join(ROOT, "scripts", "check-ism-control-references.js")],
|
|
219
219
|
ciJobName: "Data integrity (catalog + manifest snapshot)",
|
|
220
220
|
},
|
|
221
|
+
{
|
|
222
|
+
// A skill that states a CVE's CVSS, RWEP, KEV status, public-exploit or
|
|
223
|
+
// AI-discovery value must agree with data/cve-catalog.json.
|
|
224
|
+
name: "Skill catalog facts (skill CVE statements vs. the catalog)",
|
|
225
|
+
command: process.execPath,
|
|
226
|
+
args: [path.join(ROOT, "scripts", "check-skill-catalog-facts.js")],
|
|
227
|
+
ciJobName: "Data integrity (catalog + manifest snapshot)",
|
|
228
|
+
},
|
|
221
229
|
];
|
|
222
230
|
|
|
223
231
|
function runGate(gate) {
|
|
@@ -60,7 +60,7 @@ forward_watch:
|
|
|
60
60
|
- Pwn2Own Berlin 2026 (disclosed 2026-05-14, embargo ends 2026-08-12) — Chroma vector DB CWE-190 + CWE-362 chain by haehae; impacts RAG vector store integrity; track patch and downstream RAG advisory
|
|
61
61
|
- Pwn2Own Berlin 2026 (disclosed 2026-05-14, embargo ends 2026-08-12) — NVIDIA Megatron Bridge overly permissive allowed list by Satoki Tsuji; AI training-stack supply-chain exposure; track patch and SBOM advisory
|
|
62
62
|
- 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 advisory
|
|
63
|
-
last_threat_review: "2026-09-
|
|
63
|
+
last_threat_review: "2026-09-28"
|
|
64
64
|
---
|
|
65
65
|
|
|
66
66
|
# AI Attack Surface Assessment
|
|
@@ -141,7 +141,7 @@ AI-assisted reconnaissance is observed at 36,000 probes per second per campaign.
|
|
|
141
141
|
|
|
142
142
|
### 11. AI-Discovered + AI-Weaponized Supply-Chain Worms
|
|
143
143
|
|
|
144
|
-
**CVE-2026-45321** — Mini Shai-Hulud TanStack npm worm (CVSS 9.6, ~150M weekly downloads across 42 @tanstack/* packages,
|
|
144
|
+
**CVE-2026-45321** — Mini Shai-Hulud TanStack npm worm (CVSS 9.6, ~150M weekly downloads across 42 @tanstack/* packages, KEV-listed 2026-05-27). Disclosed 2026-05-11. The attack chain — Pwn-Request via `pull_request_target` on TanStack's bundle-size workflow, pnpm-store cache poisoning under the `actions/cache` key, and OIDC-token theft on the next main push — is engineering-grade and weaponizes three independently-benign primitives. While attribution (TeamPCP) records no AI-assisted exploit development for this specific instance, the worm pattern is exactly what AML.T0017-class capability development now produces at AI cadence: chained CI/CD primitives that no individual component owner recognises as exploitable. Treat the @tanstack/* surface as an exemplar of the broader AML.T0010 (AI Supply Chain Compromise) threat applied to JS toolchains that the AI assistant ecosystem depends on.
|
|
145
145
|
|
|
146
146
|
---
|
|
147
147
|
|