mirror of
https://github.com/imjasonh/govulncheck-action
synced 2026-07-07 00:12:55 +00:00
Updates the project to use ESM modules and replaces ncc with esbuild. This fixes build failures caused by newer @actions/core versions which use strict ESM exports that ncc cannot resolve. Also updates tests to use ESM and dependency injection. Signed-off-by: Jason Hall <imjasonh@gmail.com>
403 lines
12 KiB
JavaScript
403 lines
12 KiB
JavaScript
import AnnotationCreator from '../lib/annotator.js';
|
|
import { describe, it, expect, beforeEach, jest } from '@jest/globals';
|
|
|
|
describe('AnnotationCreator', () => {
|
|
let annotator;
|
|
let mockCore;
|
|
let mockFs;
|
|
let mockParser;
|
|
|
|
beforeEach(() => {
|
|
mockCore = {
|
|
warning: jest.fn(),
|
|
info: jest.fn(),
|
|
notice: jest.fn(),
|
|
};
|
|
|
|
mockFs = {
|
|
readFile: jest.fn(),
|
|
};
|
|
|
|
mockParser = {
|
|
extractUniqueModules: jest.fn(),
|
|
extractCallSites: jest.fn(),
|
|
};
|
|
|
|
annotator = new AnnotationCreator(mockCore, mockFs);
|
|
});
|
|
|
|
describe('createAnnotations', () => {
|
|
it('should create annotations for modules and call sites', async () => {
|
|
const vulnerabilities = [
|
|
{
|
|
finding: {
|
|
osv: 'GO-2023-1234',
|
|
trace: [{ module: 'example.com/vulnerable' }],
|
|
},
|
|
},
|
|
];
|
|
|
|
mockParser.extractUniqueModules.mockReturnValue(['example.com/vulnerable']);
|
|
mockParser.extractCallSites.mockReturnValue([
|
|
{
|
|
filename: 'main.go',
|
|
line: 42,
|
|
function: 'vulnerable.Function',
|
|
osv: 'GO-2023-1234',
|
|
},
|
|
]);
|
|
|
|
mockFs.readFile.mockResolvedValue(
|
|
'module example.com/app\n\nrequire example.com/vulnerable v1.0.0'
|
|
);
|
|
|
|
await annotator.createAnnotations(vulnerabilities, mockParser, '.');
|
|
|
|
expect(mockParser.extractUniqueModules).toHaveBeenCalledWith(vulnerabilities);
|
|
expect(mockParser.extractCallSites).toHaveBeenCalledWith(vulnerabilities);
|
|
expect(mockCore.warning).toHaveBeenCalledTimes(2);
|
|
});
|
|
|
|
it('should handle empty vulnerabilities', async () => {
|
|
mockParser.extractUniqueModules.mockReturnValue([]);
|
|
mockParser.extractCallSites.mockReturnValue([]);
|
|
|
|
await annotator.createAnnotations([], mockParser, '.');
|
|
|
|
expect(mockCore.warning).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe('annotateGoMod', () => {
|
|
it('should annotate vulnerable modules in go.mod', async () => {
|
|
const modules = ['example.com/vulnerable', 'another.com/package'];
|
|
const vulnerabilities = [
|
|
{
|
|
finding: {
|
|
osv: 'GO-2023-1234',
|
|
trace: [{ module: 'example.com/vulnerable' }],
|
|
},
|
|
},
|
|
{
|
|
finding: {
|
|
osv: 'GO-2023-5678',
|
|
trace: [{ module: 'another.com/package' }],
|
|
},
|
|
},
|
|
];
|
|
|
|
const goModContent = `module example.com/app
|
|
|
|
go 1.21
|
|
|
|
require (
|
|
example.com/vulnerable v1.0.0
|
|
another.com/package v2.1.0
|
|
)`;
|
|
|
|
mockFs.readFile.mockResolvedValue(goModContent);
|
|
|
|
// Create modulesWithCallSites map for testing
|
|
const modulesWithCallSites = new Map();
|
|
|
|
await annotator.annotateGoMod(modules, vulnerabilities, '.', modulesWithCallSites);
|
|
|
|
expect(mockCore.warning).toHaveBeenCalledTimes(2);
|
|
|
|
expect(mockCore.warning).toHaveBeenCalledWith(
|
|
expect.stringContaining('⚠️ Security vulnerabilities found in example.com/vulnerable'),
|
|
expect.objectContaining({
|
|
file: 'go.mod',
|
|
startLine: 6,
|
|
endLine: 6,
|
|
title: '1 vulnerabilities in example.com/vulnerable',
|
|
})
|
|
);
|
|
|
|
expect(mockCore.warning).toHaveBeenCalledWith(
|
|
expect.stringContaining('⚠️ Security vulnerabilities found in another.com/package'),
|
|
expect.objectContaining({
|
|
file: 'go.mod',
|
|
startLine: 7,
|
|
endLine: 7,
|
|
title: '1 vulnerabilities in another.com/package',
|
|
})
|
|
);
|
|
});
|
|
|
|
it('should handle missing go.mod file', async () => {
|
|
mockFs.readFile.mockRejectedValue(new Error('File not found'));
|
|
|
|
await annotator.annotateGoMod(['example.com/vulnerable'], [], '.');
|
|
|
|
expect(mockCore.warning).toHaveBeenCalledWith('Could not read go.mod: File not found');
|
|
});
|
|
|
|
it('should handle empty modules list', async () => {
|
|
await annotator.annotateGoMod([], [], '.');
|
|
|
|
expect(mockFs.readFile).not.toHaveBeenCalled();
|
|
expect(mockCore.warning).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should handle modules not found in go.mod', async () => {
|
|
mockFs.readFile.mockResolvedValue('module example.com/app\n\ngo 1.21');
|
|
|
|
await annotator.annotateGoMod(['nonexistent.com/module'], [], '.');
|
|
|
|
expect(mockCore.warning).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe('annotateCallSites', () => {
|
|
it('should create annotations for vulnerable call sites', () => {
|
|
const callSites = [
|
|
{
|
|
filename: 'main.go',
|
|
line: 42,
|
|
function: 'vulnerable.Function',
|
|
osv: 'GO-2023-1234',
|
|
},
|
|
{
|
|
filename: 'utils.go',
|
|
line: 10,
|
|
function: 'helper.Process',
|
|
osv: null,
|
|
},
|
|
];
|
|
|
|
annotator.annotateCallSites(callSites, '.');
|
|
|
|
expect(mockCore.warning).toHaveBeenCalledTimes(2);
|
|
|
|
expect(mockCore.warning).toHaveBeenCalledWith(
|
|
expect.stringContaining('🚨 Vulnerable code detected'),
|
|
expect.objectContaining({
|
|
file: 'main.go',
|
|
startLine: 42,
|
|
endLine: 42,
|
|
title: 'Vulnerable code: GO-2023-1234',
|
|
})
|
|
);
|
|
|
|
expect(mockCore.warning).toHaveBeenCalledWith(
|
|
expect.stringContaining('🚨 Vulnerable code detected'),
|
|
expect.objectContaining({
|
|
file: 'utils.go',
|
|
startLine: 10,
|
|
endLine: 10,
|
|
title: 'Vulnerable code detected',
|
|
})
|
|
);
|
|
});
|
|
|
|
it('should handle empty call sites', () => {
|
|
annotator.annotateCallSites([]);
|
|
expect(mockCore.warning).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should include OSV details with CVE aliases in call site annotations', () => {
|
|
const callSites = [
|
|
{
|
|
filename: 'main.go',
|
|
line: 42,
|
|
vulnerableFunction: 'vulnerable.Function',
|
|
osv: 'GO-2023-1234',
|
|
osvDetails: {
|
|
summary: 'Critical vulnerability in package',
|
|
aliases: ['CVE-2023-1234', 'GHSA-xxxx-yyyy', 'CVE-2023-5678'],
|
|
details: 'This vulnerability allows remote code execution',
|
|
},
|
|
fixedVersion: 'v1.2.3',
|
|
},
|
|
];
|
|
|
|
annotator.annotateCallSites(callSites, '.');
|
|
|
|
expect(mockCore.warning).toHaveBeenCalledWith(
|
|
expect.stringContaining('CVE: CVE-2023-1234, CVE-2023-5678'),
|
|
expect.anything()
|
|
);
|
|
expect(mockCore.warning).toHaveBeenCalledWith(
|
|
expect.stringContaining('Details: This vulnerability allows remote code execution'),
|
|
expect.anything()
|
|
);
|
|
expect(mockCore.warning).toHaveBeenCalledWith(
|
|
expect.stringContaining('✅ Fix available: Update the dependency to v1.2.3 or later'),
|
|
expect.anything()
|
|
);
|
|
});
|
|
|
|
it('should handle call sites without OSV details', () => {
|
|
const callSites = [
|
|
{
|
|
filename: 'main.go',
|
|
line: 42,
|
|
vulnerableFunction: 'vulnerable.Function',
|
|
osv: 'GO-2023-1234',
|
|
osvDetails: null,
|
|
fixedVersion: null,
|
|
},
|
|
];
|
|
|
|
annotator.annotateCallSites(callSites, '.');
|
|
|
|
expect(mockCore.warning).toHaveBeenCalledWith(
|
|
expect.not.stringContaining('CVE:'),
|
|
expect.anything()
|
|
);
|
|
expect(mockCore.warning).toHaveBeenCalledWith(
|
|
expect.not.stringContaining('✅ Fix available:'),
|
|
expect.anything()
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('annotateGoMod with OSV details', () => {
|
|
it('should include OSV details without aliases', async () => {
|
|
const modules = ['example.com/vulnerable'];
|
|
const vulnerabilities = [
|
|
{
|
|
finding: {
|
|
osv: 'GO-2023-1234',
|
|
trace: [{ module: 'example.com/vulnerable' }],
|
|
fixed_version: 'v1.2.3',
|
|
},
|
|
osvDetails: {
|
|
summary: 'Critical vulnerability',
|
|
},
|
|
},
|
|
];
|
|
|
|
const goModContent = 'module example.com/app\n\nrequire example.com/vulnerable v1.0.0';
|
|
mockFs.readFile.mockResolvedValue(goModContent);
|
|
|
|
await annotator.annotateGoMod(modules, vulnerabilities, '.', new Map());
|
|
|
|
expect(mockCore.warning).toHaveBeenCalledWith(
|
|
expect.stringContaining('GO-2023-1234: Critical vulnerability'),
|
|
expect.anything()
|
|
);
|
|
expect(mockCore.warning).toHaveBeenCalledWith(
|
|
expect.stringContaining('✅ Fixed in: v1.2.3'),
|
|
expect.anything()
|
|
);
|
|
expect(mockCore.warning).toHaveBeenCalledWith(
|
|
expect.stringContaining('💡 Recommended action: Update to v1.2.3 or later'),
|
|
expect.anything()
|
|
);
|
|
});
|
|
|
|
it('should include OSV details with CVE aliases', async () => {
|
|
const modules = ['example.com/vulnerable'];
|
|
const vulnerabilities = [
|
|
{
|
|
finding: {
|
|
osv: 'GO-2023-1234',
|
|
trace: [{ module: 'example.com/vulnerable' }],
|
|
},
|
|
osvDetails: {
|
|
summary: 'Critical vulnerability',
|
|
aliases: ['CVE-2023-1234', 'GHSA-xxxx-yyyy', 'CVE-2023-5678'],
|
|
},
|
|
},
|
|
];
|
|
|
|
const goModContent = 'module example.com/app\n\nrequire example.com/vulnerable v1.0.0';
|
|
mockFs.readFile.mockResolvedValue(goModContent);
|
|
|
|
await annotator.annotateGoMod(modules, vulnerabilities, '.', new Map());
|
|
|
|
expect(mockCore.warning).toHaveBeenCalledWith(
|
|
expect.stringContaining('(CVE-2023-1234, CVE-2023-5678)'),
|
|
expect.anything()
|
|
);
|
|
});
|
|
|
|
it('should create suggested edits for modules with call sites', async () => {
|
|
const modules = ['example.com/vulnerable'];
|
|
const vulnerabilities = [
|
|
{
|
|
finding: {
|
|
osv: 'GO-2023-1234',
|
|
trace: [{ module: 'example.com/vulnerable', version: 'v1.0.0' }],
|
|
fixed_version: 'v1.2.3',
|
|
},
|
|
},
|
|
];
|
|
|
|
const goModContent = 'module example.com/app\n\nrequire example.com/vulnerable v1.0.0';
|
|
mockFs.readFile.mockResolvedValue(goModContent);
|
|
|
|
const modulesWithCallSites = new Map([
|
|
[
|
|
'example.com/vulnerable',
|
|
{
|
|
currentVersion: 'v1.0.0',
|
|
fixedVersion: 'v1.2.3',
|
|
osvs: ['GO-2023-1234', 'GO-2023-5678'],
|
|
},
|
|
],
|
|
]);
|
|
|
|
await annotator.annotateGoMod(modules, vulnerabilities, '.', modulesWithCallSites);
|
|
|
|
expect(mockCore.notice).toHaveBeenCalledWith(
|
|
expect.stringContaining('🔧 Suggested fix:'),
|
|
expect.objectContaining({
|
|
title: 'Fix available for example.com/vulnerable',
|
|
file: 'go.mod',
|
|
startLine: 3,
|
|
endLine: 3,
|
|
})
|
|
);
|
|
expect(mockCore.notice).toHaveBeenCalledWith(
|
|
expect.stringContaining('Current: require example.com/vulnerable v1.0.0'),
|
|
expect.anything()
|
|
);
|
|
expect(mockCore.notice).toHaveBeenCalledWith(
|
|
expect.stringContaining('Suggested: require example.com/vulnerable v1.2.3'),
|
|
expect.anything()
|
|
);
|
|
expect(mockCore.notice).toHaveBeenCalledWith(
|
|
expect.stringContaining('• GO-2023-1234'),
|
|
expect.anything()
|
|
);
|
|
});
|
|
|
|
it('should handle vulnerabilities without OSV IDs when sorting', async () => {
|
|
const modules = ['example.com/vulnerable'];
|
|
const vulnerabilities = [
|
|
{
|
|
finding: {
|
|
osv: 'GO-2023-5678',
|
|
trace: [{ module: 'example.com/vulnerable' }],
|
|
},
|
|
},
|
|
{
|
|
finding: {
|
|
// No OSV ID
|
|
trace: [{ module: 'example.com/vulnerable' }],
|
|
},
|
|
},
|
|
{
|
|
finding: {
|
|
osv: 'GO-2023-1234',
|
|
trace: [{ module: 'example.com/vulnerable' }],
|
|
},
|
|
},
|
|
];
|
|
|
|
const goModContent = 'module example.com/app\n\nrequire example.com/vulnerable v1.0.0';
|
|
mockFs.readFile.mockResolvedValue(goModContent);
|
|
|
|
await annotator.annotateGoMod(modules, vulnerabilities, '.', new Map());
|
|
|
|
// The vulnerabilities should be sorted with empty OSV IDs first
|
|
expect(mockCore.warning).toHaveBeenCalledWith(
|
|
expect.stringContaining('⚠️ Security vulnerabilities found in example.com/vulnerable'),
|
|
expect.anything()
|
|
);
|
|
});
|
|
});
|
|
});
|