vulnsig 0.1.0

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 ADDED
@@ -0,0 +1,2 @@
1
+ # threatprint-ts
2
+ The TS implementation of Threatprint
@@ -0,0 +1,2 @@
1
+ import type { HueResult } from './types.js';
2
+ export declare function scoreToHue(score: number): HueResult;
package/dist/color.js ADDED
@@ -0,0 +1,24 @@
1
+ export function scoreToHue(score) {
2
+ const w = Math.max(0, Math.min(10, score)) / 10;
3
+ // Light Yellow (low) → Orange (mid) → Dark Red (high)
4
+ // 0 = light yellow hsl(55, 95%, 55%)
5
+ // 5 = orange hsl(25, 95%, 53%)
6
+ // 10 = dark red hsl(0, 80%, 28%)
7
+ let hue;
8
+ if (w <= 0.5) {
9
+ // light yellow → orange: hue 55 → 25
10
+ hue = 55 - (w / 0.5) * 30;
11
+ }
12
+ else {
13
+ // orange → dark red: hue 25 → 0
14
+ hue = 25 - ((w - 0.5) / 0.5) * 25;
15
+ }
16
+ const sat = 85 + (1 - w) * 10; // 95% at low → 85% at high
17
+ // Lightness multiplier: asymmetric — subtle brightening for low scores,
18
+ // aggressive darkening for high scores (8-10 distinguishability)
19
+ // score 0: 1.15, score 5: 1.0, score 10: 0.55
20
+ const light = w <= 0.5
21
+ ? 1.0 + (0.5 - w) * 0.3 // low end: 1.0 → 1.15
22
+ : 1.0 - (w - 0.5) * 0.9; // high end: 1.0 → 0.55
23
+ return { hue, sat, light };
24
+ }
@@ -0,0 +1,7 @@
1
+ export declare function arcPath(cx: number, cy: number, innerR: number, outerR: number, startDeg: number, endDeg: number): string;
2
+ export declare function starPath(cx: number, cy: number, points: number, outerR: number, innerR: number): string;
3
+ export declare function radialCuts(startDeg: number, endDeg: number, cutWidth: number, gapDeg: number): {
4
+ startDeg: number;
5
+ endDeg: number;
6
+ }[];
7
+ export declare function ringFill(magnitude: number, hue: number, sat: number, light?: number): string;
@@ -0,0 +1,52 @@
1
+ const DEG2RAD = Math.PI / 180;
2
+ export function arcPath(cx, cy, innerR, outerR, startDeg, endDeg) {
3
+ const s = startDeg * DEG2RAD;
4
+ const e = endDeg * DEG2RAD;
5
+ const la = endDeg - startDeg > 180 ? 1 : 0;
6
+ const osx = cx + Math.cos(s) * outerR;
7
+ const osy = cy + Math.sin(s) * outerR;
8
+ const oex = cx + Math.cos(e) * outerR;
9
+ const oey = cy + Math.sin(e) * outerR;
10
+ const iex = cx + Math.cos(e) * innerR;
11
+ const iey = cy + Math.sin(e) * innerR;
12
+ const isx = cx + Math.cos(s) * innerR;
13
+ const isy = cy + Math.sin(s) * innerR;
14
+ return `M${osx},${osy} A${outerR},${outerR} 0 ${la},1 ${oex},${oey} L${iex},${iey} A${innerR},${innerR} 0 ${la},0 ${isx},${isy} Z`;
15
+ }
16
+ export function starPath(cx, cy, points, outerR, innerR) {
17
+ let d = '';
18
+ for (let i = 0; i < points; i++) {
19
+ const oa = (Math.PI * 2 * i) / points - Math.PI / 2;
20
+ const ia = (Math.PI * 2 * (i + 0.5)) / points - Math.PI / 2;
21
+ const ox = cx + Math.cos(oa) * outerR;
22
+ const oy = cy + Math.sin(oa) * outerR;
23
+ const ix = cx + Math.cos(ia) * innerR;
24
+ const iy = cy + Math.sin(ia) * innerR;
25
+ d += (i === 0 ? `M${ox},${oy}` : `L${ox},${oy}`) + `L${ix},${iy}`;
26
+ }
27
+ return d + 'Z';
28
+ }
29
+ export function radialCuts(startDeg, endDeg, cutWidth, gapDeg) {
30
+ const cuts = [];
31
+ const sectorSpan = endDeg - startDeg;
32
+ const step = cutWidth + gapDeg;
33
+ // Number of visible segments = numCuts + 1, each gapDeg wide.
34
+ // Total = numCuts * cutWidth + (numCuts + 1) * gapDeg = sectorSpan
35
+ // Solve: numCuts = floor((sectorSpan - gapDeg) / step)
36
+ const numCuts = Math.floor((sectorSpan - gapDeg) / step);
37
+ const patternSpan = numCuts * cutWidth + (numCuts + 1) * gapDeg;
38
+ const offset = (sectorSpan - patternSpan) / 2;
39
+ for (let i = 0; i < numCuts; i++) {
40
+ const cutStart = startDeg + offset + (i + 1) * gapDeg + i * cutWidth;
41
+ const cutEnd = cutStart + cutWidth;
42
+ cuts.push({ startDeg: cutStart, endDeg: cutEnd });
43
+ }
44
+ return cuts;
45
+ }
46
+ export function ringFill(magnitude, hue, sat, light = 1) {
47
+ if (magnitude <= 0.01)
48
+ return `hsla(${hue}, ${sat * 0.1}%, ${12 * light}%, 0.9)`;
49
+ if (magnitude <= 0.5)
50
+ return `hsla(${hue}, ${sat * 0.5}%, ${35 * light}%, 0.92)`;
51
+ return `hsla(${hue}, ${sat * 0.9}%, ${58 * light}%, 0.95)`;
52
+ }
@@ -0,0 +1,5 @@
1
+ export { renderGlyph } from './render.js';
2
+ export { parseCVSS } from './parse.js';
3
+ export { scoreToHue } from './color.js';
4
+ export { calculateScore } from './score.js';
5
+ export type { RenderOptions, ParsedMetrics, HueResult } from './types.js';
package/dist/index.js ADDED
@@ -0,0 +1,4 @@
1
+ export { renderGlyph } from './render.js';
2
+ export { parseCVSS } from './parse.js';
3
+ export { scoreToHue } from './color.js';
4
+ export { calculateScore } from './score.js';
@@ -0,0 +1,8 @@
1
+ import type { ParsedMetrics } from './types.js';
2
+ export declare const METRIC_DEFS: Record<string, {
3
+ severity: Record<string, number>;
4
+ }>;
5
+ export declare function parseCVSS(vector: string): ParsedMetrics;
6
+ export declare function detectCVSSVersion(vector: string): '3.0' | '3.1' | '4.0';
7
+ export declare function isVersion3(version: '3.0' | '3.1' | '4.0'): boolean;
8
+ export declare function getSeverity(metrics: ParsedMetrics, key: string): number;
package/dist/parse.js ADDED
@@ -0,0 +1,49 @@
1
+ export const METRIC_DEFS = {
2
+ AV: { severity: { N: 1.0, A: 0.7, L: 0.4, P: 0.15 } },
3
+ AC: { severity: { L: 1.0, H: 0.4 } },
4
+ AT: { severity: { N: 1.0, P: 0.4 } },
5
+ PR: { severity: { N: 1.0, L: 0.6, H: 0.2 } },
6
+ UI: { severity: { N: 1.0, P: 0.6, A: 0.2, R: 0.2 } }, // R for CVSS 3.0/3.1
7
+ VC: { severity: { H: 1.0, L: 0.5, N: 0.0 } },
8
+ VI: { severity: { H: 1.0, L: 0.5, N: 0.0 } },
9
+ VA: { severity: { H: 1.0, L: 0.5, N: 0.0 } },
10
+ SC: { severity: { H: 1.0, L: 0.5, N: 0.0 } },
11
+ SI: { severity: { H: 1.0, L: 0.5, N: 0.0 } },
12
+ SA: { severity: { H: 1.0, L: 0.5, N: 0.0 } },
13
+ // CVSS 3.0/3.1 metrics (C/I/A without V prefix, S for scope)
14
+ C: { severity: { H: 1.0, L: 0.5, N: 0.0 } },
15
+ I: { severity: { H: 1.0, L: 0.5, N: 0.0 } },
16
+ A: { severity: { H: 1.0, L: 0.5, N: 0.0 } },
17
+ S: { severity: { C: 1.0, U: 0.0 } }, // Scope: Changed or Unchanged
18
+ };
19
+ export function parseCVSS(vector) {
20
+ const m = {};
21
+ for (const part of vector.split('/')) {
22
+ const [key, val] = part.split(':');
23
+ if (METRIC_DEFS[key])
24
+ m[key] = val;
25
+ }
26
+ return m;
27
+ }
28
+ export function detectCVSSVersion(vector) {
29
+ if (vector.startsWith('CVSS:3.1/')) {
30
+ return '3.1';
31
+ }
32
+ else if (vector.startsWith('CVSS:3.0/')) {
33
+ return '3.0';
34
+ }
35
+ else if (vector.startsWith('CVSS:4.0/')) {
36
+ return '4.0';
37
+ }
38
+ throw new Error(`Unsupported CVSS version. Vector must start with 'CVSS:3.0/', 'CVSS:3.1/', or 'CVSS:4.0/'`);
39
+ }
40
+ export function isVersion3(version) {
41
+ return version === '3.0' || version === '3.1';
42
+ }
43
+ export function getSeverity(metrics, key) {
44
+ const def = METRIC_DEFS[key];
45
+ const val = metrics[key];
46
+ if (!def || !val)
47
+ return 0;
48
+ return def.severity[val] ?? 0;
49
+ }
@@ -0,0 +1,2 @@
1
+ import type { RenderOptions } from './types.js';
2
+ export declare function renderGlyph(options: RenderOptions): string;
package/dist/render.js ADDED
@@ -0,0 +1,157 @@
1
+ import { parseCVSS, getSeverity, detectCVSSVersion, isVersion3 } from './parse.js';
2
+ import { calculateScore } from './score.js';
3
+ import { scoreToHue } from './color.js';
4
+ import { arcPath, starPath, radialCuts, ringFill } from './geometry.js';
5
+ export function renderGlyph(options) {
6
+ const { vector, size = 120 } = options;
7
+ const metrics = parseCVSS(vector);
8
+ const version = detectCVSSVersion(vector);
9
+ // Score precedence: explicit → auto-calculate → fallback 5.0
10
+ let score;
11
+ if (options.score != null) {
12
+ score = options.score;
13
+ }
14
+ else {
15
+ score = calculateScore(vector);
16
+ }
17
+ const { hue, sat, light } = scoreToHue(score);
18
+ // Metric severities - handle CVSS 3.0, 3.1, and 4.0
19
+ const ac = getSeverity(metrics, 'AC');
20
+ // For CVSS 3.0/3.1, AT doesn't exist, so always treat as solid (AT:N)
21
+ const at = isVersion3(version) ? 1.0 : getSeverity(metrics, 'AT');
22
+ // For CVSS 3.0/3.1, use C/I/A instead of VC/VI/VA
23
+ const vc = isVersion3(version) ? getSeverity(metrics, 'C') : getSeverity(metrics, 'VC');
24
+ const vi = isVersion3(version) ? getSeverity(metrics, 'I') : getSeverity(metrics, 'VI');
25
+ const va = isVersion3(version) ? getSeverity(metrics, 'A') : getSeverity(metrics, 'VA');
26
+ // For CVSS 3.0/3.1, if S:C (Changed), both bands mirror C/I/A. If S:U (Unchanged), no split.
27
+ let sc, si, sa;
28
+ if (isVersion3(version)) {
29
+ const scopeChanged = getSeverity(metrics, 'S') > 0.5; // S:C = 1.0, S:U = 0.0
30
+ if (scopeChanged) {
31
+ // Split band: both bands mirror C/I/A
32
+ sc = vc;
33
+ si = vi;
34
+ sa = va;
35
+ }
36
+ else {
37
+ // No split
38
+ sc = 0;
39
+ si = 0;
40
+ sa = 0;
41
+ }
42
+ }
43
+ else {
44
+ // CVSS 4.0: use SC/SI/SA directly
45
+ sc = getSeverity(metrics, 'SC');
46
+ si = getSeverity(metrics, 'SI');
47
+ sa = getSeverity(metrics, 'SA');
48
+ }
49
+ const hasAnySub = sc > 0 || si > 0 || sa > 0;
50
+ const atPresent = at < 0.5;
51
+ const cx = 60, cy = 60;
52
+ const petalCount = { N: 8, A: 6, L: 4, P: 3 }[metrics.AV] || 8;
53
+ // Geometry constants
54
+ const ringWidth = 4.375;
55
+ const ringGap = 1.5;
56
+ const outerR = 44;
57
+ const hueRingR = outerR + ringGap + ringWidth / 2;
58
+ const subInnerR = outerR - ringWidth;
59
+ const vulnOuterR = subInnerR - ringGap;
60
+ const vulnInnerR = vulnOuterR - ringWidth;
61
+ const innerR = vulnInnerR;
62
+ const gapDeg = 3;
63
+ const cutGapDeg = 4;
64
+ const cutWidthDeg = 3;
65
+ const starOuterR = innerR - 2;
66
+ const starInnerR = starOuterR * (0.55 - ac * 0.35);
67
+ // PR stroke
68
+ const prRaw = metrics.PR;
69
+ const prStrokeWidth = prRaw === 'H' ? 3.2 : prRaw === 'L' ? 1.0 : 0;
70
+ // UI spikes/bumps
71
+ const uiRaw = metrics.UI;
72
+ const spikeBase = hueRingR + ringWidth / 2 - 0.5;
73
+ // Star fill — match the outer hue ring color
74
+ const sfSat = sat;
75
+ const sfLight = 52 * light;
76
+ const sfAlpha = 0.85;
77
+ const bgColor = `hsl(${hue}, 4%, 5%)`;
78
+ // Deterministic gradient ID from vector hash
79
+ const gradId = 'sg-' + simpleHash(vector);
80
+ // Sectors
81
+ const sectors = [
82
+ { key: 'C', s: -150 + gapDeg / 2, e: -30 - gapDeg / 2, vuln: vc, sub: sc },
83
+ { key: 'I', s: -30 + gapDeg / 2, e: 90 - gapDeg / 2, vuln: vi, sub: si },
84
+ { key: 'A', s: 90 + gapDeg / 2, e: 210 - gapDeg / 2, vuln: va, sub: sa },
85
+ ];
86
+ const parts = [];
87
+ // Defs
88
+ parts.push(`<defs><radialGradient id="${gradId}" cx="50%" cy="50%" r="50%">`);
89
+ parts.push(`<stop offset="0%" stop-color="hsla(${hue}, ${sfSat * 1.1}%, ${sfLight + 6}%, ${Math.min(1, sfAlpha + 0.1)})"/>`);
90
+ parts.push(`<stop offset="100%" stop-color="hsla(${hue}, ${sfSat}%, ${sfLight}%, ${sfAlpha})"/>`);
91
+ parts.push(`</radialGradient></defs>`);
92
+ // Z-order 1: UI:N Spikes
93
+ if (uiRaw === 'N') {
94
+ for (let i = 0; i < petalCount; i++) {
95
+ const a = (Math.PI * 2 * i) / petalCount - Math.PI / 2;
96
+ const x1 = cx + Math.cos(a) * spikeBase;
97
+ const y1 = cy + Math.sin(a) * spikeBase;
98
+ const x2 = cx + Math.cos(a) * (spikeBase + 3.4);
99
+ const y2 = cy + Math.sin(a) * (spikeBase + 3.4);
100
+ parts.push(`<line x1="${x1}" y1="${y1}" x2="${x2}" y2="${y2}" stroke="hsl(${hue}, ${sat}%, ${52 * light}%)" stroke-width="3.0" stroke-linecap="round"/>`);
101
+ }
102
+ }
103
+ // Z-order 2: UI:P Bumps
104
+ if (uiRaw === 'P') {
105
+ const bumpR = 4.6;
106
+ for (let i = 0; i < petalCount; i++) {
107
+ const a = (Math.PI * 2 * i) / petalCount - Math.PI / 2;
108
+ const bx = cx + Math.cos(a) * spikeBase;
109
+ const by = cy + Math.sin(a) * spikeBase;
110
+ const perpL = a - Math.PI / 2;
111
+ const perpR = a + Math.PI / 2;
112
+ const x1 = bx + Math.cos(perpL) * bumpR;
113
+ const y1 = by + Math.sin(perpL) * bumpR;
114
+ const x2 = bx + Math.cos(perpR) * bumpR;
115
+ const y2 = by + Math.sin(perpR) * bumpR;
116
+ parts.push(`<path d="M${x1},${y1} A${bumpR},${bumpR} 0 0,1 ${x2},${y2} Z" fill="hsl(${hue}, ${sat}%, ${52 * light}%)"/>`);
117
+ }
118
+ }
119
+ // Z-order 3: Background circle
120
+ parts.push(`<circle cx="${cx}" cy="${cy}" r="${innerR}" fill="${bgColor}"/>`);
121
+ // Z-order 4: Star fill
122
+ const starD = starPath(cx, cy, petalCount, starOuterR, starInnerR);
123
+ parts.push(`<path d="${starD}" fill="url(#${gradId})" stroke="none"/>`);
124
+ // Z-order 5: Star stroke (PR:N = no stroke)
125
+ if (prStrokeWidth > 0) {
126
+ parts.push(`<path d="${starD}" fill="none" stroke="hsl(${hue}, ${sat}%, ${72 * light}%)" stroke-width="${prStrokeWidth}" stroke-linejoin="round"/>`);
127
+ }
128
+ // Z-order 6 & 7: CIA ring sectors
129
+ for (const sec of sectors) {
130
+ // Vuln band (inner)
131
+ const vulnBandOuter = hasAnySub ? vulnOuterR : outerR;
132
+ parts.push(`<path d="${arcPath(cx, cy, vulnInnerR, vulnBandOuter, sec.s, sec.e)}" fill="${ringFill(sec.vuln, hue, sat, light)}"/>`);
133
+ // Sub band (outer) — only when split
134
+ if (hasAnySub) {
135
+ parts.push(`<path d="${arcPath(cx, cy, subInnerR, outerR, sec.s, sec.e)}" fill="${ringFill(sec.sub, hue, sat, light)}"/>`);
136
+ }
137
+ }
138
+ // Z-order 8: AT:P radial cuts
139
+ if (atPresent) {
140
+ for (const sec of sectors) {
141
+ const cuts = radialCuts(sec.s, sec.e, cutWidthDeg, cutGapDeg);
142
+ for (const cut of cuts) {
143
+ parts.push(`<path d="${arcPath(cx, cy, vulnInnerR - 0.5, outerR + 0.5, cut.startDeg, cut.endDeg)}" fill="${bgColor}"/>`);
144
+ }
145
+ }
146
+ }
147
+ // Z-order 9: Outer hue ring
148
+ parts.push(`<circle cx="${cx}" cy="${cy}" r="${hueRingR}" fill="none" stroke="hsl(${hue}, ${sat}%, ${52 * light}%)" stroke-width="${ringWidth}"/>`);
149
+ return `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 120 120" style="overflow:visible">${parts.join('')}</svg>`;
150
+ }
151
+ function simpleHash(str) {
152
+ let h = 0;
153
+ for (let i = 0; i < str.length; i++) {
154
+ h = ((h << 5) - h + str.charCodeAt(i)) | 0;
155
+ }
156
+ return Math.abs(h).toString(36);
157
+ }
@@ -0,0 +1 @@
1
+ export declare function calculateScore(vector: string): number;
package/dist/score.js ADDED
@@ -0,0 +1,29 @@
1
+ import aeCvss from 'ae-cvss-calculator';
2
+ import { detectCVSSVersion } from './parse.js';
3
+ // ae-cvss-calculator is a CJS webpack bundle; the default export
4
+ // contains all named exports when imported from ESM.
5
+ const { Cvss4P0, Cvss3P1, Cvss3P0 } = aeCvss;
6
+ export function calculateScore(vector) {
7
+ try {
8
+ const version = detectCVSSVersion(vector);
9
+ if (version === '3.1') {
10
+ const cvss = new Cvss3P1();
11
+ cvss.applyVector(vector);
12
+ return cvss.calculateScores().base ?? 5.0;
13
+ }
14
+ else if (version === '3.0') {
15
+ const cvss = new Cvss3P0();
16
+ cvss.applyVector(vector);
17
+ return cvss.calculateScores().base ?? 5.0;
18
+ }
19
+ else {
20
+ // CVSS 4.0 (validated by detectCVSSVersion)
21
+ const cvss = new Cvss4P0();
22
+ cvss.applyVector(vector);
23
+ return cvss.calculateScores().base ?? cvss.calculateScores().overall;
24
+ }
25
+ }
26
+ catch {
27
+ return 5.0;
28
+ }
29
+ }
@@ -0,0 +1,30 @@
1
+ export interface RenderOptions {
2
+ /** CVSS 4.0, CVSS 3.1, or CVSS 3.0 vector string */
3
+ vector: string;
4
+ /** Explicit score override (0-10). Auto-calculated when null. */
5
+ score?: number | null;
6
+ /** Rendered size in pixels. Default: 120 */
7
+ size?: number;
8
+ }
9
+ export interface ParsedMetrics {
10
+ AV: 'N' | 'A' | 'L' | 'P';
11
+ AC: 'L' | 'H';
12
+ AT?: 'N' | 'P';
13
+ PR: 'N' | 'L' | 'H';
14
+ UI: 'N' | 'P' | 'A' | 'R';
15
+ VC?: 'H' | 'L' | 'N';
16
+ VI?: 'H' | 'L' | 'N';
17
+ VA?: 'H' | 'L' | 'N';
18
+ SC?: 'H' | 'L' | 'N';
19
+ SI?: 'H' | 'L' | 'N';
20
+ SA?: 'H' | 'L' | 'N';
21
+ C?: 'H' | 'L' | 'N';
22
+ I?: 'H' | 'L' | 'N';
23
+ A?: 'H' | 'L' | 'N';
24
+ S?: 'C' | 'U';
25
+ }
26
+ export interface HueResult {
27
+ hue: number;
28
+ sat: number;
29
+ light: number;
30
+ }
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "vulnsig",
3
+ "version": "0.1.0",
4
+ "description": "Render CVSS vulnerability vectors as expressive SVG glyphs",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "import": "./dist/index.js",
11
+ "types": "./dist/index.d.ts"
12
+ }
13
+ },
14
+ "files": [
15
+ "dist"
16
+ ],
17
+ "scripts": {
18
+ "build": "tsc",
19
+ "lint": "eslint src/",
20
+ "format": "prettier --write src/",
21
+ "format:check": "prettier --check src/",
22
+ "test": "vitest run",
23
+ "preview": "npm run build && node scripts/preview.mjs && open doc/preview.html",
24
+ "prepublishOnly": "npm run build"
25
+ },
26
+ "keywords": [
27
+ "cvss",
28
+ "vulnerability",
29
+ "svg",
30
+ "glyph",
31
+ "security",
32
+ "threatprint"
33
+ ],
34
+ "license": "MIT",
35
+ "dependencies": {
36
+ "ae-cvss-calculator": "^1.0.9"
37
+ },
38
+ "devDependencies": {
39
+ "@eslint/js": "^10.0.1",
40
+ "eslint": "^10.0.1",
41
+ "eslint-config-prettier": "^10.1.8",
42
+ "prettier": "^3.8.1",
43
+ "typescript": "^5.4.0",
44
+ "typescript-eslint": "^8.56.0",
45
+ "vitest": "^4.0.18"
46
+ }
47
+ }