Files
upstream-mattermost/docs/site/src/components/SecurityUpdates/feed.ts
upstream-mirror 240db4e2a7
Some checks failed
Server CI / Postgres (shard 0) (push) Has been skipped
Server CI / Postgres (shard 3) (push) Has been skipped
BuildEnv Docker Image / build-image (push) Failing after 4s
BuildEnv Docker Image / build-image-fips (push) Failing after 4s
Migration Review & Release Notes / Generate migration review & release notes (push) Failing after 3s
Server CI / Build Environment (push) Failing after 3s
Server CI / Postgres (shard 1) (push) Has been skipped
Tools CI / check-style (mattermost-govet) (push) Failing after 4s
Server CI / Postgres (shard 2) (push) Has been skipped
Tools CI / check-style (mmgotool) (push) Failing after 3s
Server CI / Elasticsearch v8 Compatibility (push) Has been skipped
Server CI / OpenSearch v2 Compatibility (push) Has been skipped
Server CI / Postgres FIPS (shard 0) (push) Has been skipped
Tools CI / Test (mattermost-govet) (push) Failing after 3s
Server CI / Postgres FIPS (shard 1) (push) Has been skipped
Server CI / Postgres FIPS (shard 2) (push) Has been skipped
Tools CI / Test (mmgotool) (push) Failing after 3s
Server CI / Postgres FIPS (shard 3) (push) Has been skipped
Server CI / Run mmctl tests (push) Has been skipped
Server CI / Run mmctl tests (FIPS) (push) Has been skipped
Server CI / Merge Postgres Test Results (push) Has been skipped
Server CI / Merge Postgres FIPS Test Results (push) Has been skipped
Server CI / Check backport migrations (push) Has been skipped
Server CI / Check generated files (push) Has been skipped
Server CI / check-style (push) Has been skipped
Server CI / Vet API (push) Has been skipped
Server CI / Build mattermost server app (push) Has been skipped
Server CI / Server CI Complete (push) Failing after 2s
Server CI / Server CI Artifacts (push) Has been skipped
Server CI / Sentry Upload (push) Has been skipped
Server CI / Server CI Report (push) Failing after 5m1s
Server CI Nightly Race / Race Detector (push) Has been skipped
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
Webapp CI Cache Warm / Warm Webapp node_modules Cache (push) Has been cancelled
E2E CI Cache Warm / Warm E2E npm Registry Cache (push) Has been cancelled
API / build (push) Has been cancelled
Web App CI / check-types (push) Has been cancelled
Web App CI / check-lint (push) Has been cancelled
Web App CI / check-i18n (push) Has been cancelled
Web App CI / check-external-links (push) Has been cancelled
Web App CI / test (platform) (push) Has been cancelled
Web App CI / test (mattermost-redux) (push) Has been cancelled
Web App CI / test (channels shard 1/4) (push) Has been cancelled
Web App CI / test (channels shard 2/4) (push) Has been cancelled
Web App CI / test (channels shard 3/4) (push) Has been cancelled
Web App CI / test (channels shard 4/4) (push) Has been cancelled
Web App CI / upload-coverage (push) Has been cancelled
Web App CI / build (push) Has been cancelled
YAML Lint / yamllint (push) Has been cancelled
Push mirrored docker images / cd/Push mirrored docker images (push) Has been cancelled
docs-cd / Build and deploy docs (push) Has been cancelled
Docs CI / build (push) Has been cancelled
E2E Cache Warm: Cypress deps / Warm Cypress deps cache (push) Has been cancelled
Scorecards supply-chain security / Scorecard analysis (push) Has been cancelled
Server CI Weekly / Postgres FIPS (push) Has been skipped
Server CI Weekly / Run mmctl tests (FIPS) (push) Has been skipped
Server CI Weekly / Postgres with binary parameters (push) Has been skipped
Server CI Weekly / Build Environment (push) Failing after 1s
Server CI Cache Warm / Warm Go Module and Build Cache (push) Failing after 2s
Server CI Nightly Race / Build Environment (push) Failing after 1s
snapshot of https://github.com/mattermost/mattermost.git (master, 2026-09-16; blobs > 131072B omitted)
2026-09-16 00:29:55 +05:00

159 lines
4.4 KiB
TypeScript

