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 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,
@@ -35,6 +35,7 @@ describe('runEngineOnFiles', () => {
35
35
  installedDependencyVersions: {},
36
36
  minimumDependencyVersions: {},
37
37
  standardStructure: {},
38
+ repoUrl: 'https://github.com/mock/repo',
38
39
  };
39
40
  beforeEach(() => {
40
41
  jest.clearAllMocks();
@@ -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.debug(`collectLocalDependencies: ${JSON.stringify(result)}`);
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.debug(`depGraph: ${JSON.stringify(depGraph)}`);
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.map((versionData) => {
185
+ dependencyData.installedDependencyVersions.forEach((versionData) => {
184
186
  logger_1.logger.debug(`outdatedFramework: checking ${versionData.dep}`);
185
- const requiredRange = new semver.Range(versionData.min);
186
- if (!semver.gtr(versionData.ver, requiredRange)) {
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
- globals_1.jest.mock('fs');
40
- globals_1.jest.mock('child_process');
41
- globals_1.jest.mock('../core/cli', () => ({
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: '/test/dir'
46
+ dir: '/mock/dir'
44
47
  }
45
48
  }));
46
49
  describe('repoDependencyFacts', () => {
47
50
  beforeEach(() => {
48
- globals_1.jest.clearAllMocks();
51
+ jest.clearAllMocks();
49
52
  });
50
53
  describe('collectLocalDependencies', () => {
51
54
  it('should collect Yarn dependencies when yarn.lock exists', () => {
52
- fs.existsSync.mockImplementation((file) => typeof file === 'string' && file.endsWith('yarn.lock'));
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: 'package-a@1.0.0', children: [{ name: 'package-b@2.0.0' }] }
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: 'package-a', version: '1.0.0', dependencies: [{ name: 'package-b', version: '2.0.0' }] }
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
- fs.existsSync.mockImplementation((file) => typeof file === 'string' && file.endsWith('package-lock.json'));
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
- 'package-a': { version: '1.0.0', dependencies: { 'package-b': { version: '2.0.0' } } }
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: 'package-a', version: '1.0.0', dependencies: [{ name: 'package-b', version: '2.0.0' }] }
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
- fs.existsSync.mockReturnValue(false);
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: 'package-a',
93
+ name: 'root1',
139
94
  version: '1.0.0',
140
95
  dependencies: [
141
- { name: 'package-b', version: '2.0.0' },
142
- { name: 'package-c', version: '3.0.0', dependencies: [{ name: 'package-d', version: '4.0.0' }] }
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
- 'package-a': '0.9.0',
148
- 'package-c': '2.9.0',
149
- 'package-d': '3.9.0'
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: 'package-a', ver: '1.0.0', min: '0.9.0' },
154
- { dep: 'package-a/package-c', ver: '3.0.0', min: '2.9.0' },
155
- { dep: 'package-a/package-c/package-d', ver: '4.0.0', min: '3.9.0' }
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: 'package-x', version: '1.0.0' },
161
- { name: 'package-y', version: '2.0.0' }
122
+ { name: 'package1', version: '1.0.0' },
123
+ { name: 'package2', version: '2.0.0' }
162
124
  ];
163
125
  const minVersions = {
164
- 'package-z': '3.0.0'
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
- it('should return an empty result for non-global checks', () => __awaiter(void 0, void 0, void 0, function* () {
172
- const almanac = {
173
- factValue: globals_1.jest.fn().mockResolvedValue({ fileName: 'not-global-check' })
174
- };
175
- const result = yield repoDependencyFacts.repoDependencyAnalysis({}, almanac);
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
- it('should analyze dependencies and return failures', () => __awaiter(void 0, void 0, void 0, function* () {
179
- const almanac = {
180
- factValue: globals_1.jest.fn().mockImplementation((fact) => {
181
- if (fact === 'fileData') {
182
- return Promise.resolve({ fileName: 'REPO_GLOBAL_CHECK' });
183
- }
184
- if (fact === 'dependencyData') {
185
- return Promise.resolve({
186
- installedDependencyVersions: [
187
- { dep: 'package-a', ver: '1.0.0', min: '2.0.0' },
188
- { dep: 'package-b', ver: '3.0.0', min: '2.0.0' }
189
- ]
190
- });
191
- }
192
- }),
193
- addRuntimeFact: globals_1.jest.fn()
194
- };
195
- const result = yield repoDependencyFacts.repoDependencyAnalysis({ resultFact: 'testFact' }, almanac);
196
- expect(result).toEqual({
197
- result: [
198
- { dependency: 'package-a', currentVersion: '1.0.0', requiredVersion: '2.0.0' }
199
- ]
200
- });
201
- expect(almanac.addRuntimeFact).toHaveBeenCalledWith('testFact', result);
202
- }));
203
- it('should return an empty result when all dependencies meet requirements', () => __awaiter(void 0, void 0, void 0, function* () {
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
- severityThreshold = parseInt(severityThreshold) ? parseInt(severityThreshold) : 8;
14
- let result = false;
13
+ const threshold = parseInt(severityThreshold) || 8;
15
14
  if (Array.isArray(openaiAnalysis.result) && openaiAnalysis.result.length > 0) {
16
- if (openaiAnalysis.result.some((issue) => {
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 >= severityThreshold;
19
- })) {
17
+ return !isNaN(severity) && severity >= threshold;
18
+ });
19
+ if (hasHighSeverityIssue) {
20
20
  logger_1.logger.error('openai: high severity issues found');
21
- result = true;
21
+ return true;
22
22
  }
23
23
  }
24
- return result;
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).toBe(false);
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).toBe(true);
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).toBe(true);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "x-fidelity",
3
- "version": "2.5.0",
3
+ "version": "2.7.0",
4
4
  "description": "cli for opinionated framework adherence checks",
5
5
  "main": "dist/xfidelity",
6
6
  "bin": {
@@ -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==========================`;
@@ -29,6 +29,7 @@ describe('runEngineOnFiles', () => {
29
29
  installedDependencyVersions: {},
30
30
  minimumDependencyVersions: {},
31
31
  standardStructure: {},
32
+ repoUrl: 'https://github.com/mock/repo',
32
33
  };
33
34
 
34
35
  beforeEach(() => {
@@ -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 { jest } from '@jest/globals';
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 * as repoDependencyFacts from './repoDependencyFacts';
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
- options: {
12
- dir: '/test/dir'
13
- }
11
+ options: {
12
+ dir: '/mock/dir'
13
+ }
14
14
  }));
15
15
 
16
16
  describe('repoDependencyFacts', () => {
17
- beforeEach(() => {
18
- jest.clearAllMocks();
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
- it('should throw an error when no lock file is found', () => {
55
- (fs.existsSync as jest.Mock).mockReturnValue(false);
56
-
57
- expect(() => repoDependencyFacts.collectLocalDependencies()).toThrow('Unsupported package manager');
58
- });
59
- });
60
-
61
- describe('getDependencyVersionFacts', () => {
62
- it('should return installed dependency versions', async () => {
63
- const mockArchetypeConfig: ArchetypeConfig = {
64
- name: 'test-archetype',
65
- rules: [],
66
- operators: [],
67
- facts: [],
68
- config: {
69
- minimumDependencyVersions: {
70
- 'package-a': '1.0.0',
71
- 'package-b': '2.0.0'
72
- },
73
- standardStructure: {},
74
- blacklistPatterns: [],
75
- whitelistPatterns: []
76
- }
77
- };
78
-
79
- (fs.existsSync as jest.Mock).mockImplementation((file: unknown) => typeof file === 'string' && file.endsWith('package-lock.json'));
80
- (execSync as jest.Mock).mockReturnValue(JSON.stringify({
81
- dependencies: {
82
- 'package-a': { version: '1.1.0', dependencies: { 'package-b': { version: '2.1.0' } } }
83
- }
84
- }));
85
-
86
- jest.spyOn(repoDependencyFacts, 'collectLocalDependencies' as any).mockReturnValue([
87
- { name: 'package-a', version: '1.1.0' },
88
- { name: 'package-b', version: '2.1.0' },
89
- { name: 'package-c', version: '3.0.0' }
90
- ]);
91
-
92
- const result = repoDependencyFacts.getDependencyVersionFacts(mockArchetypeConfig);
93
-
94
- expect(result).toEqual([
95
- { dep: 'package-a', ver: '1.1.0', min: '1.0.0' },
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
- it('should return an empty array when no local dependencies are found', async () => {
101
- const mockArchetypeConfig: ArchetypeConfig = {
102
- name: 'test-archetype',
103
- rules: [],
104
- operators: [],
105
- facts: [],
106
- config: {
107
- minimumDependencyVersions: {},
108
- standardStructure: {},
109
- blacklistPatterns: [],
110
- whitelistPatterns: []
111
- }
112
- };
113
-
114
- jest.spyOn(repoDependencyFacts, 'collectLocalDependencies' as any).mockReturnValue([]);
115
-
116
- const result = repoDependencyFacts.getDependencyVersionFacts(mockArchetypeConfig);
117
-
118
- expect(result).toEqual([]);
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
- const minVersions = {
157
- 'package-z': '3.0.0'
158
- };
116
+ describe('repoDependencyAnalysis', () => {
117
+ const mockAlmanac: Almanac = {
118
+ factValue: jest.fn(),
119
+ addRuntimeFact: jest.fn(),
120
+ } as unknown as Almanac;
159
121
 
160
- const result = repoDependencyFacts.findPropertiesInTree(depGraph, minVersions);
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
- expect(result).toEqual([]);
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
- const result = await repoDependencyFacts.repoDependencyAnalysis({}, almanac);
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
- it('should return an empty result when all dependencies meet requirements', async () => {
206
- const almanac = {
207
- factValue: jest.fn().mockImplementation((fact) => {
208
- if (fact === 'fileData') {
209
- return Promise.resolve({ fileName: 'REPO_GLOBAL_CHECK' });
210
- }
211
- if (fact === 'dependencyData') {
212
- return Promise.resolve({
213
- installedDependencyVersions: [
214
- { dep: 'package-a', ver: '2.1.0', min: '2.0.0' },
215
- { dep: 'package-b', ver: '3.0.0', min: '2.0.0' }
216
- ]
217
- });
218
- }
219
- }),
220
- addRuntimeFact: jest.fn()
221
- } as unknown as Almanac;
222
-
223
- const result = await repoDependencyFacts.repoDependencyAnalysis({ resultFact: 'testFact' }, almanac);
224
-
225
- expect(result).toEqual({ result: [] });
226
- expect(almanac.addRuntimeFact).toHaveBeenCalledWith('testFact', result);
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.debug(`collectLocalDependencies: ${JSON.stringify(result)}`);
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.debug(`depGraph: ${JSON.stringify(depGraph)}`);
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.map((versionData: VersionData) => {
155
+ dependencyData.installedDependencyVersions.forEach((versionData: VersionData) => {
155
156
  logger.debug(`outdatedFramework: checking ${versionData.dep}`);
156
157
 
157
- const requiredRange = new semver.Range(versionData.min);
158
- if (!semver.gtr(versionData.ver, requiredRange)) {
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).toBe(false);
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).toBe(true);
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).toBe(true);
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
- severityThreshold = parseInt(severityThreshold) ? parseInt(severityThreshold) : 8;
14
- let result = false;
13
+ const threshold = parseInt(severityThreshold) || 8;
15
14
 
16
15
  if (Array.isArray(openaiAnalysis.result) && openaiAnalysis.result.length > 0) {
17
- if (openaiAnalysis.result.some((issue: any) => {
16
+ const hasHighSeverityIssue = openaiAnalysis.result.some((issue: any) => {
18
17
  const severity = parseInt(issue?.severity);
19
- return !isNaN(severity) && severity >= severityThreshold;
20
- })) {
18
+ return !isNaN(severity) && severity >= threshold;
19
+ });
20
+
21
+ if (hasHighSeverityIssue) {
21
22
  logger.error('openai: high severity issues found');
22
- result = true;
23
+ return true;
23
24
  }
24
25
  }
25
26
 
26
- return result;
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
  }
@@ -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;