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
159 lines
4.4 KiB
TypeScript
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'),
|
|
};
|
|
}
|