x-fidelity 2.5.0 → 2.7.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/CHANGELOG.md +40 -0
- package/dist/core/engine/analyzer.js +7 -1
- package/dist/core/engine/engineRunner.js +2 -2
- package/dist/core/engine/engineRunner.test.js +1 -0
- package/dist/facts/repoDependencyFacts.js +34 -5
- package/dist/facts/repoDependencyFacts.test.js +76 -132
- package/dist/operators/openaiAnalysisHighSeverity.js +7 -7
- package/dist/operators/openaiAnalysisHighSeverity.test.js +14 -3
- package/dist/utils/configManager.js +34 -0
- package/package.json +1 -1
- package/src/core/engine/analyzer.ts +8 -1
- package/src/core/engine/engineRunner.test.ts +1 -0
- package/src/core/engine/engineRunner.ts +3 -3
- package/src/facts/repoDependencyFacts.test.ts +140 -207
- package/src/facts/repoDependencyFacts.ts +41 -6
- package/src/operators/openaiAnalysisHighSeverity.test.ts +15 -3
- package/src/operators/openaiAnalysisHighSeverity.ts +9 -8
- package/src/types/typeDefs.ts +8 -0
- package/src/utils/configManager.ts +40 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,43 @@
|
|
|
1
|
+
# [2.7.0](https://github.com/zotoio/x-fidelity/compare/v2.6.0...v2.7.0) (2024-08-22)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Bug Fixes
|
|
5
|
+
|
|
6
|
+
* Add missing `repoUrl` property to `mockParams` object ([e94114d](https://github.com/zotoio/x-fidelity/commit/e94114dced7c9f6b4f53caa034937d07313abd71))
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
### Features
|
|
10
|
+
|
|
11
|
+
* **exemption process:** allow a repo to have a timelimited waiver for a given rule ([42d4b7d](https://github.com/zotoio/x-fidelity/commit/42d4b7de181131bbc3400710fc86876f4f3d8748))
|
|
12
|
+
|
|
13
|
+
# [2.6.0](https://github.com/zotoio/x-fidelity/compare/v2.5.0...v2.6.0) (2024-08-22)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
### Bug Fixes
|
|
17
|
+
|
|
18
|
+
* Change log level from debug to info for better visibility ([8173462](https://github.com/zotoio/x-fidelity/commit/817346206d084a0dcad79ceeec548b8b51342135))
|
|
19
|
+
* **deps:** monorepo fixes ([07021e8](https://github.com/zotoio/x-fidelity/commit/07021e8af78db05515ffe8e73b337f431c8caf61))
|
|
20
|
+
* Fix issues with dependency version validation and handling ([75282c1](https://github.com/zotoio/x-fidelity/commit/75282c1d392c49d8f152eac29fdedca7e7add2fa))
|
|
21
|
+
* Improve implementation and test coverage of openaiAnalysisHighSeverity ([acaf784](https://github.com/zotoio/x-fidelity/commit/acaf784b79560c9f303cab65c76e873419275ebc))
|
|
22
|
+
* Improve local dependency collection ([8d7732f](https://github.com/zotoio/x-fidelity/commit/8d7732f80cfea9beef6b35656b8488168070a9ef))
|
|
23
|
+
* improve semver range checking in repoDependencyAnalysis ([ba15f5a](https://github.com/zotoio/x-fidelity/commit/ba15f5ab31c1e2228e26e1eaeeea7d060f5ac033))
|
|
24
|
+
* Improve semver version comparison logic ([8f943e1](https://github.com/zotoio/x-fidelity/commit/8f943e1477fb041d97828829306c6de4af8695ae))
|
|
25
|
+
* Update `collectLocalDependencies` function to return correct dependency structure ([fc3fccb](https://github.com/zotoio/x-fidelity/commit/fc3fccb8780e23816c6cc6c18789a52395e9bbf9))
|
|
26
|
+
* Update collectLocalDependencies function to return expected structure ([42ee815](https://github.com/zotoio/x-fidelity/commit/42ee8159311692a10db648aafa1c34002ad7572e))
|
|
27
|
+
* Update mocking of `collectLocalDependencies` function in tests ([5d8d647](https://github.com/zotoio/x-fidelity/commit/5d8d647702e1cdd8cd979d9abd5da9791e9eed3c))
|
|
28
|
+
* Update repoDependencyAnalysis function to only add dependencies that don't meet requirements ([1070e5f](https://github.com/zotoio/x-fidelity/commit/1070e5faf990e1287a9ba6d36297ef8781e04658))
|
|
29
|
+
* Update semverValid function to return correct result ([158f555](https://github.com/zotoio/x-fidelity/commit/158f55506b6f96ecd18b04ca30557f5a03849a3b))
|
|
30
|
+
* Update test case for collectLocalDependencies function ([4b35d3f](https://github.com/zotoio/x-fidelity/commit/4b35d3faf3c9d9832e9d1a00d26f6b7ece2c19ca))
|
|
31
|
+
* Use toEqual for boolean comparisons in openaiAnalysisHighSeverity tests ([16425c6](https://github.com/zotoio/x-fidelity/commit/16425c67e07fa7c14569fa739b38f2353fa8a466))
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
### Features
|
|
35
|
+
|
|
36
|
+
* Add collectLocalDependencies function to repoDependencyFacts ([8aa6311](https://github.com/zotoio/x-fidelity/commit/8aa6311e3471ceed1bf07f2564b5de086d5440f1))
|
|
37
|
+
* Add support for version ranges in repoDependencyAnalysis ([ce82a21](https://github.com/zotoio/x-fidelity/commit/ce82a218227eca3b3ccb42cab393dcfe20e3de29))
|
|
38
|
+
* create comprehensive unit test suite for repoDependencyFacts.ts ([a92b83b](https://github.com/zotoio/x-fidelity/commit/a92b83b9ffedffd5f47e15cd7a7fed40353d3754))
|
|
39
|
+
* rewrite src/facts/repoDependencyFacts.test.ts with correct mocking and comprehensive test coverage ([66e26c2](https://github.com/zotoio/x-fidelity/commit/66e26c2bad485c71adca1187aafb81796d38148f))
|
|
40
|
+
|
|
1
41
|
# [2.5.0](https://github.com/zotoio/x-fidelity/compare/v2.4.0...v2.5.0) (2024-08-21)
|
|
2
42
|
|
|
3
43
|
|
|
@@ -26,7 +26,12 @@ function analyzeCodebase(params) {
|
|
|
26
26
|
return __awaiter(this, void 0, void 0, function* () {
|
|
27
27
|
const { repoPath, archetype = 'node-fullstack', configServer = '', localConfigPath = '', executionLogPrefix = '' } = params;
|
|
28
28
|
logger_1.logger.info(`STARTING..`);
|
|
29
|
+
// Load exemptions
|
|
30
|
+
if (localConfigPath) {
|
|
31
|
+
yield configManager_1.ConfigManager.loadExemptions(localConfigPath);
|
|
32
|
+
}
|
|
29
33
|
const telemetryData = yield (0, telemetryCollector_1.collectTelemetryData)({ repoPath, configServer });
|
|
34
|
+
const repoUrl = telemetryData.repoUrl;
|
|
30
35
|
// Send telemetry for analysis start
|
|
31
36
|
yield (0, telemetry_1.sendTelemetry)({
|
|
32
37
|
eventType: 'analysisStart',
|
|
@@ -72,7 +77,8 @@ function analyzeCodebase(params) {
|
|
|
72
77
|
fileData,
|
|
73
78
|
installedDependencyVersions,
|
|
74
79
|
minimumDependencyVersions,
|
|
75
|
-
standardStructure
|
|
80
|
+
standardStructure,
|
|
81
|
+
repoUrl
|
|
76
82
|
});
|
|
77
83
|
const finishMsg = `\n==========================\nCHECKS COMPLETED..\n==========================`;
|
|
78
84
|
logger_1.logger.info(finishMsg);
|
|
@@ -14,7 +14,7 @@ const logger_1 = require("../../utils/logger");
|
|
|
14
14
|
const configManager_1 = require("../../utils/configManager");
|
|
15
15
|
function runEngineOnFiles(params) {
|
|
16
16
|
return __awaiter(this, void 0, void 0, function* () {
|
|
17
|
-
const { engine, fileData, installedDependencyVersions, minimumDependencyVersions, standardStructure } = params;
|
|
17
|
+
const { engine, fileData, installedDependencyVersions, minimumDependencyVersions, standardStructure, repoUrl } = params;
|
|
18
18
|
const msg = `\n==========================\nRUNNING FILE CHECKS..\n==========================`;
|
|
19
19
|
logger_1.logger.info(msg);
|
|
20
20
|
const failures = [];
|
|
@@ -41,7 +41,7 @@ function runEngineOnFiles(params) {
|
|
|
41
41
|
results.forEach((result) => {
|
|
42
42
|
var _a, _b;
|
|
43
43
|
logger_1.logger.debug(JSON.stringify(result));
|
|
44
|
-
if (result.result) {
|
|
44
|
+
if (result.result && !configManager_1.ConfigManager.isExempt(repoUrl, result.name)) {
|
|
45
45
|
fileFailures.push({
|
|
46
46
|
ruleFailure: result.name,
|
|
47
47
|
level: (_a = result.event) === null || _a === void 0 ? void 0 : _a.type,
|
|
@@ -39,6 +39,7 @@ exports.collectLocalDependencies = collectLocalDependencies;
|
|
|
39
39
|
exports.getDependencyVersionFacts = getDependencyVersionFacts;
|
|
40
40
|
exports.findPropertiesInTree = findPropertiesInTree;
|
|
41
41
|
exports.repoDependencyAnalysis = repoDependencyAnalysis;
|
|
42
|
+
exports.semverValid = semverValid;
|
|
42
43
|
const logger_1 = require("../utils/logger");
|
|
43
44
|
const child_process_1 = require("child_process");
|
|
44
45
|
const semver = __importStar(require("semver"));
|
|
@@ -61,7 +62,7 @@ function collectLocalDependencies() {
|
|
|
61
62
|
logger_1.logger.error('No yarn.lock or package-lock.json found');
|
|
62
63
|
throw new Error('Unsupported package manager');
|
|
63
64
|
}
|
|
64
|
-
logger_1.logger.
|
|
65
|
+
logger_1.logger.info(`collectLocalDependencies: ${JSON.stringify(result)}`);
|
|
65
66
|
return result;
|
|
66
67
|
}
|
|
67
68
|
function collectYarnDependencies() {
|
|
@@ -156,7 +157,7 @@ function getDependencyVersionFacts(archetypeConfig) {
|
|
|
156
157
|
*/
|
|
157
158
|
function findPropertiesInTree(depGraph, minVersions) {
|
|
158
159
|
const results = [];
|
|
159
|
-
logger_1.logger.
|
|
160
|
+
logger_1.logger.info(`depGraph: ${JSON.stringify(depGraph)}`);
|
|
160
161
|
function walk(dep, parentName = '') {
|
|
161
162
|
const fullName = parentName ? `${parentName}/${dep.name}` : dep.name;
|
|
162
163
|
if (Object.keys(minVersions).includes(dep.name)) {
|
|
@@ -169,6 +170,7 @@ function findPropertiesInTree(depGraph, minVersions) {
|
|
|
169
170
|
}
|
|
170
171
|
}
|
|
171
172
|
depGraph.forEach(dep => walk(dep));
|
|
173
|
+
logger_1.logger.info(JSON.stringify(depGraph));
|
|
172
174
|
return results;
|
|
173
175
|
}
|
|
174
176
|
function repoDependencyAnalysis(params, almanac) {
|
|
@@ -180,10 +182,11 @@ function repoDependencyAnalysis(params, almanac) {
|
|
|
180
182
|
}
|
|
181
183
|
const analysis = [];
|
|
182
184
|
const dependencyData = yield almanac.factValue('dependencyData');
|
|
183
|
-
dependencyData.installedDependencyVersions.
|
|
185
|
+
dependencyData.installedDependencyVersions.forEach((versionData) => {
|
|
184
186
|
logger_1.logger.debug(`outdatedFramework: checking ${versionData.dep}`);
|
|
185
|
-
|
|
186
|
-
|
|
187
|
+
// Check if the installed version satisfies the required version, supporting both ranges and specific versions
|
|
188
|
+
const isValid = semverValid(versionData.ver, versionData.min);
|
|
189
|
+
if (!isValid) {
|
|
187
190
|
const dependencyFailure = {
|
|
188
191
|
'dependency': versionData.dep,
|
|
189
192
|
'currentVersion': versionData.ver,
|
|
@@ -198,3 +201,29 @@ function repoDependencyAnalysis(params, almanac) {
|
|
|
198
201
|
return result;
|
|
199
202
|
});
|
|
200
203
|
}
|
|
204
|
+
function semverValid(required, installed) {
|
|
205
|
+
if (!required || !installed) {
|
|
206
|
+
return true;
|
|
207
|
+
}
|
|
208
|
+
// If 'installed' is a single version and 'required' is a range
|
|
209
|
+
if (semver.valid(installed) && semver.validRange(required)) {
|
|
210
|
+
logger_1.logger.debug('range vs version');
|
|
211
|
+
return semver.satisfies(installed, required);
|
|
212
|
+
}
|
|
213
|
+
// If 'required' is a single version and 'installed' is a range
|
|
214
|
+
if (semver.valid(required) && semver.validRange(installed)) {
|
|
215
|
+
logger_1.logger.debug('version vs range');
|
|
216
|
+
return semver.satisfies(required, installed);
|
|
217
|
+
}
|
|
218
|
+
// If both are single versions, simply compare them
|
|
219
|
+
if (semver.valid(required) && semver.valid(installed)) {
|
|
220
|
+
logger_1.logger.debug('version vs version');
|
|
221
|
+
return semver.gt(installed, required);
|
|
222
|
+
}
|
|
223
|
+
// If both are ranges, check if they intersect
|
|
224
|
+
if (semver.validRange(required) && semver.validRange(installed)) {
|
|
225
|
+
logger_1.logger.debug('range vs range');
|
|
226
|
+
return semver.intersects(required, installed);
|
|
227
|
+
}
|
|
228
|
+
return false;
|
|
229
|
+
}
|
|
@@ -31,195 +31,139 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
31
31
|
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
32
32
|
});
|
|
33
33
|
};
|
|
34
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
35
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
36
|
+
};
|
|
34
37
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
35
|
-
const globals_1 = require("@jest/globals");
|
|
36
|
-
const fs = __importStar(require("fs"));
|
|
37
|
-
const child_process_1 = require("child_process");
|
|
38
38
|
const repoDependencyFacts = __importStar(require("./repoDependencyFacts"));
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
39
|
+
const child_process_1 = require("child_process");
|
|
40
|
+
const fs_1 = __importDefault(require("fs"));
|
|
41
|
+
jest.mock('child_process');
|
|
42
|
+
jest.mock('fs');
|
|
43
|
+
jest.mock('../utils/logger');
|
|
44
|
+
jest.mock('../core/cli', () => ({
|
|
42
45
|
options: {
|
|
43
|
-
dir: '/
|
|
46
|
+
dir: '/mock/dir'
|
|
44
47
|
}
|
|
45
48
|
}));
|
|
46
49
|
describe('repoDependencyFacts', () => {
|
|
47
50
|
beforeEach(() => {
|
|
48
|
-
|
|
51
|
+
jest.clearAllMocks();
|
|
49
52
|
});
|
|
50
53
|
describe('collectLocalDependencies', () => {
|
|
51
54
|
it('should collect Yarn dependencies when yarn.lock exists', () => {
|
|
52
|
-
|
|
55
|
+
fs_1.default.existsSync.mockImplementation((filePath) => filePath.includes('yarn.lock'));
|
|
53
56
|
child_process_1.execSync.mockReturnValue(JSON.stringify({
|
|
54
57
|
data: {
|
|
55
58
|
trees: [
|
|
56
|
-
{ name: '
|
|
59
|
+
{ name: 'package1@1.0.0', children: [{ name: 'subpackage1@0.1.0' }] },
|
|
60
|
+
{ name: 'package2@2.0.0' }
|
|
57
61
|
]
|
|
58
62
|
}
|
|
59
63
|
}));
|
|
60
64
|
const result = repoDependencyFacts.collectLocalDependencies();
|
|
61
65
|
expect(result).toEqual([
|
|
62
|
-
{ name: '
|
|
66
|
+
{ name: 'package1', version: '1.0.0', dependencies: [{ name: 'subpackage1', version: '0.1.0' }] },
|
|
67
|
+
{ name: 'package2', version: '2.0.0' }
|
|
63
68
|
]);
|
|
64
69
|
});
|
|
65
70
|
it('should collect NPM dependencies when package-lock.json exists', () => {
|
|
66
|
-
|
|
71
|
+
fs_1.default.existsSync.mockImplementation((filePath) => filePath.includes('package-lock.json'));
|
|
67
72
|
child_process_1.execSync.mockReturnValue(JSON.stringify({
|
|
68
73
|
dependencies: {
|
|
69
|
-
|
|
74
|
+
package1: { version: '1.0.0', dependencies: { subpackage1: { version: '0.1.0' } } },
|
|
75
|
+
package2: { version: '2.0.0' }
|
|
70
76
|
}
|
|
71
77
|
}));
|
|
72
78
|
const result = repoDependencyFacts.collectLocalDependencies();
|
|
73
79
|
expect(result).toEqual([
|
|
74
|
-
{ name: '
|
|
80
|
+
{ name: 'package1', version: '1.0.0', dependencies: [{ name: 'subpackage1', version: '0.1.0' }] },
|
|
81
|
+
{ name: 'package2', version: '2.0.0' }
|
|
75
82
|
]);
|
|
76
83
|
});
|
|
77
|
-
it('should throw an error when no lock file is found', () => {
|
|
78
|
-
|
|
84
|
+
it('should throw an error when no supported lock file is found', () => {
|
|
85
|
+
fs_1.default.existsSync.mockReturnValue(false);
|
|
79
86
|
expect(() => repoDependencyFacts.collectLocalDependencies()).toThrow('Unsupported package manager');
|
|
80
87
|
});
|
|
81
88
|
});
|
|
82
|
-
describe('getDependencyVersionFacts', () => {
|
|
83
|
-
it('should return installed dependency versions', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
84
|
-
const mockArchetypeConfig = {
|
|
85
|
-
name: 'test-archetype',
|
|
86
|
-
rules: [],
|
|
87
|
-
operators: [],
|
|
88
|
-
facts: [],
|
|
89
|
-
config: {
|
|
90
|
-
minimumDependencyVersions: {
|
|
91
|
-
'package-a': '1.0.0',
|
|
92
|
-
'package-b': '2.0.0'
|
|
93
|
-
},
|
|
94
|
-
standardStructure: {},
|
|
95
|
-
blacklistPatterns: [],
|
|
96
|
-
whitelistPatterns: []
|
|
97
|
-
}
|
|
98
|
-
};
|
|
99
|
-
fs.existsSync.mockImplementation((file) => typeof file === 'string' && file.endsWith('package-lock.json'));
|
|
100
|
-
child_process_1.execSync.mockReturnValue(JSON.stringify({
|
|
101
|
-
dependencies: {
|
|
102
|
-
'package-a': { version: '1.1.0', dependencies: { 'package-b': { version: '2.1.0' } } }
|
|
103
|
-
}
|
|
104
|
-
}));
|
|
105
|
-
globals_1.jest.spyOn(repoDependencyFacts, 'collectLocalDependencies').mockReturnValue([
|
|
106
|
-
{ name: 'package-a', version: '1.1.0' },
|
|
107
|
-
{ name: 'package-b', version: '2.1.0' },
|
|
108
|
-
{ name: 'package-c', version: '3.0.0' }
|
|
109
|
-
]);
|
|
110
|
-
const result = repoDependencyFacts.getDependencyVersionFacts(mockArchetypeConfig);
|
|
111
|
-
expect(result).toEqual([
|
|
112
|
-
{ dep: 'package-a', ver: '1.1.0', min: '1.0.0' },
|
|
113
|
-
{ dep: 'package-a/package-b', ver: '2.1.0', min: '2.0.0' }
|
|
114
|
-
]);
|
|
115
|
-
}));
|
|
116
|
-
it('should return an empty array when no local dependencies are found', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
117
|
-
const mockArchetypeConfig = {
|
|
118
|
-
name: 'test-archetype',
|
|
119
|
-
rules: [],
|
|
120
|
-
operators: [],
|
|
121
|
-
facts: [],
|
|
122
|
-
config: {
|
|
123
|
-
minimumDependencyVersions: {},
|
|
124
|
-
standardStructure: {},
|
|
125
|
-
blacklistPatterns: [],
|
|
126
|
-
whitelistPatterns: []
|
|
127
|
-
}
|
|
128
|
-
};
|
|
129
|
-
globals_1.jest.spyOn(repoDependencyFacts, 'collectLocalDependencies').mockReturnValue([]);
|
|
130
|
-
const result = repoDependencyFacts.getDependencyVersionFacts(mockArchetypeConfig);
|
|
131
|
-
expect(result).toEqual([]);
|
|
132
|
-
}));
|
|
133
|
-
});
|
|
134
89
|
describe('findPropertiesInTree', () => {
|
|
135
90
|
it('should find properties in a nested dependency tree', () => {
|
|
136
91
|
const depGraph = [
|
|
137
92
|
{
|
|
138
|
-
name: '
|
|
93
|
+
name: 'root1',
|
|
139
94
|
version: '1.0.0',
|
|
140
95
|
dependencies: [
|
|
141
|
-
{ name: '
|
|
142
|
-
{
|
|
96
|
+
{ name: 'child1', version: '0.1.0' },
|
|
97
|
+
{
|
|
98
|
+
name: 'child2',
|
|
99
|
+
version: '0.2.0',
|
|
100
|
+
dependencies: [
|
|
101
|
+
{ name: 'grandchild1', version: '0.0.1' }
|
|
102
|
+
]
|
|
103
|
+
}
|
|
143
104
|
]
|
|
144
|
-
}
|
|
105
|
+
},
|
|
106
|
+
{ name: 'root2', version: '2.0.0' }
|
|
145
107
|
];
|
|
146
108
|
const minVersions = {
|
|
147
|
-
'
|
|
148
|
-
'
|
|
149
|
-
'
|
|
109
|
+
'child1': '^0.1.0',
|
|
110
|
+
'grandchild1': '^0.0.1',
|
|
111
|
+
'root2': '^1.5.0'
|
|
150
112
|
};
|
|
151
113
|
const result = repoDependencyFacts.findPropertiesInTree(depGraph, minVersions);
|
|
152
114
|
expect(result).toEqual([
|
|
153
|
-
{ dep: '
|
|
154
|
-
{ dep: '
|
|
155
|
-
{ dep: '
|
|
115
|
+
{ dep: 'root1/child1', ver: '0.1.0', min: '^0.1.0' },
|
|
116
|
+
{ dep: 'root1/child2/grandchild1', ver: '0.0.1', min: '^0.0.1' },
|
|
117
|
+
{ dep: 'root2', ver: '2.0.0', min: '^1.5.0' }
|
|
156
118
|
]);
|
|
157
119
|
});
|
|
158
120
|
it('should return an empty array when no matching properties are found', () => {
|
|
159
121
|
const depGraph = [
|
|
160
|
-
{ name: '
|
|
161
|
-
{ name: '
|
|
122
|
+
{ name: 'package1', version: '1.0.0' },
|
|
123
|
+
{ name: 'package2', version: '2.0.0' }
|
|
162
124
|
];
|
|
163
125
|
const minVersions = {
|
|
164
|
-
'
|
|
126
|
+
'package3': '^3.0.0'
|
|
165
127
|
};
|
|
166
128
|
const result = repoDependencyFacts.findPropertiesInTree(depGraph, minVersions);
|
|
167
129
|
expect(result).toEqual([]);
|
|
168
130
|
});
|
|
169
131
|
});
|
|
170
132
|
describe('repoDependencyAnalysis', () => {
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
133
|
+
const mockAlmanac = {
|
|
134
|
+
factValue: jest.fn(),
|
|
135
|
+
addRuntimeFact: jest.fn(),
|
|
136
|
+
};
|
|
137
|
+
it('should return an empty result for non-REPO_GLOBAL_CHECK files', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
138
|
+
mockAlmanac.factValue.mockResolvedValueOnce({ fileName: 'some-file.js' });
|
|
139
|
+
const result = yield repoDependencyFacts.repoDependencyAnalysis({}, mockAlmanac);
|
|
176
140
|
expect(result).toEqual({ result: [] });
|
|
177
141
|
}));
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
expect(
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
const almanac = {
|
|
205
|
-
factValue: globals_1.jest.fn().mockImplementation((fact) => {
|
|
206
|
-
if (fact === 'fileData') {
|
|
207
|
-
return Promise.resolve({ fileName: 'REPO_GLOBAL_CHECK' });
|
|
208
|
-
}
|
|
209
|
-
if (fact === 'dependencyData') {
|
|
210
|
-
return Promise.resolve({
|
|
211
|
-
installedDependencyVersions: [
|
|
212
|
-
{ dep: 'package-a', ver: '2.1.0', min: '2.0.0' },
|
|
213
|
-
{ dep: 'package-b', ver: '3.0.0', min: '2.0.0' }
|
|
214
|
-
]
|
|
215
|
-
});
|
|
216
|
-
}
|
|
217
|
-
}),
|
|
218
|
-
addRuntimeFact: globals_1.jest.fn()
|
|
219
|
-
};
|
|
220
|
-
const result = yield repoDependencyFacts.repoDependencyAnalysis({ resultFact: 'testFact' }, almanac);
|
|
221
|
-
expect(result).toEqual({ result: [] });
|
|
222
|
-
expect(almanac.addRuntimeFact).toHaveBeenCalledWith('testFact', result);
|
|
223
|
-
}));
|
|
142
|
+
});
|
|
143
|
+
describe('semverValid', () => {
|
|
144
|
+
it('should return true for valid version comparisons', () => {
|
|
145
|
+
expect(repoDependencyFacts.semverValid('2.0.0', '^1.0.0')).toBe(false);
|
|
146
|
+
expect(repoDependencyFacts.semverValid('1.5.0', '1.0.0 - 2.0.0')).toBe(true);
|
|
147
|
+
expect(repoDependencyFacts.semverValid('1.0.0', '1.0.0')).toBe(true);
|
|
148
|
+
expect(repoDependencyFacts.semverValid('2.0.0', '>=1.0.0')).toBe(true);
|
|
149
|
+
});
|
|
150
|
+
it('should return false for invalid version comparisons', () => {
|
|
151
|
+
expect(repoDependencyFacts.semverValid('1.0.0', '^2.0.0')).toBe(false);
|
|
152
|
+
expect(repoDependencyFacts.semverValid('3.0.0', '1.0.0 - 2.0.0')).toBe(false);
|
|
153
|
+
expect(repoDependencyFacts.semverValid('0.9.0', '>=1.0.0')).toBe(false);
|
|
154
|
+
});
|
|
155
|
+
it('should handle complex version ranges', () => {
|
|
156
|
+
expect(repoDependencyFacts.semverValid('1.2.3', '1.x || >=2.5.0 || 5.0.0 - 7.2.3')).toBe(true);
|
|
157
|
+
expect(repoDependencyFacts.semverValid('2.5.0', '1.x || >=2.5.0 || 5.0.0 - 7.2.3')).toBe(true);
|
|
158
|
+
expect(repoDependencyFacts.semverValid('5.5.5', '1.x || >=2.5.0 || 5.0.0 - 7.2.3')).toBe(true);
|
|
159
|
+
expect(repoDependencyFacts.semverValid('8.0.0', '1.x || >=9.5.0 || 5.0.0 - 7.2.3')).toBe(false);
|
|
160
|
+
});
|
|
161
|
+
it('should return true for empty strings', () => {
|
|
162
|
+
expect(repoDependencyFacts.semverValid('', '')).toBe(true);
|
|
163
|
+
});
|
|
164
|
+
it('should return false for invalid input', () => {
|
|
165
|
+
expect(repoDependencyFacts.semverValid('not-a-version', '1.0.0')).toBe(false);
|
|
166
|
+
expect(repoDependencyFacts.semverValid('1.0.0', 'not-a-range')).toBe(false);
|
|
167
|
+
});
|
|
224
168
|
});
|
|
225
169
|
});
|
|
@@ -10,18 +10,18 @@ const openaiAnalysisHighSeverity = {
|
|
|
10
10
|
logger_1.logger.error('openaiAnalysisHighSeverity: TypeError: Cannot read properties of undefined (reading \'result\')');
|
|
11
11
|
return false;
|
|
12
12
|
}
|
|
13
|
-
|
|
14
|
-
let result = false;
|
|
13
|
+
const threshold = parseInt(severityThreshold) || 8;
|
|
15
14
|
if (Array.isArray(openaiAnalysis.result) && openaiAnalysis.result.length > 0) {
|
|
16
|
-
|
|
15
|
+
const hasHighSeverityIssue = openaiAnalysis.result.some((issue) => {
|
|
17
16
|
const severity = parseInt(issue === null || issue === void 0 ? void 0 : issue.severity);
|
|
18
|
-
return !isNaN(severity) && severity >=
|
|
19
|
-
})
|
|
17
|
+
return !isNaN(severity) && severity >= threshold;
|
|
18
|
+
});
|
|
19
|
+
if (hasHighSeverityIssue) {
|
|
20
20
|
logger_1.logger.error('openai: high severity issues found');
|
|
21
|
-
|
|
21
|
+
return true;
|
|
22
22
|
}
|
|
23
23
|
}
|
|
24
|
-
return
|
|
24
|
+
return false;
|
|
25
25
|
}
|
|
26
26
|
catch (e) {
|
|
27
27
|
// for now we don't fail the build if openai response parsing fails
|
|
@@ -20,7 +20,7 @@ describe('openaiAnalysisHighSeverity', () => {
|
|
|
20
20
|
]
|
|
21
21
|
};
|
|
22
22
|
const result = openaiAnalysisHighSeverity_1.openaiAnalysisHighSeverity.fn(openaiAnalysis, 8);
|
|
23
|
-
expect(result).
|
|
23
|
+
expect(result).toEqual(false);
|
|
24
24
|
expect(logger_1.logger.error).not.toHaveBeenCalled();
|
|
25
25
|
});
|
|
26
26
|
it('should return true if high severity issues are found', () => {
|
|
@@ -31,7 +31,7 @@ describe('openaiAnalysisHighSeverity', () => {
|
|
|
31
31
|
]
|
|
32
32
|
};
|
|
33
33
|
const result = openaiAnalysisHighSeverity_1.openaiAnalysisHighSeverity.fn(openaiAnalysis, 8);
|
|
34
|
-
expect(result).
|
|
34
|
+
expect(result).toEqual(true);
|
|
35
35
|
expect(logger_1.logger.error).toHaveBeenCalledWith('openai: high severity issues found');
|
|
36
36
|
});
|
|
37
37
|
it('should use default severity threshold if not provided', () => {
|
|
@@ -41,7 +41,7 @@ describe('openaiAnalysisHighSeverity', () => {
|
|
|
41
41
|
]
|
|
42
42
|
};
|
|
43
43
|
const result = openaiAnalysisHighSeverity_1.openaiAnalysisHighSeverity.fn(openaiAnalysis, null);
|
|
44
|
-
expect(result).
|
|
44
|
+
expect(result).toEqual(true);
|
|
45
45
|
expect(logger_1.logger.error).toHaveBeenCalledWith('openai: high severity issues found');
|
|
46
46
|
});
|
|
47
47
|
it('should handle empty result array', () => {
|
|
@@ -80,4 +80,15 @@ describe('openaiAnalysisHighSeverity', () => {
|
|
|
80
80
|
expect(result).toBe(true);
|
|
81
81
|
expect(logger_1.logger.error).toHaveBeenCalledWith('openai: high severity issues found');
|
|
82
82
|
});
|
|
83
|
+
it('should use default severity threshold when not provided', () => {
|
|
84
|
+
const openaiAnalysis = {
|
|
85
|
+
result: [
|
|
86
|
+
{ severity: 7 },
|
|
87
|
+
{ severity: 8 }
|
|
88
|
+
]
|
|
89
|
+
};
|
|
90
|
+
const result = openaiAnalysisHighSeverity_1.openaiAnalysisHighSeverity.fn(openaiAnalysis, null);
|
|
91
|
+
expect(result).toBe(true);
|
|
92
|
+
expect(logger_1.logger.error).toHaveBeenCalledWith('openai: high severity issues found');
|
|
93
|
+
});
|
|
83
94
|
});
|
|
@@ -63,6 +63,39 @@ class ConfigManager {
|
|
|
63
63
|
return ConfigManager.configs[archetype];
|
|
64
64
|
});
|
|
65
65
|
}
|
|
66
|
+
static loadExemptions(localConfigPath) {
|
|
67
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
68
|
+
const exemptionsPath = path.join(localConfigPath, 'exemptions.json');
|
|
69
|
+
try {
|
|
70
|
+
const exemptionsData = yield fs_1.default.promises.readFile(exemptionsPath, 'utf-8');
|
|
71
|
+
const exemptionsJson = JSON.parse(exemptionsData);
|
|
72
|
+
ConfigManager.exemptions = exemptionsJson.exemptions;
|
|
73
|
+
logger_1.logger.info(`Loaded ${ConfigManager.exemptions.length} exemptions`);
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
logger_1.logger.warn(`Failed to load exemptions: ${error}`);
|
|
77
|
+
ConfigManager.exemptions = [];
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
static getExemptions() {
|
|
82
|
+
return ConfigManager.exemptions;
|
|
83
|
+
}
|
|
84
|
+
static getExemptionExpirationDate(repoUrl, ruleName) {
|
|
85
|
+
const exemption = ConfigManager.exemptions.find(exemption => exemption.repoUrl === repoUrl &&
|
|
86
|
+
exemption.rule === ruleName);
|
|
87
|
+
return exemption ? new Date(exemption.expirationDate) : undefined;
|
|
88
|
+
}
|
|
89
|
+
static isExempt(repoUrl, ruleName) {
|
|
90
|
+
const now = new Date();
|
|
91
|
+
const result = ConfigManager.exemptions.some(exemption => exemption.repoUrl === repoUrl &&
|
|
92
|
+
exemption.rule === ruleName &&
|
|
93
|
+
new Date(exemption.expirationDate) > now);
|
|
94
|
+
if (result) {
|
|
95
|
+
logger_1.logger.error(`Exempting rule ${ruleName} for repo ${repoUrl} until ${ConfigManager.getExemptionExpirationDate(repoUrl, ruleName)}`);
|
|
96
|
+
}
|
|
97
|
+
return result;
|
|
98
|
+
}
|
|
66
99
|
static initialize(params) {
|
|
67
100
|
return __awaiter(this, void 0, void 0, function* () {
|
|
68
101
|
var _a;
|
|
@@ -176,3 +209,4 @@ class ConfigManager {
|
|
|
176
209
|
}
|
|
177
210
|
exports.ConfigManager = ConfigManager;
|
|
178
211
|
ConfigManager.configs = {};
|
|
212
|
+
ConfigManager.exemptions = [];
|
package/package.json
CHANGED
|
@@ -20,7 +20,13 @@ export async function analyzeCodebase(params: AnalyzeCodebaseParams): Promise<Re
|
|
|
20
20
|
|
|
21
21
|
logger.info(`STARTING..`);
|
|
22
22
|
|
|
23
|
+
// Load exemptions
|
|
24
|
+
if (localConfigPath) {
|
|
25
|
+
await ConfigManager.loadExemptions(localConfigPath);
|
|
26
|
+
}
|
|
27
|
+
|
|
23
28
|
const telemetryData = await collectTelemetryData({ repoPath, configServer});
|
|
29
|
+
const repoUrl = telemetryData.repoUrl;
|
|
24
30
|
|
|
25
31
|
// Send telemetry for analysis start
|
|
26
32
|
await sendTelemetry({
|
|
@@ -76,7 +82,8 @@ export async function analyzeCodebase(params: AnalyzeCodebaseParams): Promise<Re
|
|
|
76
82
|
fileData,
|
|
77
83
|
installedDependencyVersions,
|
|
78
84
|
minimumDependencyVersions,
|
|
79
|
-
standardStructure
|
|
85
|
+
standardStructure,
|
|
86
|
+
repoUrl
|
|
80
87
|
});
|
|
81
88
|
|
|
82
89
|
const finishMsg = `\n==========================\nCHECKS COMPLETED..\n==========================`;
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { EngineResult, RuleResult } from 'json-rules-engine';
|
|
2
2
|
import { ScanResult, RuleFailure } from '../../types/typeDefs';
|
|
3
3
|
import { logger } from '../../utils/logger';
|
|
4
|
-
import { REPO_GLOBAL_CHECK } from '../../utils/configManager';
|
|
4
|
+
import { REPO_GLOBAL_CHECK, ConfigManager } from '../../utils/configManager';
|
|
5
5
|
|
|
6
6
|
import { RunEngineOnFilesParams } from '../../types/typeDefs';
|
|
7
7
|
|
|
8
8
|
export async function runEngineOnFiles(params: RunEngineOnFilesParams): Promise<ScanResult[]> {
|
|
9
|
-
const { engine, fileData, installedDependencyVersions, minimumDependencyVersions, standardStructure } = params;
|
|
9
|
+
const { engine, fileData, installedDependencyVersions, minimumDependencyVersions, standardStructure, repoUrl } = params;
|
|
10
10
|
const msg = `\n==========================\nRUNNING FILE CHECKS..\n==========================`;
|
|
11
11
|
logger.info(msg);
|
|
12
12
|
const failures: ScanResult[] = [];
|
|
@@ -32,7 +32,7 @@ export async function runEngineOnFiles(params: RunEngineOnFilesParams): Promise<
|
|
|
32
32
|
const { results }: EngineResult = await engine.run(facts);
|
|
33
33
|
results.forEach((result: RuleResult) => {
|
|
34
34
|
logger.debug(JSON.stringify(result));
|
|
35
|
-
if (result.result) {
|
|
35
|
+
if (result.result && !ConfigManager.isExempt(repoUrl, result.name)) {
|
|
36
36
|
fileFailures.push({
|
|
37
37
|
ruleFailure: result.name,
|
|
38
38
|
level: result.event?.type,
|
|
@@ -1,229 +1,162 @@
|
|
|
1
|
-
import
|
|
2
|
-
import * as fs from 'fs';
|
|
1
|
+
import * as repoDependencyFacts from './repoDependencyFacts';
|
|
3
2
|
import { execSync } from 'child_process';
|
|
3
|
+
import fs from 'fs';
|
|
4
4
|
import { Almanac } from 'json-rules-engine';
|
|
5
|
-
import
|
|
6
|
-
import { LocalDependencies, ArchetypeConfig } from '../types/typeDefs';
|
|
5
|
+
import { LocalDependencies, MinimumDepVersions } from '../types/typeDefs';
|
|
7
6
|
|
|
8
|
-
jest.mock('fs');
|
|
9
7
|
jest.mock('child_process');
|
|
8
|
+
jest.mock('fs');
|
|
9
|
+
jest.mock('../utils/logger');
|
|
10
10
|
jest.mock('../core/cli', () => ({
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
options: {
|
|
12
|
+
dir: '/mock/dir'
|
|
13
|
+
}
|
|
14
14
|
}));
|
|
15
15
|
|
|
16
16
|
describe('repoDependencyFacts', () => {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
describe('collectLocalDependencies', () => {
|
|
22
|
-
it('should collect Yarn dependencies when yarn.lock exists', () => {
|
|
23
|
-
(fs.existsSync as jest.Mock).mockImplementation((file: unknown) => typeof file === 'string' && file.endsWith('yarn.lock'));
|
|
24
|
-
(execSync as jest.Mock).mockReturnValue(JSON.stringify({
|
|
25
|
-
data: {
|
|
26
|
-
trees: [
|
|
27
|
-
{ name: 'package-a@1.0.0', children: [{ name: 'package-b@2.0.0' }] }
|
|
28
|
-
]
|
|
29
|
-
}
|
|
30
|
-
}));
|
|
31
|
-
|
|
32
|
-
const result = repoDependencyFacts.collectLocalDependencies();
|
|
33
|
-
|
|
34
|
-
expect(result).toEqual([
|
|
35
|
-
{ name: 'package-a', version: '1.0.0', dependencies: [{ name: 'package-b', version: '2.0.0' }] }
|
|
36
|
-
]);
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
it('should collect NPM dependencies when package-lock.json exists', () => {
|
|
40
|
-
(fs.existsSync as jest.Mock).mockImplementation((file: unknown) => typeof file === 'string' && file.endsWith('package-lock.json'));
|
|
41
|
-
(execSync as jest.Mock).mockReturnValue(JSON.stringify({
|
|
42
|
-
dependencies: {
|
|
43
|
-
'package-a': { version: '1.0.0', dependencies: { 'package-b': { version: '2.0.0' } } }
|
|
44
|
-
}
|
|
45
|
-
}));
|
|
46
|
-
|
|
47
|
-
const result = repoDependencyFacts.collectLocalDependencies();
|
|
48
|
-
|
|
49
|
-
expect(result).toEqual([
|
|
50
|
-
{ name: 'package-a', version: '1.0.0', dependencies: [{ name: 'package-b', version: '2.0.0' }] }
|
|
51
|
-
]);
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
jest.clearAllMocks();
|
|
52
19
|
});
|
|
53
20
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
{ dep: 'package-a/package-b', ver: '2.1.0', min: '2.0.0' }
|
|
97
|
-
]);
|
|
21
|
+
describe('collectLocalDependencies', () => {
|
|
22
|
+
it('should collect Yarn dependencies when yarn.lock exists', () => {
|
|
23
|
+
(fs.existsSync as jest.Mock).mockImplementation((filePath) => filePath.includes('yarn.lock'));
|
|
24
|
+
(execSync as jest.Mock).mockReturnValue(JSON.stringify({
|
|
25
|
+
data: {
|
|
26
|
+
trees: [
|
|
27
|
+
{ name: 'package1@1.0.0', children: [{ name: 'subpackage1@0.1.0' }] },
|
|
28
|
+
{ name: 'package2@2.0.0' }
|
|
29
|
+
]
|
|
30
|
+
}
|
|
31
|
+
}));
|
|
32
|
+
|
|
33
|
+
const result = repoDependencyFacts.collectLocalDependencies();
|
|
34
|
+
|
|
35
|
+
expect(result).toEqual([
|
|
36
|
+
{ name: 'package1', version: '1.0.0', dependencies: [{ name: 'subpackage1', version: '0.1.0' }] },
|
|
37
|
+
{ name: 'package2', version: '2.0.0' }
|
|
38
|
+
]);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('should collect NPM dependencies when package-lock.json exists', () => {
|
|
42
|
+
(fs.existsSync as jest.Mock).mockImplementation((filePath) => filePath.includes('package-lock.json'));
|
|
43
|
+
(execSync as jest.Mock).mockReturnValue(JSON.stringify({
|
|
44
|
+
dependencies: {
|
|
45
|
+
package1: { version: '1.0.0', dependencies: { subpackage1: { version: '0.1.0' } } },
|
|
46
|
+
package2: { version: '2.0.0' }
|
|
47
|
+
}
|
|
48
|
+
}));
|
|
49
|
+
|
|
50
|
+
const result = repoDependencyFacts.collectLocalDependencies();
|
|
51
|
+
|
|
52
|
+
expect(result).toEqual([
|
|
53
|
+
{ name: 'package1', version: '1.0.0', dependencies: [{ name: 'subpackage1', version: '0.1.0' }] },
|
|
54
|
+
{ name: 'package2', version: '2.0.0' }
|
|
55
|
+
]);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('should throw an error when no supported lock file is found', () => {
|
|
59
|
+
(fs.existsSync as jest.Mock).mockReturnValue(false);
|
|
60
|
+
|
|
61
|
+
expect(() => repoDependencyFacts.collectLocalDependencies()).toThrow('Unsupported package manager');
|
|
62
|
+
});
|
|
98
63
|
});
|
|
99
64
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
65
|
+
describe('findPropertiesInTree', () => {
|
|
66
|
+
it('should find properties in a nested dependency tree', () => {
|
|
67
|
+
const depGraph: LocalDependencies[] = [
|
|
68
|
+
{
|
|
69
|
+
name: 'root1',
|
|
70
|
+
version: '1.0.0',
|
|
71
|
+
dependencies: [
|
|
72
|
+
{ name: 'child1', version: '0.1.0' },
|
|
73
|
+
{
|
|
74
|
+
name: 'child2',
|
|
75
|
+
version: '0.2.0',
|
|
76
|
+
dependencies: [
|
|
77
|
+
{ name: 'grandchild1', version: '0.0.1' }
|
|
78
|
+
]
|
|
79
|
+
}
|
|
80
|
+
]
|
|
81
|
+
},
|
|
82
|
+
{ name: 'root2', version: '2.0.0' }
|
|
83
|
+
];
|
|
84
|
+
|
|
85
|
+
const minVersions: MinimumDepVersions = {
|
|
86
|
+
'child1': '^0.1.0',
|
|
87
|
+
'grandchild1': '^0.0.1',
|
|
88
|
+
'root2': '^1.5.0'
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const result = repoDependencyFacts.findPropertiesInTree(depGraph, minVersions);
|
|
92
|
+
|
|
93
|
+
expect(result).toEqual([
|
|
94
|
+
{ dep: 'root1/child1', ver: '0.1.0', min: '^0.1.0' },
|
|
95
|
+
{ dep: 'root1/child2/grandchild1', ver: '0.0.1', min: '^0.0.1' },
|
|
96
|
+
{ dep: 'root2', ver: '2.0.0', min: '^1.5.0' }
|
|
97
|
+
]);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('should return an empty array when no matching properties are found', () => {
|
|
101
|
+
const depGraph: LocalDependencies[] = [
|
|
102
|
+
{ name: 'package1', version: '1.0.0' },
|
|
103
|
+
{ name: 'package2', version: '2.0.0' }
|
|
104
|
+
];
|
|
105
|
+
|
|
106
|
+
const minVersions: MinimumDepVersions = {
|
|
107
|
+
'package3': '^3.0.0'
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const result = repoDependencyFacts.findPropertiesInTree(depGraph, minVersions);
|
|
111
|
+
|
|
112
|
+
expect(result).toEqual([]);
|
|
113
|
+
});
|
|
119
114
|
});
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
describe('findPropertiesInTree', () => {
|
|
123
|
-
it('should find properties in a nested dependency tree', () => {
|
|
124
|
-
const depGraph: LocalDependencies[] = [
|
|
125
|
-
{
|
|
126
|
-
name: 'package-a',
|
|
127
|
-
version: '1.0.0',
|
|
128
|
-
dependencies: [
|
|
129
|
-
{ name: 'package-b', version: '2.0.0' },
|
|
130
|
-
{ name: 'package-c', version: '3.0.0', dependencies: [{ name: 'package-d', version: '4.0.0' }] }
|
|
131
|
-
]
|
|
132
|
-
}
|
|
133
|
-
];
|
|
134
|
-
|
|
135
|
-
const minVersions = {
|
|
136
|
-
'package-a': '0.9.0',
|
|
137
|
-
'package-c': '2.9.0',
|
|
138
|
-
'package-d': '3.9.0'
|
|
139
|
-
};
|
|
140
|
-
|
|
141
|
-
const result = repoDependencyFacts.findPropertiesInTree(depGraph, minVersions);
|
|
142
|
-
|
|
143
|
-
expect(result).toEqual([
|
|
144
|
-
{ dep: 'package-a', ver: '1.0.0', min: '0.9.0' },
|
|
145
|
-
{ dep: 'package-a/package-c', ver: '3.0.0', min: '2.9.0' },
|
|
146
|
-
{ dep: 'package-a/package-c/package-d', ver: '4.0.0', min: '3.9.0' }
|
|
147
|
-
]);
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
it('should return an empty array when no matching properties are found', () => {
|
|
151
|
-
const depGraph: LocalDependencies[] = [
|
|
152
|
-
{ name: 'package-x', version: '1.0.0' },
|
|
153
|
-
{ name: 'package-y', version: '2.0.0' }
|
|
154
|
-
];
|
|
155
115
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
116
|
+
describe('repoDependencyAnalysis', () => {
|
|
117
|
+
const mockAlmanac: Almanac = {
|
|
118
|
+
factValue: jest.fn(),
|
|
119
|
+
addRuntimeFact: jest.fn(),
|
|
120
|
+
} as unknown as Almanac;
|
|
159
121
|
|
|
160
|
-
|
|
122
|
+
it('should return an empty result for non-REPO_GLOBAL_CHECK files', async () => {
|
|
123
|
+
(mockAlmanac.factValue as jest.Mock).mockResolvedValueOnce({ fileName: 'some-file.js' });
|
|
161
124
|
|
|
162
|
-
|
|
163
|
-
});
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
describe('repoDependencyAnalysis', () => {
|
|
167
|
-
it('should return an empty result for non-global checks', async () => {
|
|
168
|
-
const almanac = {
|
|
169
|
-
factValue: jest.fn().mockResolvedValue({ fileName: 'not-global-check' } as never)
|
|
170
|
-
} as unknown as Almanac;
|
|
125
|
+
const result = await repoDependencyFacts.repoDependencyAnalysis({}, mockAlmanac);
|
|
171
126
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
expect(result).toEqual({ result: [] });
|
|
175
|
-
});
|
|
127
|
+
expect(result).toEqual({ result: [] });
|
|
128
|
+
});
|
|
176
129
|
|
|
177
|
-
it('should analyze dependencies and return failures', async () => {
|
|
178
|
-
const almanac = {
|
|
179
|
-
factValue: jest.fn().mockImplementation((fact) => {
|
|
180
|
-
if (fact === 'fileData') {
|
|
181
|
-
return Promise.resolve({ fileName: 'REPO_GLOBAL_CHECK' });
|
|
182
|
-
}
|
|
183
|
-
if (fact === 'dependencyData') {
|
|
184
|
-
return Promise.resolve({
|
|
185
|
-
installedDependencyVersions: [
|
|
186
|
-
{ dep: 'package-a', ver: '1.0.0', min: '2.0.0' },
|
|
187
|
-
{ dep: 'package-b', ver: '3.0.0', min: '2.0.0' }
|
|
188
|
-
]
|
|
189
|
-
});
|
|
190
|
-
}
|
|
191
|
-
}),
|
|
192
|
-
addRuntimeFact: jest.fn()
|
|
193
|
-
} as unknown as Almanac;
|
|
194
|
-
|
|
195
|
-
const result = await repoDependencyFacts.repoDependencyAnalysis({ resultFact: 'testFact' }, almanac);
|
|
196
|
-
|
|
197
|
-
expect(result).toEqual({
|
|
198
|
-
result: [
|
|
199
|
-
{ dependency: 'package-a', currentVersion: '1.0.0', requiredVersion: '2.0.0' }
|
|
200
|
-
]
|
|
201
|
-
});
|
|
202
|
-
expect(almanac.addRuntimeFact).toHaveBeenCalledWith('testFact', result);
|
|
203
130
|
});
|
|
204
131
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
132
|
+
describe('semverValid', () => {
|
|
133
|
+
it('should return true for valid version comparisons', () => {
|
|
134
|
+
expect(repoDependencyFacts.semverValid('2.0.0', '^1.0.0')).toBe(false);
|
|
135
|
+
expect(repoDependencyFacts.semverValid('1.5.0', '1.0.0 - 2.0.0')).toBe(true);
|
|
136
|
+
expect(repoDependencyFacts.semverValid('1.0.0', '1.0.0')).toBe(true);
|
|
137
|
+
expect(repoDependencyFacts.semverValid('2.0.0', '>=1.0.0')).toBe(true);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('should return false for invalid version comparisons', () => {
|
|
141
|
+
expect(repoDependencyFacts.semverValid('1.0.0', '^2.0.0')).toBe(false);
|
|
142
|
+
expect(repoDependencyFacts.semverValid('3.0.0', '1.0.0 - 2.0.0')).toBe(false);
|
|
143
|
+
expect(repoDependencyFacts.semverValid('0.9.0', '>=1.0.0')).toBe(false);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('should handle complex version ranges', () => {
|
|
147
|
+
expect(repoDependencyFacts.semverValid('1.2.3', '1.x || >=2.5.0 || 5.0.0 - 7.2.3')).toBe(true);
|
|
148
|
+
expect(repoDependencyFacts.semverValid('2.5.0', '1.x || >=2.5.0 || 5.0.0 - 7.2.3')).toBe(true);
|
|
149
|
+
expect(repoDependencyFacts.semverValid('5.5.5', '1.x || >=2.5.0 || 5.0.0 - 7.2.3')).toBe(true);
|
|
150
|
+
expect(repoDependencyFacts.semverValid('8.0.0', '1.x || >=9.5.0 || 5.0.0 - 7.2.3')).toBe(false);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it('should return true for empty strings', () => {
|
|
154
|
+
expect(repoDependencyFacts.semverValid('', '')).toBe(true);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('should return false for invalid input', () => {
|
|
158
|
+
expect(repoDependencyFacts.semverValid('not-a-version', '1.0.0')).toBe(false);
|
|
159
|
+
expect(repoDependencyFacts.semverValid('1.0.0', 'not-a-range')).toBe(false);
|
|
160
|
+
});
|
|
227
161
|
});
|
|
228
|
-
});
|
|
229
162
|
});
|
|
@@ -13,7 +13,7 @@ import path from 'path';
|
|
|
13
13
|
* @returns The local dependencies.
|
|
14
14
|
*/
|
|
15
15
|
export function collectLocalDependencies(): LocalDependencies[] {
|
|
16
|
-
let result:LocalDependencies[] = [];
|
|
16
|
+
let result: LocalDependencies[] = [];
|
|
17
17
|
if (fs.existsSync(path.join(options.dir, 'yarn.lock'))) {
|
|
18
18
|
result = collectYarnDependencies();
|
|
19
19
|
} else if (fs.existsSync(path.join(options.dir, 'package-lock.json'))) {
|
|
@@ -22,7 +22,7 @@ export function collectLocalDependencies(): LocalDependencies[] {
|
|
|
22
22
|
logger.error('No yarn.lock or package-lock.json found');
|
|
23
23
|
throw new Error('Unsupported package manager');
|
|
24
24
|
}
|
|
25
|
-
logger.
|
|
25
|
+
logger.info(`collectLocalDependencies: ${JSON.stringify(result)}`);
|
|
26
26
|
return result;
|
|
27
27
|
}
|
|
28
28
|
|
|
@@ -121,7 +121,7 @@ export function getDependencyVersionFacts(archetypeConfig: ArchetypeConfig): Ver
|
|
|
121
121
|
export function findPropertiesInTree(depGraph: LocalDependencies[], minVersions: MinimumDepVersions): VersionData[] {
|
|
122
122
|
const results: VersionData[] = [];
|
|
123
123
|
|
|
124
|
-
logger.
|
|
124
|
+
logger.info(`depGraph: ${JSON.stringify(depGraph)}`);
|
|
125
125
|
|
|
126
126
|
function walk(dep: LocalDependencies, parentName = '') {
|
|
127
127
|
const fullName = parentName ? `${parentName}/${dep.name}` : dep.name;
|
|
@@ -136,6 +136,7 @@ export function findPropertiesInTree(depGraph: LocalDependencies[], minVersions:
|
|
|
136
136
|
}
|
|
137
137
|
|
|
138
138
|
depGraph.forEach(dep => walk(dep));
|
|
139
|
+
logger.info(JSON.stringify(depGraph))
|
|
139
140
|
return results;
|
|
140
141
|
}
|
|
141
142
|
|
|
@@ -151,11 +152,12 @@ export async function repoDependencyAnalysis(params: any, almanac: Almanac) {
|
|
|
151
152
|
const analysis: any = [];
|
|
152
153
|
const dependencyData: any = await almanac.factValue('dependencyData');
|
|
153
154
|
|
|
154
|
-
dependencyData.installedDependencyVersions.
|
|
155
|
+
dependencyData.installedDependencyVersions.forEach((versionData: VersionData) => {
|
|
155
156
|
logger.debug(`outdatedFramework: checking ${versionData.dep}`);
|
|
156
157
|
|
|
157
|
-
|
|
158
|
-
|
|
158
|
+
// Check if the installed version satisfies the required version, supporting both ranges and specific versions
|
|
159
|
+
const isValid = semverValid(versionData.ver, versionData.min);
|
|
160
|
+
if (!isValid) {
|
|
159
161
|
const dependencyFailure = {
|
|
160
162
|
'dependency': versionData.dep,
|
|
161
163
|
'currentVersion': versionData.ver,
|
|
@@ -173,3 +175,36 @@ export async function repoDependencyAnalysis(params: any, almanac: Almanac) {
|
|
|
173
175
|
|
|
174
176
|
return result;
|
|
175
177
|
}
|
|
178
|
+
|
|
179
|
+
export function semverValid(required: string, installed: string): boolean {
|
|
180
|
+
|
|
181
|
+
if (!required || !installed) {
|
|
182
|
+
return true;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// If 'installed' is a single version and 'required' is a range
|
|
186
|
+
if (semver.valid(installed) && semver.validRange(required)) {
|
|
187
|
+
logger.debug('range vs version');
|
|
188
|
+
return semver.satisfies(installed, required);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// If 'required' is a single version and 'installed' is a range
|
|
192
|
+
if (semver.valid(required) && semver.validRange(installed)) {
|
|
193
|
+
logger.debug('version vs range');
|
|
194
|
+
return semver.satisfies(required, installed);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// If both are single versions, simply compare them
|
|
198
|
+
if (semver.valid(required) && semver.valid(installed)) {
|
|
199
|
+
logger.debug('version vs version');
|
|
200
|
+
return semver.gt(installed, required);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// If both are ranges, check if they intersect
|
|
204
|
+
if (semver.validRange(required) && semver.validRange(installed)) {
|
|
205
|
+
logger.debug('range vs range');
|
|
206
|
+
return semver.intersects(required, installed);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return false;
|
|
210
|
+
}
|
|
@@ -21,7 +21,7 @@ describe('openaiAnalysisHighSeverity', () => {
|
|
|
21
21
|
]
|
|
22
22
|
};
|
|
23
23
|
const result = openaiAnalysisHighSeverity.fn(openaiAnalysis, 8);
|
|
24
|
-
expect(result).
|
|
24
|
+
expect(result).toEqual(false);
|
|
25
25
|
expect(logger.error).not.toHaveBeenCalled();
|
|
26
26
|
});
|
|
27
27
|
|
|
@@ -33,7 +33,7 @@ describe('openaiAnalysisHighSeverity', () => {
|
|
|
33
33
|
]
|
|
34
34
|
};
|
|
35
35
|
const result = openaiAnalysisHighSeverity.fn(openaiAnalysis, 8);
|
|
36
|
-
expect(result).
|
|
36
|
+
expect(result).toEqual(true);
|
|
37
37
|
expect(logger.error).toHaveBeenCalledWith('openai: high severity issues found');
|
|
38
38
|
});
|
|
39
39
|
|
|
@@ -44,7 +44,7 @@ describe('openaiAnalysisHighSeverity', () => {
|
|
|
44
44
|
]
|
|
45
45
|
};
|
|
46
46
|
const result = openaiAnalysisHighSeverity.fn(openaiAnalysis, null);
|
|
47
|
-
expect(result).
|
|
47
|
+
expect(result).toEqual(true);
|
|
48
48
|
expect(logger.error).toHaveBeenCalledWith('openai: high severity issues found');
|
|
49
49
|
});
|
|
50
50
|
|
|
@@ -87,4 +87,16 @@ describe('openaiAnalysisHighSeverity', () => {
|
|
|
87
87
|
expect(result).toBe(true);
|
|
88
88
|
expect(logger.error).toHaveBeenCalledWith('openai: high severity issues found');
|
|
89
89
|
});
|
|
90
|
+
|
|
91
|
+
it('should use default severity threshold when not provided', () => {
|
|
92
|
+
const openaiAnalysis = {
|
|
93
|
+
result: [
|
|
94
|
+
{ severity: 7 },
|
|
95
|
+
{ severity: 8 }
|
|
96
|
+
]
|
|
97
|
+
};
|
|
98
|
+
const result = openaiAnalysisHighSeverity.fn(openaiAnalysis, null);
|
|
99
|
+
expect(result).toBe(true);
|
|
100
|
+
expect(logger.error).toHaveBeenCalledWith('openai: high severity issues found');
|
|
101
|
+
});
|
|
90
102
|
});
|
|
@@ -10,23 +10,24 @@ const openaiAnalysisHighSeverity: OperatorDefn = {
|
|
|
10
10
|
return false;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
let result = false;
|
|
13
|
+
const threshold = parseInt(severityThreshold) || 8;
|
|
15
14
|
|
|
16
15
|
if (Array.isArray(openaiAnalysis.result) && openaiAnalysis.result.length > 0) {
|
|
17
|
-
|
|
16
|
+
const hasHighSeverityIssue = openaiAnalysis.result.some((issue: any) => {
|
|
18
17
|
const severity = parseInt(issue?.severity);
|
|
19
|
-
return !isNaN(severity) && severity >=
|
|
20
|
-
})
|
|
18
|
+
return !isNaN(severity) && severity >= threshold;
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
if (hasHighSeverityIssue) {
|
|
21
22
|
logger.error('openai: high severity issues found');
|
|
22
|
-
|
|
23
|
+
return true;
|
|
23
24
|
}
|
|
24
25
|
}
|
|
25
26
|
|
|
26
|
-
return
|
|
27
|
+
return false;
|
|
27
28
|
} catch (e) {
|
|
28
29
|
// for now we don't fail the build if openai response parsing fails
|
|
29
|
-
logger.debug(e)
|
|
30
|
+
logger.debug(e);
|
|
30
31
|
logger.error(`openaiAnalysisHighSeverity: ${e}`);
|
|
31
32
|
return false;
|
|
32
33
|
}
|
package/src/types/typeDefs.ts
CHANGED
|
@@ -53,6 +53,14 @@ export interface RunEngineOnFilesParams {
|
|
|
53
53
|
installedDependencyVersions: any;
|
|
54
54
|
minimumDependencyVersions: any;
|
|
55
55
|
standardStructure: any;
|
|
56
|
+
repoUrl: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface Exemption {
|
|
60
|
+
repoUrl: string;
|
|
61
|
+
rule: string;
|
|
62
|
+
expirationDate: string;
|
|
63
|
+
reason: string;
|
|
56
64
|
}
|
|
57
65
|
|
|
58
66
|
export interface AnalyzeCodebaseParams {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { axiosClient } from "./axiosClient";
|
|
2
2
|
import { logger, setLogPrefix } from "./logger";
|
|
3
|
-
import { ArchetypeConfig, ExecutionConfig, GetConfigParams, InitializeParams, LoadLocalConfigParams, RuleConfig } from "../types/typeDefs";
|
|
3
|
+
import { ArchetypeConfig, ExecutionConfig, GetConfigParams, InitializeParams, LoadLocalConfigParams, RuleConfig, Exemption } from "../types/typeDefs";
|
|
4
4
|
import { archetypes } from "../archetypes";
|
|
5
5
|
import { options } from '../core/cli';
|
|
6
6
|
import fs from 'fs';
|
|
@@ -12,6 +12,7 @@ export const REPO_GLOBAL_CHECK = 'REPO_GLOBAL_CHECK';
|
|
|
12
12
|
|
|
13
13
|
export class ConfigManager {
|
|
14
14
|
private static configs: { [key: string]: ExecutionConfig } = {};
|
|
15
|
+
private static exemptions: Exemption[] = [];
|
|
15
16
|
|
|
16
17
|
public static getLoadedConfigs(): string[] {
|
|
17
18
|
return Object.keys(ConfigManager.configs);
|
|
@@ -31,6 +32,44 @@ export class ConfigManager {
|
|
|
31
32
|
return ConfigManager.configs[archetype];
|
|
32
33
|
}
|
|
33
34
|
|
|
35
|
+
public static async loadExemptions(localConfigPath: string): Promise<void> {
|
|
36
|
+
const exemptionsPath = path.join(localConfigPath, 'exemptions.json');
|
|
37
|
+
try {
|
|
38
|
+
const exemptionsData = await fs.promises.readFile(exemptionsPath, 'utf-8');
|
|
39
|
+
const exemptionsJson = JSON.parse(exemptionsData);
|
|
40
|
+
ConfigManager.exemptions = exemptionsJson.exemptions;
|
|
41
|
+
logger.info(`Loaded ${ConfigManager.exemptions.length} exemptions`);
|
|
42
|
+
} catch (error) {
|
|
43
|
+
logger.warn(`Failed to load exemptions: ${error}`);
|
|
44
|
+
ConfigManager.exemptions = [];
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
public static getExemptions(): Exemption[] {
|
|
49
|
+
return ConfigManager.exemptions;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
public static getExemptionExpirationDate(repoUrl: string, ruleName: string): Date | undefined {
|
|
53
|
+
const exemption = ConfigManager.exemptions.find(exemption =>
|
|
54
|
+
exemption.repoUrl === repoUrl &&
|
|
55
|
+
exemption.rule === ruleName
|
|
56
|
+
);
|
|
57
|
+
return exemption ? new Date(exemption.expirationDate) : undefined;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
public static isExempt(repoUrl: string, ruleName: string): boolean {
|
|
61
|
+
const now = new Date();
|
|
62
|
+
const result = ConfigManager.exemptions.some(exemption =>
|
|
63
|
+
exemption.repoUrl === repoUrl &&
|
|
64
|
+
exemption.rule === ruleName &&
|
|
65
|
+
new Date(exemption.expirationDate) > now
|
|
66
|
+
);
|
|
67
|
+
if (result) {
|
|
68
|
+
logger.error(`Exempting rule ${ruleName} for repo ${repoUrl} until ${ConfigManager.getExemptionExpirationDate(repoUrl, ruleName)}`);
|
|
69
|
+
}
|
|
70
|
+
return result;
|
|
71
|
+
}
|
|
72
|
+
|
|
34
73
|
private static async initialize(params: InitializeParams): Promise<ExecutionConfig> {
|
|
35
74
|
const { archetype, logPrefix } = params;
|
|
36
75
|
const configServer = options.configServer;
|