import {parseAffectedLines, type VersionRange} from './versions';
export const FEED_URL = 'https://securityupdates.mattermost.com/security_updates.json';
export type TabId = 'server' | 'desktop' | 'mobile' | 'plugins';
export type SecurityUpdateRow = {
issueId: string;
cveId?: string;
severity: string;
affectedVersions: string[];
affectedRanges: VersionRange[];
releaseDate: string;
fixVersions: string[];
details: string;
};
export type SecurityUpdateTab = {
id: TabId;
label: string;
rows: SecurityUpdateRow[];
};
export const TAB_ORDER: {id: TabId; label: string}[] = [
{id: 'server', label: 'Server'},
{id: 'desktop', label: 'Desktop'},
{id: 'mobile', label: 'Mobile'},
{id: 'plugins', label: 'Plugins'},
];
const PLATFORM_MAP: Record<string, TabId> = {
'Mattermost Server': 'server',
'Mattermost Desktop App': 'desktop',
'Mattermost Mobile Apps': 'mobile',
'Mattermost Plugins': 'plugins',
};
type FeedItem = {
issue_id?: string;
cve_id?: string;
severity?: string;
affected_versions?: unknown;
fix_release_date?: string;
fix_versions?: unknown;
details?: string;
platform?: string;
};
function normalizeVersions(value: unknown): string[] {
if (value == null || value === '') {
return [];
}
if (Array.isArray(value)) {
return value.flatMap((entry) => normalizeVersions(entry)).map((entry) => entry.trim()).filter(Boolean);
}
if (typeof value === 'string') {
const trimmed = value.trim();
if (!trimmed) {
return [];
}
if (trimmed.startsWith('[')) {
try {
return normalizeVersions(JSON.parse(trimmed));
} catch {
/* keep as plain string */
}
}
return [trimmed];
}
return [String(value)];
}
function normalizeSeverity(value: string): string {
const trimmed = value.trim();
if (!trimmed || /^n\/?a$/i.test(trimmed)) {
return 'N/A';
}
return trimmed.replace(/^\w/, (c) => c.toUpperCase());
}
function parseIssueParts(issueId: string) {
const parts = issueId.split('-');
return {
year: Number.parseInt(parts[1] || '', 10) || 0,
num: Number.parseInt(parts[2] || '', 10) || 0,
extra: Number.parseInt(parts[3] || '', 10) || 0,
};
}
function toRow(item: FeedItem): SecurityUpdateRow {
const cveId = String(item.cve_id || '').trim();
const affectedVersions = normalizeVersions(item.affected_versions);
const fixVersions = normalizeVersions(item.fix_versions);
return {
issueId: String(item.issue_id || '').trim(),
...(cveId ? {cveId} : {}),
severity: normalizeSeverity(String(item.severity || '')),
affectedVersions,
affectedRanges: parseAffectedLines(affectedVersions, fixVersions),
releaseDate: String(item.fix_release_date || '').slice(0, 10),
fixVersions,
details: String(item.details || '').replace(/\s+/g, ' ').trim(),
};
}
function compareFeedItems(a: FeedItem, b: FeedItem): number {
const dateA = Date.parse(String(a.fix_release_date || '').slice(0, 10)) || 0;
const dateB = Date.parse(String(b.fix_release_date || '').slice(0, 10)) || 0;
if (dateA !== dateB) {
return dateB - dateA;
}
const pa = parseIssueParts(String(a.issue_id || ''));
const pb = parseIssueParts(String(b.issue_id || ''));
if (pa.year !== pb.year) {
return pb.year - pa.year;
}
if (pa.num !== pb.num) {
return pb.num - pa.num;
}
return pb.extra - pa.extra;
}
export function normalizeFeed(feed: unknown): SecurityUpdateTab[] {
if (!Array.isArray(feed)) {
throw new Error('Unexpected feed shape: expected an array of advisories.');
}
const tabs = Object.fromEntries(
TAB_ORDER.map((tab) => [tab.id, {...tab, rows: [] as SecurityUpdateRow[]}]),
) as Record<TabId, SecurityUpdateTab>;
[...(feed as FeedItem[])]
.sort(compareFeedItems)
.forEach((item) => {
const tabId = PLATFORM_MAP[String(item.platform || '').trim()];
if (!tabId) {
return;
}
tabs[tabId].rows.push(toRow(item));
});
return TAB_ORDER.map((tab) => tabs[tab.id]);
}
export async function fetchSecurityUpdates(): Promise<{
tabs: SecurityUpdateTab[];
lastModified: string | null;
}> {
const response = await fetch(FEED_URL, {cache: 'no-store'});
if (!response.ok) {
throw new Error(`Security updates feed returned ${response.status}.`);
}
const payload: unknown = await response.json();
return {
tabs: normalizeFeed(payload),
lastModified: response.headers.get('Last-Modified'),
};
}