@blamejs/exceptd-skills 0.12.13 → 0.12.15
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 +150 -0
- package/bin/exceptd.js +147 -9
- package/data/_indexes/_meta.json +45 -45
- package/data/_indexes/activity-feed.json +4 -4
- package/data/_indexes/catalog-summaries.json +29 -29
- package/data/_indexes/chains.json +3238 -3210
- package/data/_indexes/frequency.json +3 -0
- package/data/_indexes/jurisdiction-map.json +5 -3
- package/data/_indexes/section-offsets.json +712 -685
- package/data/_indexes/theater-fingerprints.json +1 -1
- package/data/_indexes/token-budget.json +355 -340
- package/data/atlas-ttps.json +144 -129
- package/data/attack-techniques.json +319 -76
- package/data/cve-catalog.json +515 -475
- package/data/cwe-catalog.json +1081 -759
- package/data/exploit-availability.json +63 -15
- package/data/framework-control-gaps.json +867 -843
- package/data/rfc-references.json +276 -276
- package/keys/EXPECTED_FINGERPRINT +1 -0
- package/lib/auto-discovery.js +21 -4
- package/lib/cross-ref-api.js +39 -6
- package/lib/cve-curation.js +18 -5
- package/lib/lint-skills.js +6 -1
- package/lib/playbook-runner.js +742 -78
- package/lib/refresh-external.js +40 -22
- package/lib/refresh-network.js +193 -17
- package/lib/scoring.js +20 -7
- package/lib/source-ghsa.js +219 -37
- package/lib/source-osv.js +381 -122
- package/lib/validate-catalog-meta.js +64 -9
- package/lib/validate-cve-catalog.js +56 -18
- package/lib/validate-indexes.js +88 -37
- package/lib/verify.js +72 -0
- package/manifest-snapshot.json +1 -1
- package/manifest-snapshot.sha256 +1 -0
- package/manifest.json +73 -73
- package/orchestrator/dispatcher.js +21 -1
- package/orchestrator/event-bus.js +52 -8
- package/orchestrator/index.js +279 -20
- package/orchestrator/pipeline.js +63 -2
- package/orchestrator/scanner.js +32 -10
- package/orchestrator/scheduler.js +150 -17
- package/package.json +3 -1
- package/sbom.cdx.json +7 -7
- package/scripts/check-manifest-snapshot.js +32 -0
- package/scripts/check-sbom-currency.js +65 -3
- package/scripts/check-test-coverage.js +142 -19
- package/scripts/predeploy.js +83 -39
- package/scripts/refresh-manifest-snapshot.js +55 -4
- package/scripts/validate-vendor-online.js +169 -0
- package/scripts/verify-shipped-tarball.js +106 -3
- package/skills/ai-attack-surface/skill.md +18 -10
- package/skills/ai-c2-detection/skill.md +7 -2
- package/skills/ai-risk-management/skill.md +5 -4
- package/skills/api-security/skill.md +3 -3
- package/skills/attack-surface-pentest/skill.md +5 -5
- package/skills/cloud-security/skill.md +1 -1
- package/skills/compliance-theater/skill.md +8 -8
- package/skills/container-runtime-security/skill.md +1 -1
- package/skills/dlp-gap-analysis/skill.md +5 -1
- package/skills/email-security-anti-phishing/skill.md +1 -1
- package/skills/exploit-scoring/skill.md +18 -18
- package/skills/framework-gap-analysis/skill.md +6 -6
- package/skills/global-grc/skill.md +3 -2
- package/skills/identity-assurance/skill.md +2 -2
- package/skills/incident-response-playbook/skill.md +4 -4
- package/skills/kernel-lpe-triage/skill.md +21 -2
- package/skills/mcp-agent-trust/skill.md +17 -10
- package/skills/mlops-security/skill.md +2 -1
- package/skills/ot-ics-security/skill.md +1 -1
- package/skills/policy-exception-gen/skill.md +3 -3
- package/skills/pqc-first/skill.md +1 -1
- package/skills/rag-pipeline-security/skill.md +7 -3
- package/skills/researcher/skill.md +20 -3
- package/skills/sector-energy/skill.md +1 -1
- package/skills/sector-federal-government/skill.md +1 -1
- package/skills/sector-financial/skill.md +3 -3
- package/skills/sector-healthcare/skill.md +2 -2
- package/skills/security-maturity-tiers/skill.md +7 -7
- package/skills/skill-update-loop/skill.md +19 -3
- package/skills/supply-chain-integrity/skill.md +1 -1
- package/skills/threat-model-currency/skill.md +11 -11
- package/skills/threat-modeling-methodology/skill.md +3 -3
- package/skills/webapp-security/skill.md +1 -1
- package/skills/zeroday-gap-learn/skill.md +51 -7
- package/vendor/blamejs/_PROVENANCE.json +4 -1
- package/vendor/blamejs/worker-pool.js +38 -0
package/lib/source-ghsa.js
CHANGED
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
|
|
27
27
|
const https = require("https");
|
|
28
28
|
const fs = require("fs");
|
|
29
|
+
const { withRetry } = require("../vendor/blamejs/retry.js");
|
|
29
30
|
|
|
30
31
|
const GHSA_HOST = "api.github.com";
|
|
31
32
|
const GHSA_PATH = "/advisories?per_page=50&type=reviewed&sort=published&direction=desc";
|
|
@@ -33,40 +34,93 @@ const REQUEST_TIMEOUT_MS = 10000;
|
|
|
33
34
|
const USER_AGENT = "exceptd-security/source-ghsa (+https://exceptd.com)";
|
|
34
35
|
|
|
35
36
|
/**
|
|
36
|
-
*
|
|
37
|
+
* Field-dropped watch set — fields the buildDiff regression-detector
|
|
38
|
+
* watches when the upstream still has an entry but a previously-populated
|
|
39
|
+
* local value has gone null. Mirrors lib/source-osv.js. Finding 9.
|
|
40
|
+
*/
|
|
41
|
+
const FIELD_DROPPED_WATCH = Object.freeze([
|
|
42
|
+
"cvss_score",
|
|
43
|
+
"cisa_kev_pending",
|
|
44
|
+
"active_exploitation",
|
|
45
|
+
"ai_discovered",
|
|
46
|
+
"poc_available",
|
|
47
|
+
]);
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Return true when the runtime context requests air-gap mode. Sources MUST
|
|
51
|
+
* refuse network calls when this is set — fall through to fixture or return
|
|
52
|
+
* a structured `air-gap: no fixture available` error so the operator sees
|
|
53
|
+
* an explicit refusal, not a silent network attempt. Mirrors source-osv.
|
|
54
|
+
*/
|
|
55
|
+
function isAirGap(opts) {
|
|
56
|
+
if (opts && opts.airGap) return true;
|
|
57
|
+
if (process.env.EXCEPTD_AIR_GAP === "1") return true;
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Read EXCEPTD_GHSA_FIXTURE and return a structured envelope. Finding 5:
|
|
63
|
+
* mirror the OSV-source convention so a fixture file containing `null`,
|
|
64
|
+
* a number, or a string at its root doesn't slip through as an empty
|
|
65
|
+
* advisories array — the strict catalog validator would later swallow the
|
|
66
|
+
* resulting drift as "no advisories returned" instead of surfacing it as
|
|
67
|
+
* a fixture configuration error. Returns:
|
|
37
68
|
*
|
|
38
|
-
*
|
|
39
|
-
* { ok: true,
|
|
40
|
-
* { ok: false, error, source: "offline" }
|
|
69
|
+
* null when env var is unset
|
|
70
|
+
* { ok: true, advisories: [...], source } on success
|
|
71
|
+
* { ok: false, error, source: "offline" } on any failure
|
|
41
72
|
*/
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
}
|
|
73
|
+
function readFixture() {
|
|
74
|
+
const fp = process.env.EXCEPTD_GHSA_FIXTURE;
|
|
75
|
+
if (!fp) return null;
|
|
76
|
+
let raw;
|
|
77
|
+
try {
|
|
78
|
+
raw = fs.readFileSync(fp, "utf8");
|
|
79
|
+
} catch (e) {
|
|
80
|
+
return { ok: false, error: `fixture: ${e.message}`, source: "offline" };
|
|
81
|
+
}
|
|
82
|
+
let parsed;
|
|
83
|
+
try {
|
|
84
|
+
parsed = JSON.parse(raw);
|
|
85
|
+
} catch (e) {
|
|
86
|
+
return { ok: false, error: `fixture: ${e.message}`, source: "offline" };
|
|
50
87
|
}
|
|
88
|
+
if (parsed == null || typeof parsed !== "object") {
|
|
89
|
+
return { ok: false, error: `fixture: invalid root shape (got ${typeof parsed}); expected GHSA advisory object or array`, source: "offline" };
|
|
90
|
+
}
|
|
91
|
+
return { ok: true, advisories: Array.isArray(parsed) ? parsed : [parsed], source: "fixture" };
|
|
92
|
+
}
|
|
51
93
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
headers.Authorization = `Bearer ${token || process.env.GITHUB_TOKEN}`;
|
|
60
|
-
}
|
|
94
|
+
/**
|
|
95
|
+
* One HTTPS GET against api.github.com. Throws on retryable conditions so
|
|
96
|
+
* withRetry's default classifier (HTTP 408/425/429/5xx + ECONNRESET et al)
|
|
97
|
+
* picks them up; resolves to a structured envelope on permanent conditions.
|
|
98
|
+
*/
|
|
99
|
+
function ghsaRequestOnce({ path, headers, timeoutMs }) {
|
|
100
|
+
return new Promise((resolve, reject) => {
|
|
61
101
|
const req = https.get({
|
|
62
102
|
host: GHSA_HOST,
|
|
63
103
|
path,
|
|
64
104
|
headers,
|
|
65
105
|
timeout: timeoutMs,
|
|
66
106
|
}, (res) => {
|
|
67
|
-
|
|
107
|
+
const status = res.statusCode;
|
|
108
|
+
if (status === 429 || status === 503 ||
|
|
109
|
+
(status >= 500 && status <= 599) ||
|
|
110
|
+
status === 408 || status === 425) {
|
|
68
111
|
res.resume();
|
|
69
|
-
|
|
112
|
+
const err = new Error(`GHSA returned HTTP ${status}`);
|
|
113
|
+
err.statusCode = status;
|
|
114
|
+
const ra = res.headers["retry-after"];
|
|
115
|
+
if (ra) {
|
|
116
|
+
const secs = parseInt(ra, 10);
|
|
117
|
+
if (Number.isFinite(secs)) err.retryAfterMs = secs * 1000;
|
|
118
|
+
}
|
|
119
|
+
return reject(err);
|
|
120
|
+
}
|
|
121
|
+
if (status !== 200) {
|
|
122
|
+
res.resume();
|
|
123
|
+
return resolve({ ok: false, error: `GHSA returned HTTP ${status}`, source: "offline" });
|
|
70
124
|
}
|
|
71
125
|
const chunks = [];
|
|
72
126
|
res.on("data", (c) => chunks.push(c));
|
|
@@ -88,11 +142,60 @@ async function fetchAdvisories({ timeoutMs = REQUEST_TIMEOUT_MS, path = GHSA_PAT
|
|
|
88
142
|
}
|
|
89
143
|
});
|
|
90
144
|
});
|
|
91
|
-
req.on("timeout", () =>
|
|
92
|
-
|
|
145
|
+
req.on("timeout", () => {
|
|
146
|
+
const err = new Error("timeout");
|
|
147
|
+
err.code = "ETIMEDOUT";
|
|
148
|
+
req.destroy(err);
|
|
149
|
+
});
|
|
150
|
+
req.on("error", (e) => {
|
|
151
|
+
if (e && e.code && /^(ECONNRESET|ECONNREFUSED|ECONNABORTED|ETIMEDOUT|EPIPE|EAGAIN|ENOTFOUND|ENETUNREACH)$/.test(e.code)) {
|
|
152
|
+
return reject(e);
|
|
153
|
+
}
|
|
154
|
+
resolve({ ok: false, error: e.message, source: "offline" });
|
|
155
|
+
});
|
|
93
156
|
});
|
|
94
157
|
}
|
|
95
158
|
|
|
159
|
+
/**
|
|
160
|
+
* Fetch a page of advisories (default: latest 50). Wraps the underlying
|
|
161
|
+
* HTTPS request in withRetry so transient 429/503/5xx + net errors back off
|
|
162
|
+
* automatically.
|
|
163
|
+
*
|
|
164
|
+
* Returns:
|
|
165
|
+
* { ok: true, advisories: [...], source: "github-api" | "fixture", rate_limit?: { remaining, reset } }
|
|
166
|
+
* { ok: false, error, source: "offline" }
|
|
167
|
+
*/
|
|
168
|
+
async function fetchAdvisories({ timeoutMs = REQUEST_TIMEOUT_MS, path = GHSA_PATH, token = null, airGap = false } = {}) {
|
|
169
|
+
const fixture = readFixture();
|
|
170
|
+
if (fixture) return fixture;
|
|
171
|
+
// Finding 7: air-gap hard-refuses network when no fixture is configured.
|
|
172
|
+
if (isAirGap({ airGap })) {
|
|
173
|
+
return { ok: false, error: "air-gap: no fixture available (set EXCEPTD_GHSA_FIXTURE)", source: "offline" };
|
|
174
|
+
}
|
|
175
|
+
const headers = {
|
|
176
|
+
"Accept": "application/vnd.github+json",
|
|
177
|
+
"User-Agent": USER_AGENT,
|
|
178
|
+
"X-GitHub-Api-Version": "2022-11-28",
|
|
179
|
+
};
|
|
180
|
+
if (token || process.env.GITHUB_TOKEN) {
|
|
181
|
+
headers.Authorization = `Bearer ${token || process.env.GITHUB_TOKEN}`;
|
|
182
|
+
}
|
|
183
|
+
try {
|
|
184
|
+
return await withRetry(() => ghsaRequestOnce({ path, headers, timeoutMs }), {
|
|
185
|
+
maxAttempts: 3,
|
|
186
|
+
baseDelayMs: 100,
|
|
187
|
+
maxDelayMs: 2000,
|
|
188
|
+
jitterFactor: 0.5,
|
|
189
|
+
});
|
|
190
|
+
} catch (e) {
|
|
191
|
+
const status = typeof e?.statusCode === "number" ? e.statusCode : null;
|
|
192
|
+
const error = status
|
|
193
|
+
? `GHSA returned HTTP ${status}`
|
|
194
|
+
: `GHSA request failed: ${e.message || e}`;
|
|
195
|
+
return { ok: false, error, status, source: "offline" };
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
96
199
|
/**
|
|
97
200
|
* Fetch a single advisory by ID — accepts CVE-* or GHSA-* identifiers.
|
|
98
201
|
*
|
|
@@ -103,12 +206,18 @@ async function fetchAdvisoryById(id, opts = {}) {
|
|
|
103
206
|
if (!id || typeof id !== "string") {
|
|
104
207
|
return { ok: false, error: "id is required (CVE-* or GHSA-*)", source: "offline" };
|
|
105
208
|
}
|
|
209
|
+
// Finding 8: trim whitespace at the entry seam.
|
|
210
|
+
id = id.trim();
|
|
211
|
+
if (!id) {
|
|
212
|
+
return { ok: false, error: "id is required (CVE-* or GHSA-*)", source: "offline" };
|
|
213
|
+
}
|
|
106
214
|
if (process.env.EXCEPTD_GHSA_FIXTURE) {
|
|
107
215
|
const r = await fetchAdvisories(opts);
|
|
108
216
|
if (!r.ok) return r;
|
|
217
|
+
const want = id.toUpperCase();
|
|
109
218
|
const match = r.advisories.find(a =>
|
|
110
|
-
(a.ghsa_id && a.ghsa_id.toUpperCase() ===
|
|
111
|
-
(a.cve_id && a.cve_id.toUpperCase() ===
|
|
219
|
+
(a.ghsa_id && String(a.ghsa_id).toUpperCase() === want) ||
|
|
220
|
+
(a.cve_id && String(a.cve_id).toUpperCase() === want)
|
|
112
221
|
);
|
|
113
222
|
if (!match) return { ok: false, error: `${id} not in fixture`, source: "fixture" };
|
|
114
223
|
return { ok: true, advisories: [match], source: "fixture" };
|
|
@@ -128,6 +237,23 @@ async function fetchAdvisoryById(id, opts = {}) {
|
|
|
128
237
|
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" };
|
|
129
238
|
}
|
|
130
239
|
|
|
240
|
+
/**
|
|
241
|
+
* Validate + slice a published_at timestamp. Findings 2 + 17:
|
|
242
|
+
* - typeof guard so non-string inputs (number, object, undefined) become
|
|
243
|
+
* null instead of throwing on .slice().
|
|
244
|
+
* - ISO-prefix + year sanity bound so garbage timestamps don't pollute
|
|
245
|
+
* downstream surfaces.
|
|
246
|
+
*/
|
|
247
|
+
function safeDateSlice(value) {
|
|
248
|
+
if (typeof value !== "string") return null;
|
|
249
|
+
const head = value.slice(0, 10);
|
|
250
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(head)) return null;
|
|
251
|
+
const year = parseInt(head.slice(0, 4), 10);
|
|
252
|
+
const now = new Date().getUTCFullYear();
|
|
253
|
+
if (!Number.isFinite(year) || year < 1990 || year > now + 1) return null;
|
|
254
|
+
return head;
|
|
255
|
+
}
|
|
256
|
+
|
|
131
257
|
/**
|
|
132
258
|
* Normalize a GHSA advisory object to the exceptd catalog draft shape.
|
|
133
259
|
* Fields the GHSA carries authoritatively: cve_id, ghsa_id, summary,
|
|
@@ -146,7 +272,9 @@ function normalizeAdvisory(adv) {
|
|
|
146
272
|
const ecosystems = new Set();
|
|
147
273
|
const affected = [];
|
|
148
274
|
const ecosystemPackages = [];
|
|
149
|
-
|
|
275
|
+
// Finding 3: vulnerabilities may not be an array — guard before iterating.
|
|
276
|
+
const vulnList = Array.isArray(adv.vulnerabilities) ? adv.vulnerabilities : [];
|
|
277
|
+
for (const v of vulnList) {
|
|
150
278
|
if (v?.package?.ecosystem) ecosystems.add(v.package.ecosystem);
|
|
151
279
|
if (v?.package?.name) {
|
|
152
280
|
ecosystemPackages.push(`${v.package.ecosystem || "?"}:${v.package.name}`);
|
|
@@ -156,7 +284,14 @@ function normalizeAdvisory(adv) {
|
|
|
156
284
|
}
|
|
157
285
|
}
|
|
158
286
|
|
|
159
|
-
|
|
287
|
+
// Finding 4: cvss.score may arrive as a string ("9.8") rather than a
|
|
288
|
+
// number. Number-coerce + finite-check so the catalog field stays
|
|
289
|
+
// numeric across upstream shape drift.
|
|
290
|
+
let cvssScore = null;
|
|
291
|
+
if (adv.cvss != null && adv.cvss.score !== undefined && adv.cvss.score !== null) {
|
|
292
|
+
const n = Number(adv.cvss.score);
|
|
293
|
+
cvssScore = Number.isFinite(n) ? n : null;
|
|
294
|
+
}
|
|
160
295
|
const cvssVector = adv.cvss?.vector_string || null;
|
|
161
296
|
const severity = (adv.severity || "").toLowerCase();
|
|
162
297
|
// Derive a coarse type from package ecosystem when nothing better available.
|
|
@@ -166,6 +301,13 @@ function normalizeAdvisory(adv) {
|
|
|
166
301
|
: ecosystems.has("rubygems") ? "supply-chain-gem"
|
|
167
302
|
: "supply-chain-other";
|
|
168
303
|
|
|
304
|
+
// Finding 2 + 17: type-safe + format-validated published_at slicing.
|
|
305
|
+
const publishedDate = safeDateSlice(adv.published_at);
|
|
306
|
+
|
|
307
|
+
// Finding 20: references may not be an array — guard the spread before
|
|
308
|
+
// it silently truncates to an empty list.
|
|
309
|
+
const refList = Array.isArray(adv.references) ? adv.references : [];
|
|
310
|
+
|
|
169
311
|
return {
|
|
170
312
|
[adv.cve_id]: {
|
|
171
313
|
name: adv.summary || adv.cve_id,
|
|
@@ -205,7 +347,7 @@ function normalizeAdvisory(adv) {
|
|
|
205
347
|
verification_sources: [
|
|
206
348
|
...(adv.html_url ? [adv.html_url] : []),
|
|
207
349
|
...(adv.cve_id ? [`https://nvd.nist.gov/vuln/detail/${adv.cve_id}`] : []),
|
|
208
|
-
...
|
|
350
|
+
...refList.slice(0, 10),
|
|
209
351
|
],
|
|
210
352
|
vendor_advisories: [
|
|
211
353
|
{
|
|
@@ -213,7 +355,7 @@ function normalizeAdvisory(adv) {
|
|
|
213
355
|
advisory_id: adv.ghsa_id || null,
|
|
214
356
|
url: adv.html_url || `https://github.com/advisories?query=${encodeURIComponent(adv.cve_id)}`,
|
|
215
357
|
severity: severity || null,
|
|
216
|
-
published_date:
|
|
358
|
+
published_date: publishedDate,
|
|
217
359
|
},
|
|
218
360
|
],
|
|
219
361
|
iocs: null,
|
|
@@ -231,19 +373,51 @@ function normalizeAdvisory(adv) {
|
|
|
231
373
|
* Build a refresh diff for the existing refresh-external orchestrator.
|
|
232
374
|
* Compares the latest 50 advisories' CVE IDs against the local catalog;
|
|
233
375
|
* any CVE ID not in the catalog becomes an "add" diff.
|
|
376
|
+
*
|
|
377
|
+
* Finding 9: when the advisory is already in the catalog but a watched
|
|
378
|
+
* field has dropped from populated -> null, surface a `field_dropped`
|
|
379
|
+
* diff so curators don't silently lose signal.
|
|
380
|
+
*
|
|
381
|
+
* Finding 18: count + surface GHSA-only advisories (no CVE id) that were
|
|
382
|
+
* skipped, so the summary explains why N upstream advisories produced
|
|
383
|
+
* fewer than N diffs.
|
|
234
384
|
*/
|
|
235
385
|
async function buildDiff(ctx) {
|
|
236
|
-
const result = await fetchAdvisories({});
|
|
386
|
+
const result = await fetchAdvisories({ airGap: ctx?.airGap });
|
|
237
387
|
if (!result.ok) {
|
|
238
388
|
return { status: "unreachable", diffs: [], errors: 1, summary: `GHSA fetch failed: ${result.error}` };
|
|
239
389
|
}
|
|
240
|
-
const
|
|
390
|
+
const cveCatalog = ctx.cveCatalog || {};
|
|
391
|
+
const existing = new Set(Object.keys(cveCatalog).filter(k => /^CVE-/.test(k)));
|
|
241
392
|
const diffs = [];
|
|
393
|
+
let ghsaOnlySkipped = 0;
|
|
242
394
|
for (const adv of result.advisories) {
|
|
243
|
-
if (!adv.cve_id) continue;
|
|
244
|
-
if (existing.has(adv.cve_id)) continue;
|
|
395
|
+
if (!adv.cve_id) { ghsaOnlySkipped++; continue; }
|
|
245
396
|
const normalized = normalizeAdvisory(adv);
|
|
246
397
|
if (!normalized) continue;
|
|
398
|
+
if (existing.has(adv.cve_id)) {
|
|
399
|
+
// Finding 9: field-dropped detection on the existing entry.
|
|
400
|
+
const before = cveCatalog[adv.cve_id] || {};
|
|
401
|
+
const after = normalized[adv.cve_id];
|
|
402
|
+
for (const field of FIELD_DROPPED_WATCH) {
|
|
403
|
+
const had = before[field];
|
|
404
|
+
const has = after[field];
|
|
405
|
+
const wasPopulated = had !== null && had !== undefined && had !== "" && had !== false;
|
|
406
|
+
const isNowEmpty = has === null || has === undefined;
|
|
407
|
+
if (wasPopulated && isNowEmpty) {
|
|
408
|
+
diffs.push({
|
|
409
|
+
id: adv.cve_id,
|
|
410
|
+
field,
|
|
411
|
+
before: had,
|
|
412
|
+
after: null,
|
|
413
|
+
severity: null,
|
|
414
|
+
source: "ghsa",
|
|
415
|
+
variant: "field_dropped",
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
continue;
|
|
420
|
+
}
|
|
247
421
|
diffs.push({
|
|
248
422
|
id: adv.cve_id,
|
|
249
423
|
field: "_new_entry",
|
|
@@ -257,9 +431,17 @@ async function buildDiff(ctx) {
|
|
|
257
431
|
status: "ok",
|
|
258
432
|
diffs,
|
|
259
433
|
errors: 0,
|
|
260
|
-
|
|
434
|
+
ghsa_only_skipped: ghsaOnlySkipped,
|
|
435
|
+
summary: `GHSA returned ${result.advisories.length} reviewed advisories; ${diffs.length} new CVE ID(s) not yet in local catalog, ${ghsaOnlySkipped} ghsa_only_skipped.`,
|
|
261
436
|
rate_limit: result.rate_limit || null,
|
|
262
437
|
};
|
|
263
438
|
}
|
|
264
439
|
|
|
265
|
-
module.exports = {
|
|
440
|
+
module.exports = {
|
|
441
|
+
fetchAdvisories,
|
|
442
|
+
fetchAdvisoryById,
|
|
443
|
+
normalizeAdvisory,
|
|
444
|
+
buildDiff,
|
|
445
|
+
FIELD_DROPPED_WATCH,
|
|
446
|
+
safeDateSlice,
|
|
447
|
+
};
|