@webcrawl/mcp 1.4.2 → 1.4.3
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/README.md +1 -1
- package/dist/bin.js +5 -7
- package/dist/core/engine/awswaf.d.ts +59 -0
- package/dist/core/engine/awswaf.js +164 -0
- package/dist/core/engine/cache.d.ts +70 -0
- package/dist/core/engine/cache.js +297 -0
- package/dist/core/engine/chrome.d.ts +14 -0
- package/dist/core/engine/chrome.js +140 -0
- package/dist/core/engine/crawl-policy.d.ts +36 -0
- package/dist/core/engine/crawl-policy.js +134 -0
- package/dist/core/engine/crawler.d.ts +152 -0
- package/dist/core/engine/crawler.js +918 -0
- package/dist/core/engine/fetcher.d.ts +92 -0
- package/dist/core/engine/fetcher.js +494 -0
- package/dist/core/engine/index.d.ts +7 -0
- package/dist/core/engine/index.js +7 -0
- package/dist/core/engine/markdown.d.ts +33 -0
- package/dist/core/engine/markdown.js +334 -0
- package/dist/core/engine/path-scope.d.ts +21 -0
- package/dist/core/engine/path-scope.js +88 -0
- package/dist/core/engine/render.d.ts +41 -0
- package/dist/core/engine/render.js +138 -0
- package/dist/core/engine/safe-url.d.ts +29 -0
- package/dist/core/engine/safe-url.js +109 -0
- package/dist/core/engine/session.d.ts +93 -0
- package/dist/core/engine/session.js +250 -0
- package/dist/core/engine/stealth.d.ts +83 -0
- package/dist/core/engine/stealth.js +222 -0
- package/dist/core/engine.d.ts +1 -0
- package/dist/core/engine.js +1 -0
- package/dist/core/extract/business.d.ts +28 -0
- package/dist/core/extract/business.js +344 -0
- package/dist/core/extract/currency.d.ts +15 -0
- package/dist/core/extract/currency.js +259 -0
- package/dist/core/extract/cve-cache.d.ts +62 -0
- package/dist/core/extract/cve-cache.js +277 -0
- package/dist/core/extract/cve.d.ts +57 -0
- package/dist/core/extract/cve.js +397 -0
- package/dist/core/extract/dedupe-key.d.ts +12 -0
- package/dist/core/extract/dedupe-key.js +36 -0
- package/dist/core/extract/flights.d.ts +37 -0
- package/dist/core/extract/flights.js +373 -0
- package/dist/core/extract/hotels.d.ts +35 -0
- package/dist/core/extract/hotels.js +473 -0
- package/dist/core/extract/index.d.ts +9 -0
- package/dist/core/extract/index.js +9 -0
- package/dist/core/extract/real-estate.d.ts +53 -0
- package/dist/core/extract/real-estate.js +835 -0
- package/dist/core/extract/scaledown.d.ts +14 -0
- package/dist/core/extract/scaledown.js +484 -0
- package/dist/core/extract/shopping.d.ts +44 -0
- package/dist/core/extract/shopping.js +393 -0
- package/dist/core/extract/structured.d.ts +14 -0
- package/dist/core/extract/structured.js +144 -0
- package/dist/core/image-url.d.ts +5 -0
- package/dist/core/image-url.js +13 -0
- package/dist/core/interact/driver.d.ts +72 -0
- package/dist/core/interact/driver.js +384 -0
- package/dist/core/interact/remote.d.ts +41 -0
- package/dist/core/interact/remote.js +133 -0
- package/dist/core/interact/spans.d.ts +46 -0
- package/dist/core/interact/spans.js +32 -0
- package/dist/core/interact/validate.d.ts +34 -0
- package/dist/core/interact/validate.js +135 -0
- package/dist/core/interact/vm.d.ts +43 -0
- package/dist/core/interact/vm.js +338 -0
- package/dist/core/local-engine.d.ts +56 -0
- package/dist/core/local-engine.js +667 -0
- package/dist/core/parse/detect.d.ts +29 -0
- package/dist/core/parse/detect.js +152 -0
- package/dist/core/parse/fetch.d.ts +20 -0
- package/dist/core/parse/fetch.js +39 -0
- package/dist/core/parse/image.d.ts +13 -0
- package/dist/core/parse/image.js +100 -0
- package/dist/core/parse/index.d.ts +53 -0
- package/dist/core/parse/index.js +153 -0
- package/dist/core/parse/limits.d.ts +12 -0
- package/dist/core/parse/limits.js +29 -0
- package/dist/core/parse/media.d.ts +42 -0
- package/dist/core/parse/media.js +238 -0
- package/dist/core/parse/merge.d.ts +19 -0
- package/dist/core/parse/merge.js +48 -0
- package/dist/core/parse/ocr.d.ts +25 -0
- package/dist/core/parse/ocr.js +121 -0
- package/dist/core/parse/office.d.ts +28 -0
- package/dist/core/parse/office.js +83 -0
- package/dist/core/parse/pdf.d.ts +27 -0
- package/dist/core/parse/pdf.js +156 -0
- package/dist/core/parse/types.d.ts +38 -0
- package/dist/core/parse/types.js +1 -0
- package/dist/core/parse/youtube.d.ts +30 -0
- package/dist/core/parse/youtube.js +264 -0
- package/dist/core/ranker/decision.d.ts +28 -0
- package/dist/core/ranker/decision.js +151 -0
- package/dist/core/ranker/index.d.ts +5 -0
- package/dist/core/ranker/index.js +361 -0
- package/dist/core/ranker/lexical.d.ts +82 -0
- package/dist/core/ranker/lexical.js +643 -0
- package/dist/core/ranker/types.d.ts +90 -0
- package/dist/core/ranker/types.js +1 -0
- package/dist/core/ranker.d.ts +1 -0
- package/dist/core/ranker.js +1 -0
- package/dist/core/sandbox.d.ts +29 -0
- package/dist/core/sandbox.js +222 -0
- package/dist/core/search/catalog.d.ts +176 -0
- package/dist/core/search/catalog.js +488 -0
- package/dist/core/search/index.d.ts +3 -0
- package/dist/core/search/index.js +3 -0
- package/dist/core/search/pipeline.d.ts +7 -0
- package/dist/core/search/pipeline.js +719 -0
- package/dist/core/search/providers.d.ts +72 -0
- package/dist/core/search/providers.js +1270 -0
- package/dist/core/search.d.ts +1 -0
- package/dist/core/search.js +1 -0
- package/dist/core/targets/developer.d.ts +44 -0
- package/dist/core/targets/developer.js +338 -0
- package/dist/core/targets/index.d.ts +3 -0
- package/dist/core/targets/index.js +3 -0
- package/dist/core/targets/registry.d.ts +32 -0
- package/dist/core/targets/registry.js +411 -0
- package/dist/core/targets/types.d.ts +72 -0
- package/dist/core/targets/types.js +8 -0
- package/dist/core/targets.d.ts +1 -0
- package/dist/core/targets.js +1 -0
- package/dist/format.js +5 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/load-local-engine.d.ts +7 -0
- package/dist/load-local-engine.js +22 -0
- package/dist/server.js +3 -0
- package/package.json +11 -3
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
// Structured CVE records for the `security` search category.
|
|
2
|
+
// Sources: NVD CVE API 2.0 (scores, CWE, CPE products, KEV flag) and OSV
|
|
3
|
+
// (ecosystem package ranges and fixed versions, GHSA <-> CVE aliases).
|
|
4
|
+
const CVE_RE = /\bCVE-(\d{4})-(\d{4,7})\b/gi;
|
|
5
|
+
const GHSA_RE = /\bGHSA(?:-[23456789cfghjmpqrvwx]{4}){3}\b/gi;
|
|
6
|
+
export const SEVERITY_ORDER = {
|
|
7
|
+
critical: 4,
|
|
8
|
+
high: 3,
|
|
9
|
+
medium: 2,
|
|
10
|
+
low: 1,
|
|
11
|
+
none: 0,
|
|
12
|
+
};
|
|
13
|
+
const roundUp1 = (x) => {
|
|
14
|
+
const i = Math.round(x * 100000);
|
|
15
|
+
return i % 10000 === 0 ? i / 100000 : (Math.floor(i / 10000) + 1) / 10;
|
|
16
|
+
};
|
|
17
|
+
/** CVSS v3.0/v3.1 base score from a vector string (FIRST spec). Undefined for v4 or malformed vectors. */
|
|
18
|
+
export function cvss3BaseScore(vector) {
|
|
19
|
+
if (!/^CVSS:3\.[01]\//.test(vector))
|
|
20
|
+
return undefined;
|
|
21
|
+
const m = Object.fromEntries(vector.split("/").slice(1).map((p) => p.split(":")));
|
|
22
|
+
const AV = { N: 0.85, A: 0.62, L: 0.55, P: 0.2 }[m.AV];
|
|
23
|
+
const AC = { L: 0.77, H: 0.44 }[m.AC];
|
|
24
|
+
const UI = { N: 0.85, R: 0.62 }[m.UI];
|
|
25
|
+
const changed = m.S === "C";
|
|
26
|
+
const PR = (changed ? { N: 0.85, L: 0.68, H: 0.5 } : { N: 0.85, L: 0.62, H: 0.27 })[m.PR];
|
|
27
|
+
const cia = { H: 0.56, L: 0.22, N: 0 };
|
|
28
|
+
const C = cia[m.C];
|
|
29
|
+
const I = cia[m.I];
|
|
30
|
+
const A = cia[m.A];
|
|
31
|
+
if ([AV, AC, UI, PR, C, I, A].some((v) => v === undefined) || (m.S !== "U" && m.S !== "C"))
|
|
32
|
+
return undefined;
|
|
33
|
+
const iss = 1 - (1 - C) * (1 - I) * (1 - A);
|
|
34
|
+
const impact = changed ? 7.52 * (iss - 0.029) - 3.25 * Math.pow(iss - 0.02, 15) : 6.42 * iss;
|
|
35
|
+
if (impact <= 0)
|
|
36
|
+
return 0;
|
|
37
|
+
const exploitability = 8.22 * AV * AC * PR * UI;
|
|
38
|
+
return changed
|
|
39
|
+
? roundUp1(Math.min(1.08 * (impact + exploitability), 10))
|
|
40
|
+
: roundUp1(Math.min(impact + exploitability, 10));
|
|
41
|
+
}
|
|
42
|
+
/** CVSS v3/v4 qualitative rating bands. */
|
|
43
|
+
export function severityFromScore(score) {
|
|
44
|
+
if (score === undefined || !Number.isFinite(score))
|
|
45
|
+
return undefined;
|
|
46
|
+
if (score >= 9)
|
|
47
|
+
return "critical";
|
|
48
|
+
if (score >= 7)
|
|
49
|
+
return "high";
|
|
50
|
+
if (score >= 4)
|
|
51
|
+
return "medium";
|
|
52
|
+
if (score > 0)
|
|
53
|
+
return "low";
|
|
54
|
+
return "none";
|
|
55
|
+
}
|
|
56
|
+
function normalizeSeverity(raw) {
|
|
57
|
+
const s = String(raw ?? "").trim().toLowerCase();
|
|
58
|
+
if (s === "critical")
|
|
59
|
+
return "critical";
|
|
60
|
+
if (s === "high" || s === "important")
|
|
61
|
+
return "high";
|
|
62
|
+
if (s === "medium" || s === "moderate")
|
|
63
|
+
return "medium";
|
|
64
|
+
if (s === "low")
|
|
65
|
+
return "low";
|
|
66
|
+
if (s === "none")
|
|
67
|
+
return "none";
|
|
68
|
+
return undefined;
|
|
69
|
+
}
|
|
70
|
+
export function normalizeCveId(id) {
|
|
71
|
+
return id.trim().toUpperCase();
|
|
72
|
+
}
|
|
73
|
+
/** Unique CVE ids in order of first appearance. */
|
|
74
|
+
export function extractCveIdsFromText(text, max = 20) {
|
|
75
|
+
const out = [];
|
|
76
|
+
const seen = new Set();
|
|
77
|
+
for (const m of text.matchAll(CVE_RE)) {
|
|
78
|
+
const id = normalizeCveId(m[0]);
|
|
79
|
+
if (seen.has(id))
|
|
80
|
+
continue;
|
|
81
|
+
seen.add(id);
|
|
82
|
+
out.push(id);
|
|
83
|
+
if (out.length >= max)
|
|
84
|
+
break;
|
|
85
|
+
}
|
|
86
|
+
return out;
|
|
87
|
+
}
|
|
88
|
+
export function extractGhsaIdsFromText(text, max = 10) {
|
|
89
|
+
const out = new Set();
|
|
90
|
+
for (const m of text.matchAll(GHSA_RE)) {
|
|
91
|
+
out.add(`GHSA${m[0].slice(4).toLowerCase()}`);
|
|
92
|
+
if (out.size >= max)
|
|
93
|
+
break;
|
|
94
|
+
}
|
|
95
|
+
return [...out];
|
|
96
|
+
}
|
|
97
|
+
export function looksLikeVulnId(query) {
|
|
98
|
+
return /\bCVE-\d{4}-\d{4,7}\b/i.test(query) || /\bGHSA(?:-[a-z0-9]{4}){3}\b/i.test(query);
|
|
99
|
+
}
|
|
100
|
+
export const nvdUrl = (id) => `https://nvd.nist.gov/vuln/detail/${id}`;
|
|
101
|
+
function englishDescription(list) {
|
|
102
|
+
if (!Array.isArray(list))
|
|
103
|
+
return "";
|
|
104
|
+
const en = list.find((d) => d?.lang === "en") ?? list[0];
|
|
105
|
+
return String(en?.value ?? "").replace(/\s+/g, " ").trim();
|
|
106
|
+
}
|
|
107
|
+
function pickNvdCvss(metrics) {
|
|
108
|
+
if (!metrics)
|
|
109
|
+
return {};
|
|
110
|
+
const order = [
|
|
111
|
+
["cvssMetricV40", "4.0"],
|
|
112
|
+
["cvssMetricV31", "3.1"],
|
|
113
|
+
["cvssMetricV30", "3.0"],
|
|
114
|
+
["cvssMetricV2", "2.0"],
|
|
115
|
+
];
|
|
116
|
+
for (const [key, version] of order) {
|
|
117
|
+
const list = metrics[key];
|
|
118
|
+
if (!Array.isArray(list) || list.length === 0)
|
|
119
|
+
continue;
|
|
120
|
+
// Prefer NVD's own ("Primary") assessment over a CNA's secondary one.
|
|
121
|
+
const m = list.find((x) => x?.type === "Primary") ?? list[0];
|
|
122
|
+
const data = m?.cvssData ?? {};
|
|
123
|
+
const score = typeof data.baseScore === "number" ? data.baseScore : undefined;
|
|
124
|
+
return {
|
|
125
|
+
cvssScore: score,
|
|
126
|
+
cvssVector: data.vectorString,
|
|
127
|
+
cvssVersion: version,
|
|
128
|
+
severity: normalizeSeverity(data.baseSeverity ?? m?.baseSeverity) ?? severityFromScore(score),
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
return {};
|
|
132
|
+
}
|
|
133
|
+
function rangeText(m) {
|
|
134
|
+
const parts = [];
|
|
135
|
+
if (m.versionStartIncluding)
|
|
136
|
+
parts.push(`>=${m.versionStartIncluding}`);
|
|
137
|
+
if (m.versionStartExcluding)
|
|
138
|
+
parts.push(`>${m.versionStartExcluding}`);
|
|
139
|
+
if (m.versionEndIncluding)
|
|
140
|
+
parts.push(`<=${m.versionEndIncluding}`);
|
|
141
|
+
if (m.versionEndExcluding)
|
|
142
|
+
parts.push(`<${m.versionEndExcluding}`);
|
|
143
|
+
return parts.length ? parts.join(" ") : undefined;
|
|
144
|
+
}
|
|
145
|
+
function nvdAffected(configurations) {
|
|
146
|
+
if (!Array.isArray(configurations))
|
|
147
|
+
return [];
|
|
148
|
+
const byKey = new Map();
|
|
149
|
+
const walk = (nodes) => {
|
|
150
|
+
if (!Array.isArray(nodes))
|
|
151
|
+
return;
|
|
152
|
+
for (const node of nodes) {
|
|
153
|
+
for (const m of (node?.cpeMatch ?? [])) {
|
|
154
|
+
if (!m?.vulnerable || typeof m.criteria !== "string")
|
|
155
|
+
continue;
|
|
156
|
+
// cpe:2.3:<part>:<vendor>:<product>:<version>:...
|
|
157
|
+
const [, , , vendor, product, version] = m.criteria.split(":");
|
|
158
|
+
if (!product || product === "*")
|
|
159
|
+
continue;
|
|
160
|
+
const key = `${vendor}:${product}`;
|
|
161
|
+
const entry = byKey.get(key) ??
|
|
162
|
+
{ vendor: vendor && vendor !== "*" ? vendor : undefined, product, _versions: [] };
|
|
163
|
+
const range = rangeText(m) ?? (version && version !== "*" && version !== "-" ? version : undefined);
|
|
164
|
+
if (range && !entry._versions.includes(range))
|
|
165
|
+
entry._versions.push(range);
|
|
166
|
+
if (m.versionEndExcluding && !entry.fixed)
|
|
167
|
+
entry.fixed = m.versionEndExcluding;
|
|
168
|
+
byKey.set(key, entry);
|
|
169
|
+
}
|
|
170
|
+
walk(node?.children);
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
for (const c of configurations)
|
|
174
|
+
walk(c?.nodes);
|
|
175
|
+
return [...byKey.values()].slice(0, 8).map(({ _versions, ...a }) => ({
|
|
176
|
+
...a,
|
|
177
|
+
...(_versions.length ? { versions: _versions.slice(0, 4).join(", ") } : {}),
|
|
178
|
+
}));
|
|
179
|
+
}
|
|
180
|
+
/** One NVD `vulnerabilities[].cve` object (or the wrapper) -> CveRecord. */
|
|
181
|
+
export function parseNvdCve(input) {
|
|
182
|
+
const cve = input?.cve ?? input;
|
|
183
|
+
if (!cve || typeof cve.id !== "string")
|
|
184
|
+
return null;
|
|
185
|
+
const id = normalizeCveId(cve.id);
|
|
186
|
+
if (!/^CVE-\d{4}-\d{4,}$/.test(id))
|
|
187
|
+
return null;
|
|
188
|
+
const summary = englishDescription(cve.descriptions);
|
|
189
|
+
const cwe = new Set();
|
|
190
|
+
for (const w of (cve.weaknesses ?? [])) {
|
|
191
|
+
for (const d of (w?.description ?? [])) {
|
|
192
|
+
const v = String(d?.value ?? "");
|
|
193
|
+
if (/^CWE-\d+$/.test(v))
|
|
194
|
+
cwe.add(v);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
const affected = nvdAffected(cve.configurations);
|
|
198
|
+
const refs = (cve.references ?? [])
|
|
199
|
+
.map((r) => r?.url)
|
|
200
|
+
.filter((u) => typeof u === "string" && /^https?:\/\//.test(u));
|
|
201
|
+
return {
|
|
202
|
+
id,
|
|
203
|
+
...(cve.cisaVulnerabilityName ? { title: String(cve.cisaVulnerabilityName) } : {}),
|
|
204
|
+
summary,
|
|
205
|
+
...pickNvdCvss(cve.metrics),
|
|
206
|
+
...(cwe.size ? { cwe: [...cwe] } : {}),
|
|
207
|
+
...(affected.length ? { affected } : {}),
|
|
208
|
+
...(cve.published ? { published: toIso(cve.published) } : {}),
|
|
209
|
+
...(cve.lastModified ? { modified: toIso(cve.lastModified) } : {}),
|
|
210
|
+
...(cve.cisaExploitAdd ? { kev: true } : {}),
|
|
211
|
+
references: [...new Set(refs)].slice(0, 12),
|
|
212
|
+
url: nvdUrl(id),
|
|
213
|
+
source: "nvd",
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
/** NVD 2.0 response body -> records. */
|
|
217
|
+
export function parseNvdResponse(body) {
|
|
218
|
+
const list = Array.isArray(body?.vulnerabilities) ? body.vulnerabilities : [];
|
|
219
|
+
return list.map(parseNvdCve).filter((r) => r !== null);
|
|
220
|
+
}
|
|
221
|
+
function toIso(s) {
|
|
222
|
+
// NVD omits the zone ("2024-03-29T17:15:21.150"); treat as UTC.
|
|
223
|
+
const withZone = /[zZ]|[+-]\d\d:?\d\d$/.test(s) ? s : `${s}Z`;
|
|
224
|
+
const d = new Date(withZone);
|
|
225
|
+
return Number.isNaN(d.getTime()) ? s : d.toISOString();
|
|
226
|
+
}
|
|
227
|
+
function osvAffected(affected) {
|
|
228
|
+
if (!Array.isArray(affected))
|
|
229
|
+
return [];
|
|
230
|
+
const out = [];
|
|
231
|
+
for (const a of affected) {
|
|
232
|
+
const name = a?.package?.name;
|
|
233
|
+
if (typeof name !== "string" || !name)
|
|
234
|
+
continue;
|
|
235
|
+
const ranges = [];
|
|
236
|
+
let fixed;
|
|
237
|
+
for (const r of (a.ranges ?? [])) {
|
|
238
|
+
if (r?.type === "GIT")
|
|
239
|
+
continue;
|
|
240
|
+
let intro;
|
|
241
|
+
for (const e of (r?.events ?? [])) {
|
|
242
|
+
if (e.introduced !== undefined)
|
|
243
|
+
intro = String(e.introduced);
|
|
244
|
+
if (e.fixed !== undefined) {
|
|
245
|
+
fixed ??= String(e.fixed);
|
|
246
|
+
ranges.push(intro && intro !== "0" ? `>=${intro} <${e.fixed}` : `<${e.fixed}`);
|
|
247
|
+
intro = undefined;
|
|
248
|
+
}
|
|
249
|
+
if (e.last_affected !== undefined) {
|
|
250
|
+
ranges.push(intro && intro !== "0" ? `>=${intro} <=${e.last_affected}` : `<=${e.last_affected}`);
|
|
251
|
+
intro = undefined;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
if (intro !== undefined)
|
|
255
|
+
ranges.push(intro === "0" ? "all versions" : `>=${intro}`);
|
|
256
|
+
}
|
|
257
|
+
if (!ranges.length && Array.isArray(a.versions) && a.versions.length) {
|
|
258
|
+
ranges.push(a.versions.slice(0, 4).join(", ") + (a.versions.length > 4 ? ", …" : ""));
|
|
259
|
+
}
|
|
260
|
+
out.push({
|
|
261
|
+
product: name,
|
|
262
|
+
...(a.package.ecosystem ? { ecosystem: String(a.package.ecosystem) } : {}),
|
|
263
|
+
...(ranges.length ? { versions: [...new Set(ranges)].slice(0, 4).join(", ") } : {}),
|
|
264
|
+
...(fixed ? { fixed } : {}),
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
return out.slice(0, 8);
|
|
268
|
+
}
|
|
269
|
+
/** One OSV vulnerability object -> CveRecord, keyed by its CVE alias when it has one. */
|
|
270
|
+
export function parseOsvVuln(v) {
|
|
271
|
+
if (typeof v?.id !== "string")
|
|
272
|
+
return null;
|
|
273
|
+
const ids = [v.id, ...(Array.isArray(v.aliases) ? v.aliases : [])].map(String);
|
|
274
|
+
const cveId = ids.find((x) => /^CVE-\d{4}-\d{4,}$/i.test(x));
|
|
275
|
+
const id = cveId ? normalizeCveId(cveId) : v.id;
|
|
276
|
+
const aliases = [...new Set(ids.filter((x) => normalizeCveId(x) !== normalizeCveId(id)))];
|
|
277
|
+
const vector = (v.severity ?? []).find((s) => /^CVSS_V[34]/.test(s?.type))?.score;
|
|
278
|
+
const sevRaw = v.database_specific?.severity ?? v.affected?.[0]?.database_specific?.severity;
|
|
279
|
+
const cweIds = v.database_specific?.cwe_ids;
|
|
280
|
+
const affected = osvAffected(v.affected);
|
|
281
|
+
const refs = (v.references ?? [])
|
|
282
|
+
.map((r) => r?.url)
|
|
283
|
+
.filter((u) => typeof u === "string" && /^https?:\/\//.test(u));
|
|
284
|
+
const summary = String(v.summary || v.details || "").replace(/\s+/g, " ").trim();
|
|
285
|
+
const score = typeof vector === "string" ? cvss3BaseScore(vector) : undefined;
|
|
286
|
+
return {
|
|
287
|
+
id,
|
|
288
|
+
...(aliases.length ? { aliases } : {}),
|
|
289
|
+
...(v.summary && v.details ? { title: String(v.summary) } : {}),
|
|
290
|
+
summary: summary.length > 600 ? `${summary.slice(0, 597)}…` : summary,
|
|
291
|
+
...(score !== undefined ? { cvssScore: score } : {}),
|
|
292
|
+
...(normalizeSeverity(sevRaw) || severityFromScore(score)
|
|
293
|
+
? { severity: normalizeSeverity(sevRaw) ?? severityFromScore(score) }
|
|
294
|
+
: {}),
|
|
295
|
+
...(typeof vector === "string"
|
|
296
|
+
? { cvssVector: vector, cvssVersion: vector.startsWith("CVSS:4.0") ? "4.0" : vector.startsWith("CVSS:3.0") ? "3.0" : "3.1" }
|
|
297
|
+
: {}),
|
|
298
|
+
...(Array.isArray(cweIds) && cweIds.length ? { cwe: cweIds.map(String) } : {}),
|
|
299
|
+
...(affected.length ? { affected } : {}),
|
|
300
|
+
...(v.published ? { published: toIso(v.published) } : {}),
|
|
301
|
+
...(v.modified ? { modified: toIso(v.modified) } : {}),
|
|
302
|
+
references: [...new Set(refs)].slice(0, 12),
|
|
303
|
+
url: cveId ? nvdUrl(id) : `https://osv.dev/vulnerability/${v.id}`,
|
|
304
|
+
source: "osv",
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
/**
|
|
308
|
+
* Merge records for the same vulnerability across sources (matched on id or any alias).
|
|
309
|
+
* NVD wins for scores, CWE and KEV; OSV fills ecosystem packages and fixed versions.
|
|
310
|
+
*/
|
|
311
|
+
export function mergeCveRecords(records, max = 25) {
|
|
312
|
+
const byKey = new Map();
|
|
313
|
+
const keyOf = new Map();
|
|
314
|
+
const rank = (r) => (r.source === "nvd" ? 0 : r.source === "osv" ? 1 : 2);
|
|
315
|
+
for (const rec of [...records].sort((a, b) => rank(a) - rank(b))) {
|
|
316
|
+
const names = [rec.id, ...(rec.aliases ?? [])].map(normalizeCveId);
|
|
317
|
+
const existingKey = names.map((n) => keyOf.get(n)).find(Boolean);
|
|
318
|
+
if (!existingKey) {
|
|
319
|
+
byKey.set(rec.id, { ...rec });
|
|
320
|
+
for (const n of names)
|
|
321
|
+
keyOf.set(n, rec.id);
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
324
|
+
const cur = byKey.get(existingKey);
|
|
325
|
+
const aliasMap = new Map();
|
|
326
|
+
for (const a of [...(cur.aliases ?? []), rec.id, ...(rec.aliases ?? [])]) {
|
|
327
|
+
const n = normalizeCveId(a);
|
|
328
|
+
if (n !== normalizeCveId(cur.id) && !aliasMap.has(n))
|
|
329
|
+
aliasMap.set(n, a);
|
|
330
|
+
}
|
|
331
|
+
const aliases = new Set(aliasMap.values());
|
|
332
|
+
const affected = [...(cur.affected ?? [])];
|
|
333
|
+
for (const a of rec.affected ?? []) {
|
|
334
|
+
const match = affected.find((x) => x.product.toLowerCase() === a.product.toLowerCase());
|
|
335
|
+
if (!match)
|
|
336
|
+
affected.push(a);
|
|
337
|
+
else {
|
|
338
|
+
match.ecosystem ??= a.ecosystem;
|
|
339
|
+
match.fixed ??= a.fixed;
|
|
340
|
+
match.versions ??= a.versions;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
byKey.set(existingKey, {
|
|
344
|
+
...cur,
|
|
345
|
+
aliases: aliases.size ? [...aliases] : undefined,
|
|
346
|
+
title: cur.title ?? rec.title,
|
|
347
|
+
summary: cur.summary || rec.summary,
|
|
348
|
+
severity: cur.severity ?? rec.severity,
|
|
349
|
+
cvssScore: cur.cvssScore ?? rec.cvssScore,
|
|
350
|
+
cvssVector: cur.cvssVector ?? rec.cvssVector,
|
|
351
|
+
cvssVersion: cur.cvssVersion ?? rec.cvssVersion,
|
|
352
|
+
cwe: cur.cwe ?? rec.cwe,
|
|
353
|
+
affected: affected.length ? affected.slice(0, 8) : undefined,
|
|
354
|
+
published: cur.published ?? rec.published,
|
|
355
|
+
modified: cur.modified ?? rec.modified,
|
|
356
|
+
kev: cur.kev || rec.kev || undefined,
|
|
357
|
+
references: [...new Set([...cur.references, ...rec.references])].slice(0, 16),
|
|
358
|
+
});
|
|
359
|
+
for (const n of names)
|
|
360
|
+
keyOf.set(n, existingKey);
|
|
361
|
+
}
|
|
362
|
+
const out = [...byKey.values()].map((r) => {
|
|
363
|
+
const clean = Object.fromEntries(Object.entries(r).filter(([, v]) => v !== undefined));
|
|
364
|
+
if (!clean.severity && clean.cvssScore !== undefined)
|
|
365
|
+
clean.severity = severityFromScore(clean.cvssScore);
|
|
366
|
+
return clean;
|
|
367
|
+
});
|
|
368
|
+
return sortCveRecords(out).slice(0, max);
|
|
369
|
+
}
|
|
370
|
+
/** Exact-id hits first, then KEV, then severity, then newest. */
|
|
371
|
+
export function sortCveRecords(records, pinnedIds = []) {
|
|
372
|
+
const pinned = new Set(pinnedIds.map(normalizeCveId));
|
|
373
|
+
const isPinned = (r) => pinned.has(r.id) || (r.aliases ?? []).some((a) => pinned.has(normalizeCveId(a)));
|
|
374
|
+
return [...records].sort((a, b) => {
|
|
375
|
+
const p = Number(isPinned(b)) - Number(isPinned(a));
|
|
376
|
+
if (p)
|
|
377
|
+
return p;
|
|
378
|
+
const k = Number(!!b.kev) - Number(!!a.kev);
|
|
379
|
+
if (k)
|
|
380
|
+
return k;
|
|
381
|
+
const s = SEVERITY_ORDER[b.severity ?? "none"] - SEVERITY_ORDER[a.severity ?? "none"];
|
|
382
|
+
if (s)
|
|
383
|
+
return s;
|
|
384
|
+
const sc = (b.cvssScore ?? -1) - (a.cvssScore ?? -1);
|
|
385
|
+
if (sc)
|
|
386
|
+
return sc;
|
|
387
|
+
return (b.published ?? "").localeCompare(a.published ?? "");
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
/** "9.8 Critical" / "High" for cards and MCP text. */
|
|
391
|
+
export function cveSeverityLabel(r) {
|
|
392
|
+
const sev = r.severity ?? severityFromScore(r.cvssScore);
|
|
393
|
+
const word = sev ? sev[0].toUpperCase() + sev.slice(1) : undefined;
|
|
394
|
+
if (r.cvssScore !== undefined && word)
|
|
395
|
+
return `${r.cvssScore.toFixed(1)} ${word}`;
|
|
396
|
+
return word;
|
|
397
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical form of a result URL for duplicate detection: lower-case host without
|
|
3
|
+
* `www.`, no hash, no tracking params, no trailing slash. Distinct paths/queries stay
|
|
4
|
+
* distinct, so two listings on the same provider are never merged.
|
|
5
|
+
*/
|
|
6
|
+
export declare function canonicalResultUrl(rawUrl: string | undefined): string;
|
|
7
|
+
export declare function normalizeIdentity(text: string | undefined): string;
|
|
8
|
+
/**
|
|
9
|
+
* Duplicate key = same URL AND same identity (address / name). Different URLs from the
|
|
10
|
+
* same provider are kept, and so is one search-results URL that yields several items.
|
|
11
|
+
*/
|
|
12
|
+
export declare function resultDedupeKey(url: string | undefined, identity: string | undefined): string;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
const TRACKING_PARAM = /^(utm_|fbclid$|gclid$|msclkid$|mc_cid$|mc_eid$|_ga$|_gl$|ref$|ref_src$)/i;
|
|
2
|
+
/**
|
|
3
|
+
* Canonical form of a result URL for duplicate detection: lower-case host without
|
|
4
|
+
* `www.`, no hash, no tracking params, no trailing slash. Distinct paths/queries stay
|
|
5
|
+
* distinct, so two listings on the same provider are never merged.
|
|
6
|
+
*/
|
|
7
|
+
export function canonicalResultUrl(rawUrl) {
|
|
8
|
+
if (!rawUrl) {
|
|
9
|
+
return "";
|
|
10
|
+
}
|
|
11
|
+
try {
|
|
12
|
+
const u = new URL(rawUrl.trim());
|
|
13
|
+
u.hash = "";
|
|
14
|
+
u.hostname = u.hostname.toLowerCase().replace(/^www\./, "");
|
|
15
|
+
for (const key of Array.from(u.searchParams.keys())) {
|
|
16
|
+
if (TRACKING_PARAM.test(key)) {
|
|
17
|
+
u.searchParams.delete(key);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
u.searchParams.sort();
|
|
21
|
+
return `${u.hostname}${u.pathname.replace(/\/+$/, "")}${u.search}`;
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return rawUrl.trim().toLowerCase();
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
export function normalizeIdentity(text) {
|
|
28
|
+
return (text ?? "").toLowerCase().replace(/[^a-z0-9]+/g, " ").trim();
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Duplicate key = same URL AND same identity (address / name). Different URLs from the
|
|
32
|
+
* same provider are kept, and so is one search-results URL that yields several items.
|
|
33
|
+
*/
|
|
34
|
+
export function resultDedupeKey(url, identity) {
|
|
35
|
+
return `${canonicalResultUrl(url)}::${normalizeIdentity(identity)}`;
|
|
36
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export interface FlightOffer {
|
|
2
|
+
title: string;
|
|
3
|
+
airline?: string;
|
|
4
|
+
flightNumber?: string;
|
|
5
|
+
origin?: string;
|
|
6
|
+
destination?: string;
|
|
7
|
+
departureTime?: string;
|
|
8
|
+
arrivalTime?: string;
|
|
9
|
+
duration?: string;
|
|
10
|
+
stops?: number | string;
|
|
11
|
+
cabinClass?: "economy" | "premium_economy" | "business" | "first";
|
|
12
|
+
tripType?: "one_way" | "round_trip";
|
|
13
|
+
price?: number;
|
|
14
|
+
priceText: string;
|
|
15
|
+
currency?: string;
|
|
16
|
+
url: string;
|
|
17
|
+
provider: string;
|
|
18
|
+
extractedVia: "json-state" | "rsc-payload" | "json-ld" | "scaledown-extract-1" | "serp-snippet";
|
|
19
|
+
}
|
|
20
|
+
export declare function inferRouteFromText(text: string): {
|
|
21
|
+
origin?: string;
|
|
22
|
+
destination?: string;
|
|
23
|
+
};
|
|
24
|
+
export declare function inferCabinClass(text: string): "economy" | "premium_economy" | "business" | "first" | undefined;
|
|
25
|
+
export declare function inferTripType(text: string): "one_way" | "round_trip" | undefined;
|
|
26
|
+
export declare function inferDurationAndStops(text: string): {
|
|
27
|
+
duration?: string;
|
|
28
|
+
stops?: number | string;
|
|
29
|
+
};
|
|
30
|
+
export declare function parseFlightPrice(rawPrice: string | number | undefined | null, countryCode?: string, explicitCurrency?: string): {
|
|
31
|
+
price?: number;
|
|
32
|
+
priceText: string;
|
|
33
|
+
currency: string;
|
|
34
|
+
};
|
|
35
|
+
export declare function extractFlightOffersFromHtml(html: string, pageUrl: string, countryCode?: string, queryHint?: string): FlightOffer[];
|
|
36
|
+
export declare function extractFlightOfferFromSerpResult(title: string, description: string, url: string, countryCode?: string, queryHint?: string): FlightOffer | null;
|
|
37
|
+
export declare function dedupeFlightOffers(offers: FlightOffer[], maxItems?: number): FlightOffer[];
|