Skip to content

Commit 3c96b83

Browse files
author
AvrAlexandra
committed
added vulnerability by severity metric
1 parent b3f12ec commit 3c96b83

File tree

2 files changed

+103
-33
lines changed

2 files changed

+103
-33
lines changed

src/commands/history-metrics/metrics-command.ts

Lines changed: 53 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ import path from 'path'
33
import {Command} from 'commander'
44
import {
55
GrowthPatternMetric,
6-
VersionChangeMetric
6+
VersionChangeMetric,
7+
VulnerabilityFixBySeverityMetric
78
} from './metrics-generator';
89
import {
910
generateGrowthPatternChartData,
@@ -32,12 +33,12 @@ export interface MetricOptions {
3233
inputFiles: string[];
3334
}
3435

35-
type MetricType = 'growth-pattern' | 'version-changes';
36+
type MetricType = 'growth-pattern' | 'version-changes' | 'vulnerability-fixes-by-severity';
3637

3738
interface MetricConfig {
38-
processor: (data: any) => any;
39+
processor: (data: any, libraryInfo?: any) => any;
3940
requiredPrefixes: string[];
40-
chartGenerator: (results: any, options: MetricOptions) => { data: any[]; layout: any }[];
41+
chartGenerator?: (results: any, options: MetricOptions) => { data: any[]; layout: any }[];
4142
}
4243

4344
const metricsRegistry: Record<MetricType, MetricConfig> = {
@@ -50,6 +51,10 @@ const metricsRegistry: Record<MetricType, MetricConfig> = {
5051
processor: VersionChangeMetric,
5152
requiredPrefixes: ['dependency-history'],
5253
chartGenerator: generateVersionChangeChartData
54+
},
55+
'vulnerability-fixes-by-severity': {
56+
processor: VulnerabilityFixBySeverityMetric,
57+
requiredPrefixes: ['commit-dependency-history', 'library-info']
5358
}
5459
};
5560

@@ -72,44 +77,59 @@ export async function runMetrics(historyFolder: string, options: MetricOptions):
7277
return;
7378
}
7479

