Skip to content

Commit f95cc0b

Browse files
authored
feat: display vulunerability info (#16)
* init * done
1 parent 2706b6e commit f95cc0b

File tree

3 files changed

+150
-4
lines changed

3 files changed

+150
-4
lines changed

src/providers/diagnostics/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { languages } from 'vscode'
1010
import { displayName } from '../../generated-meta'
1111
import { checkDeprecation } from './rules/deprecation'
1212
import { checkReplacement } from './rules/replacement'
13+
import { checkVulnerability } from './rules/vulnerability'
1314

1415
export interface NodeDiagnosticInfo extends Pick<Diagnostic, 'message' | 'severity'> {
1516
node: ValidNode
@@ -19,6 +20,7 @@ export type DiagnosticRule = (dep: DependencyInfo, pkg: ResolvedPackument) => Aw
1920
const rules: DiagnosticRule[] = [
2021
checkDeprecation,
2122
checkReplacement,
23+
checkVulnerability,
2224
]
2325

2426
export function registerDiagnosticCollection(mapping: Record<string, Extractor | undefined>) {
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import type { OsvSeverityLevel } from '#utils/npmx'
2+
import type { DiagnosticRule } from '..'
3+
import { getVulnerability, SEVERITY_LEVELS } from '#utils/npmx'
4+
import { extractVersion } from '#utils/version'
5+
import { DiagnosticSeverity } from 'vscode'
6+
7+
const DIAGNOSTIC_MAPPING: Record<Exclude<OsvSeverityLevel, 'unknown'>, DiagnosticSeverity> = {
8+
critical: DiagnosticSeverity.Error,
9+
high: DiagnosticSeverity.Warning,
10+
moderate: DiagnosticSeverity.Information,
11+
low: DiagnosticSeverity.Hint,
12+
}
13+
14+
export const checkVulnerability: DiagnosticRule = async (dep, pkg) => {
15+
const exactVersion = extractVersion(dep.version)
16+
const versionInfo = pkg.versions[exactVersion]
17+
18+
if (!versionInfo)
19+
return
20+
21+
const { totalCounts } = await getVulnerability({ name: dep.name, version: versionInfo.version })
22+
23+
const message: string[] = []
24+
let severity: DiagnosticSeverity | null = null
25+
26+
for (const s of SEVERITY_LEVELS) {
27+
const count = totalCounts[s]
28+
29+
if (count <= 0)
30+
continue
31+
32+
if (!severity)
33+
severity = DIAGNOSTIC_MAPPING[s]
34+
35+
message.push(`${count} ${s}`)
36+
}
37+
38+
if (!message.length)
39+
return
40+
41+
return {
42+
node: dep.versionNode,
43+
message: `This version has ${message.join(', ')} ${message.length === 1 ? 'vulnerability' : 'vulnerabilities'}`,
44+
severity: DiagnosticSeverity.Error,
45+
}
46+
}

src/utils/npmx.ts

Lines changed: 102 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,110 @@ import { encodePackageName } from './npm'
77
export const NPMX_DEV_API = 'https://npmx.dev/api'
88

99
export const getReplacement = memoize<string, Promise<ModuleReplacement>>(async (name) => {
10-
logger.info(`Fetching replacement for ${name}`)
10+
logger.info(`Fetching replacements for ${name}`)
1111
const encodedName = encodePackageName(name)
1212

1313
const result = await ofetch<ModuleReplacement>(`${NPMX_DEV_API}/replacements/${encodedName}`)
14-
// Fallback for cache compatibility (LRUCache rejects null/undefined)
15-
?? {}
16-
logger.info(`Fetched replacement for ${name}`)
14+
logger.info(`Fetched replacements for ${name}`)
15+
16+
return result
17+
})
18+
19+
/**
20+
* Severity levels in priority order (highest first)
21+
*/
22+
export const SEVERITY_LEVELS = ['critical', 'high', 'moderate', 'low'] as const
23+
24+
/**
25+
* Severity level derived from CVSS score
26+
*/
27+
export type OsvSeverityLevel = (typeof SEVERITY_LEVELS)[number] | 'unknown'
28+
29+
/**
30+
* Simplified vulnerability info for display
31+
*/
32+
export interface VulnerabilitySummary {
33+
id: string
34+
summary: string
35+
severity: OsvSeverityLevel
36+
aliases: string[]
37+
url: string
38+
}
39+
40+
/** Depth in dependency tree */
41+
export type DependencyDepth = 'root' | 'direct' | 'transitive'
42+
43+
/**
44+
* Vulnerability info for a single package in the tree
45+
*/
46+
export interface PackageVulnerabilityInfo {
47+
name: string
48+
version: string
49+
/** Depth in dependency tree: root (0), direct (1), transitive (2+) */
50+
depth: DependencyDepth
51+
/** Dependency path from root package */
52+
path: string[]
53+
vulnerabilities: VulnerabilitySummary[]
54+
counts: {
55+
total: number
56+
critical: number
57+
high: number
58+
moderate: number
59+
low: number
60+
}
61+
}
62+
63+
/**
64+
* Deprecated package info in the dependency tree
65+
*/
66+
export interface DeprecatedPackageInfo {
67+
name: string
68+
version: string
69+
/** Depth in dependency tree: root (0), direct (1), transitive (2+) */
70+
depth: DependencyDepth
71+
/** Dependency path from root package */
72+
path: string[]
73+
/** Deprecation message */
74+
message: string
75+
}
76+
77+
/**
78+
* Result of dependency tree analysis
79+
*/
80+
export interface VulnerabilityTreeResult {
81+
/** Root package name */
82+
package: string
83+
/** Root package version */
84+
version: string
85+
/** All packages with vulnerabilities in the tree */
86+
vulnerablePackages: PackageVulnerabilityInfo[]
87+
/** All deprecated packages in the tree */
88+
deprecatedPackages: DeprecatedPackageInfo[]
89+
/** Total packages analyzed */
90+
totalPackages: number
91+
/** Number of packages that could not be checked (OSV query failed) */
92+
failedQueries: number
93+
/** Aggregated counts across all packages */
94+
totalCounts: {
95+
total: number
96+
critical: number
97+
high: number
98+
moderate: number
99+
low: number
100+
}
101+
}
102+
103+
export const getVulnerability = memoize<{
104+
name: string
105+
version: string
106+
}, Promise<VulnerabilityTreeResult>>(async ({ name, version }) => {
107+
logger.info(`Fetching vulnerabilities for ${name}`)
108+
const encodedName = encodePackageName(name)
109+
110+
const result = await ofetch(`${NPMX_DEV_API}/registry/vulnerabilities/${encodedName}/v/${version}`)
111+
logger.info(`Fetched vulnerabilities for ${name}`)
112+
17113
return result
114+
}, {
115+
getKey: ({ name: packageName, version }) => `${packageName}:${version}`,
18116
})

0 commit comments

Comments
 (0)