@mitre/hdf-utilities 3.3.0 → 3.3.1
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/dist/index.d.ts +2 -2
- package/dist/index.js +3 -3
- package/dist/index.js.map +1 -1
- package/dist/string/index.d.ts +35 -7
- package/dist/string/index.d.ts.map +1 -1
- package/dist/string/index.js +48 -8
- package/dist/string/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { CsvBuildOptions, CsvParseOptions, buildCsv, buildCsvArray, isValidCsv,
|
|
|
3
3
|
import { HashAlgorithm, HashEncoding, HashOptions, generateHash, hashObject, sha256, sha512, verifyHash } from "./hash/index.js";
|
|
4
4
|
import { StringifyOptions, isValidJSON, parseJSON, stringifyJSON } from "./json/index.js";
|
|
5
5
|
import { buildXml, extractTextFromXml, isValidXml, parseXml, parseXmlWithArrays } from "./xml/index.js";
|
|
6
|
-
import { parseTimestamp, stripHtml } from "./string/index.js";
|
|
6
|
+
import { formatTimestamp, formatTimestampSeconds, parseTimestamp, stripHtml, trimUtcFraction } from "./string/index.js";
|
|
7
7
|
|
|
8
8
|
//#region src/severity/index.d.ts
|
|
9
9
|
/**
|
|
@@ -238,5 +238,5 @@ interface ParsedPurl {
|
|
|
238
238
|
*/
|
|
239
239
|
declare function parsePurl(purlStr: string): ParsedPurl | null;
|
|
240
240
|
//#endregion
|
|
241
|
-
export { type CpePart, type CsvBuildOptions, type CsvParseOptions, type CvssValidationResult, type HashAlgorithm, type HashEncoding, type HashOptions, type ParsedCpe, type ParsedCvssVector, type ParsedPurl, type StringifyOptions, buildCsv, buildCsvArray, buildXml, cvssScoreToSeverity, extractColumn, extractColumn as extractCsvColumn, extractTextFromXml, findRows as findCsvRows, findRows, findValuesByKey as findJsonValues, findValuesByKey, findValuesByKey as findXmlValues, generateHash, hashObject, impactToSeverity, isValidCsv, isValidJSON, isValidXml, parseCpe, parseCsv, parseCsvArray, parseCvssVector, parseJSON, parsePurl, parseTimestamp, parseXml, parseXmlWithArrays, sanitizeCsvArray, sanitizeCsvObject, sanitizeCsvValue, severityToImpact, sha256, sha512, stringifyJSON, stripHtml, validateCvssVector, verifyHash };
|
|
241
|
+
export { type CpePart, type CsvBuildOptions, type CsvParseOptions, type CvssValidationResult, type HashAlgorithm, type HashEncoding, type HashOptions, type ParsedCpe, type ParsedCvssVector, type ParsedPurl, type StringifyOptions, buildCsv, buildCsvArray, buildXml, cvssScoreToSeverity, extractColumn, extractColumn as extractCsvColumn, extractTextFromXml, findRows as findCsvRows, findRows, findValuesByKey as findJsonValues, findValuesByKey, findValuesByKey as findXmlValues, formatTimestamp, formatTimestampSeconds, generateHash, hashObject, impactToSeverity, isValidCsv, isValidJSON, isValidXml, parseCpe, parseCsv, parseCsvArray, parseCvssVector, parseJSON, parsePurl, parseTimestamp, parseXml, parseXmlWithArrays, sanitizeCsvArray, sanitizeCsvObject, sanitizeCsvValue, severityToImpact, sha256, sha512, stringifyJSON, stripHtml, trimUtcFraction, validateCvssVector, verifyHash };
|
|
242
242
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
CHANGED
|
@@ -3,7 +3,7 @@ import { isValidJSON, parseJSON, stringifyJSON } from "./json/index.js";
|
|
|
3
3
|
import { generateHash, hashObject, sha256, sha512, verifyHash } from "./hash/index.js";
|
|
4
4
|
import { buildXml, extractTextFromXml, isValidXml, parseXml, parseXmlWithArrays } from "./xml/index.js";
|
|
5
5
|
import { buildCsv, buildCsvArray, isValidCsv, parseCsv, parseCsvArray, sanitizeCsvArray, sanitizeCsvObject, sanitizeCsvValue } from "./csv/index.js";
|
|
6
|
-
import { parseTimestamp, stripHtml } from "./string/index.js";
|
|
6
|
+
import { formatTimestamp, formatTimestampSeconds, parseTimestamp, stripHtml, trimUtcFraction } from "./string/index.js";
|
|
7
7
|
//#region src/severity/index.ts
|
|
8
8
|
/**
|
|
9
9
|
* Severity-to-impact mapping utilities.
|
|
@@ -708,7 +708,7 @@ function validateCvssVector(vector, version) {
|
|
|
708
708
|
const CPE_PREFIX = "cpe:2.3:";
|
|
709
709
|
const EXPECTED_TOTAL_FIELDS = 13;
|
|
710
710
|
const PRODUCT_FIELD_COUNT = 11;
|
|
711
|
-
const VALID_PARTS = new Set([
|
|
711
|
+
const VALID_PARTS = /* @__PURE__ */ new Set([
|
|
712
712
|
"a",
|
|
713
713
|
"o",
|
|
714
714
|
"h",
|
|
@@ -925,6 +925,6 @@ function safeDecode(s) {
|
|
|
925
925
|
}
|
|
926
926
|
}
|
|
927
927
|
//#endregion
|
|
928
|
-
export { buildCsv, buildCsvArray, buildXml, cvssScoreToSeverity, extractColumn, extractColumn as extractCsvColumn, extractTextFromXml, findRows as findCsvRows, findRows, findValuesByKey as findJsonValues, findValuesByKey, findValuesByKey as findXmlValues, generateHash, hashObject, impactToSeverity, isValidCsv, isValidJSON, isValidXml, parseCpe, parseCsv, parseCsvArray, parseCvssVector, parseJSON, parsePurl, parseTimestamp, parseXml, parseXmlWithArrays, sanitizeCsvArray, sanitizeCsvObject, sanitizeCsvValue, severityToImpact, sha256, sha512, stringifyJSON, stripHtml, validateCvssVector, verifyHash };
|
|
928
|
+
export { buildCsv, buildCsvArray, buildXml, cvssScoreToSeverity, extractColumn, extractColumn as extractCsvColumn, extractTextFromXml, findRows as findCsvRows, findRows, findValuesByKey as findJsonValues, findValuesByKey, findValuesByKey as findXmlValues, formatTimestamp, formatTimestampSeconds, generateHash, hashObject, impactToSeverity, isValidCsv, isValidJSON, isValidXml, parseCpe, parseCsv, parseCsvArray, parseCvssVector, parseJSON, parsePurl, parseTimestamp, parseXml, parseXmlWithArrays, sanitizeCsvArray, sanitizeCsvObject, sanitizeCsvValue, severityToImpact, sha256, sha512, stringifyJSON, stripHtml, trimUtcFraction, validateCvssVector, verifyHash };
|
|
929
929
|
|
|
930
930
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../src/severity/index.ts","../src/cvss/index.ts","../src/cpe/index.ts","../src/purl/index.ts"],"sourcesContent":["/**\n * Severity-to-impact mapping utilities.\n *\n * These define the canonical bidirectional mapping between severity labels\n * (critical, high, medium, low, informational) and HDF impact scores (0.0–1.0),\n * aligned with CVSS 3.x severity bands normalized to 0–1.\n *\n * Threshold ranges (used by impactToSeverity):\n * >=0.9 = critical (CVSS 9.0–10.0)\n * [0.7, 0.9) = high (CVSS 7.0–8.9)\n * [0.4, 0.7) = medium (CVSS 4.0–6.9)\n * (0.0, 0.4) = low (CVSS 0.1–3.9)\n * 0.0 = informational (CVSS 0.0)\n */\n\nconst severityMap: Record<string, number> = {\n critical: 0.9,\n high: 0.7,\n medium: 0.5,\n low: 0.3,\n info: 0.0,\n none: 0.0,\n informational: 0.0,\n information: 0.0,\n};\n\n/**\n * Map a severity string to an impact score.\n *\n * @param severity - Severity level (case-insensitive), or null for no severity assessment\n * @returns Impact score between 0.0 and 1.0; null when input is null; defaults to 0.5 for unrecognized values\n */\nexport function severityToImpact(severity: null): null;\nexport function severityToImpact(severity: string): number;\nexport function severityToImpact(severity: string | null): number | null;\nexport function severityToImpact(severity: string | null): number | null {\n if (severity === null) return null;\n const normalized = severity.toLowerCase();\n return severityMap[normalized] ?? 0.5;\n}\n\n/**\n * Map an impact score to a severity string.\n *\n * @param impact - Impact score (0.0 to 1.0), or null for no impact assessment\n * @returns Severity level string, or null when input is null\n */\nexport function impactToSeverity(impact: null): null;\nexport function impactToSeverity(impact: number): string;\nexport function impactToSeverity(impact: number | null): string | null;\nexport function impactToSeverity(impact: number | null): string | null {\n if (impact === null) return null;\n if (impact >= 0.9) return 'critical';\n if (impact >= 0.7) return 'high';\n if (impact >= 0.4) return 'medium';\n if (impact > 0.0) return 'low';\n return 'informational';\n}\n\n/**\n * Map a raw CVSS base score (0.0–10.0) to a FIRST qualitative severity band.\n *\n * Bands per FIRST CVSS v3.x / v4.0 spec:\n * none = 0.0\n * low = 0.1 – 3.9\n * medium = 4.0 – 6.9\n * high = 7.0 – 8.9\n * critical = 9.0 – 10.0\n *\n * Out-of-range inputs are clamped: scores < 0.1 → \"none\" (band floor),\n * scores > 10.0 → \"critical\". This matches scanner behavior of treating\n * malformed scores as informational/critical extremes rather than throwing.\n *\n * @param score - CVSS base score\n * @returns FIRST severity band label\n */\nexport function cvssScoreToSeverity(\n score: number,\n): 'none' | 'low' | 'medium' | 'high' | 'critical' {\n if (!Number.isFinite(score) || score < 0.1) return 'none';\n if (score < 4.0) return 'low';\n if (score < 7.0) return 'medium';\n if (score < 9.0) return 'high';\n return 'critical';\n}\n","/**\n * CVSS vector parsing and validation utilities.\n *\n * Supports CVSS 2.0 (legacy, no prefix), 3.0, 3.1, and 4.0 vector strings.\n * Parsing is permissive: it splits any well-shaped key:value pair separated\n * by `/`. Validation is strict per the FIRST grammar for each version.\n *\n * References:\n * - CVSS 2.0: https://www.first.org/cvss/v2/guide\n * - CVSS 3.1: https://www.first.org/cvss/v3.1/specification-document\n * - CVSS 4.0: https://www.first.org/cvss/v4.0/specification-document\n */\n\nexport interface ParsedCvssVector {\n /** \"2.0\" | \"3.0\" | \"3.1\" | \"4.0\" | \"unknown\" */\n version: string;\n /** Metric name -> metric value (e.g. \"AV\" -> \"N\"). Order-preserving via Map. */\n metrics: Map<string, string>;\n}\n\nexport interface CvssValidationResult {\n valid: boolean;\n errors: string[];\n}\n\n/**\n * Allowed values for each metric, keyed by CVSS major version.\n *\n * Sourced from FIRST specifications (linked above). Only metrics present\n * in a given version's grammar appear in that version's map; unknown metrics\n * encountered during validation are ignored (forward-compat).\n */\nconst METRIC_VALUES: Record<string, Record<string, string[]>> = {\n '2.0': {\n AV: ['L', 'A', 'N'],\n AC: ['H', 'M', 'L'],\n Au: ['M', 'S', 'N'],\n C: ['N', 'P', 'C'],\n I: ['N', 'P', 'C'],\n A: ['N', 'P', 'C'],\n // Temporal\n E: ['U', 'POC', 'F', 'H', 'ND'],\n RL: ['OF', 'TF', 'W', 'U', 'ND'],\n RC: ['UC', 'UR', 'C', 'ND'],\n // Environmental\n CDP: ['N', 'L', 'LM', 'MH', 'H', 'ND'],\n TD: ['N', 'L', 'M', 'H', 'ND'],\n CR: ['L', 'M', 'H', 'ND'],\n IR: ['L', 'M', 'H', 'ND'],\n AR: ['L', 'M', 'H', 'ND'],\n },\n '3.0': {\n AV: ['N', 'A', 'L', 'P'],\n AC: ['L', 'H'],\n PR: ['N', 'L', 'H'],\n UI: ['N', 'R'],\n S: ['U', 'C'],\n C: ['N', 'L', 'H'],\n I: ['N', 'L', 'H'],\n A: ['N', 'L', 'H'],\n // Temporal\n E: ['X', 'U', 'P', 'F', 'H'],\n RL: ['X', 'O', 'T', 'W', 'U'],\n RC: ['X', 'U', 'R', 'C'],\n // Environmental\n CR: ['X', 'L', 'M', 'H'],\n IR: ['X', 'L', 'M', 'H'],\n AR: ['X', 'L', 'M', 'H'],\n MAV: ['X', 'N', 'A', 'L', 'P'],\n MAC: ['X', 'L', 'H'],\n MPR: ['X', 'N', 'L', 'H'],\n MUI: ['X', 'N', 'R'],\n MS: ['X', 'U', 'C'],\n MC: ['X', 'N', 'L', 'H'],\n MI: ['X', 'N', 'L', 'H'],\n MA: ['X', 'N', 'L', 'H'],\n },\n '3.1': {\n AV: ['N', 'A', 'L', 'P'],\n AC: ['L', 'H'],\n PR: ['N', 'L', 'H'],\n UI: ['N', 'R'],\n S: ['U', 'C'],\n C: ['N', 'L', 'H'],\n I: ['N', 'L', 'H'],\n A: ['N', 'L', 'H'],\n // Temporal\n E: ['X', 'U', 'P', 'F', 'H'],\n RL: ['X', 'O', 'T', 'W', 'U'],\n RC: ['X', 'U', 'R', 'C'],\n // Environmental\n CR: ['X', 'L', 'M', 'H'],\n IR: ['X', 'L', 'M', 'H'],\n AR: ['X', 'L', 'M', 'H'],\n MAV: ['X', 'N', 'A', 'L', 'P'],\n MAC: ['X', 'L', 'H'],\n MPR: ['X', 'N', 'L', 'H'],\n MUI: ['X', 'N', 'R'],\n MS: ['X', 'U', 'C'],\n MC: ['X', 'N', 'L', 'H'],\n MI: ['X', 'N', 'L', 'H'],\n MA: ['X', 'N', 'L', 'H'],\n },\n '4.0': {\n // Base\n AV: ['N', 'A', 'L', 'P'],\n AC: ['L', 'H'],\n AT: ['N', 'P'],\n PR: ['N', 'L', 'H'],\n UI: ['N', 'P', 'A'],\n VC: ['H', 'L', 'N'],\n VI: ['H', 'L', 'N'],\n VA: ['H', 'L', 'N'],\n SC: ['H', 'L', 'N'],\n SI: ['H', 'L', 'N'],\n SA: ['H', 'L', 'N'],\n // Threat\n E: ['X', 'A', 'P', 'U'],\n // Environmental (modified base) — same enums as their base counterparts plus X\n MAV: ['X', 'N', 'A', 'L', 'P'],\n MAC: ['X', 'L', 'H'],\n MAT: ['X', 'N', 'P'],\n MPR: ['X', 'N', 'L', 'H'],\n MUI: ['X', 'N', 'P', 'A'],\n MVC: ['X', 'H', 'L', 'N'],\n MVI: ['X', 'H', 'L', 'N'],\n MVA: ['X', 'H', 'L', 'N'],\n MSC: ['X', 'H', 'L', 'N'],\n MSI: ['X', 'S', 'H', 'L', 'N'],\n MSA: ['X', 'S', 'H', 'L', 'N'],\n // Requirements\n CR: ['X', 'H', 'M', 'L'],\n IR: ['X', 'H', 'M', 'L'],\n AR: ['X', 'H', 'M', 'L'],\n // Supplemental\n S: ['X', 'N', 'P'],\n AU: ['X', 'N', 'Y'],\n R: ['X', 'A', 'U', 'I'],\n V: ['X', 'D', 'C'],\n RE: ['X', 'L', 'M', 'H'],\n U: ['X', 'Clear', 'Green', 'Amber', 'Red'],\n },\n};\n\nconst REQUIRED_METRICS: Record<string, string[]> = {\n '2.0': ['AV', 'AC', 'Au', 'C', 'I', 'A'],\n '3.0': ['AV', 'AC', 'PR', 'UI', 'S', 'C', 'I', 'A'],\n '3.1': ['AV', 'AC', 'PR', 'UI', 'S', 'C', 'I', 'A'],\n '4.0': ['AV', 'AC', 'AT', 'PR', 'UI', 'VC', 'VI', 'VA', 'SC', 'SI', 'SA'],\n};\n\n/**\n * Parse a CVSS vector string into version + metric map.\n *\n * Version detection rule: a leading `CVSS:X.Y` segment yields version \"X.Y\".\n * Otherwise (legacy v2 vectors), version is \"2.0\". Malformed input yields\n * \"unknown\" with an empty metric map; no exceptions are thrown.\n *\n * @param vector - CVSS vector string (e.g. \"CVSS:3.1/AV:N/AC:L/...\")\n * @returns Parsed version + metric map\n */\nexport function parseCvssVector(vector: string): ParsedCvssVector {\n const metrics = new Map<string, string>();\n if (typeof vector !== 'string' || vector.length === 0) {\n return { version: 'unknown', metrics };\n }\n\n const segments = vector.split('/').filter((s) => s.length > 0);\n if (segments.length === 0) {\n return { version: 'unknown', metrics };\n }\n\n let version = '2.0';\n let start = 0;\n const first = segments[0];\n if (first === undefined) {\n return { version: 'unknown', metrics };\n }\n if (first.startsWith('CVSS:')) {\n version = first.slice(5);\n start = 1;\n } else if (!first.includes(':')) {\n // No prefix AND first segment isn't a key:value pair — bail.\n return { version: 'unknown', metrics };\n }\n\n for (let i = start; i < segments.length; i++) {\n const seg = segments[i];\n if (seg === undefined) {\n continue;\n }\n const colon = seg.indexOf(':');\n if (colon <= 0 || colon === seg.length - 1) {\n // Skip malformed segments (no colon, empty key, or empty value).\n continue;\n }\n const key = seg.slice(0, colon);\n const value = seg.slice(colon + 1);\n metrics.set(key, value);\n }\n\n return { version, metrics };\n}\n\n/**\n * Validate a CVSS vector string against the FIRST grammar for its version.\n *\n * Checks:\n * - Version is one of the supported set (2.0, 3.0, 3.1, 4.0).\n * - All required base metrics for the version are present.\n * - All known metric values are within the allowed enum.\n *\n * Unknown metric keys are tolerated (forward-compat with future minor versions\n * or scanner-specific extensions). To check a v2 vector without the CVSS:\n * prefix, callers may pass version=\"2.0\" explicitly; otherwise the parser\n * defaults to \"2.0\" on prefix absence.\n *\n * @param vector - CVSS vector string\n * @param version - Optional explicit version override (else inferred)\n * @returns `{ valid, errors[] }` — never throws\n */\nexport function validateCvssVector(\n vector: string,\n version?: string,\n): CvssValidationResult {\n const errors: string[] = [];\n\n if (typeof vector !== 'string' || vector.length === 0) {\n return { valid: false, errors: ['vector is empty'] };\n }\n\n const parsed = parseCvssVector(vector);\n const v = version ?? parsed.version;\n\n const grammar = METRIC_VALUES[v];\n const required = REQUIRED_METRICS[v];\n if (!grammar || !required) {\n errors.push(`unsupported CVSS version: ${v}`);\n return { valid: false, errors };\n }\n\n for (const req of required) {\n if (!parsed.metrics.has(req)) {\n errors.push(`missing required metric: ${req}`);\n }\n }\n\n for (const [key, value] of parsed.metrics.entries()) {\n const allowed = grammar[key];\n if (!allowed) {\n // Unknown metric: forward-compat — no error.\n continue;\n }\n if (!allowed.includes(value)) {\n errors.push(`invalid value for metric ${key}: ${value} (allowed: ${allowed.join(',')})`);\n }\n }\n\n return { valid: errors.length === 0, errors };\n}\n","/**\n * CPE 2.3 URI parser for HDF converters.\n *\n * Common Platform Enumeration (CPE) 2.3 URIs identify affected products.\n * Container scanners (grype, twistlock, snyk) emit them in vulnerability\n * findings. This module provides an accept-and-warn parser: any input that\n * carries the `cpe:2.3:` prefix is best-effort parsed; deviations from the\n * canonical 13-field form are reported via `warnings` rather than rejected.\n *\n * Canonical form:\n * `cpe:2.3:part:vendor:product:version:update:edition:language:sw_edition:target_sw:target_hw:other`\n *\n * Reference: NIST IR 7695 (https://csrc.nist.gov/publications/detail/nistir/7695/final)\n */\n\n/** Valid CPE 2.3 `part` values. */\nexport type CpePart = 'a' | 'o' | 'h' | '*';\n\n/**\n * Parsed CPE 2.3 URI.\n *\n * `warnings` collects any deviations from the canonical form (truncation,\n * extra fields, unknown `part`). An empty array means the input was a strict\n * 13-field, valid-part CPE.\n */\nexport interface ParsedCpe {\n /** The original input string, unmodified. */\n raw: string;\n /** Application kind. `'a'` (app), `'o'` (OS), `'h'` (hardware), `'*'` (any). */\n part: CpePart | string;\n vendor: string;\n product: string;\n version: string;\n update: string;\n edition: string;\n language: string;\n swEdition: string;\n targetSw: string;\n targetHw: string;\n other: string;\n /** Deviation messages; empty when the input is a canonical 13-field CPE. */\n warnings: string[];\n}\n\nconst CPE_PREFIX = 'cpe:2.3:';\nconst EXPECTED_TOTAL_FIELDS = 13; // \"cpe\", \"2.3\", and 11 product fields = 13 colon-separated tokens\nconst PRODUCT_FIELD_COUNT = 11; // part + 10 attribute fields\nconst VALID_PARTS = new Set(['a', 'o', 'h', '*']);\n\n/**\n * Split a CPE 2.3 body on unescaped `:` separators.\n *\n * Per CPE 2.3 spec section 6.1.2.4, `:` and `\\` inside a field must be\n * escaped as `\\:` and `\\\\`. We respect those escapes during the split and\n * unescape the result.\n */\nfunction splitOnUnescapedColons(body: string): string[] {\n const result: string[] = [];\n let current = '';\n for (let i = 0; i < body.length; i++) {\n const ch = body[i];\n if (ch === '\\\\' && i + 1 < body.length) {\n const next = body[i + 1];\n // Preserve known escapes (\\: and \\\\) as a literal next character.\n if (next === ':' || next === '\\\\') {\n current += next;\n i++;\n continue;\n }\n // Unknown escape — keep the backslash and continue.\n current += ch;\n continue;\n }\n if (ch === ':') {\n result.push(current);\n current = '';\n continue;\n }\n current += ch;\n }\n result.push(current);\n return result;\n}\n\n/**\n * Parse a CPE 2.3 URI.\n *\n * Accept-and-warn semantics:\n * - Input missing the `cpe:2.3:` prefix → returns `null`.\n * - Input with the prefix but fewer than 11 product fields → fields are\n * padded with `\"*\"` and a `truncated:` warning is added.\n * - Input with more than 11 product fields → extras are dropped and an\n * `\"extra fields ignored\"` warning is added.\n * - Unknown `part` value → kept as-is in the result, `\"unknown part: X\"`\n * warning is added.\n * - `\\:` and `\\\\` escapes inside fields are honored during splitting and\n * unescaped in the returned field values.\n *\n * @param cpeUri - CPE 2.3 URI string\n * @returns Parsed CPE with warnings, or `null` if input lacks the prefix\n *\n * @example\n * ```typescript\n * parseCpe('cpe:2.3:a:openssl:openssl:1.1.1k:*:*:*:*:*:*:*');\n * // { part: 'a', vendor: 'openssl', product: 'openssl', version: '1.1.1k', ... warnings: [] }\n *\n * parseCpe('cpe:2.3:a:openssl:openssl:1.1.1k');\n * // truncated input → padded with '*', warnings: ['truncated: expected 13 colon-separated fields, got 6']\n *\n * parseCpe('openssl:1.1.1k'); // null — no prefix\n * ```\n */\nexport function parseCpe(cpeUri: string): ParsedCpe | null {\n if (!cpeUri.startsWith(CPE_PREFIX)) {\n return null;\n }\n\n const warnings: string[] = [];\n const body = cpeUri.slice(CPE_PREFIX.length);\n\n // Special case: bare prefix `cpe:2.3:` — all fields empty, still warn.\n // This is the \"I have nothing useful here\" form; padding with \"*\" would\n // suggest the parser inferred wildcards, which it didn't.\n const isBarePrefix = body.length === 0;\n\n const bodyParts = isBarePrefix ? [] : splitOnUnescapedColons(body);\n\n // Total colon-separated fields including the prefix \"cpe\" and \"2.3\" markers.\n const totalFields = isBarePrefix ? 2 : 2 + bodyParts.length;\n\n if (bodyParts.length > PRODUCT_FIELD_COUNT) {\n warnings.push('extra fields ignored');\n } else if (bodyParts.length < PRODUCT_FIELD_COUNT) {\n warnings.push(\n `truncated: expected ${EXPECTED_TOTAL_FIELDS} colon-separated fields, got ${totalFields}`,\n );\n }\n\n // Pad missing fields. For a bare prefix, pad with \"\" (no inferred data).\n // For partial input, pad missing trailing fields with \"*\" (wildcard).\n const padValue = isBarePrefix ? '' : '*';\n const fields: string[] = bodyParts.slice(0, PRODUCT_FIELD_COUNT);\n while (fields.length < PRODUCT_FIELD_COUNT) {\n fields.push(padValue);\n }\n\n // fields is padded to PRODUCT_FIELD_COUNT above, so every slot is present;\n // the destructuring defaults only satisfy the compiler's indexed-access check.\n const [\n part = '',\n vendor = '',\n product = '',\n version = '',\n update = '',\n edition = '',\n language = '',\n swEdition = '',\n targetSw = '',\n targetHw = '',\n other = '',\n ] = fields;\n\n if (!VALID_PARTS.has(part)) {\n warnings.push(`unknown part: ${part}`);\n }\n\n return {\n raw: cpeUri,\n part,\n vendor,\n product,\n version,\n update,\n edition,\n language,\n swEdition,\n targetSw,\n targetHw,\n other,\n warnings,\n };\n}\n","/**\n * Package URL (PURL) parser for SBOM and CVE-scanner ingestion.\n *\n * PURL is the canonical package identifier used by CycloneDX, SPDX, OSV,\n * GitHub Advisory Database, and most modern scanners. Spec:\n * https://github.com/package-url/purl-spec/blob/master/PURL-SPECIFICATION.rst\n *\n * Grammar: `pkg:type/namespace/name@version?qualifiers#subpath`\n *\n * This parser is intentionally accept-and-warn. Scanners occasionally emit\n * slightly malformed PURLs (qualifier with no `=`, missing name, etc.) and\n * we'd rather record a warning than fail ingestion.\n */\n\n/**\n * Result of parsing a PURL string.\n */\nexport interface ParsedPurl {\n /** Original input string, unmodified. */\n raw: string;\n /** Package type (e.g., \"npm\", \"rpm\", \"pypi\", \"maven\"). Lowercased. */\n type: string;\n /** Namespace segment (e.g., \"redhat\" for RPM, \"org.apache...\" for Maven). */\n namespace: string | null;\n /** Package name. Empty string (with a warning) if the input lacks one. */\n name: string;\n /** Version after the `@` separator. URL-decoded. */\n version: string | null;\n /** Qualifier key-value pairs from the `?` segment. */\n qualifiers: Map<string, string>;\n /** Subpath from the `#` fragment. */\n subpath: string | null;\n /** Deviations or oddities encountered during parsing. */\n warnings: string[];\n}\n\nconst PURL_PREFIX = 'pkg:';\n\n/**\n * Parse a Package URL string.\n *\n * Returns `null` only when the input is missing the mandatory `pkg:` prefix\n * or the type segment. Other deviations are surfaced via `warnings` on the\n * returned object.\n *\n * @param purlStr - PURL string, e.g. `pkg:npm/lodash@4.17.21`\n * @returns Parsed PURL or `null` if the input is unrecoverably malformed\n *\n * @example\n * ```typescript\n * const r = parsePurl('pkg:rpm/redhat/openssl@1.1.1k-7.el8_4?arch=x86_64');\n * r?.type; // \"rpm\"\n * r?.namespace; // \"redhat\"\n * r?.name; // \"openssl\"\n * r?.qualifiers.get('arch'); // \"x86_64\"\n * ```\n */\nexport function parsePurl(purlStr: string): ParsedPurl | null {\n if (!purlStr || !purlStr.startsWith(PURL_PREFIX)) {\n return null;\n }\n\n const raw = purlStr;\n const warnings: string[] = [];\n\n // Strip prefix. Per spec, scheme is case-insensitive but only `pkg:` is\n // valid; we already matched it.\n let remainder = purlStr.slice(PURL_PREFIX.length);\n\n // Per spec, leading slashes after the scheme are stripped. This also means\n // `pkg:/foo` and `pkg:foo` are equivalent, but `pkg:/` with no following\n // type segment is still invalid.\n while (remainder.startsWith('/')) {\n remainder = remainder.slice(1);\n }\n\n if (remainder.length === 0) {\n return null;\n }\n\n // Extract subpath (fragment). Per spec, the fragment is everything after\n // the first `#`. Splitting before qualifiers keeps `#` inside qualifier\n // values out of scope (which is fine — qualifier values do not legally\n // contain raw `#`; they would be percent-encoded).\n let subpath: string | null = null;\n const hashIdx = remainder.indexOf('#');\n if (hashIdx !== -1) {\n subpath = remainder.slice(hashIdx + 1);\n remainder = remainder.slice(0, hashIdx);\n if (subpath.length === 0) {\n subpath = null;\n }\n }\n\n // Extract qualifiers (query string). Everything after the first `?`.\n const qualifiers = new Map<string, string>();\n const qIdx = remainder.indexOf('?');\n if (qIdx !== -1) {\n const qStr = remainder.slice(qIdx + 1);\n remainder = remainder.slice(0, qIdx);\n parseQualifiers(qStr, qualifiers, warnings);\n }\n\n // Strip a trailing slash from the path portion. Spec allows it; we silently\n // normalize.\n while (remainder.endsWith('/')) {\n remainder = remainder.slice(0, -1);\n }\n\n // Type is everything up to the first `/`.\n const firstSlash = remainder.indexOf('/');\n let type: string;\n let pathPart: string;\n if (firstSlash === -1) {\n type = remainder;\n pathPart = '';\n } else {\n type = remainder.slice(0, firstSlash);\n pathPart = remainder.slice(firstSlash + 1);\n }\n\n if (type.length === 0) {\n return null;\n }\n type = type.toLowerCase();\n\n // Split version off the path part. Per spec, the version separator is the\n // LAST `@` in the path — names may contain `@` (e.g., npm scoped packages\n // when not percent-encoded).\n let version: string | null = null;\n const lastAt = pathPart.lastIndexOf('@');\n let nameAndNs = pathPart;\n if (lastAt !== -1) {\n version = pathPart.slice(lastAt + 1);\n nameAndNs = pathPart.slice(0, lastAt);\n version = safeDecode(version);\n }\n\n // Namespace is everything before the last `/` in the name+namespace\n // section; the final segment is the name.\n let namespace: string | null = null;\n let name: string;\n const lastSlash = nameAndNs.lastIndexOf('/');\n if (lastSlash === -1) {\n name = nameAndNs;\n } else {\n namespace = nameAndNs.slice(0, lastSlash);\n name = nameAndNs.slice(lastSlash + 1);\n }\n\n // Decode percent-encoded segments.\n if (namespace !== null) {\n // Decode each path segment independently to preserve embedded `/`.\n namespace = namespace.split('/').map(safeDecode).join('/');\n }\n name = safeDecode(name);\n\n if (name.length === 0) {\n warnings.push('PURL is missing the name segment');\n }\n\n // Normalize empty namespace to null.\n if (namespace !== null && namespace.length === 0) {\n namespace = null;\n }\n\n // Normalize empty version to null.\n if (version !== null && version.length === 0) {\n version = null;\n }\n\n return {\n raw,\n type,\n namespace,\n name,\n version,\n qualifiers,\n subpath,\n warnings,\n };\n}\n\n/**\n * Parse the `?key=value&key=value` qualifier string. Empty segments are\n * skipped silently; segments with no `=` are recorded with empty value and\n * a warning. Values are URL-decoded.\n */\nfunction parseQualifiers(\n qStr: string,\n out: Map<string, string>,\n warnings: string[],\n): void {\n const parts = qStr.split('&');\n for (const part of parts) {\n if (part.length === 0) {\n continue;\n }\n const eq = part.indexOf('=');\n if (eq === -1) {\n out.set(part, '');\n warnings.push(`qualifier \"${part}\" has no value`);\n continue;\n }\n const key = part.slice(0, eq);\n const value = part.slice(eq + 1);\n out.set(key, safeDecode(value));\n }\n}\n\n/**\n * decodeURIComponent that returns the original string on failure rather than\n * throwing. PURLs in the wild occasionally contain stray `%` characters that\n * aren't legitimate percent-encoding.\n */\nfunction safeDecode(s: string): string {\n try {\n return decodeURIComponent(s);\n } catch {\n return s;\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../src/severity/index.ts","../src/cvss/index.ts","../src/cpe/index.ts","../src/purl/index.ts"],"sourcesContent":["/**\n * Severity-to-impact mapping utilities.\n *\n * These define the canonical bidirectional mapping between severity labels\n * (critical, high, medium, low, informational) and HDF impact scores (0.0–1.0),\n * aligned with CVSS 3.x severity bands normalized to 0–1.\n *\n * Threshold ranges (used by impactToSeverity):\n * >=0.9 = critical (CVSS 9.0–10.0)\n * [0.7, 0.9) = high (CVSS 7.0–8.9)\n * [0.4, 0.7) = medium (CVSS 4.0–6.9)\n * (0.0, 0.4) = low (CVSS 0.1–3.9)\n * 0.0 = informational (CVSS 0.0)\n */\n\nconst severityMap: Record<string, number> = {\n critical: 0.9,\n high: 0.7,\n medium: 0.5,\n low: 0.3,\n info: 0.0,\n none: 0.0,\n informational: 0.0,\n information: 0.0,\n};\n\n/**\n * Map a severity string to an impact score.\n *\n * @param severity - Severity level (case-insensitive), or null for no severity assessment\n * @returns Impact score between 0.0 and 1.0; null when input is null; defaults to 0.5 for unrecognized values\n */\nexport function severityToImpact(severity: null): null;\nexport function severityToImpact(severity: string): number;\nexport function severityToImpact(severity: string | null): number | null;\nexport function severityToImpact(severity: string | null): number | null {\n if (severity === null) return null;\n const normalized = severity.toLowerCase();\n return severityMap[normalized] ?? 0.5;\n}\n\n/**\n * Map an impact score to a severity string.\n *\n * @param impact - Impact score (0.0 to 1.0), or null for no impact assessment\n * @returns Severity level string, or null when input is null\n */\nexport function impactToSeverity(impact: null): null;\nexport function impactToSeverity(impact: number): string;\nexport function impactToSeverity(impact: number | null): string | null;\nexport function impactToSeverity(impact: number | null): string | null {\n if (impact === null) return null;\n if (impact >= 0.9) return 'critical';\n if (impact >= 0.7) return 'high';\n if (impact >= 0.4) return 'medium';\n if (impact > 0.0) return 'low';\n return 'informational';\n}\n\n/**\n * Map a raw CVSS base score (0.0–10.0) to a FIRST qualitative severity band.\n *\n * Bands per FIRST CVSS v3.x / v4.0 spec:\n * none = 0.0\n * low = 0.1 – 3.9\n * medium = 4.0 – 6.9\n * high = 7.0 – 8.9\n * critical = 9.0 – 10.0\n *\n * Out-of-range inputs are clamped: scores < 0.1 → \"none\" (band floor),\n * scores > 10.0 → \"critical\". This matches scanner behavior of treating\n * malformed scores as informational/critical extremes rather than throwing.\n *\n * @param score - CVSS base score\n * @returns FIRST severity band label\n */\nexport function cvssScoreToSeverity(\n score: number,\n): 'none' | 'low' | 'medium' | 'high' | 'critical' {\n if (!Number.isFinite(score) || score < 0.1) return 'none';\n if (score < 4.0) return 'low';\n if (score < 7.0) return 'medium';\n if (score < 9.0) return 'high';\n return 'critical';\n}\n","/**\n * CVSS vector parsing and validation utilities.\n *\n * Supports CVSS 2.0 (legacy, no prefix), 3.0, 3.1, and 4.0 vector strings.\n * Parsing is permissive: it splits any well-shaped key:value pair separated\n * by `/`. Validation is strict per the FIRST grammar for each version.\n *\n * References:\n * - CVSS 2.0: https://www.first.org/cvss/v2/guide\n * - CVSS 3.1: https://www.first.org/cvss/v3.1/specification-document\n * - CVSS 4.0: https://www.first.org/cvss/v4.0/specification-document\n */\n\nexport interface ParsedCvssVector {\n /** \"2.0\" | \"3.0\" | \"3.1\" | \"4.0\" | \"unknown\" */\n version: string;\n /** Metric name -> metric value (e.g. \"AV\" -> \"N\"). Order-preserving via Map. */\n metrics: Map<string, string>;\n}\n\nexport interface CvssValidationResult {\n valid: boolean;\n errors: string[];\n}\n\n/**\n * Allowed values for each metric, keyed by CVSS major version.\n *\n * Sourced from FIRST specifications (linked above). Only metrics present\n * in a given version's grammar appear in that version's map; unknown metrics\n * encountered during validation are ignored (forward-compat).\n */\nconst METRIC_VALUES: Record<string, Record<string, string[]>> = {\n '2.0': {\n AV: ['L', 'A', 'N'],\n AC: ['H', 'M', 'L'],\n Au: ['M', 'S', 'N'],\n C: ['N', 'P', 'C'],\n I: ['N', 'P', 'C'],\n A: ['N', 'P', 'C'],\n // Temporal\n E: ['U', 'POC', 'F', 'H', 'ND'],\n RL: ['OF', 'TF', 'W', 'U', 'ND'],\n RC: ['UC', 'UR', 'C', 'ND'],\n // Environmental\n CDP: ['N', 'L', 'LM', 'MH', 'H', 'ND'],\n TD: ['N', 'L', 'M', 'H', 'ND'],\n CR: ['L', 'M', 'H', 'ND'],\n IR: ['L', 'M', 'H', 'ND'],\n AR: ['L', 'M', 'H', 'ND'],\n },\n '3.0': {\n AV: ['N', 'A', 'L', 'P'],\n AC: ['L', 'H'],\n PR: ['N', 'L', 'H'],\n UI: ['N', 'R'],\n S: ['U', 'C'],\n C: ['N', 'L', 'H'],\n I: ['N', 'L', 'H'],\n A: ['N', 'L', 'H'],\n // Temporal\n E: ['X', 'U', 'P', 'F', 'H'],\n RL: ['X', 'O', 'T', 'W', 'U'],\n RC: ['X', 'U', 'R', 'C'],\n // Environmental\n CR: ['X', 'L', 'M', 'H'],\n IR: ['X', 'L', 'M', 'H'],\n AR: ['X', 'L', 'M', 'H'],\n MAV: ['X', 'N', 'A', 'L', 'P'],\n MAC: ['X', 'L', 'H'],\n MPR: ['X', 'N', 'L', 'H'],\n MUI: ['X', 'N', 'R'],\n MS: ['X', 'U', 'C'],\n MC: ['X', 'N', 'L', 'H'],\n MI: ['X', 'N', 'L', 'H'],\n MA: ['X', 'N', 'L', 'H'],\n },\n '3.1': {\n AV: ['N', 'A', 'L', 'P'],\n AC: ['L', 'H'],\n PR: ['N', 'L', 'H'],\n UI: ['N', 'R'],\n S: ['U', 'C'],\n C: ['N', 'L', 'H'],\n I: ['N', 'L', 'H'],\n A: ['N', 'L', 'H'],\n // Temporal\n E: ['X', 'U', 'P', 'F', 'H'],\n RL: ['X', 'O', 'T', 'W', 'U'],\n RC: ['X', 'U', 'R', 'C'],\n // Environmental\n CR: ['X', 'L', 'M', 'H'],\n IR: ['X', 'L', 'M', 'H'],\n AR: ['X', 'L', 'M', 'H'],\n MAV: ['X', 'N', 'A', 'L', 'P'],\n MAC: ['X', 'L', 'H'],\n MPR: ['X', 'N', 'L', 'H'],\n MUI: ['X', 'N', 'R'],\n MS: ['X', 'U', 'C'],\n MC: ['X', 'N', 'L', 'H'],\n MI: ['X', 'N', 'L', 'H'],\n MA: ['X', 'N', 'L', 'H'],\n },\n '4.0': {\n // Base\n AV: ['N', 'A', 'L', 'P'],\n AC: ['L', 'H'],\n AT: ['N', 'P'],\n PR: ['N', 'L', 'H'],\n UI: ['N', 'P', 'A'],\n VC: ['H', 'L', 'N'],\n VI: ['H', 'L', 'N'],\n VA: ['H', 'L', 'N'],\n SC: ['H', 'L', 'N'],\n SI: ['H', 'L', 'N'],\n SA: ['H', 'L', 'N'],\n // Threat\n E: ['X', 'A', 'P', 'U'],\n // Environmental (modified base) — same enums as their base counterparts plus X\n MAV: ['X', 'N', 'A', 'L', 'P'],\n MAC: ['X', 'L', 'H'],\n MAT: ['X', 'N', 'P'],\n MPR: ['X', 'N', 'L', 'H'],\n MUI: ['X', 'N', 'P', 'A'],\n MVC: ['X', 'H', 'L', 'N'],\n MVI: ['X', 'H', 'L', 'N'],\n MVA: ['X', 'H', 'L', 'N'],\n MSC: ['X', 'H', 'L', 'N'],\n MSI: ['X', 'S', 'H', 'L', 'N'],\n MSA: ['X', 'S', 'H', 'L', 'N'],\n // Requirements\n CR: ['X', 'H', 'M', 'L'],\n IR: ['X', 'H', 'M', 'L'],\n AR: ['X', 'H', 'M', 'L'],\n // Supplemental\n S: ['X', 'N', 'P'],\n AU: ['X', 'N', 'Y'],\n R: ['X', 'A', 'U', 'I'],\n V: ['X', 'D', 'C'],\n RE: ['X', 'L', 'M', 'H'],\n U: ['X', 'Clear', 'Green', 'Amber', 'Red'],\n },\n};\n\nconst REQUIRED_METRICS: Record<string, string[]> = {\n '2.0': ['AV', 'AC', 'Au', 'C', 'I', 'A'],\n '3.0': ['AV', 'AC', 'PR', 'UI', 'S', 'C', 'I', 'A'],\n '3.1': ['AV', 'AC', 'PR', 'UI', 'S', 'C', 'I', 'A'],\n '4.0': ['AV', 'AC', 'AT', 'PR', 'UI', 'VC', 'VI', 'VA', 'SC', 'SI', 'SA'],\n};\n\n/**\n * Parse a CVSS vector string into version + metric map.\n *\n * Version detection rule: a leading `CVSS:X.Y` segment yields version \"X.Y\".\n * Otherwise (legacy v2 vectors), version is \"2.0\". Malformed input yields\n * \"unknown\" with an empty metric map; no exceptions are thrown.\n *\n * @param vector - CVSS vector string (e.g. \"CVSS:3.1/AV:N/AC:L/...\")\n * @returns Parsed version + metric map\n */\nexport function parseCvssVector(vector: string): ParsedCvssVector {\n const metrics = new Map<string, string>();\n if (typeof vector !== 'string' || vector.length === 0) {\n return { version: 'unknown', metrics };\n }\n\n const segments = vector.split('/').filter((s) => s.length > 0);\n if (segments.length === 0) {\n return { version: 'unknown', metrics };\n }\n\n let version = '2.0';\n let start = 0;\n const first = segments[0];\n if (first === undefined) {\n return { version: 'unknown', metrics };\n }\n if (first.startsWith('CVSS:')) {\n version = first.slice(5);\n start = 1;\n } else if (!first.includes(':')) {\n // No prefix AND first segment isn't a key:value pair — bail.\n return { version: 'unknown', metrics };\n }\n\n for (let i = start; i < segments.length; i++) {\n const seg = segments[i];\n if (seg === undefined) {\n continue;\n }\n const colon = seg.indexOf(':');\n if (colon <= 0 || colon === seg.length - 1) {\n // Skip malformed segments (no colon, empty key, or empty value).\n continue;\n }\n const key = seg.slice(0, colon);\n const value = seg.slice(colon + 1);\n metrics.set(key, value);\n }\n\n return { version, metrics };\n}\n\n/**\n * Validate a CVSS vector string against the FIRST grammar for its version.\n *\n * Checks:\n * - Version is one of the supported set (2.0, 3.0, 3.1, 4.0).\n * - All required base metrics for the version are present.\n * - All known metric values are within the allowed enum.\n *\n * Unknown metric keys are tolerated (forward-compat with future minor versions\n * or scanner-specific extensions). To check a v2 vector without the CVSS:\n * prefix, callers may pass version=\"2.0\" explicitly; otherwise the parser\n * defaults to \"2.0\" on prefix absence.\n *\n * @param vector - CVSS vector string\n * @param version - Optional explicit version override (else inferred)\n * @returns `{ valid, errors[] }` — never throws\n */\nexport function validateCvssVector(\n vector: string,\n version?: string,\n): CvssValidationResult {\n const errors: string[] = [];\n\n if (typeof vector !== 'string' || vector.length === 0) {\n return { valid: false, errors: ['vector is empty'] };\n }\n\n const parsed = parseCvssVector(vector);\n const v = version ?? parsed.version;\n\n const grammar = METRIC_VALUES[v];\n const required = REQUIRED_METRICS[v];\n if (!grammar || !required) {\n errors.push(`unsupported CVSS version: ${v}`);\n return { valid: false, errors };\n }\n\n for (const req of required) {\n if (!parsed.metrics.has(req)) {\n errors.push(`missing required metric: ${req}`);\n }\n }\n\n for (const [key, value] of parsed.metrics.entries()) {\n const allowed = grammar[key];\n if (!allowed) {\n // Unknown metric: forward-compat — no error.\n continue;\n }\n if (!allowed.includes(value)) {\n errors.push(`invalid value for metric ${key}: ${value} (allowed: ${allowed.join(',')})`);\n }\n }\n\n return { valid: errors.length === 0, errors };\n}\n","/**\n * CPE 2.3 URI parser for HDF converters.\n *\n * Common Platform Enumeration (CPE) 2.3 URIs identify affected products.\n * Container scanners (grype, twistlock, snyk) emit them in vulnerability\n * findings. This module provides an accept-and-warn parser: any input that\n * carries the `cpe:2.3:` prefix is best-effort parsed; deviations from the\n * canonical 13-field form are reported via `warnings` rather than rejected.\n *\n * Canonical form:\n * `cpe:2.3:part:vendor:product:version:update:edition:language:sw_edition:target_sw:target_hw:other`\n *\n * Reference: NIST IR 7695 (https://csrc.nist.gov/publications/detail/nistir/7695/final)\n */\n\n/** Valid CPE 2.3 `part` values. */\nexport type CpePart = 'a' | 'o' | 'h' | '*';\n\n/**\n * Parsed CPE 2.3 URI.\n *\n * `warnings` collects any deviations from the canonical form (truncation,\n * extra fields, unknown `part`). An empty array means the input was a strict\n * 13-field, valid-part CPE.\n */\nexport interface ParsedCpe {\n /** The original input string, unmodified. */\n raw: string;\n /** Application kind. `'a'` (app), `'o'` (OS), `'h'` (hardware), `'*'` (any). */\n part: CpePart | string;\n vendor: string;\n product: string;\n version: string;\n update: string;\n edition: string;\n language: string;\n swEdition: string;\n targetSw: string;\n targetHw: string;\n other: string;\n /** Deviation messages; empty when the input is a canonical 13-field CPE. */\n warnings: string[];\n}\n\nconst CPE_PREFIX = 'cpe:2.3:';\nconst EXPECTED_TOTAL_FIELDS = 13; // \"cpe\", \"2.3\", and 11 product fields = 13 colon-separated tokens\nconst PRODUCT_FIELD_COUNT = 11; // part + 10 attribute fields\nconst VALID_PARTS = new Set(['a', 'o', 'h', '*']);\n\n/**\n * Split a CPE 2.3 body on unescaped `:` separators.\n *\n * Per CPE 2.3 spec section 6.1.2.4, `:` and `\\` inside a field must be\n * escaped as `\\:` and `\\\\`. We respect those escapes during the split and\n * unescape the result.\n */\nfunction splitOnUnescapedColons(body: string): string[] {\n const result: string[] = [];\n let current = '';\n for (let i = 0; i < body.length; i++) {\n const ch = body[i];\n if (ch === '\\\\' && i + 1 < body.length) {\n const next = body[i + 1];\n // Preserve known escapes (\\: and \\\\) as a literal next character.\n if (next === ':' || next === '\\\\') {\n current += next;\n i++;\n continue;\n }\n // Unknown escape — keep the backslash and continue.\n current += ch;\n continue;\n }\n if (ch === ':') {\n result.push(current);\n current = '';\n continue;\n }\n current += ch;\n }\n result.push(current);\n return result;\n}\n\n/**\n * Parse a CPE 2.3 URI.\n *\n * Accept-and-warn semantics:\n * - Input missing the `cpe:2.3:` prefix → returns `null`.\n * - Input with the prefix but fewer than 11 product fields → fields are\n * padded with `\"*\"` and a `truncated:` warning is added.\n * - Input with more than 11 product fields → extras are dropped and an\n * `\"extra fields ignored\"` warning is added.\n * - Unknown `part` value → kept as-is in the result, `\"unknown part: X\"`\n * warning is added.\n * - `\\:` and `\\\\` escapes inside fields are honored during splitting and\n * unescaped in the returned field values.\n *\n * @param cpeUri - CPE 2.3 URI string\n * @returns Parsed CPE with warnings, or `null` if input lacks the prefix\n *\n * @example\n * ```typescript\n * parseCpe('cpe:2.3:a:openssl:openssl:1.1.1k:*:*:*:*:*:*:*');\n * // { part: 'a', vendor: 'openssl', product: 'openssl', version: '1.1.1k', ... warnings: [] }\n *\n * parseCpe('cpe:2.3:a:openssl:openssl:1.1.1k');\n * // truncated input → padded with '*', warnings: ['truncated: expected 13 colon-separated fields, got 6']\n *\n * parseCpe('openssl:1.1.1k'); // null — no prefix\n * ```\n */\nexport function parseCpe(cpeUri: string): ParsedCpe | null {\n if (!cpeUri.startsWith(CPE_PREFIX)) {\n return null;\n }\n\n const warnings: string[] = [];\n const body = cpeUri.slice(CPE_PREFIX.length);\n\n // Special case: bare prefix `cpe:2.3:` — all fields empty, still warn.\n // This is the \"I have nothing useful here\" form; padding with \"*\" would\n // suggest the parser inferred wildcards, which it didn't.\n const isBarePrefix = body.length === 0;\n\n const bodyParts = isBarePrefix ? [] : splitOnUnescapedColons(body);\n\n // Total colon-separated fields including the prefix \"cpe\" and \"2.3\" markers.\n const totalFields = isBarePrefix ? 2 : 2 + bodyParts.length;\n\n if (bodyParts.length > PRODUCT_FIELD_COUNT) {\n warnings.push('extra fields ignored');\n } else if (bodyParts.length < PRODUCT_FIELD_COUNT) {\n warnings.push(\n `truncated: expected ${EXPECTED_TOTAL_FIELDS} colon-separated fields, got ${totalFields}`,\n );\n }\n\n // Pad missing fields. For a bare prefix, pad with \"\" (no inferred data).\n // For partial input, pad missing trailing fields with \"*\" (wildcard).\n const padValue = isBarePrefix ? '' : '*';\n const fields: string[] = bodyParts.slice(0, PRODUCT_FIELD_COUNT);\n while (fields.length < PRODUCT_FIELD_COUNT) {\n fields.push(padValue);\n }\n\n // fields is padded to PRODUCT_FIELD_COUNT above, so every slot is present;\n // the destructuring defaults only satisfy the compiler's indexed-access check.\n const [\n part = '',\n vendor = '',\n product = '',\n version = '',\n update = '',\n edition = '',\n language = '',\n swEdition = '',\n targetSw = '',\n targetHw = '',\n other = '',\n ] = fields;\n\n if (!VALID_PARTS.has(part)) {\n warnings.push(`unknown part: ${part}`);\n }\n\n return {\n raw: cpeUri,\n part,\n vendor,\n product,\n version,\n update,\n edition,\n language,\n swEdition,\n targetSw,\n targetHw,\n other,\n warnings,\n };\n}\n","/**\n * Package URL (PURL) parser for SBOM and CVE-scanner ingestion.\n *\n * PURL is the canonical package identifier used by CycloneDX, SPDX, OSV,\n * GitHub Advisory Database, and most modern scanners. Spec:\n * https://github.com/package-url/purl-spec/blob/master/PURL-SPECIFICATION.rst\n *\n * Grammar: `pkg:type/namespace/name@version?qualifiers#subpath`\n *\n * This parser is intentionally accept-and-warn. Scanners occasionally emit\n * slightly malformed PURLs (qualifier with no `=`, missing name, etc.) and\n * we'd rather record a warning than fail ingestion.\n */\n\n/**\n * Result of parsing a PURL string.\n */\nexport interface ParsedPurl {\n /** Original input string, unmodified. */\n raw: string;\n /** Package type (e.g., \"npm\", \"rpm\", \"pypi\", \"maven\"). Lowercased. */\n type: string;\n /** Namespace segment (e.g., \"redhat\" for RPM, \"org.apache...\" for Maven). */\n namespace: string | null;\n /** Package name. Empty string (with a warning) if the input lacks one. */\n name: string;\n /** Version after the `@` separator. URL-decoded. */\n version: string | null;\n /** Qualifier key-value pairs from the `?` segment. */\n qualifiers: Map<string, string>;\n /** Subpath from the `#` fragment. */\n subpath: string | null;\n /** Deviations or oddities encountered during parsing. */\n warnings: string[];\n}\n\nconst PURL_PREFIX = 'pkg:';\n\n/**\n * Parse a Package URL string.\n *\n * Returns `null` only when the input is missing the mandatory `pkg:` prefix\n * or the type segment. Other deviations are surfaced via `warnings` on the\n * returned object.\n *\n * @param purlStr - PURL string, e.g. `pkg:npm/lodash@4.17.21`\n * @returns Parsed PURL or `null` if the input is unrecoverably malformed\n *\n * @example\n * ```typescript\n * const r = parsePurl('pkg:rpm/redhat/openssl@1.1.1k-7.el8_4?arch=x86_64');\n * r?.type; // \"rpm\"\n * r?.namespace; // \"redhat\"\n * r?.name; // \"openssl\"\n * r?.qualifiers.get('arch'); // \"x86_64\"\n * ```\n */\nexport function parsePurl(purlStr: string): ParsedPurl | null {\n if (!purlStr || !purlStr.startsWith(PURL_PREFIX)) {\n return null;\n }\n\n const raw = purlStr;\n const warnings: string[] = [];\n\n // Strip prefix. Per spec, scheme is case-insensitive but only `pkg:` is\n // valid; we already matched it.\n let remainder = purlStr.slice(PURL_PREFIX.length);\n\n // Per spec, leading slashes after the scheme are stripped. This also means\n // `pkg:/foo` and `pkg:foo` are equivalent, but `pkg:/` with no following\n // type segment is still invalid.\n while (remainder.startsWith('/')) {\n remainder = remainder.slice(1);\n }\n\n if (remainder.length === 0) {\n return null;\n }\n\n // Extract subpath (fragment). Per spec, the fragment is everything after\n // the first `#`. Splitting before qualifiers keeps `#` inside qualifier\n // values out of scope (which is fine — qualifier values do not legally\n // contain raw `#`; they would be percent-encoded).\n let subpath: string | null = null;\n const hashIdx = remainder.indexOf('#');\n if (hashIdx !== -1) {\n subpath = remainder.slice(hashIdx + 1);\n remainder = remainder.slice(0, hashIdx);\n if (subpath.length === 0) {\n subpath = null;\n }\n }\n\n // Extract qualifiers (query string). Everything after the first `?`.\n const qualifiers = new Map<string, string>();\n const qIdx = remainder.indexOf('?');\n if (qIdx !== -1) {\n const qStr = remainder.slice(qIdx + 1);\n remainder = remainder.slice(0, qIdx);\n parseQualifiers(qStr, qualifiers, warnings);\n }\n\n // Strip a trailing slash from the path portion. Spec allows it; we silently\n // normalize.\n while (remainder.endsWith('/')) {\n remainder = remainder.slice(0, -1);\n }\n\n // Type is everything up to the first `/`.\n const firstSlash = remainder.indexOf('/');\n let type: string;\n let pathPart: string;\n if (firstSlash === -1) {\n type = remainder;\n pathPart = '';\n } else {\n type = remainder.slice(0, firstSlash);\n pathPart = remainder.slice(firstSlash + 1);\n }\n\n if (type.length === 0) {\n return null;\n }\n type = type.toLowerCase();\n\n // Split version off the path part. Per spec, the version separator is the\n // LAST `@` in the path — names may contain `@` (e.g., npm scoped packages\n // when not percent-encoded).\n let version: string | null = null;\n const lastAt = pathPart.lastIndexOf('@');\n let nameAndNs = pathPart;\n if (lastAt !== -1) {\n version = pathPart.slice(lastAt + 1);\n nameAndNs = pathPart.slice(0, lastAt);\n version = safeDecode(version);\n }\n\n // Namespace is everything before the last `/` in the name+namespace\n // section; the final segment is the name.\n let namespace: string | null = null;\n let name: string;\n const lastSlash = nameAndNs.lastIndexOf('/');\n if (lastSlash === -1) {\n name = nameAndNs;\n } else {\n namespace = nameAndNs.slice(0, lastSlash);\n name = nameAndNs.slice(lastSlash + 1);\n }\n\n // Decode percent-encoded segments.\n if (namespace !== null) {\n // Decode each path segment independently to preserve embedded `/`.\n namespace = namespace.split('/').map(safeDecode).join('/');\n }\n name = safeDecode(name);\n\n if (name.length === 0) {\n warnings.push('PURL is missing the name segment');\n }\n\n // Normalize empty namespace to null.\n if (namespace !== null && namespace.length === 0) {\n namespace = null;\n }\n\n // Normalize empty version to null.\n if (version !== null && version.length === 0) {\n version = null;\n }\n\n return {\n raw,\n type,\n namespace,\n name,\n version,\n qualifiers,\n subpath,\n warnings,\n };\n}\n\n/**\n * Parse the `?key=value&key=value` qualifier string. Empty segments are\n * skipped silently; segments with no `=` are recorded with empty value and\n * a warning. Values are URL-decoded.\n */\nfunction parseQualifiers(\n qStr: string,\n out: Map<string, string>,\n warnings: string[],\n): void {\n const parts = qStr.split('&');\n for (const part of parts) {\n if (part.length === 0) {\n continue;\n }\n const eq = part.indexOf('=');\n if (eq === -1) {\n out.set(part, '');\n warnings.push(`qualifier \"${part}\" has no value`);\n continue;\n }\n const key = part.slice(0, eq);\n const value = part.slice(eq + 1);\n out.set(key, safeDecode(value));\n }\n}\n\n/**\n * decodeURIComponent that returns the original string on failure rather than\n * throwing. PURLs in the wild occasionally contain stray `%` characters that\n * aren't legitimate percent-encoding.\n */\nfunction safeDecode(s: string): string {\n try {\n return decodeURIComponent(s);\n } catch {\n return s;\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;AAeA,MAAM,cAAsC;CAC1C,UAAU;CACV,MAAM;CACN,QAAQ;CACR,KAAK;CACL,MAAM;CACN,MAAM;CACN,eAAe;CACf,aAAa;AACf;AAWA,SAAgB,iBAAiB,UAAwC;CACvE,IAAI,aAAa,MAAM,OAAO;CAE9B,OAAO,YADY,SAAS,YACA,MAAM;AACpC;AAWA,SAAgB,iBAAiB,QAAsC;CACrE,IAAI,WAAW,MAAM,OAAO;CAC5B,IAAI,UAAU,IAAK,OAAO;CAC1B,IAAI,UAAU,IAAK,OAAO;CAC1B,IAAI,UAAU,IAAK,OAAO;CAC1B,IAAI,SAAS,GAAK,OAAO;CACzB,OAAO;AACT;;;;;;;;;;;;;;;;;;AAmBA,SAAgB,oBACd,OACiD;CACjD,IAAI,CAAC,OAAO,SAAS,KAAK,KAAK,QAAQ,IAAK,OAAO;CACnD,IAAI,QAAQ,GAAK,OAAO;CACxB,IAAI,QAAQ,GAAK,OAAO;CACxB,IAAI,QAAQ,GAAK,OAAO;CACxB,OAAO;AACT;;;;;;;;;;ACpDA,MAAM,gBAA0D;CAC9D,OAAO;EACL,IAAI;GAAC;GAAK;GAAK;EAAG;EAClB,IAAI;GAAC;GAAK;GAAK;EAAG;EAClB,IAAI;GAAC;GAAK;GAAK;EAAG;EAClB,GAAG;GAAC;GAAK;GAAK;EAAG;EACjB,GAAG;GAAC;GAAK;GAAK;EAAG;EACjB,GAAG;GAAC;GAAK;GAAK;EAAG;EAEjB,GAAG;GAAC;GAAK;GAAO;GAAK;GAAK;EAAI;EAC9B,IAAI;GAAC;GAAM;GAAM;GAAK;GAAK;EAAI;EAC/B,IAAI;GAAC;GAAM;GAAM;GAAK;EAAI;EAE1B,KAAK;GAAC;GAAK;GAAK;GAAM;GAAM;GAAK;EAAI;EACrC,IAAI;GAAC;GAAK;GAAK;GAAK;GAAK;EAAI;EAC7B,IAAI;GAAC;GAAK;GAAK;GAAK;EAAI;EACxB,IAAI;GAAC;GAAK;GAAK;GAAK;EAAI;EACxB,IAAI;GAAC;GAAK;GAAK;GAAK;EAAI;CAC1B;CACA,OAAO;EACL,IAAI;GAAC;GAAK;GAAK;GAAK;EAAG;EACvB,IAAI,CAAC,KAAK,GAAG;EACb,IAAI;GAAC;GAAK;GAAK;EAAG;EAClB,IAAI,CAAC,KAAK,GAAG;EACb,GAAG,CAAC,KAAK,GAAG;EACZ,GAAG;GAAC;GAAK;GAAK;EAAG;EACjB,GAAG;GAAC;GAAK;GAAK;EAAG;EACjB,GAAG;GAAC;GAAK;GAAK;EAAG;EAEjB,GAAG;GAAC;GAAK;GAAK;GAAK;GAAK;EAAG;EAC3B,IAAI;GAAC;GAAK;GAAK;GAAK;GAAK;EAAG;EAC5B,IAAI;GAAC;GAAK;GAAK;GAAK;EAAG;EAEvB,IAAI;GAAC;GAAK;GAAK;GAAK;EAAG;EACvB,IAAI;GAAC;GAAK;GAAK;GAAK;EAAG;EACvB,IAAI;GAAC;GAAK;GAAK;GAAK;EAAG;EACvB,KAAK;GAAC;GAAK;GAAK;GAAK;GAAK;EAAG;EAC7B,KAAK;GAAC;GAAK;GAAK;EAAG;EACnB,KAAK;GAAC;GAAK;GAAK;GAAK;EAAG;EACxB,KAAK;GAAC;GAAK;GAAK;EAAG;EACnB,IAAI;GAAC;GAAK;GAAK;EAAG;EAClB,IAAI;GAAC;GAAK;GAAK;GAAK;EAAG;EACvB,IAAI;GAAC;GAAK;GAAK;GAAK;EAAG;EACvB,IAAI;GAAC;GAAK;GAAK;GAAK;EAAG;CACzB;CACA,OAAO;EACL,IAAI;GAAC;GAAK;GAAK;GAAK;EAAG;EACvB,IAAI,CAAC,KAAK,GAAG;EACb,IAAI;GAAC;GAAK;GAAK;EAAG;EAClB,IAAI,CAAC,KAAK,GAAG;EACb,GAAG,CAAC,KAAK,GAAG;EACZ,GAAG;GAAC;GAAK;GAAK;EAAG;EACjB,GAAG;GAAC;GAAK;GAAK;EAAG;EACjB,GAAG;GAAC;GAAK;GAAK;EAAG;EAEjB,GAAG;GAAC;GAAK;GAAK;GAAK;GAAK;EAAG;EAC3B,IAAI;GAAC;GAAK;GAAK;GAAK;GAAK;EAAG;EAC5B,IAAI;GAAC;GAAK;GAAK;GAAK;EAAG;EAEvB,IAAI;GAAC;GAAK;GAAK;GAAK;EAAG;EACvB,IAAI;GAAC;GAAK;GAAK;GAAK;EAAG;EACvB,IAAI;GAAC;GAAK;GAAK;GAAK;EAAG;EACvB,KAAK;GAAC;GAAK;GAAK;GAAK;GAAK;EAAG;EAC7B,KAAK;GAAC;GAAK;GAAK;EAAG;EACnB,KAAK;GAAC;GAAK;GAAK;GAAK;EAAG;EACxB,KAAK;GAAC;GAAK;GAAK;EAAG;EACnB,IAAI;GAAC;GAAK;GAAK;EAAG;EAClB,IAAI;GAAC;GAAK;GAAK;GAAK;EAAG;EACvB,IAAI;GAAC;GAAK;GAAK;GAAK;EAAG;EACvB,IAAI;GAAC;GAAK;GAAK;GAAK;EAAG;CACzB;CACA,OAAO;EAEL,IAAI;GAAC;GAAK;GAAK;GAAK;EAAG;EACvB,IAAI,CAAC,KAAK,GAAG;EACb,IAAI,CAAC,KAAK,GAAG;EACb,IAAI;GAAC;GAAK;GAAK;EAAG;EAClB,IAAI;GAAC;GAAK;GAAK;EAAG;EAClB,IAAI;GAAC;GAAK;GAAK;EAAG;EAClB,IAAI;GAAC;GAAK;GAAK;EAAG;EAClB,IAAI;GAAC;GAAK;GAAK;EAAG;EAClB,IAAI;GAAC;GAAK;GAAK;EAAG;EAClB,IAAI;GAAC;GAAK;GAAK;EAAG;EAClB,IAAI;GAAC;GAAK;GAAK;EAAG;EAElB,GAAG;GAAC;GAAK;GAAK;GAAK;EAAG;EAEtB,KAAK;GAAC;GAAK;GAAK;GAAK;GAAK;EAAG;EAC7B,KAAK;GAAC;GAAK;GAAK;EAAG;EACnB,KAAK;GAAC;GAAK;GAAK;EAAG;EACnB,KAAK;GAAC;GAAK;GAAK;GAAK;EAAG;EACxB,KAAK;GAAC;GAAK;GAAK;GAAK;EAAG;EACxB,KAAK;GAAC;GAAK;GAAK;GAAK;EAAG;EACxB,KAAK;GAAC;GAAK;GAAK;GAAK;EAAG;EACxB,KAAK;GAAC;GAAK;GAAK;GAAK;EAAG;EACxB,KAAK;GAAC;GAAK;GAAK;GAAK;EAAG;EACxB,KAAK;GAAC;GAAK;GAAK;GAAK;GAAK;EAAG;EAC7B,KAAK;GAAC;GAAK;GAAK;GAAK;GAAK;EAAG;EAE7B,IAAI;GAAC;GAAK;GAAK;GAAK;EAAG;EACvB,IAAI;GAAC;GAAK;GAAK;GAAK;EAAG;EACvB,IAAI;GAAC;GAAK;GAAK;GAAK;EAAG;EAEvB,GAAG;GAAC;GAAK;GAAK;EAAG;EACjB,IAAI;GAAC;GAAK;GAAK;EAAG;EAClB,GAAG;GAAC;GAAK;GAAK;GAAK;EAAG;EACtB,GAAG;GAAC;GAAK;GAAK;EAAG;EACjB,IAAI;GAAC;GAAK;GAAK;GAAK;EAAG;EACvB,GAAG;GAAC;GAAK;GAAS;GAAS;GAAS;EAAK;CAC3C;AACF;AAEA,MAAM,mBAA6C;CACjD,OAAO;EAAC;EAAM;EAAM;EAAM;EAAK;EAAK;CAAG;CACvC,OAAO;EAAC;EAAM;EAAM;EAAM;EAAM;EAAK;EAAK;EAAK;CAAG;CAClD,OAAO;EAAC;EAAM;EAAM;EAAM;EAAM;EAAK;EAAK;EAAK;CAAG;CAClD,OAAO;EAAC;EAAM;EAAM;EAAM;EAAM;EAAM;EAAM;EAAM;EAAM;EAAM;EAAM;CAAI;AAC1E;;;;;;;;;;;AAYA,SAAgB,gBAAgB,QAAkC;CAChE,MAAM,0BAAU,IAAI,IAAoB;CACxC,IAAI,OAAO,WAAW,YAAY,OAAO,WAAW,GAClD,OAAO;EAAE,SAAS;EAAW;CAAQ;CAGvC,MAAM,WAAW,OAAO,MAAM,GAAG,CAAC,CAAC,QAAQ,MAAM,EAAE,SAAS,CAAC;CAC7D,IAAI,SAAS,WAAW,GACtB,OAAO;EAAE,SAAS;EAAW;CAAQ;CAGvC,IAAI,UAAU;CACd,IAAI,QAAQ;CACZ,MAAM,QAAQ,SAAS;CACvB,IAAI,UAAU,KAAA,GACZ,OAAO;EAAE,SAAS;EAAW;CAAQ;CAEvC,IAAI,MAAM,WAAW,OAAO,GAAG;EAC7B,UAAU,MAAM,MAAM,CAAC;EACvB,QAAQ;CACV,OAAO,IAAI,CAAC,MAAM,SAAS,GAAG,GAE5B,OAAO;EAAE,SAAS;EAAW;CAAQ;CAGvC,KAAK,IAAI,IAAI,OAAO,IAAI,SAAS,QAAQ,KAAK;EAC5C,MAAM,MAAM,SAAS;EACrB,IAAI,QAAQ,KAAA,GACV;EAEF,MAAM,QAAQ,IAAI,QAAQ,GAAG;EAC7B,IAAI,SAAS,KAAK,UAAU,IAAI,SAAS,GAEvC;EAEF,MAAM,MAAM,IAAI,MAAM,GAAG,KAAK;EAC9B,MAAM,QAAQ,IAAI,MAAM,QAAQ,CAAC;EACjC,QAAQ,IAAI,KAAK,KAAK;CACxB;CAEA,OAAO;EAAE;EAAS;CAAQ;AAC5B;;;;;;;;;;;;;;;;;;AAmBA,SAAgB,mBACd,QACA,SACsB;CACtB,MAAM,SAAmB,CAAC;CAE1B,IAAI,OAAO,WAAW,YAAY,OAAO,WAAW,GAClD,OAAO;EAAE,OAAO;EAAO,QAAQ,CAAC,iBAAiB;CAAE;CAGrD,MAAM,SAAS,gBAAgB,MAAM;CACrC,MAAM,IAAI,WAAW,OAAO;CAE5B,MAAM,UAAU,cAAc;CAC9B,MAAM,WAAW,iBAAiB;CAClC,IAAI,CAAC,WAAW,CAAC,UAAU;EACzB,OAAO,KAAK,6BAA6B,GAAG;EAC5C,OAAO;GAAE,OAAO;GAAO;EAAO;CAChC;CAEA,KAAK,MAAM,OAAO,UAChB,IAAI,CAAC,OAAO,QAAQ,IAAI,GAAG,GACzB,OAAO,KAAK,4BAA4B,KAAK;CAIjD,KAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,QAAQ,GAAG;EACnD,MAAM,UAAU,QAAQ;EACxB,IAAI,CAAC,SAEH;EAEF,IAAI,CAAC,QAAQ,SAAS,KAAK,GACzB,OAAO,KAAK,4BAA4B,IAAI,IAAI,MAAM,aAAa,QAAQ,KAAK,GAAG,EAAE,EAAE;CAE3F;CAEA,OAAO;EAAE,OAAO,OAAO,WAAW;EAAG;CAAO;AAC9C;;;ACvNA,MAAM,aAAa;AACnB,MAAM,wBAAwB;AAC9B,MAAM,sBAAsB;AAC5B,MAAM,8BAAc,IAAI,IAAI;CAAC;CAAK;CAAK;CAAK;AAAG,CAAC;;;;;;;;AAShD,SAAS,uBAAuB,MAAwB;CACtD,MAAM,SAAmB,CAAC;CAC1B,IAAI,UAAU;CACd,KAAK,IAAI,IAAI,GAAG,IAAI,KAAK,QAAQ,KAAK;EACpC,MAAM,KAAK,KAAK;EAChB,IAAI,OAAO,QAAQ,IAAI,IAAI,KAAK,QAAQ;GACtC,MAAM,OAAO,KAAK,IAAI;GAEtB,IAAI,SAAS,OAAO,SAAS,MAAM;IACjC,WAAW;IACX;IACA;GACF;GAEA,WAAW;GACX;EACF;EACA,IAAI,OAAO,KAAK;GACd,OAAO,KAAK,OAAO;GACnB,UAAU;GACV;EACF;EACA,WAAW;CACb;CACA,OAAO,KAAK,OAAO;CACnB,OAAO;AACT;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8BA,SAAgB,SAAS,QAAkC;CACzD,IAAI,CAAC,OAAO,WAAW,UAAU,GAC/B,OAAO;CAGT,MAAM,WAAqB,CAAC;CAC5B,MAAM,OAAO,OAAO,MAAM,CAAiB;CAK3C,MAAM,eAAe,KAAK,WAAW;CAErC,MAAM,YAAY,eAAe,CAAC,IAAI,uBAAuB,IAAI;CAGjE,MAAM,cAAc,eAAe,IAAI,IAAI,UAAU;CAErD,IAAI,UAAU,SAAS,qBACrB,SAAS,KAAK,sBAAsB;MAC/B,IAAI,UAAU,SAAS,qBAC5B,SAAS,KACP,uBAAuB,sBAAsB,+BAA+B,aAC9E;CAKF,MAAM,WAAW,eAAe,KAAK;CACrC,MAAM,SAAmB,UAAU,MAAM,GAAG,mBAAmB;CAC/D,OAAO,OAAO,SAAS,qBACrB,OAAO,KAAK,QAAQ;CAKtB,MAAM,CACJ,OAAO,IACP,SAAS,IACT,UAAU,IACV,UAAU,IACV,SAAS,IACT,UAAU,IACV,WAAW,IACX,YAAY,IACZ,WAAW,IACX,WAAW,IACX,QAAQ,MACN;CAEJ,IAAI,CAAC,YAAY,IAAI,IAAI,GACvB,SAAS,KAAK,iBAAiB,MAAM;CAGvC,OAAO;EACL,KAAK;EACL;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;CACF;AACF;;;ACjJA,MAAM,cAAc;;;;;;;;;;;;;;;;;;;;AAqBpB,SAAgB,UAAU,SAAoC;CAC5D,IAAI,CAAC,WAAW,CAAC,QAAQ,WAAW,WAAW,GAC7C,OAAO;CAGT,MAAM,MAAM;CACZ,MAAM,WAAqB,CAAC;CAI5B,IAAI,YAAY,QAAQ,MAAM,CAAkB;CAKhD,OAAO,UAAU,WAAW,GAAG,GAC7B,YAAY,UAAU,MAAM,CAAC;CAG/B,IAAI,UAAU,WAAW,GACvB,OAAO;CAOT,IAAI,UAAyB;CAC7B,MAAM,UAAU,UAAU,QAAQ,GAAG;CACrC,IAAI,YAAY,IAAI;EAClB,UAAU,UAAU,MAAM,UAAU,CAAC;EACrC,YAAY,UAAU,MAAM,GAAG,OAAO;EACtC,IAAI,QAAQ,WAAW,GACrB,UAAU;CAEd;CAGA,MAAM,6BAAa,IAAI,IAAoB;CAC3C,MAAM,OAAO,UAAU,QAAQ,GAAG;CAClC,IAAI,SAAS,IAAI;EACf,MAAM,OAAO,UAAU,MAAM,OAAO,CAAC;EACrC,YAAY,UAAU,MAAM,GAAG,IAAI;EACnC,gBAAgB,MAAM,YAAY,QAAQ;CAC5C;CAIA,OAAO,UAAU,SAAS,GAAG,GAC3B,YAAY,UAAU,MAAM,GAAG,EAAE;CAInC,MAAM,aAAa,UAAU,QAAQ,GAAG;CACxC,IAAI;CACJ,IAAI;CACJ,IAAI,eAAe,IAAI;EACrB,OAAO;EACP,WAAW;CACb,OAAO;EACL,OAAO,UAAU,MAAM,GAAG,UAAU;EACpC,WAAW,UAAU,MAAM,aAAa,CAAC;CAC3C;CAEA,IAAI,KAAK,WAAW,GAClB,OAAO;CAET,OAAO,KAAK,YAAY;CAKxB,IAAI,UAAyB;CAC7B,MAAM,SAAS,SAAS,YAAY,GAAG;CACvC,IAAI,YAAY;CAChB,IAAI,WAAW,IAAI;EACjB,UAAU,SAAS,MAAM,SAAS,CAAC;EACnC,YAAY,SAAS,MAAM,GAAG,MAAM;EACpC,UAAU,WAAW,OAAO;CAC9B;CAIA,IAAI,YAA2B;CAC/B,IAAI;CACJ,MAAM,YAAY,UAAU,YAAY,GAAG;CAC3C,IAAI,cAAc,IAChB,OAAO;MACF;EACL,YAAY,UAAU,MAAM,GAAG,SAAS;EACxC,OAAO,UAAU,MAAM,YAAY,CAAC;CACtC;CAGA,IAAI,cAAc,MAEhB,YAAY,UAAU,MAAM,GAAG,CAAC,CAAC,IAAI,UAAU,CAAC,CAAC,KAAK,GAAG;CAE3D,OAAO,WAAW,IAAI;CAEtB,IAAI,KAAK,WAAW,GAClB,SAAS,KAAK,kCAAkC;CAIlD,IAAI,cAAc,QAAQ,UAAU,WAAW,GAC7C,YAAY;CAId,IAAI,YAAY,QAAQ,QAAQ,WAAW,GACzC,UAAU;CAGZ,OAAO;EACL;EACA;EACA;EACA;EACA;EACA;EACA;EACA;CACF;AACF;;;;;;AAOA,SAAS,gBACP,MACA,KACA,UACM;CACN,MAAM,QAAQ,KAAK,MAAM,GAAG;CAC5B,KAAK,MAAM,QAAQ,OAAO;EACxB,IAAI,KAAK,WAAW,GAClB;EAEF,MAAM,KAAK,KAAK,QAAQ,GAAG;EAC3B,IAAI,OAAO,IAAI;GACb,IAAI,IAAI,MAAM,EAAE;GAChB,SAAS,KAAK,cAAc,KAAK,eAAe;GAChD;EACF;EACA,MAAM,MAAM,KAAK,MAAM,GAAG,EAAE;EAC5B,MAAM,QAAQ,KAAK,MAAM,KAAK,CAAC;EAC/B,IAAI,IAAI,KAAK,WAAW,KAAK,CAAC;CAChC;AACF;;;;;;AAOA,SAAS,WAAW,GAAmB;CACrC,IAAI;EACF,OAAO,mBAAmB,CAAC;CAC7B,QAAQ;EACN,OAAO;CACT;AACF"}
|
package/dist/string/index.d.ts
CHANGED
|
@@ -27,27 +27,55 @@
|
|
|
27
27
|
*/
|
|
28
28
|
declare function stripHtml(html: string): string;
|
|
29
29
|
/**
|
|
30
|
-
* Parse a timestamp string in various common formats.
|
|
30
|
+
* Parse a timestamp string in various common formats into a Date.
|
|
31
31
|
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
32
|
+
* A zone-less ISO datetime (e.g. `2024-01-15T10:30:00`) is interpreted as
|
|
33
|
+
* UTC — matching Go's `hdfutil.ParseTimestamp` — rather than host-local time,
|
|
34
|
+
* so converter output does not depend on the machine's timezone.
|
|
34
35
|
*
|
|
35
36
|
* Returns `null` if the input is empty or cannot be parsed.
|
|
36
37
|
*
|
|
37
|
-
* Equivalent to the Go `ParseTimestamp` in shared/go/converterutil.go.
|
|
38
|
-
*
|
|
39
38
|
* @param s - Timestamp string to parse
|
|
40
39
|
* @returns Parsed Date or null if unparseable
|
|
41
40
|
*
|
|
42
41
|
* @example
|
|
43
42
|
* ```typescript
|
|
44
43
|
* parseTimestamp('2024-01-15T10:30:00Z'); // Date object
|
|
45
|
-
* parseTimestamp('
|
|
44
|
+
* parseTimestamp('2024-01-15T10:30:00'); // Date object (interpreted as UTC)
|
|
46
45
|
* parseTimestamp('not a date'); // null
|
|
47
46
|
* parseTimestamp(''); // null
|
|
48
47
|
* ```
|
|
49
48
|
*/
|
|
50
49
|
declare function parseTimestamp(s: string): Date | null;
|
|
50
|
+
/**
|
|
51
|
+
* Trim trailing fractional-second zeros from an RFC3339 UTC timestamp string,
|
|
52
|
+
* matching Go's RFC3339Nano marshaling (which drops trailing zeros and the
|
|
53
|
+
* decimal point when the fraction is all zeros).
|
|
54
|
+
*
|
|
55
|
+
* @example
|
|
56
|
+
* trimUtcFraction('2024-11-15T10:30:00.000Z'); // '2024-11-15T10:30:00Z'
|
|
57
|
+
* trimUtcFraction('2024-01-01T00:00:00.120Z'); // '2024-01-01T00:00:00.12Z'
|
|
58
|
+
*/
|
|
59
|
+
declare function trimUtcFraction(s: string): string;
|
|
60
|
+
/**
|
|
61
|
+
* Format a Date as HDF's canonical timestamp: RFC3339 in UTC with trailing
|
|
62
|
+
* fractional-second zeros trimmed. Byte-identical to what the Go converters
|
|
63
|
+
* emit for the same instant (a UTC `time.Time` marshaled as RFC3339Nano).
|
|
64
|
+
*
|
|
65
|
+
* @example
|
|
66
|
+
* formatTimestamp(new Date('2024-11-15T10:30:00Z')); // '2024-11-15T10:30:00Z'
|
|
67
|
+
*/
|
|
68
|
+
declare function formatTimestamp(d: Date): string;
|
|
69
|
+
/**
|
|
70
|
+
* Format a Date as RFC3339 in UTC truncated to whole seconds (no fractional
|
|
71
|
+
* part). For exporters whose target format conventionally uses whole-second
|
|
72
|
+
* timestamps (CSAF, OpenVEX, CycloneDX). Use {@link formatTimestamp} for
|
|
73
|
+
* canonical HDF output instead — it preserves sub-second precision.
|
|
74
|
+
*
|
|
75
|
+
* @example
|
|
76
|
+
* formatTimestampSeconds(new Date('2024-11-15T10:30:00.123Z')); // '2024-11-15T10:30:00Z'
|
|
77
|
+
*/
|
|
78
|
+
declare function formatTimestampSeconds(d: Date): string;
|
|
51
79
|
//#endregion
|
|
52
|
-
export { parseTimestamp, stripHtml };
|
|
80
|
+
export { formatTimestamp, formatTimestampSeconds, parseTimestamp, stripHtml, trimUtcFraction };
|
|
53
81
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","names":[],"sources":["../../src/string/index.ts"],"mappings":";;AA2BA;;;;AAAsC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","names":[],"sources":["../../src/string/index.ts"],"mappings":";;AA2BA;;;;AAAsC;AAoCtC;;;;AAA+C;AA8B/C;;;;AAAyC;AAYzC;;;;AAAuC;AAavC;;;;AAA8C;iBA3F9B,SAAA,CAAU,IAAY;;;;;;;;;;;;;;;;;;;;;iBAoCtB,cAAA,CAAe,CAAA,WAAY,IAAI;;;;;;;;;;iBA8B/B,eAAA,CAAgB,CAAS;;;;;;;;;iBAYzB,eAAA,CAAgB,CAAO,EAAJ,IAAI;;;;;;;;;;iBAavB,sBAAA,CAAuB,CAAO,EAAJ,IAAI"}
|
package/dist/string/index.js
CHANGED
|
@@ -28,34 +28,74 @@
|
|
|
28
28
|
function stripHtml(html) {
|
|
29
29
|
return html.replace(/<[^>]*>/g, " ").replace(/\s+/g, " ").trim();
|
|
30
30
|
}
|
|
31
|
+
const ISO_DATETIME_NO_ZONE = /^\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}(:\d{2})?(\.\d+)?$/;
|
|
32
|
+
const CTIME_NO_ZONE = /^[A-Za-z]{3} [A-Za-z]{3} +\d{1,2} \d{2}:\d{2}:\d{2} \d{4}$/;
|
|
31
33
|
/**
|
|
32
|
-
* Parse a timestamp string in various common formats.
|
|
34
|
+
* Parse a timestamp string in various common formats into a Date.
|
|
33
35
|
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
+
* A zone-less ISO datetime (e.g. `2024-01-15T10:30:00`) is interpreted as
|
|
37
|
+
* UTC — matching Go's `hdfutil.ParseTimestamp` — rather than host-local time,
|
|
38
|
+
* so converter output does not depend on the machine's timezone.
|
|
36
39
|
*
|
|
37
40
|
* Returns `null` if the input is empty or cannot be parsed.
|
|
38
41
|
*
|
|
39
|
-
* Equivalent to the Go `ParseTimestamp` in shared/go/converterutil.go.
|
|
40
|
-
*
|
|
41
42
|
* @param s - Timestamp string to parse
|
|
42
43
|
* @returns Parsed Date or null if unparseable
|
|
43
44
|
*
|
|
44
45
|
* @example
|
|
45
46
|
* ```typescript
|
|
46
47
|
* parseTimestamp('2024-01-15T10:30:00Z'); // Date object
|
|
47
|
-
* parseTimestamp('
|
|
48
|
+
* parseTimestamp('2024-01-15T10:30:00'); // Date object (interpreted as UTC)
|
|
48
49
|
* parseTimestamp('not a date'); // null
|
|
49
50
|
* parseTimestamp(''); // null
|
|
50
51
|
* ```
|
|
51
52
|
*/
|
|
52
53
|
function parseTimestamp(s) {
|
|
53
54
|
if (!s || s.trim().length === 0) return null;
|
|
54
|
-
const
|
|
55
|
+
const trimmed = s.trim();
|
|
56
|
+
let normalized = trimmed;
|
|
57
|
+
if (ISO_DATETIME_NO_ZONE.test(trimmed)) normalized = `${trimmed.replace(" ", "T")}Z`;
|
|
58
|
+
else if (CTIME_NO_ZONE.test(trimmed)) normalized = `${trimmed} GMT`;
|
|
59
|
+
const parsed = new Date(normalized);
|
|
55
60
|
if (isNaN(parsed.getTime())) return null;
|
|
56
61
|
return parsed;
|
|
57
62
|
}
|
|
63
|
+
/**
|
|
64
|
+
* Trim trailing fractional-second zeros from an RFC3339 UTC timestamp string,
|
|
65
|
+
* matching Go's RFC3339Nano marshaling (which drops trailing zeros and the
|
|
66
|
+
* decimal point when the fraction is all zeros).
|
|
67
|
+
*
|
|
68
|
+
* @example
|
|
69
|
+
* trimUtcFraction('2024-11-15T10:30:00.000Z'); // '2024-11-15T10:30:00Z'
|
|
70
|
+
* trimUtcFraction('2024-01-01T00:00:00.120Z'); // '2024-01-01T00:00:00.12Z'
|
|
71
|
+
*/
|
|
72
|
+
function trimUtcFraction(s) {
|
|
73
|
+
return s.replace(/\.(\d*?)0+Z$/, (_m, keep) => keep ? `.${keep}Z` : "Z");
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Format a Date as HDF's canonical timestamp: RFC3339 in UTC with trailing
|
|
77
|
+
* fractional-second zeros trimmed. Byte-identical to what the Go converters
|
|
78
|
+
* emit for the same instant (a UTC `time.Time` marshaled as RFC3339Nano).
|
|
79
|
+
*
|
|
80
|
+
* @example
|
|
81
|
+
* formatTimestamp(new Date('2024-11-15T10:30:00Z')); // '2024-11-15T10:30:00Z'
|
|
82
|
+
*/
|
|
83
|
+
function formatTimestamp(d) {
|
|
84
|
+
return trimUtcFraction(d.toISOString());
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Format a Date as RFC3339 in UTC truncated to whole seconds (no fractional
|
|
88
|
+
* part). For exporters whose target format conventionally uses whole-second
|
|
89
|
+
* timestamps (CSAF, OpenVEX, CycloneDX). Use {@link formatTimestamp} for
|
|
90
|
+
* canonical HDF output instead — it preserves sub-second precision.
|
|
91
|
+
*
|
|
92
|
+
* @example
|
|
93
|
+
* formatTimestampSeconds(new Date('2024-11-15T10:30:00.123Z')); // '2024-11-15T10:30:00Z'
|
|
94
|
+
*/
|
|
95
|
+
function formatTimestampSeconds(d) {
|
|
96
|
+
return d.toISOString().replace(/\.\d+Z$/, "Z");
|
|
97
|
+
}
|
|
58
98
|
//#endregion
|
|
59
|
-
export { parseTimestamp, stripHtml };
|
|
99
|
+
export { formatTimestamp, formatTimestampSeconds, parseTimestamp, stripHtml, trimUtcFraction };
|
|
60
100
|
|
|
61
101
|
//# sourceMappingURL=index.js.map
|
package/dist/string/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../../src/string/index.ts"],"sourcesContent":["/**\n * String manipulation utilities for HDF converters.\n *\n * These handle common operations on security tool output strings:\n * HTML stripping for STIG descriptions and SonarQube messages,\n * and multi-format timestamp parsing for tool output dates.\n */\n\n/**\n * Remove HTML tags from a string and normalize whitespace.\n *\n * Strips all HTML/XML tags, collapses multiple whitespace characters\n * into single spaces, and trims leading/trailing whitespace.\n *\n * For XML documents, use {@link extractTextFromXml} from the xml module instead.\n * This function is for inline HTML in non-XML strings (STIG descriptions,\n * SonarQube messages, etc.).\n *\n * @param html - String potentially containing HTML tags\n * @returns Plain text with tags removed and whitespace normalized\n *\n * @example\n * ```typescript\n * stripHtml('<p>hello</p> <b>world</b>'); // 'hello world'\n * stripHtml('no tags here'); // 'no tags here'\n * ```\n */\nexport function stripHtml(html: string): string {\n const stripped = html.replace(/<[^>]*>/g, ' ');\n return stripped.replace(/\\s+/g, ' ').trim();\n}\n\n/**\n * Parse a timestamp string in various common formats.\n *\n *
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../src/string/index.ts"],"sourcesContent":["/**\n * String manipulation utilities for HDF converters.\n *\n * These handle common operations on security tool output strings:\n * HTML stripping for STIG descriptions and SonarQube messages,\n * and multi-format timestamp parsing for tool output dates.\n */\n\n/**\n * Remove HTML tags from a string and normalize whitespace.\n *\n * Strips all HTML/XML tags, collapses multiple whitespace characters\n * into single spaces, and trims leading/trailing whitespace.\n *\n * For XML documents, use {@link extractTextFromXml} from the xml module instead.\n * This function is for inline HTML in non-XML strings (STIG descriptions,\n * SonarQube messages, etc.).\n *\n * @param html - String potentially containing HTML tags\n * @returns Plain text with tags removed and whitespace normalized\n *\n * @example\n * ```typescript\n * stripHtml('<p>hello</p> <b>world</b>'); // 'hello world'\n * stripHtml('no tags here'); // 'no tags here'\n * ```\n */\nexport function stripHtml(html: string): string {\n const stripped = html.replace(/<[^>]*>/g, ' ');\n return stripped.replace(/\\s+/g, ' ').trim();\n}\n\n// A bare ISO-8601 datetime with a time component but no timezone designator.\n// JavaScript's Date constructor interprets such a value as host-LOCAL time,\n// whereas Go's time.Parse (and HDF's canonical model) treats it as UTC. We\n// append 'Z' to force UTC so output is host-timezone-independent.\nconst ISO_DATETIME_NO_ZONE = /^\\d{4}-\\d{2}-\\d{2}[T ]\\d{2}:\\d{2}(:\\d{2})?(\\.\\d+)?$/;\n\n// C ctime layout (\"Tue Mar 22 14:54:47 2022\", or \"Mon Jan 2 ...\" for a\n// single-digit day). Go's hdfutil.ParseTimestamp parses this layout as UTC;\n// V8's Date treats it as host-local, so we append a 'GMT' designator to match.\nconst CTIME_NO_ZONE = /^[A-Za-z]{3} [A-Za-z]{3} +\\d{1,2} \\d{2}:\\d{2}:\\d{2} \\d{4}$/;\n\n/**\n * Parse a timestamp string in various common formats into a Date.\n *\n * A zone-less ISO datetime (e.g. `2024-01-15T10:30:00`) is interpreted as\n * UTC — matching Go's `hdfutil.ParseTimestamp` — rather than host-local time,\n * so converter output does not depend on the machine's timezone.\n *\n * Returns `null` if the input is empty or cannot be parsed.\n *\n * @param s - Timestamp string to parse\n * @returns Parsed Date or null if unparseable\n *\n * @example\n * ```typescript\n * parseTimestamp('2024-01-15T10:30:00Z'); // Date object\n * parseTimestamp('2024-01-15T10:30:00'); // Date object (interpreted as UTC)\n * parseTimestamp('not a date'); // null\n * parseTimestamp(''); // null\n * ```\n */\nexport function parseTimestamp(s: string): Date | null {\n if (!s || s.trim().length === 0) {\n return null;\n }\n\n const trimmed = s.trim();\n let normalized = trimmed;\n if (ISO_DATETIME_NO_ZONE.test(trimmed)) {\n normalized = `${trimmed.replace(' ', 'T')}Z`;\n } else if (CTIME_NO_ZONE.test(trimmed)) {\n normalized = `${trimmed} GMT`;\n }\n\n const parsed = new Date(normalized);\n if (isNaN(parsed.getTime())) {\n return null;\n }\n\n return parsed;\n}\n\n/**\n * Trim trailing fractional-second zeros from an RFC3339 UTC timestamp string,\n * matching Go's RFC3339Nano marshaling (which drops trailing zeros and the\n * decimal point when the fraction is all zeros).\n *\n * @example\n * trimUtcFraction('2024-11-15T10:30:00.000Z'); // '2024-11-15T10:30:00Z'\n * trimUtcFraction('2024-01-01T00:00:00.120Z'); // '2024-01-01T00:00:00.12Z'\n */\nexport function trimUtcFraction(s: string): string {\n return s.replace(/\\.(\\d*?)0+Z$/, (_m, keep: string) => (keep ? `.${keep}Z` : 'Z'));\n}\n\n/**\n * Format a Date as HDF's canonical timestamp: RFC3339 in UTC with trailing\n * fractional-second zeros trimmed. Byte-identical to what the Go converters\n * emit for the same instant (a UTC `time.Time` marshaled as RFC3339Nano).\n *\n * @example\n * formatTimestamp(new Date('2024-11-15T10:30:00Z')); // '2024-11-15T10:30:00Z'\n */\nexport function formatTimestamp(d: Date): string {\n return trimUtcFraction(d.toISOString());\n}\n\n/**\n * Format a Date as RFC3339 in UTC truncated to whole seconds (no fractional\n * part). For exporters whose target format conventionally uses whole-second\n * timestamps (CSAF, OpenVEX, CycloneDX). Use {@link formatTimestamp} for\n * canonical HDF output instead — it preserves sub-second precision.\n *\n * @example\n * formatTimestampSeconds(new Date('2024-11-15T10:30:00.123Z')); // '2024-11-15T10:30:00Z'\n */\nexport function formatTimestampSeconds(d: Date): string {\n return d.toISOString().replace(/\\.\\d+Z$/, 'Z');\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;AA2BA,SAAgB,UAAU,MAAsB;CAE9C,OADiB,KAAK,QAAQ,YAAY,GAC5B,CAAC,CAAC,QAAQ,QAAQ,GAAG,CAAC,CAAC,KAAK;AAC5C;AAMA,MAAM,uBAAuB;AAK7B,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;;;;AAsBtB,SAAgB,eAAe,GAAwB;CACrD,IAAI,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC,WAAW,GAC5B,OAAO;CAGT,MAAM,UAAU,EAAE,KAAK;CACvB,IAAI,aAAa;CACjB,IAAI,qBAAqB,KAAK,OAAO,GACnC,aAAa,GAAG,QAAQ,QAAQ,KAAK,GAAG,EAAE;MACrC,IAAI,cAAc,KAAK,OAAO,GACnC,aAAa,GAAG,QAAQ;CAG1B,MAAM,SAAS,IAAI,KAAK,UAAU;CAClC,IAAI,MAAM,OAAO,QAAQ,CAAC,GACxB,OAAO;CAGT,OAAO;AACT;;;;;;;;;;AAWA,SAAgB,gBAAgB,GAAmB;CACjD,OAAO,EAAE,QAAQ,iBAAiB,IAAI,SAAkB,OAAO,IAAI,KAAK,KAAK,GAAI;AACnF;;;;;;;;;AAUA,SAAgB,gBAAgB,GAAiB;CAC/C,OAAO,gBAAgB,EAAE,YAAY,CAAC;AACxC;;;;;;;;;;AAWA,SAAgB,uBAAuB,GAAiB;CACtD,OAAO,EAAE,YAAY,CAAC,CAAC,QAAQ,WAAW,GAAG;AAC/C"}
|