75-
const validInputFiles = options.inputFiles.filter(file =>
76-
config.requiredPrefixes.some(prefix => file.startsWith(prefix))
80+
const inputBase = path.join(historyFolder, options.inputDir || '');
81+
const outputBase = path.join(historyFolder, options.results);
82+
fs.mkdirSync(outputBase, { recursive: true });
83+
84+
let libraryInfo: any = undefined;
85+
const libraryInfoFile = options.inputFiles.find(file => file.startsWith('library-info'));
86+
87+
if (libraryInfoFile) {
88+
const libCandidates = [
89+
path.join(inputBase, libraryInfoFile),
90+
path.join(inputBase, `${libraryInfoFile}.json`)
91+
];
92+
const libPath = libCandidates.find(p => fs.existsSync(p));
93+
if (libPath) {
94+
const libContent = fs.readFileSync(libPath, 'utf-8');
95+
libraryInfo = JSON.parse(libContent);
96+
} else {
97+
console.warn(`⚠️ Specified library-info file not found for: ${libraryInfoFile}`);
98+
}
99+
}
100+
101+
const mainDataFile = options.inputFiles.find(file =>
102+
config.requiredPrefixes.some(prefix => file.startsWith(prefix) && prefix !== 'library-info')
77103
);
78104

79-
if (!validInputFiles.length) {
80-
console.warn(`⚠️ No input files match the required prefixes: ${config.requiredPrefixes.join(', ')}`);
105+
if (!mainDataFile) {
106+
console.warn(`⚠️ No valid main data file found. Expected prefix: ${config.requiredPrefixes.join(', ')}`);
81107
return;
82108
}
83109

84-
const inputBase = path.join(historyFolder, options.inputDir || '');
85-
const outputBase = path.join(historyFolder, options.results);
86-
fs.mkdirSync(outputBase, { recursive: true });
110+
const fileName = mainDataFile.endsWith('.json') ? mainDataFile : `${mainDataFile}.json`;
111+
const filePath = path.join(inputBase, fileName);
87112

88-
for (const relativeName of validInputFiles) {
89-
const fileName = relativeName.endsWith('.json') ? relativeName : `${relativeName}.json`;
90-
const filePath = path.join(inputBase, fileName);
91-
92-
if (!fs.existsSync(filePath)) {
93-
console.warn(`⚠️ File not found: ${filePath}`);
94-
continue;
95-
}
113+
if (!fs.existsSync(filePath)) {
114+
console.warn(`⚠️ File not found: ${filePath}`);
115+
return;
116+
}
96117

97-
const fileContents = fs.readFileSync(filePath, 'utf-8');
98-
const data = JSON.parse(fileContents);
99-
const results = config.processor(data);
118+
const fileContents = fs.readFileSync(filePath, 'utf-8');
119+
const data = JSON.parse(fileContents);
120+
const results = config.processor(data, libraryInfo);
100121

101-
const outputFile = path.join(outputBase, `${path.parse(fileName).name}-${metricType}-metric.json`);
102-
fs.writeFileSync(outputFile, JSON.stringify(results, null, 2));
122+
const outputFile = path.join(outputBase, `${metricType}-metric.json`);
123+
fs.writeFileSync(outputFile, JSON.stringify(results, null, 2));
103124

104-
if (options.chart) {
105-
const chartConfigs = config.chartGenerator(results, options);
106-
if (chartConfigs?.length) {
107-
await generateHtmlChart(outputFile, chartConfigs);
108-
} else {
109-
console.warn(`⚠️ No chart data generated for metric '${metricType}'.`);
110-
}
125+
if (options.chart && config.chartGenerator) {
126+
const chartConfigs = config.chartGenerator(results, options);
127+
if (chartConfigs?.length) {
128+
await generateHtmlChart(outputFile, chartConfigs);
129+
} else {
130+
console.warn(`⚠️ No chart data generated for metric '${metricType}'.`);
111131
}
112-
113-
console.log(`✅ Metric calculated for ${filePath} and chart generated (if requested).`);
114132
}
133+
134+
console.log(`✅ Metric calculated for ${filePath} and chart generated (if requested).`);
115135
}

src/commands/history-metrics/metrics-generator.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import semver from "semver/preload";
2+
import { LibraryInfo } from "../../extension-points/registrar";
23

34
export function GrowthPatternMetric(data: CommitDependencyHistory): any {
45
const summary: Record<string, any> = {};
@@ -78,3 +79,52 @@ export function VersionChangeMetric(dependencies: Record<string, { history: any[
7879

7980
return results;
8081
}
82+
83+
export function VulnerabilityFixBySeverityMetric(
84+
commitHistory: CommitDependencyHistory,
85+
libraryInfoMap: Record<string, { plugin: string; info: LibraryInfo }>
86+
): Record<string, Record<string, number>> {
87+
const timeline: Record<string, Record<string, number>> = {};
88+
89+
for (const [, commitEntry] of Object.entries(commitHistory)) {
90+
if (!commitEntry || !Array.isArray(commitEntry.history)) continue;
91+
const historyArray = commitEntry.history;
92+
93+
for (const entry of historyArray) {
94+
if (
95+
entry.action !== 'MODIFIED' ||
96+
!entry.fromVersion ||
97+
!entry.toVersion ||
98+
!entry.date ||
99+
!entry.depinderDependencyName
100+
) continue;
101+
102+
const libKey = Object.keys(libraryInfoMap).find(k => k.endsWith(`:${entry.depinderDependencyName}`));
103+
if (!libKey) continue;
104+
105+
const libInfo = libraryInfoMap[libKey];
106+
const libVulnerabilities = libInfo?.info?.vulnerabilities || [];
107+
108+
const month = entry.date.slice(0, 7);
109+
110+
for (const vuln of libVulnerabilities) {
111+
if (!vuln.vulnerableRange || !vuln.severity) continue;
112+
const cleanRange = vuln.vulnerableRange.replace(/,/g, ' ').trim();
113+
114+
const wasVulnerable =
115+
semver.valid(entry.fromVersion) && semver.satisfies(entry.fromVersion, cleanRange);
116+
const stillVulnerable =
117+
semver.valid(entry.toVersion) && semver.satisfies(entry.toVersion, cleanRange);
118+
const isFixed = wasVulnerable && !stillVulnerable;
119+
120+
if (isFixed) {
121+
if (!timeline[month]) timeline[month] = {};
122+
if (!timeline[month][vuln.severity]) timeline[month][vuln.severity] = 0;
123+
timeline[month][vuln.severity]++;
124+
}
125+
}
126+
}
127+
}
128+
129+
return timeline;
130+
}

0 commit comments

Comments
 (0)