@blamejs/exceptd-skills 0.19.33 → 0.19.34
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 +10 -0
- package/bin/exceptd.js +896 -2824
- package/data/_indexes/_meta.json +2 -2
- package/lib/auto-discovery.js +56 -286
- package/lib/canonical-eq.js +7 -40
- package/lib/citation-resolve.js +22 -70
- package/lib/collectors/ai-api.js +20 -54
- package/lib/collectors/cicd-pipeline-compromise.js +40 -108
- package/lib/collectors/citation-hygiene.js +72 -210
- package/lib/collectors/containers.js +41 -130
- package/lib/collectors/cred-stores.js +31 -115
- package/lib/collectors/crypto-codebase.js +55 -138
- package/lib/collectors/crypto.js +24 -54
- package/lib/collectors/hardening.js +20 -78
- package/lib/collectors/kernel.js +16 -46
- package/lib/collectors/library-author.js +57 -206
- package/lib/collectors/mcp.js +24 -70
- package/lib/collectors/runtime.js +24 -86
- package/lib/collectors/sbom.js +34 -106
- package/lib/collectors/scan-excludes.js +31 -138
- package/lib/collectors/secrets.js +62 -178
- package/lib/cross-ref-api.js +39 -123
- package/lib/currency-severity.js +8 -27
- package/lib/cve-batch.js +13 -21
- package/lib/cve-cli.js +13 -20
- package/lib/cve-curation.js +72 -239
- package/lib/cve-regression-watcher.js +29 -152
- package/lib/cvss.js +13 -54
- package/lib/doctor-bucketing.js +3 -19
- package/lib/exit-codes.js +10 -42
- package/lib/flag-suggest.js +7 -25
- package/lib/framework-gap.js +35 -114
- package/lib/gap-detectors.js +37 -159
- package/lib/id-validation.js +9 -30
- package/lib/job-queue.js +13 -36
- package/lib/lint-skills.js +64 -232
- package/lib/playbook-runner.js +693 -2095
- package/lib/prefetch.js +100 -376
- package/lib/refresh-external.js +199 -627
- package/lib/refresh-network.js +75 -307
- package/lib/rfc-cli.js +23 -68
- package/lib/scoring.js +77 -145
- package/lib/sign.js +43 -229
- package/lib/source-advisories.js +43 -194
- package/lib/source-ghsa.js +37 -120
- package/lib/source-osv.js +94 -266
- package/lib/ttp-mapper.js +14 -24
- package/lib/upstream-check-cli.js +10 -28
- package/lib/upstream-check.js +19 -44
- package/lib/validate-catalog-meta.js +17 -61
- package/lib/validate-cve-catalog.js +43 -119
- package/lib/validate-indexes.js +25 -76
- package/lib/validate-package.js +16 -62
- package/lib/validate-playbooks.js +69 -275
- package/lib/validate-vendor.js +16 -49
- package/lib/verify.js +56 -286
- package/lib/version-pins.js +5 -34
- package/lib/worker-pool.js +11 -30
- package/lib/xml-tokenizer.js +47 -152
- package/manifest.json +53 -53
- package/orchestrator/dispatcher.js +17 -68
- package/orchestrator/event-bus.js +11 -74
- package/orchestrator/index.js +138 -412
- package/orchestrator/pipeline.js +28 -85
- package/orchestrator/scanner.js +34 -138
- package/orchestrator/scheduler.js +20 -84
- package/package.json +1 -1
- package/sbom.cdx.json +241 -241
- package/scripts/audit-catalog-gaps.js +9 -62
- package/scripts/audit-cross-skill.js +5 -31
- package/scripts/audit-perf.js +6 -16
- package/scripts/backfill-theater-test.js +7 -64
- package/scripts/bootstrap.js +12 -44
- package/scripts/build-indexes.js +40 -154
- package/scripts/builders/activity-feed.js +4 -14
- package/scripts/builders/catalog-summaries.js +3 -10
- package/scripts/builders/currency.js +7 -20
- package/scripts/builders/cwe-chains.js +7 -30
- package/scripts/builders/did-ladders.js +6 -13
- package/scripts/builders/frequency.js +5 -19
- package/scripts/builders/jurisdiction-clocks.js +6 -25
- package/scripts/builders/recipes.js +6 -14
- package/scripts/builders/section-offsets.js +13 -51
- package/scripts/builders/stale-content.js +7 -28
- package/scripts/builders/summary-cards.js +8 -29
- package/scripts/builders/theater-fingerprints.js +12 -27
- package/scripts/builders/token-budget.js +4 -31
- package/scripts/check-agents-md-collectors.js +11 -54
- package/scripts/check-catalog-gap-budget.js +15 -32
- package/scripts/check-changelog-extract.js +18 -48
- package/scripts/check-codebase-patterns-currency.js +6 -22
- package/scripts/check-codebase-patterns.js +50 -143
- package/scripts/check-epss-consistency.js +9 -64
- package/scripts/check-framework-gap-coverage.js +13 -31
- package/scripts/check-manifest-snapshot.js +13 -73
- package/scripts/check-sbom-currency.js +44 -142
- package/scripts/check-test-count.js +15 -52
- package/scripts/check-test-coverage.js +66 -197
- package/scripts/check-test-subjects.js +21 -62
- package/scripts/check-ttp-references.js +14 -38
- package/scripts/check-ttp-upstream.js +8 -40
- package/scripts/check-version-bump.js +9 -61
- package/scripts/check-version-tags.js +20 -121
- package/scripts/predeploy.js +38 -184
- package/scripts/refresh-manifest-snapshot.js +16 -38
- package/scripts/refresh-mitre-atlas.js +3 -8
- package/scripts/refresh-mitre-attack.js +1 -8
- package/scripts/refresh-mitre-d3fend.js +3 -9
- package/scripts/refresh-mitre-ics-attack.js +3 -8
- package/scripts/refresh-reverse-refs.js +27 -94
- package/scripts/refresh-rfc-index.js +2 -10
- package/scripts/refresh-sbom.js +31 -161
- package/scripts/refresh-upstream-catalogs.js +40 -137
- package/scripts/release.js +69 -232
- package/scripts/run-e2e-scenarios.js +24 -71
- package/scripts/sync-manifest-metadata.js +10 -34
- package/scripts/sync-package-description.js +8 -17
- package/scripts/validate-vendor-online.js +13 -44
- package/scripts/verify-shipped-tarball.js +35 -140
package/lib/source-ghsa.js
CHANGED
|
@@ -1,27 +1,11 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* NVD (~10 days) or KEV (variable, often days).
|
|
10
|
-
*
|
|
11
|
-
* Endpoint: GET https://api.github.com/advisories
|
|
12
|
-
* - Unauthenticated: 60 req/hr (sufficient for nightly refresh)
|
|
13
|
-
* - Authenticated: 5000 req/hr (set GITHUB_TOKEN env var)
|
|
14
|
-
*
|
|
15
|
-
* Returns drafts — every imported entry carries `_auto_imported: true`
|
|
16
|
-
* + `_draft: true` so the strict catalog validator treats them as
|
|
17
|
-
* warnings, not errors. Editorial fields (framework_control_gaps,
|
|
18
|
-
* iocs, atlas_refs) are left null until a human or AI assistant
|
|
19
|
-
* fills them in via the seven-phase playbook flow.
|
|
20
|
-
*
|
|
21
|
-
* Honors EXCEPTD_GHSA_FIXTURE env var for offline testing — value is a
|
|
22
|
-
* path to a JSON array matching the api.github.com/advisories shape.
|
|
23
|
-
*
|
|
24
|
-
* Zero npm deps. Node 24 stdlib only.
|
|
4
|
+
* GitHub Advisory Database fetcher — GET https://api.github.com/advisories,
|
|
5
|
+
* rate-limited to 60 req/hr unauthenticated and 5000 with GITHUB_TOKEN set.
|
|
6
|
+
* EXCEPTD_GHSA_FIXTURE points at a JSON file of the same shape for offline runs.
|
|
7
|
+
* Everything imported is a draft: entries carry `_auto_imported` + `_draft`, so
|
|
8
|
+
* the strict validator warns rather than errors and editorial fields stay null.
|
|
25
9
|
*/
|
|
26
10
|
|
|
27
11
|
const https = require("https");
|
|
@@ -33,37 +17,24 @@ const GHSA_PATH = "/advisories?per_page=50&type=reviewed&sort=published&directio
|
|
|
33
17
|
const REQUEST_TIMEOUT_MS = 10000;
|
|
34
18
|
const USER_AGENT = "exceptd-security/source-ghsa (+https://exceptd.com)";
|
|
35
19
|
|
|
36
|
-
//
|
|
37
|
-
//
|
|
38
|
-
// well under a MB; 16 MB is generous headroom. Symmetric with the JSON cap
|
|
39
|
-
// getJsonOnce enforces in lib/refresh-network.js and the OSV poller's cap.
|
|
40
|
-
// Tunable via EXCEPTD_GHSA_RESPONSE_CAP_BYTES for future API growth.
|
|
20
|
+
// Caps the streamed response so anything impersonating api.github.com cannot push
|
|
21
|
+
// unbounded bytes into RAM. Symmetric with getJsonOnce in lib/refresh-network.js.
|
|
41
22
|
function ghsaResponseCapBytes() {
|
|
42
23
|
const env = parseInt(process.env.EXCEPTD_GHSA_RESPONSE_CAP_BYTES, 10);
|
|
43
24
|
return Number.isFinite(env) && env > 0 ? env : 16 * 1024 * 1024;
|
|
44
25
|
}
|
|
45
26
|
|
|
46
|
-
/**
|
|
47
|
-
* Field-dropped watch set — fields the buildDiff regression-detector
|
|
48
|
-
* watches when the upstream still has an entry but a previously-populated
|
|
49
|
-
* local value has gone null. Mirrors lib/source-osv.js. Finding 9.
|
|
50
|
-
*/
|
|
27
|
+
/** Fields buildDiff watches going null while upstream still has the entry; mirrors lib/source-osv.js. */
|
|
51
28
|
const FIELD_DROPPED_WATCH = Object.freeze([
|
|
52
|
-
// Only fields
|
|
53
|
-
//
|
|
54
|
-
// poc_available are EDITORIAL (curated by hand, never upstream-sourced), so
|
|
55
|
-
// the normalize step always nulls them — watching them flagged a field_dropped
|
|
56
|
-
// "regression" on every single curated re-import. Restrict the watch to the
|
|
57
|
-
// upstream-sourced fields.
|
|
29
|
+
// Only upstream-populated fields: normalize nulls the editorial ones on every
|
|
30
|
+
// import, so watching them reports a regression on each re-import.
|
|
58
31
|
"cvss_score",
|
|
59
32
|
"cisa_kev_pending",
|
|
60
33
|
]);
|
|
61
34
|
|
|
62
35
|
/**
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
* a structured `air-gap: no fixture available` error so the operator sees
|
|
66
|
-
* an explicit refusal, not a silent network attempt. Mirrors source-osv.
|
|
36
|
+
* True when air-gap mode is requested; a source must then fall through to a
|
|
37
|
+
* fixture or return the structured `air-gap: no fixture available` error.
|
|
67
38
|
*/
|
|
68
39
|
function isAirGap(opts) {
|
|
69
40
|
if (opts && opts.airGap) return true;
|
|
@@ -72,16 +43,9 @@ function isAirGap(opts) {
|
|
|
72
43
|
}
|
|
73
44
|
|
|
74
45
|
/**
|
|
75
|
-
* Read EXCEPTD_GHSA_FIXTURE
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
* advisories array — the strict catalog validator would later swallow the
|
|
79
|
-
* resulting drift as "no advisories returned" instead of surfacing it as
|
|
80
|
-
* a fixture configuration error. Returns:
|
|
81
|
-
*
|
|
82
|
-
* null when env var is unset
|
|
83
|
-
* { ok: true, advisories: [...], source } on success
|
|
84
|
-
* { ok: false, error, source: "offline" } on any failure
|
|
46
|
+
* Read EXCEPTD_GHSA_FIXTURE. Null when unset, `{ ok: true, advisories, source }`
|
|
47
|
+
* on success, `{ ok: false, error, source }` on any failure — including a
|
|
48
|
+
* non-object root, which would otherwise pass as an empty advisory list.
|
|
85
49
|
*/
|
|
86
50
|
function readFixture() {
|
|
87
51
|
const fp = process.env.EXCEPTD_GHSA_FIXTURE;
|
|
@@ -105,9 +69,8 @@ function readFixture() {
|
|
|
105
69
|
}
|
|
106
70
|
|
|
107
71
|
/**
|
|
108
|
-
* One HTTPS GET against api.github.com.
|
|
109
|
-
* withRetry's
|
|
110
|
-
* picks them up; resolves to a structured envelope on permanent conditions.
|
|
72
|
+
* One HTTPS GET against api.github.com. Rejects on retryable conditions so
|
|
73
|
+
* withRetry's classifier picks them up, resolves an envelope on permanent ones.
|
|
111
74
|
*/
|
|
112
75
|
function ghsaRequestOnce({ path, headers, timeoutMs }) {
|
|
113
76
|
return new Promise((resolve, reject) => {
|
|
@@ -149,8 +112,7 @@ function ghsaRequestOnce({ path, headers, timeoutMs }) {
|
|
|
149
112
|
chunks.push(c);
|
|
150
113
|
});
|
|
151
114
|
res.on("end", () => {
|
|
152
|
-
//
|
|
153
|
-
// don't also resolve here (avoids a double-settle on the promise).
|
|
115
|
+
// The cap already routed through req.on("error"); resolving here settles twice.
|
|
154
116
|
if (capped) return;
|
|
155
117
|
try {
|
|
156
118
|
const body = JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
@@ -184,18 +146,13 @@ function ghsaRequestOnce({ path, headers, timeoutMs }) {
|
|
|
184
146
|
}
|
|
185
147
|
|
|
186
148
|
/**
|
|
187
|
-
* Fetch a page of advisories
|
|
188
|
-
*
|
|
189
|
-
*
|
|
190
|
-
*
|
|
191
|
-
* Returns:
|
|
192
|
-
* { ok: true, advisories: [...], source: "github-api" | "fixture", rate_limit?: { remaining, reset } }
|
|
193
|
-
* { ok: false, error, source: "offline" }
|
|
149
|
+
* Fetch a page of advisories, backing off through withRetry on transient
|
|
150
|
+
* failures. `{ ok: true, advisories, source: "github-api" | "fixture",
|
|
151
|
+
* rate_limit? }` or `{ ok: false, error, source: "offline" }`.
|
|
194
152
|
*/
|
|
195
153
|
async function fetchAdvisories({ timeoutMs = REQUEST_TIMEOUT_MS, path = GHSA_PATH, token = null, airGap = false } = {}) {
|
|
196
154
|
const fixture = readFixture();
|
|
197
155
|
if (fixture) return fixture;
|
|
198
|
-
// Finding 7: air-gap hard-refuses network when no fixture is configured.
|
|
199
156
|
if (isAirGap({ airGap })) {
|
|
200
157
|
return { ok: false, error: "air-gap: no fixture available (set EXCEPTD_GHSA_FIXTURE)", source: "offline" };
|
|
201
158
|
}
|
|
@@ -224,16 +181,13 @@ async function fetchAdvisories({ timeoutMs = REQUEST_TIMEOUT_MS, path = GHSA_PAT
|
|
|
224
181
|
}
|
|
225
182
|
|
|
226
183
|
/**
|
|
227
|
-
* Fetch a single advisory by
|
|
228
|
-
*
|
|
229
|
-
* GHSA-IDs hit /advisories/<ghsa-id> directly. CVE-IDs require a search
|
|
230
|
-
* since the API is keyed by GHSA. We fall back to a query-string search.
|
|
184
|
+
* Fetch a single advisory by CVE-* or GHSA-* id. A GHSA id addresses
|
|
185
|
+
* /advisories/<ghsa-id>; the API is keyed by GHSA, so a CVE id goes via search.
|
|
231
186
|
*/
|
|
232
187
|
async function fetchAdvisoryById(id, opts = {}) {
|
|
233
188
|
if (!id || typeof id !== "string") {
|
|
234
189
|
return { ok: false, error: "id is required (CVE-* or GHSA-*)", source: "offline" };
|
|
235
190
|
}
|
|
236
|
-
// Finding 8: trim whitespace at the entry seam.
|
|
237
191
|
id = id.trim();
|
|
238
192
|
if (!id) {
|
|
239
193
|
return { ok: false, error: "id is required (CVE-* or GHSA-*)", source: "offline" };
|
|
@@ -249,33 +203,21 @@ async function fetchAdvisoryById(id, opts = {}) {
|
|
|
249
203
|
if (!match) return { ok: false, error: `${id} not in fixture`, source: "fixture" };
|
|
250
204
|
return { ok: true, advisories: [match], source: "fixture" };
|
|
251
205
|
}
|
|
252
|
-
//
|
|
253
|
-
//
|
|
254
|
-
// branch below. A prefix-only `/^GHSA-/i` left the remainder unconstrained, so
|
|
255
|
-
// an operator-supplied id like `GHSA-aaaa/../../meta` survived verbatim into
|
|
256
|
-
// `/advisories/...` as path-control segments; the strict shape now routes a
|
|
257
|
-
// malformed id to the unrecognized-id error envelope instead.
|
|
206
|
+
// The full GHSA token shape, not a `/^GHSA-/` prefix: `GHSA-aaaa/../../meta`
|
|
207
|
+
// would otherwise reach `/advisories/` as path-control segments.
|
|
258
208
|
if (/^GHSA-[0-9a-z]{4}-[0-9a-z]{4}-[0-9a-z]{4}$/i.test(id)) {
|
|
259
209
|
return fetchAdvisories({ ...opts, path: `/advisories/${encodeURIComponent(id.toLowerCase())}` });
|
|
260
210
|
}
|
|
261
211
|
if (/^CVE-\d{4}-\d+$/i.test(id)) {
|
|
262
212
|
return fetchAdvisories({ ...opts, path: `/advisories?cve_id=${encodeURIComponent(id.toUpperCase())}` });
|
|
263
213
|
}
|
|
264
|
-
//
|
|
265
|
-
// running `exceptd refresh --advisory FOO-BAR` previously got an error
|
|
266
|
-
// mentioning only CVE / GHSA, even though MAL-*, SNYK-*, RUSTSEC-*,
|
|
267
|
-
// USN-*, PYSEC-*, GO-*, MGASA-*, UVI- are also valid id shapes routed
|
|
268
|
-
// through source-osv. The hint here mirrors lib/refresh-external.js
|
|
269
|
-
// seedSingleAdvisory's documented acceptance set.
|
|
214
|
+
// The accepted set mirrors seedSingleAdvisory in lib/refresh-external.js.
|
|
270
215
|
return { ok: false, error: `unrecognized id format: ${id}. Expected one of: CVE-YYYY-NNNN, GHSA-* (routed through source-ghsa); MAL-* / SNYK-* / RUSTSEC-* / USN-* / PYSEC-* / GO-* / MGASA-* / UVI- (routed through source-osv).`, source: "offline" };
|
|
271
216
|
}
|
|
272
217
|
|
|
273
218
|
/**
|
|
274
|
-
*
|
|
275
|
-
*
|
|
276
|
-
* null instead of throwing on .slice().
|
|
277
|
-
* - ISO-prefix + year sanity bound so garbage timestamps don't pollute
|
|
278
|
-
* downstream surfaces.
|
|
219
|
+
* The YYYY-MM-DD head of an upstream timestamp, or null — a non-string, a
|
|
220
|
+
* non-ISO prefix or a year outside [1990, next year] all yield null.
|
|
279
221
|
*/
|
|
280
222
|
function safeDateSlice(value) {
|
|
281
223
|
if (typeof value !== "string") return null;
|
|
@@ -288,16 +230,9 @@ function safeDateSlice(value) {
|
|
|
288
230
|
}
|
|
289
231
|
|
|
290
232
|
/**
|
|
291
|
-
* Normalize a GHSA advisory
|
|
292
|
-
*
|
|
293
|
-
*
|
|
294
|
-
* references. Editorial fields (framework_control_gaps, iocs, atlas_refs,
|
|
295
|
-
* attack_refs, rwep_factors) are LEFT NULL — drafts. The seven-phase
|
|
296
|
-
* playbook flow OR a human reviewer fills these in.
|
|
297
|
-
*
|
|
298
|
-
* Returns null if the advisory lacks a CVE ID (we don't import GHSA-only
|
|
299
|
-
* advisories into the CVE catalog — they belong in a separate GHSA index
|
|
300
|
-
* which is a v0.13 design).
|
|
233
|
+
* Normalize a GHSA advisory into the catalog draft shape. Only what GHSA carries
|
|
234
|
+
* authoritatively is populated; editorial fields stay null for a curator. Null
|
|
235
|
+
* for an advisory with no CVE id — a GHSA-only advisory is not a CVE entry.
|
|
301
236
|
*/
|
|
302
237
|
function normalizeAdvisory(adv) {
|
|
303
238
|
if (!adv || !adv.cve_id) return null;
|
|
@@ -305,7 +240,6 @@ function normalizeAdvisory(adv) {
|
|
|
305
240
|
const ecosystems = new Set();
|
|
306
241
|
const affected = [];
|
|
307
242
|
const ecosystemPackages = [];
|
|
308
|
-
// Finding 3: vulnerabilities may not be an array — guard before iterating.
|
|
309
243
|
const vulnList = Array.isArray(adv.vulnerabilities) ? adv.vulnerabilities : [];
|
|
310
244
|
for (const v of vulnList) {
|
|
311
245
|
if (v?.package?.ecosystem) ecosystems.add(v.package.ecosystem);
|
|
@@ -317,9 +251,7 @@ function normalizeAdvisory(adv) {
|
|
|
317
251
|
}
|
|
318
252
|
}
|
|
319
253
|
|
|
320
|
-
//
|
|
321
|
-
// number. Number-coerce + finite-check so the catalog field stays
|
|
322
|
-
// numeric across upstream shape drift.
|
|
254
|
+
// cvss.score arrives as a string often enough to need coercion + a finite check.
|
|
323
255
|
let cvssScore = null;
|
|
324
256
|
if (adv.cvss != null && adv.cvss.score !== undefined && adv.cvss.score !== null) {
|
|
325
257
|
const n = Number(adv.cvss.score);
|
|
@@ -334,11 +266,8 @@ function normalizeAdvisory(adv) {
|
|
|
334
266
|
: ecosystems.has("rubygems") ? "supply-chain-gem"
|
|
335
267
|
: "supply-chain-other";
|
|
336
268
|
|
|
337
|
-
// Finding 2 + 17: type-safe + format-validated published_at slicing.
|
|
338
269
|
const publishedDate = safeDateSlice(adv.published_at);
|
|
339
270
|
|
|
340
|
-
// Finding 20: references may not be an array — guard the spread before
|
|
341
|
-
// it silently truncates to an empty list.
|
|
342
271
|
const refList = Array.isArray(adv.references) ? adv.references : [];
|
|
343
272
|
|
|
344
273
|
return {
|
|
@@ -397,8 +326,7 @@ function normalizeAdvisory(adv) {
|
|
|
397
326
|
_draft_reason: "Imported from GHSA on " + new Date().toISOString().slice(0, 10) + ". Editorial fields (framework_control_gaps, atlas_refs, attack_refs, iocs, vector, complexity, rwep_factors) require human review. Run `exceptd run sbom --evidence -` against an affected repo to gather IoCs; consult MITRE ATLAS + ATT&CK catalogs for refs.",
|
|
398
327
|
_source_ghsa_id: adv.ghsa_id || null,
|
|
399
328
|
_source_published_at: adv.published_at || null,
|
|
400
|
-
//
|
|
401
|
-
// structured status so a withdrawn advisory is flagged, not imported as live.
|
|
329
|
+
// `withdrawn_at` marks a retracted advisory; carried as status, not imported live.
|
|
402
330
|
...(adv.withdrawn_at ? { status: "withdrawn", status_source: "ghsa:withdrawn_at", status_verified: new Date().toISOString().slice(0, 10) } : {}),
|
|
403
331
|
last_updated: new Date().toISOString().slice(0, 10),
|
|
404
332
|
},
|
|
@@ -406,17 +334,9 @@ function normalizeAdvisory(adv) {
|
|
|
406
334
|
}
|
|
407
335
|
|
|
408
336
|
/**
|
|
409
|
-
* Build a refresh diff for the
|
|
410
|
-
*
|
|
411
|
-
*
|
|
412
|
-
*
|
|
413
|
-
* Finding 9: when the advisory is already in the catalog but a watched
|
|
414
|
-
* field has dropped from populated -> null, surface a `field_dropped`
|
|
415
|
-
* diff so curators don't silently lose signal.
|
|
416
|
-
*
|
|
417
|
-
* Finding 18: count + surface GHSA-only advisories (no CVE id) that were
|
|
418
|
-
* skipped, so the summary explains why N upstream advisories produced
|
|
419
|
-
* fewer than N diffs.
|
|
337
|
+
* Build a refresh diff for the refresh-external orchestrator. A CVE id absent
|
|
338
|
+
* from the local catalog becomes a `_new_entry` diff; one already present whose
|
|
339
|
+
* FIELD_DROPPED_WATCH value has gone null becomes a `field_dropped` diff.
|
|
420
340
|
*/
|
|
421
341
|
async function buildDiff(ctx) {
|
|
422
342
|
const result = await fetchAdvisories({ airGap: ctx?.airGap });
|
|
@@ -432,7 +352,6 @@ async function buildDiff(ctx) {
|
|
|
432
352
|
const normalized = normalizeAdvisory(adv);
|
|
433
353
|
if (!normalized) continue;
|
|
434
354
|
if (existing.has(adv.cve_id)) {
|
|
435
|
-
// Finding 9: field-dropped detection on the existing entry.
|
|
436
355
|
const before = cveCatalog[adv.cve_id] || {};
|
|
437
356
|
const after = normalized[adv.cve_id];
|
|
438
357
|
for (const field of FIELD_DROPPED_WATCH) {
|
|
@@ -463,9 +382,7 @@ async function buildDiff(ctx) {
|
|
|
463
382
|
source: "ghsa",
|
|
464
383
|
});
|
|
465
384
|
}
|
|
466
|
-
// diffs holds
|
|
467
|
-
// separately so the summary doesn't mislabel field-dropped regressions as
|
|
468
|
-
// brand-new CVE IDs.
|
|
385
|
+
// diffs holds both kinds; count separately so a field_dropped is not a new CVE.
|
|
469
386
|
const newCount = diffs.filter((d) => d.field === "_new_entry").length;
|
|
470
387
|
const droppedCount = diffs.filter((d) => d.variant === "field_dropped").length;
|
|
471
388
|
return {
|